Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 969e39c541 | |||
| b256939cc4 | |||
| c46137644b | |||
| 1bd7ef56c4 | |||
| c258b99cec | |||
| d3abb32cb4 | |||
| 223ede9ff9 | |||
| 9d31a4eba1 | |||
| ea0e40c269 | |||
| bc24ea45a4 | |||
| 86d0c9b397 | |||
| c275ccf0da | |||
| 447a48f306 | |||
| 70b5cbc8db | |||
| 3459de0375 | |||
| c6817479bd | |||
| f02aae21f6 | |||
| 87a62a1791 | |||
| a0d1643021 | |||
| 856a946fa7 | |||
| 9d97164c20 | |||
| 6d526e7043 | |||
| 946072fc5a | |||
| 6b8dfdf02d | |||
| ba85fdf470 | |||
| 295f8601e3 | |||
| a38ccd8624 | |||
| b2dcec2f8a | |||
| e8355029d9 | |||
| 2b374b98d2 |
+111
@@ -1,5 +1,116 @@
|
||||
# Changelog
|
||||
|
||||
## v0.16.10
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.16.9...v0.16.10)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Customize when content scripts are registered, in the manifest or at runtime ([#471](https://github.com/wxt-dev/wxt/pull/471))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Don't assume react when importing JSX entrypoints during build ([#470](https://github.com/wxt-dev/wxt/pull/470))
|
||||
- Respect `configFile` option ([#472](https://github.com/wxt-dev/wxt/pull/472))
|
||||
|
||||
## v0.16.9
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.16.8...v0.16.9)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Support setting side panel options in HTML file ([#468](https://github.com/wxt-dev/wxt/pull/468))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Fix order of ShadowRootUI hooks calling ([#459](https://github.com/wxt-dev/wxt/pull/459))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Add wrapper div to react's `createShadowRootUi` example ([bc24ea4](https://github.com/wxt-dev/wxt/commit/bc24ea4))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Simplify entrypoint types ([#464](https://github.com/wxt-dev/wxt/pull/464))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Okou ([@ookkoouu](https://github.com/ookkoouu))
|
||||
|
||||
## v0.16.8
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.16.7...v0.16.8)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Watch files outside project root during development ([#454](https://github.com/wxt-dev/wxt/pull/454))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Add loading and error states for "Who's using WXT" section ([447a48f](https://github.com/wxt-dev/wxt/commit/447a48f))
|
||||
|
||||
## v0.16.7
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.16.6...v0.16.7)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Generate ESLint globals file for auto-imports ([#450](https://github.com/wxt-dev/wxt/pull/450))
|
||||
|
||||
### 🔥 Performance
|
||||
|
||||
- Upgrade Vite to 5.1 ([#452](https://github.com/wxt-dev/wxt/pull/452))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Add section about dev mode differences ([a0d1643](https://github.com/wxt-dev/wxt/commit/a0d1643))
|
||||
- Remove anchor from content script ui examples ([87a62a1](https://github.com/wxt-dev/wxt/commit/87a62a1))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- **e2e:** Use `wxt prepare` instead of `wxt build` when possible to speed up E2E tests ([#451](https://github.com/wxt-dev/wxt/pull/451))
|
||||
|
||||
## v0.16.6
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.16.5...v0.16.6)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Add option to customize the analysis artifacts output ([#431](https://github.com/wxt-dev/wxt/pull/431))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Use `insertBefore` on mounting content script UI ([ba85fdf](https://github.com/wxt-dev/wxt/commit/ba85fdf))
|
||||
|
||||
### 💅 Refactors
|
||||
|
||||
- Use `Element.prepend` on mounting UI ([295f860](https://github.com/wxt-dev/wxt/commit/295f860))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Fix `createShadowRootUi` unmount calls ([946072f](https://github.com/wxt-dev/wxt/commit/946072f))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Enable skipped test since it works now ([6b8dfdf](https://github.com/wxt-dev/wxt/commit/6b8dfdf))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Lionelhorn ([@Lionelhorn](https://github.com/Lionelhorn))
|
||||
- Okou ([@ookkoouu](https://github.com/ookkoouu))
|
||||
|
||||
## v0.16.5
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.16.4...v0.16.5)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Support node 20 when running `wxt submit` ([e835502](https://github.com/wxt-dev/wxt/commit/e835502))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Remove "coming soon" from automated publishing feature ([2b374b9](https://github.com/wxt-dev/wxt/commit/2b374b9))
|
||||
|
||||
## v0.16.4
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.16.3...v0.16.4)
|
||||
|
||||
@@ -55,14 +55,11 @@ Or see the [installation guide](https://wxt.dev/guide/installation.html) to get
|
||||
- 📂 File based entrypoints
|
||||
- 🚔 TypeScript
|
||||
- 🦾 Auto-imports
|
||||
- ⬇️ Download and bundle remote URL imports
|
||||
- 🤖 Automated publishing
|
||||
- 🎨 Frontend framework agnostic: works with Vue, React, Svelte, etc
|
||||
- 🖍️ Quickly bootstrap a new project
|
||||
- 📏 Bundle analysis
|
||||
|
||||
### Coming Soon
|
||||
|
||||
- 🤖 Automated publishing
|
||||
- ⬇️ Download and bundle remote URL imports
|
||||
|
||||
## Contributors
|
||||
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@
|
||||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"vitest": "^1.1.0"
|
||||
"vitest": "^1.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.34",
|
||||
|
||||
@@ -20,11 +20,12 @@ const chromeExtensionIds = [
|
||||
'agjnjboanicjcpenljmaaigopkgdnihi', // PreMiD
|
||||
];
|
||||
|
||||
const { data } = useListExtensionDetails(chromeExtensionIds);
|
||||
const { data, err, isLoading } = useListExtensionDetails(chromeExtensionIds);
|
||||
const sortedExtensions = computed(() => {
|
||||
if (!data.value?.length) return [];
|
||||
|
||||
return [...data.value]
|
||||
.filter((item) => item != null)
|
||||
.map((item) => ({
|
||||
...item,
|
||||
// Sort based on the user count weighted by the rating
|
||||
@@ -49,7 +50,16 @@ function getStoreUrl(extension: ChromeExtension) {
|
||||
Battle tested and ready for production. Explore chrome extensions made
|
||||
with WXT.
|
||||
</p>
|
||||
<ul>
|
||||
<p v-if="isLoading" style="text-align: center; opacity: 50%">
|
||||
Loading...
|
||||
</p>
|
||||
<p
|
||||
v-else-if="err || sortedExtensions.length === 0"
|
||||
style="text-align: center; opacity: 50%"
|
||||
>
|
||||
Failed to load extension details.
|
||||
</p>
|
||||
<ul v-else>
|
||||
<li
|
||||
v-for="extension of sortedExtensions"
|
||||
:key="extension.id"
|
||||
|
||||
@@ -26,6 +26,7 @@ const query = `query ${operationName}($ids:[String!]!) {
|
||||
export default function (ids: string[]) {
|
||||
const data = ref<ChromeExtension[]>();
|
||||
const err = ref<unknown>();
|
||||
const isLoading = ref(true);
|
||||
|
||||
fetch('https://queue.wxt.dev/api', {
|
||||
method: 'POST',
|
||||
@@ -36,6 +37,7 @@ export default function (ids: string[]) {
|
||||
}),
|
||||
})
|
||||
.then(async (res) => {
|
||||
isLoading.value = false;
|
||||
const {
|
||||
data: { chromeExtensions },
|
||||
} = await res.json();
|
||||
@@ -43,6 +45,7 @@ export default function (ids: string[]) {
|
||||
err.value = undefined;
|
||||
})
|
||||
.catch((error) => {
|
||||
isLoading.value = false;
|
||||
console.error(error);
|
||||
data.value = undefined;
|
||||
err.value = error;
|
||||
@@ -51,5 +54,6 @@ export default function (ids: string[]) {
|
||||
return {
|
||||
data,
|
||||
err,
|
||||
isLoading,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -37,6 +37,9 @@ export default defineContentScript({
|
||||
// Configure how CSS is injected onto the page
|
||||
cssInjectionMode: undefined | "manifest" | "manual" | "ui",
|
||||
|
||||
// Configure how/when content script will be registered
|
||||
registration: undefined | "manifest" | "runtime",
|
||||
|
||||
main(ctx) {
|
||||
// Executed when content script is loaded
|
||||
},
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
[Chrome Docs](https://developer.chrome.com/docs/extensions/reference/sidePanel/) • [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/user_interface/Sidebars)
|
||||
|
||||
In Chrome, side panels use the "side_panel" API, while Firefox uses the "sidebar_action" API.
|
||||
|
||||
:::warning
|
||||
Chrome added support for sidepanels in Manifest V3, they are not available in Manfiest V2.
|
||||
:::
|
||||
@@ -25,7 +27,17 @@ Chrome added support for sidepanels in Manifest V3, they are not available in Ma
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Title</title>
|
||||
<title>Default Side Panel Title</title>
|
||||
<meta
|
||||
name="manifest.default_icon"
|
||||
content="{
|
||||
16: '/icon-16.png',
|
||||
24: '/icon-24.png',
|
||||
...
|
||||
}"
|
||||
/>
|
||||
<meta name="manifest.open_at_install" content="true|false" />
|
||||
<meta name="manifest.browser_style" content="true|false" />
|
||||
<!-- Set include/exclude if the page should be removed from some builds -->
|
||||
<meta name="manifest.include" content="['chrome', ...]" />
|
||||
<meta name="manifest.exclude" content="['chrome', ...]" />
|
||||
|
||||
@@ -26,7 +26,6 @@ export default defineContentScript({
|
||||
main(ctx) {
|
||||
const ui = createIntegratedUi(ctx, {
|
||||
position: 'inline',
|
||||
anchor: '#anchor',
|
||||
onMount: (container) => {
|
||||
// Append children to the container
|
||||
const app = document.createElement('p');
|
||||
@@ -52,7 +51,6 @@ export default defineContentScript({
|
||||
main(ctx) {
|
||||
const ui = createIntegratedUi(ctx, {
|
||||
position: 'inline',
|
||||
anchor: '#anchor',
|
||||
onMount: (container) => {
|
||||
// Create the app and mount it to the UI container
|
||||
const app = createApp(App);
|
||||
@@ -82,7 +80,6 @@ export default defineContentScript({
|
||||
main(ctx) {
|
||||
const ui = createIntegratedUi(ctx, {
|
||||
position: 'inline',
|
||||
anchor: '#anchor',
|
||||
onMount: (container) => {
|
||||
// Create a root on the UI container and render a component
|
||||
const root = ReactDOM.createRoot(container);
|
||||
@@ -111,7 +108,6 @@ export default defineContentScript({
|
||||
main(ctx) {
|
||||
const ui = createIntegratedUi(ctx, {
|
||||
position: 'inline',
|
||||
anchor: '#anchor',
|
||||
onMount: (container) => {
|
||||
// Create the Svelte app inside the UI container
|
||||
const app = new App({
|
||||
@@ -141,7 +137,6 @@ export default defineContentScript({
|
||||
main(ctx) {
|
||||
const ui = createIntegratedUi(ctx, {
|
||||
position: 'inline',
|
||||
anchor: '#anchor',
|
||||
onMount: (container) => {
|
||||
// Render your app to the UI container
|
||||
const unmount = render(() => <div>...</div>, container);
|
||||
@@ -162,7 +157,7 @@ export default defineContentScript({
|
||||
|
||||
See the [API Reference](/api/wxt/client/functions/createIntegratedUi) for the complete list of options.
|
||||
|
||||
You can control how CSS is injected for an integrated content script UI with the [`cssInjectionMode`](/api/wxt/interfaces/ContentScriptBaseDefinition#cssinjectionmode) property. Usually, you'll want to leave it as `"manifest"`, the default, so the UI inherits its style from the website's CSS.
|
||||
You can control how CSS is injected for an integrated content script UI with the [`cssInjectionMode`](/api/wxt/interfaces/BaseContentScriptEntrypointOptions#cssinjectionmode) property. Usually, you'll want to leave it as `"manifest"`, the default, so the UI inherits its style from the website's CSS.
|
||||
|
||||
## Shadow Root
|
||||
|
||||
@@ -173,7 +168,7 @@ WXT's [`createShadowRootUi`](/api/wxt/client/functions/createShadowRootUi) abstr
|
||||
To use `createShadowRootUi`, follow these steps:
|
||||
|
||||
1. Import your CSS file at the top of your content script
|
||||
2. Set [`cssInjectionMode: "ui"`](/api/wxt/interfaces/ContentScriptBaseDefinition#cssinjectionmode) inside `defineContentScript`
|
||||
2. Set [`cssInjectionMode: "ui"`](/api/wxt/interfaces/BaseContentScriptEntrypointOptions#cssinjectionmode) inside `defineContentScript`
|
||||
3. Define your UI with `createShadowRootUi()`
|
||||
4. Mount the UI so it is visible to users
|
||||
|
||||
@@ -192,7 +187,6 @@ export default defineContentScript({
|
||||
// 3. Define your UI
|
||||
const ui = await createShadowRootUi(ctx, {
|
||||
name: 'example-ui',
|
||||
anchor: '#anchor',
|
||||
position: 'inline',
|
||||
onMount(container) {
|
||||
// Define how your UI will be mounted inside the container
|
||||
@@ -223,7 +217,6 @@ export default defineContentScript({
|
||||
// 3. Define your UI
|
||||
const ui = await createShadowRootUi(ctx, {
|
||||
name: 'example-ui',
|
||||
anchor: '#anchor',
|
||||
position: 'inline',
|
||||
onMount: (container) => {
|
||||
// Define how your UI will be mounted inside the container
|
||||
@@ -233,7 +226,7 @@ export default defineContentScript({
|
||||
},
|
||||
onRemove: (app) => {
|
||||
// Unmount the app when the UI is removed
|
||||
app.unmount();
|
||||
app?.unmount();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -259,16 +252,19 @@ export default defineContentScript({
|
||||
const ui = await createShadowRootUi(ctx, {
|
||||
name: 'example-ui',
|
||||
position: 'inline',
|
||||
anchor: '#anchor',
|
||||
onMount: (container) => {
|
||||
// Container is a body, and React warns when creating a root on the body, so create a wrapper div
|
||||
const app = document.createElement('div');
|
||||
container.append(app);
|
||||
|
||||
// Create a root on the UI container and render a component
|
||||
const root = ReactDOM.createRoot(container);
|
||||
const root = ReactDOM.createRoot(app);
|
||||
root.render(<App />);
|
||||
return root;
|
||||
},
|
||||
onRemove: (root) => {
|
||||
// Unmount the root when the UI is removed
|
||||
root.unmount();
|
||||
root?.unmount();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -293,7 +289,6 @@ export default defineContentScript({
|
||||
const ui = await createShadowRootUi(ctx, {
|
||||
name: 'example-ui',
|
||||
position: 'inline',
|
||||
anchor: '#anchor',
|
||||
onMount: (container) => {
|
||||
// Create the Svelte app inside the UI container
|
||||
const app = new App({
|
||||
@@ -303,7 +298,7 @@ export default defineContentScript({
|
||||
},
|
||||
onRemove: (app) => {
|
||||
// Destroy the app when the UI is removed
|
||||
app.$destroy();
|
||||
app?.$destroy();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -328,14 +323,13 @@ export default defineContentScript({
|
||||
const ui = await createShadowRootUi(ctx, {
|
||||
name: 'example-ui',
|
||||
position: 'inline',
|
||||
anchor: '#anchor',
|
||||
onMount: (container) => {
|
||||
// Render your app to the UI container
|
||||
const unmount = render(() => <div>...</div>, container);
|
||||
},
|
||||
onRemove: (unmount) => {
|
||||
// Unmount the app when the UI is removed
|
||||
unmount();
|
||||
unmount?.();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -398,7 +392,6 @@ WXT provides a helper function, [`createIframeUi`](/api/wxt/client/functions/cre
|
||||
// Define the UI
|
||||
const ui = await createIframeUi(ctx, {
|
||||
page: '/example-iframe.html',
|
||||
anchor: '#anchor',
|
||||
position: 'inline',
|
||||
onMount: (wrapper, iframe) => {
|
||||
// Add styles to the iframe like width
|
||||
|
||||
@@ -13,6 +13,24 @@ WXT's main goal is providing the best DX it possibly can. When running your exte
|
||||
| `wxt.config.ts` | | | | 🟡 See [#10](https://github.com/wxt-dev/wxt/issues/10) |
|
||||
| `web-ext.config.ts` | | | | 🟡 See [#10](https://github.com/wxt-dev/wxt/issues/10) |
|
||||
|
||||
## Dev Mode vs Production Builds
|
||||
|
||||
There are some notible differences between the development and production versions of an extension. During development:
|
||||
|
||||
1. **Content scripts are not listed in the `manifest.json`** when targetting MV3. Instead, the [`scripting`](https://developer.chrome.com/docs/extensions/reference/api/scripting) permission is used to register content scripts at runtime so they can be reloaded individually.
|
||||
|
||||
To get the list of content scripts during development, run the following in the background's console:
|
||||
|
||||
```ts
|
||||
await chrome.scripting.getRegisteredContentScripts();
|
||||
```
|
||||
|
||||
2. **The CSP is modified to allow loading scripts from the dev server**. Make sure you're using Chrome v110 or above for HMR to work.
|
||||
|
||||
3. If you don't include a background script/service worker, one will be created to perform various tasks in dev mode, mostly related to reloading different parts of the extension on change.
|
||||
|
||||
For production builds, none of the above modifications will be applied, and you're extension/manifest will only include what you have defined.
|
||||
|
||||
## Configure Browser Startup
|
||||
|
||||
WXT uses [`web-ext` by Mozilla](https://github.com/mozilla/web-ext) to automatically open a browser with the extension installed. You can configure the runner's behavior via the [`runner`](/api/wxt/interfaces/InlineConfig#runner) option, or in a separate gitignored file, `web-ext.config.ts`.
|
||||
@@ -83,6 +101,6 @@ Normally, to manually reload an extension, you have to visit `chrome://extension
|
||||
|
||||
When running `wxt` command to start the dev server, WXT adds a keyboard shortcut, `ctrl+E` for Windows/Linux and `cmd+E` for Mac, that reloads the extension when pressed, without visiting `chrome://extensions`.
|
||||
|
||||
:::note
|
||||
:::info
|
||||
This shortcut is only available during development, and is not be added to your extension when running `wxt build` or `wxt-zip`.
|
||||
:::
|
||||
|
||||
+9
-9
@@ -6,7 +6,7 @@ title: Next-gen Web Extension Framework
|
||||
hero:
|
||||
name: WXT
|
||||
text: Next-gen Web Extension Framework
|
||||
tagline: An open source tool that makes Chrome Extension devlopment faster than ever before.
|
||||
tagline: An open source tool that makes Chrome Extension development faster than ever before.
|
||||
image:
|
||||
src: /hero-logo.svg
|
||||
alt: WXT
|
||||
@@ -47,11 +47,9 @@ features:
|
||||
details: Nuxt-like auto-imports to speed up development.
|
||||
link: /guide/auto-imports
|
||||
linkText: Read docs
|
||||
- icon: ⬇️
|
||||
title: Bundle Remote Code
|
||||
details: Downloads and bundles remote code imported from URLs.
|
||||
link: /guide/remote-code
|
||||
linkText: Read docs
|
||||
- icon: 🤖
|
||||
title: Automated Publishing
|
||||
details: Automatically zip, upload, submit, and publish extensions.
|
||||
- icon: 🎨
|
||||
title: Frontend Framework Agnostic
|
||||
details: Works with any front-end framework with a Vite plugin.
|
||||
@@ -65,9 +63,11 @@ features:
|
||||
- icon: 📏
|
||||
title: Bundle Analysis
|
||||
details: Tools for analyizing the final extension bundle and minimizing your extension's size.
|
||||
- icon: 🤖
|
||||
title: Automated Publishing
|
||||
details: 'Coming soon. Automatically zip, upload, and release extensions.'
|
||||
- icon: ⬇️
|
||||
title: Bundle Remote Code
|
||||
details: Downloads and bundles remote code imported from URLs.
|
||||
link: /guide/remote-code
|
||||
linkText: Read docs
|
||||
---
|
||||
|
||||
<section class="vp-doc">
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { TestProject } from '../utils';
|
||||
import { resetBundleIncrement } from '~/core/builders/vite/plugins';
|
||||
|
||||
describe('Analysis', () => {
|
||||
beforeEach(() => {
|
||||
resetBundleIncrement();
|
||||
});
|
||||
|
||||
it('should outptut a stats.html with no part files by default', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/popup.html');
|
||||
project.addFile('entrypoints/options.html');
|
||||
project.addFile(
|
||||
'entrypoints/background.ts',
|
||||
'export default defineBackground(() => {});',
|
||||
);
|
||||
|
||||
await project.build({
|
||||
analysis: {
|
||||
enabled: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(await project.fileExists('stats.html')).toBe(true);
|
||||
expect(await project.fileExists('.output/chrome-mv3/stats-0.json')).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('should save part files when requested', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/popup.html');
|
||||
project.addFile('entrypoints/options.html');
|
||||
project.addFile(
|
||||
'entrypoints/background.ts',
|
||||
'export default defineBackground(() => {});',
|
||||
);
|
||||
|
||||
await project.build({
|
||||
analysis: {
|
||||
enabled: true,
|
||||
keepArtifacts: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(await project.fileExists('stats.html')).toBe(true);
|
||||
expect(await project.fileExists('stats-0.json')).toBe(true);
|
||||
expect(await project.fileExists('stats-1.json')).toBe(true);
|
||||
});
|
||||
|
||||
it('should support customizing the stats output directory', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/popup.html');
|
||||
project.addFile('entrypoints/options.html');
|
||||
project.addFile(
|
||||
'entrypoints/background.ts',
|
||||
'export default defineBackground(() => {});',
|
||||
);
|
||||
|
||||
await project.build({
|
||||
analysis: {
|
||||
enabled: true,
|
||||
outputFile: 'stats/bundle.html',
|
||||
},
|
||||
});
|
||||
|
||||
expect(await project.fileExists('stats/bundle.html')).toBe(true);
|
||||
});
|
||||
|
||||
it('should place artifacts next to the custom output file', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/popup.html');
|
||||
project.addFile('entrypoints/options.html');
|
||||
project.addFile(
|
||||
'entrypoints/background.ts',
|
||||
'export default defineBackground(() => {});',
|
||||
);
|
||||
|
||||
await project.build({
|
||||
analysis: {
|
||||
enabled: true,
|
||||
outputFile: 'stats/bundle.html',
|
||||
keepArtifacts: true,
|
||||
},
|
||||
});
|
||||
console.log(project.root);
|
||||
|
||||
expect(await project.fileExists('stats/bundle.html')).toBe(true);
|
||||
expect(await project.fileExists('stats/bundle-0.json')).toBe(true);
|
||||
expect(await project.fileExists('stats/bundle-1.json')).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -7,7 +7,7 @@ describe('Auto Imports', () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/popup.html', `<html></html>`);
|
||||
|
||||
await project.build();
|
||||
await project.prepare();
|
||||
|
||||
expect(await project.serializeFile('.wxt/types/imports.d.ts'))
|
||||
.toMatchInlineSnapshot(`
|
||||
@@ -38,7 +38,7 @@ describe('Auto Imports', () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/popup.html', `<html></html>`);
|
||||
|
||||
await project.build();
|
||||
await project.prepare();
|
||||
|
||||
expect(await project.serializeFile('.wxt/wxt.d.ts'))
|
||||
.toMatchInlineSnapshot(`
|
||||
@@ -63,7 +63,7 @@ describe('Auto Imports', () => {
|
||||
});
|
||||
project.addFile('entrypoints/popup.html', `<html></html>`);
|
||||
|
||||
await project.build();
|
||||
await project.prepare();
|
||||
|
||||
expect(await project.fileExists('.wxt/types/imports.d.ts')).toBe(false);
|
||||
});
|
||||
@@ -75,7 +75,7 @@ describe('Auto Imports', () => {
|
||||
});
|
||||
project.addFile('entrypoints/popup.html', `<html></html>`);
|
||||
|
||||
await project.build();
|
||||
await project.prepare();
|
||||
|
||||
expect(
|
||||
await project.serializeFile('.wxt/wxt.d.ts'),
|
||||
@@ -93,4 +93,82 @@ describe('Auto Imports', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('eslintrc', () => {
|
||||
it('should output the globals list for ESLint to consume', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/popup.html', `<html></html>`);
|
||||
|
||||
await project.prepare({
|
||||
imports: {
|
||||
eslintrc: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(await project.serializeFile('.wxt/eslintrc-auto-import.json'))
|
||||
.toMatchInlineSnapshot(`
|
||||
".wxt/eslintrc-auto-import.json
|
||||
----------------------------------------
|
||||
{
|
||||
"globals": {
|
||||
"ContentScriptContext": true,
|
||||
"InvalidMatchPattern": true,
|
||||
"MatchPattern": true,
|
||||
"browser": true,
|
||||
"createIframeUi": true,
|
||||
"createIntegratedUi": true,
|
||||
"createShadowRootUi": true,
|
||||
"defineBackground": true,
|
||||
"defineConfig": true,
|
||||
"defineContentScript": true,
|
||||
"defineUnlistedScript": true,
|
||||
"fakeBrowser": true,
|
||||
"storage": true
|
||||
}
|
||||
}
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should allow customizing the output', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/popup.html', `<html></html>`);
|
||||
|
||||
await project.prepare({
|
||||
imports: {
|
||||
eslintrc: {
|
||||
enabled: true,
|
||||
filePath: project.resolvePath('example.json'),
|
||||
globalsPropValue: 'readonly',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(await project.serializeFile('example.json'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"example.json
|
||||
----------------------------------------
|
||||
{
|
||||
"globals": {
|
||||
"ContentScriptContext": "readonly",
|
||||
"InvalidMatchPattern": "readonly",
|
||||
"MatchPattern": "readonly",
|
||||
"browser": "readonly",
|
||||
"createIframeUi": "readonly",
|
||||
"createIntegratedUi": "readonly",
|
||||
"createShadowRootUi": "readonly",
|
||||
"defineBackground": "readonly",
|
||||
"defineConfig": "readonly",
|
||||
"defineContentScript": "readonly",
|
||||
"defineUnlistedScript": "readonly",
|
||||
"fakeBrowser": "readonly",
|
||||
"storage": "readonly"
|
||||
}
|
||||
}
|
||||
"
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ describe('TypeScript Project', () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/unlisted.html');
|
||||
|
||||
await project.build();
|
||||
await project.prepare();
|
||||
|
||||
const output = await project.serializeFile('.wxt/types/globals.d.ts');
|
||||
expect(output).toMatchInlineSnapshot(`
|
||||
@@ -38,7 +38,7 @@ describe('TypeScript Project', () => {
|
||||
project.addFile('entrypoints/options.html');
|
||||
project.addFile('entrypoints/sandbox.html');
|
||||
|
||||
await project.build();
|
||||
await project.prepare();
|
||||
|
||||
const output = await project.serializeFile('.wxt/types/paths.d.ts');
|
||||
expect(output).toMatchInlineSnapshot(`
|
||||
@@ -103,7 +103,7 @@ describe('TypeScript Project', () => {
|
||||
},
|
||||
});
|
||||
|
||||
await project.build();
|
||||
await project.prepare();
|
||||
|
||||
const output = await project.serializeFile('.wxt/types/i18n.d.ts');
|
||||
expect(output).toMatchInlineSnapshot(`
|
||||
@@ -225,7 +225,7 @@ describe('TypeScript Project', () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/unlisted.html');
|
||||
|
||||
await project.build();
|
||||
await project.prepare();
|
||||
|
||||
const output = await project.serializeFile('.wxt/wxt.d.ts');
|
||||
expect(output).toMatchInlineSnapshot(`
|
||||
@@ -245,7 +245,7 @@ describe('TypeScript Project', () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/unlisted.html');
|
||||
|
||||
await project.build();
|
||||
await project.prepare();
|
||||
|
||||
const output = await project.serializeFile('.wxt/tsconfig.json');
|
||||
expect(output).toMatchInlineSnapshot(`
|
||||
@@ -289,7 +289,7 @@ describe('TypeScript Project', () => {
|
||||
srcDir: 'src',
|
||||
});
|
||||
|
||||
await project.build();
|
||||
await project.prepare();
|
||||
|
||||
const output = await project.serializeFile('.wxt/tsconfig.json');
|
||||
expect(output).toMatchInlineSnapshot(`
|
||||
@@ -337,7 +337,7 @@ describe('TypeScript Project', () => {
|
||||
},
|
||||
});
|
||||
|
||||
await project.build();
|
||||
await project.prepare();
|
||||
|
||||
const output = await project.serializeFile('.wxt/tsconfig.json');
|
||||
expect(output).toMatchInlineSnapshot(`
|
||||
|
||||
@@ -107,4 +107,29 @@ describe('User Config', () => {
|
||||
});
|
||||
expect(withoutPolyfill).not.toBe(withPolyfill);
|
||||
});
|
||||
|
||||
it('should respect changing config files', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile(
|
||||
'src/entrypoints/background.ts',
|
||||
`export default defineBackground(
|
||||
() => console.log('Hello background'),
|
||||
);`,
|
||||
);
|
||||
project.addFile(
|
||||
'test.config.ts',
|
||||
`import { defineConfig } from 'wxt';
|
||||
|
||||
export default defineConfig({
|
||||
outDir: ".custom-output",
|
||||
srcDir: "src",
|
||||
});`,
|
||||
);
|
||||
|
||||
await project.build({ configFile: 'test.config.ts' });
|
||||
|
||||
expect(
|
||||
await project.fileExists('.custom-output/chrome-mv3/background.js'),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
+13
-6
@@ -92,12 +92,19 @@ export class TestProject {
|
||||
return server;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call `path.resolve` relative to the project's root directory.
|
||||
*/
|
||||
resolvePath(...path: string[]): string {
|
||||
return resolve(this.root, ...path);
|
||||
}
|
||||
|
||||
private async writeProjectToDisk() {
|
||||
if (this.config == null) this.setConfigFileConfig();
|
||||
|
||||
for (const file of this.files) {
|
||||
const [name, content] = file;
|
||||
const filePath = resolve(this.root, name);
|
||||
const filePath = this.resolvePath(name);
|
||||
const fileDir = dirname(filePath);
|
||||
await fs.ensureDir(fileDir);
|
||||
await fs.writeFile(filePath, content ?? '', 'utf-8');
|
||||
@@ -140,13 +147,13 @@ export class TestProject {
|
||||
ignoreContentsOfFilenames?: string[],
|
||||
): Promise<string> {
|
||||
const outputFiles = await glob('**/*', {
|
||||
cwd: resolve(this.root, dir),
|
||||
cwd: this.resolvePath(dir),
|
||||
ignore: ['**/node_modules', '**/.output'],
|
||||
});
|
||||
outputFiles.sort();
|
||||
const fileContents = [];
|
||||
for (const file of outputFiles) {
|
||||
const path = resolve(this.root, dir, file);
|
||||
const path = this.resolvePath(dir, file);
|
||||
const isContentIgnored = !!ignoreContentsOfFilenames?.find(
|
||||
(ignoredFile) => normalizePath(path).endsWith(ignoredFile),
|
||||
);
|
||||
@@ -161,7 +168,7 @@ export class TestProject {
|
||||
* the file contents.
|
||||
*/
|
||||
async serializeFile(path: string, ignoreContents?: boolean): Promise<string> {
|
||||
const absolutePath = resolve(this.root, path);
|
||||
const absolutePath = this.resolvePath(path);
|
||||
return [
|
||||
normalizePath(relative(this.root, absolutePath)),
|
||||
ignoreContents ? '<contents-ignored>' : await fs.readFile(absolutePath),
|
||||
@@ -169,12 +176,12 @@ export class TestProject {
|
||||
}
|
||||
|
||||
fileExists(path: string): Promise<boolean> {
|
||||
return fs.exists(resolve(this.root, path));
|
||||
return fs.exists(this.resolvePath(path));
|
||||
}
|
||||
|
||||
async getOutputManifest(
|
||||
path: string = '.output/chrome-mv3/manifest.json',
|
||||
): Promise<any> {
|
||||
return await fs.readJson(resolve(this.root, path));
|
||||
return await fs.readJson(this.resolvePath(path));
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "wxt",
|
||||
"type": "module",
|
||||
"version": "0.16.4",
|
||||
"version": "0.16.10",
|
||||
"description": "Next gen framework for developing web extensions",
|
||||
"engines": {
|
||||
"node": ">=18",
|
||||
@@ -132,10 +132,10 @@
|
||||
"ora": "^7.0.1",
|
||||
"picocolors": "^1.0.0",
|
||||
"prompts": "^2.4.2",
|
||||
"publish-browser-extension": "^2.1.1",
|
||||
"publish-browser-extension": "^2.1.2",
|
||||
"rollup-plugin-visualizer": "^5.9.2",
|
||||
"unimport": "^3.4.0",
|
||||
"vite": "^5.0.12",
|
||||
"vite": "^5.1.3",
|
||||
"web-ext-run": "^0.2.0",
|
||||
"webextension-polyfill": "^0.10.0",
|
||||
"zip-dir": "^2.0.0"
|
||||
@@ -165,7 +165,7 @@
|
||||
"typedoc-vitepress-theme": "1.0.0-next.3",
|
||||
"typescript": "^5.3.2",
|
||||
"vitepress": "1.0.0-rc.34",
|
||||
"vitest": "^1.1.0",
|
||||
"vitest": "^1.2.2",
|
||||
"vitest-mock-extended": "^1.3.1",
|
||||
"vue": "^3.3.10"
|
||||
},
|
||||
|
||||
Generated
+71
-57
@@ -96,8 +96,8 @@ importers:
|
||||
specifier: ^2.4.2
|
||||
version: 2.4.2
|
||||
publish-browser-extension:
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1
|
||||
specifier: ^2.1.2
|
||||
version: 2.1.2
|
||||
rollup-plugin-visualizer:
|
||||
specifier: ^5.9.2
|
||||
version: 5.12.0
|
||||
@@ -105,8 +105,8 @@ importers:
|
||||
specifier: ^3.4.0
|
||||
version: 3.4.0
|
||||
vite:
|
||||
specifier: ^5.0.12
|
||||
version: 5.0.12(@types/node@20.10.3)(sass@1.69.5)
|
||||
specifier: ^5.1.3
|
||||
version: 5.1.3(@types/node@20.10.3)(sass@1.69.5)
|
||||
web-ext-run:
|
||||
specifier: ^0.2.0
|
||||
version: 0.2.0
|
||||
@@ -140,7 +140,7 @@ importers:
|
||||
version: 2.4.9
|
||||
'@vitest/coverage-v8':
|
||||
specifier: ^1.0.1
|
||||
version: 1.2.2(vitest@1.1.0)
|
||||
version: 1.2.2(vitest@1.2.2)
|
||||
execa:
|
||||
specifier: ^8.0.1
|
||||
version: 8.0.1
|
||||
@@ -190,11 +190,11 @@ importers:
|
||||
specifier: 1.0.0-rc.34
|
||||
version: 1.0.0-rc.34(@types/node@20.10.3)(typescript@5.3.3)
|
||||
vitest:
|
||||
specifier: ^1.1.0
|
||||
version: 1.1.0(@types/node@20.10.3)(happy-dom@13.3.8)(sass@1.69.5)
|
||||
specifier: ^1.2.2
|
||||
version: 1.2.2(@types/node@20.10.3)(happy-dom@13.3.8)(sass@1.69.5)
|
||||
vitest-mock-extended:
|
||||
specifier: ^1.3.1
|
||||
version: 1.3.1(typescript@5.3.3)(vitest@1.1.0)
|
||||
version: 1.3.1(typescript@5.3.3)(vitest@1.2.2)
|
||||
vue:
|
||||
specifier: ^3.3.10
|
||||
version: 3.3.10(typescript@5.3.3)
|
||||
@@ -208,8 +208,8 @@ importers:
|
||||
specifier: ^18.2.0
|
||||
version: 18.2.0(react@18.2.0)
|
||||
vitest:
|
||||
specifier: ^1.1.0
|
||||
version: 1.1.0(@types/node@20.10.3)(happy-dom@13.3.8)(sass@1.69.5)
|
||||
specifier: ^1.2.2
|
||||
version: 1.2.2(@types/node@20.10.3)(happy-dom@13.3.8)(sass@1.69.5)
|
||||
devDependencies:
|
||||
'@types/react':
|
||||
specifier: ^18.2.34
|
||||
@@ -1058,7 +1058,6 @@ packages:
|
||||
|
||||
/@types/estree@1.0.4:
|
||||
resolution: {integrity: sha512-2JwWnHK9H+wUZNorf2Zr6ves96WHoWDJIftkcxPKsS7Djta6Zu519LarhRNljPXkpsZR2ZMwNCPeW7omW07BJw==}
|
||||
dev: false
|
||||
|
||||
/@types/fs-extra@11.0.4:
|
||||
resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==}
|
||||
@@ -1168,18 +1167,18 @@ packages:
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@vitejs/plugin-vue@5.0.2(vite@5.0.12)(vue@3.4.3):
|
||||
/@vitejs/plugin-vue@5.0.2(vite@5.1.3)(vue@3.4.3):
|
||||
resolution: {integrity: sha512-kEjJHrLb5ePBvjD0SPZwJlw1QTRcjjCA9sB5VyfonoXVBxTS7TMnqL6EkLt1Eu61RDeiuZ/WN9Hf6PxXhPI2uA==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
peerDependencies:
|
||||
vite: ^5.0.0
|
||||
vue: ^3.2.25
|
||||
dependencies:
|
||||
vite: 5.0.12(@types/node@20.10.3)(sass@1.69.5)
|
||||
vite: 5.1.3(@types/node@20.10.3)(sass@1.69.5)
|
||||
vue: 3.4.3(typescript@5.3.3)
|
||||
dev: true
|
||||
|
||||
/@vitest/coverage-v8@1.2.2(vitest@1.1.0):
|
||||
/@vitest/coverage-v8@1.2.2(vitest@1.2.2):
|
||||
resolution: {integrity: sha512-IHyKnDz18SFclIEEAHb9Y4Uxx0sPKC2VO1kdDCs1BF6Ip4S8rQprs971zIsooLUn7Afs71GRxWMWpkCGZpRMhw==}
|
||||
peerDependencies:
|
||||
vitest: ^1.0.0
|
||||
@@ -1197,41 +1196,42 @@ packages:
|
||||
std-env: 3.6.0
|
||||
test-exclude: 6.0.0
|
||||
v8-to-istanbul: 9.2.0
|
||||
vitest: 1.1.0(@types/node@20.10.3)(happy-dom@13.3.8)(sass@1.69.5)
|
||||
vitest: 1.2.2(@types/node@20.10.3)(happy-dom@13.3.8)(sass@1.69.5)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@vitest/expect@1.1.0:
|
||||
resolution: {integrity: sha512-9IE2WWkcJo2BR9eqtY5MIo3TPmS50Pnwpm66A6neb2hvk/QSLfPXBz2qdiwUOQkwyFuuXEUj5380CbwfzW4+/w==}
|
||||
/@vitest/expect@1.2.2:
|
||||
resolution: {integrity: sha512-3jpcdPAD7LwHUUiT2pZTj2U82I2Tcgg2oVPvKxhn6mDI2On6tfvPQTjAI4628GUGDZrCm4Zna9iQHm5cEexOAg==}
|
||||
dependencies:
|
||||
'@vitest/spy': 1.1.0
|
||||
'@vitest/utils': 1.1.0
|
||||
'@vitest/spy': 1.2.2
|
||||
'@vitest/utils': 1.2.2
|
||||
chai: 4.3.10
|
||||
|
||||
/@vitest/runner@1.1.0:
|
||||
resolution: {integrity: sha512-zdNLJ00pm5z/uhbWF6aeIJCGMSyTyWImy3Fcp9piRGvueERFlQFbUwCpzVce79OLm2UHk9iwaMSOaU9jVHgNVw==}
|
||||
/@vitest/runner@1.2.2:
|
||||
resolution: {integrity: sha512-JctG7QZ4LSDXr5CsUweFgcpEvrcxOV1Gft7uHrvkQ+fsAVylmWQvnaAr/HDp3LAH1fztGMQZugIheTWjaGzYIg==}
|
||||
dependencies:
|
||||
'@vitest/utils': 1.1.0
|
||||
'@vitest/utils': 1.2.2
|
||||
p-limit: 5.0.0
|
||||
pathe: 1.1.1
|
||||
|
||||
/@vitest/snapshot@1.1.0:
|
||||
resolution: {integrity: sha512-5O/wyZg09V5qmNmAlUgCBqflvn2ylgsWJRRuPrnHEfDNT6tQpQ8O1isNGgo+VxofISHqz961SG3iVvt3SPK/QQ==}
|
||||
/@vitest/snapshot@1.2.2:
|
||||
resolution: {integrity: sha512-SmGY4saEw1+bwE1th6S/cZmPxz/Q4JWsl7LvbQIky2tKE35US4gd0Mjzqfr84/4OD0tikGWaWdMja/nWL5NIPA==}
|
||||
dependencies:
|
||||
magic-string: 0.30.5
|
||||
pathe: 1.1.1
|
||||
pretty-format: 29.7.0
|
||||
|
||||
/@vitest/spy@1.1.0:
|
||||
resolution: {integrity: sha512-sNOVSU/GE+7+P76qYo+VXdXhXffzWZcYIPQfmkiRxaNCSPiLANvQx5Mx6ZURJ/ndtEkUJEpvKLXqAYTKEY+lTg==}
|
||||
/@vitest/spy@1.2.2:
|
||||
resolution: {integrity: sha512-k9Gcahssw8d7X3pSLq3e3XEu/0L78mUkCjivUqCQeXJm9clfXR/Td8+AP+VC1O6fKPIDLcHDTAmBOINVuv6+7g==}
|
||||
dependencies:
|
||||
tinyspy: 2.2.0
|
||||
|
||||
/@vitest/utils@1.1.0:
|
||||
resolution: {integrity: sha512-z+s510fKmYz4Y41XhNs3vcuFTFhcij2YF7F8VQfMEYAAUfqQh0Zfg7+w9xdgFGhPf3tX3TicAe+8BDITk6ampQ==}
|
||||
/@vitest/utils@1.2.2:
|
||||
resolution: {integrity: sha512-WKITBHLsBHlpjnDQahr+XK6RE7MiAsgrIkr0pGhQ9ygoxBfUeG0lUG5iLlzqjmKSlBv3+j5EGsriBzh+C3Tq9g==}
|
||||
dependencies:
|
||||
diff-sequences: 29.6.3
|
||||
estree-walker: 3.0.3
|
||||
loupe: 2.3.7
|
||||
pretty-format: 29.7.0
|
||||
|
||||
@@ -1293,7 +1293,7 @@ packages:
|
||||
'@vue/shared': 3.4.3
|
||||
estree-walker: 2.0.2
|
||||
magic-string: 0.30.5
|
||||
postcss: 8.4.32
|
||||
postcss: 8.4.35
|
||||
source-map-js: 1.0.2
|
||||
dev: true
|
||||
|
||||
@@ -1484,8 +1484,8 @@ packages:
|
||||
resolution: {integrity: sha512-NY39ACqCxdKBmHgw361M9pfJma8e4AZo20w9AY+5ZjIj1W2dvXC8J31G5fjfOGbulW9w4WKpT8fPooi0mLkn9A==}
|
||||
dev: false
|
||||
|
||||
/acorn-walk@8.3.0:
|
||||
resolution: {integrity: sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA==}
|
||||
/acorn-walk@8.3.2:
|
||||
resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
|
||||
/acorn@8.11.2:
|
||||
@@ -2406,6 +2406,11 @@ packages:
|
||||
/estree-walker@2.0.2:
|
||||
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
|
||||
|
||||
/estree-walker@3.0.3:
|
||||
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
|
||||
dependencies:
|
||||
'@types/estree': 1.0.4
|
||||
|
||||
/eventemitter3@5.0.1:
|
||||
resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
|
||||
|
||||
@@ -4109,6 +4114,15 @@ packages:
|
||||
nanoid: 3.3.7
|
||||
picocolors: 1.0.0
|
||||
source-map-js: 1.0.2
|
||||
dev: true
|
||||
|
||||
/postcss@8.4.35:
|
||||
resolution: {integrity: sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
dependencies:
|
||||
nanoid: 3.3.7
|
||||
picocolors: 1.0.0
|
||||
source-map-js: 1.0.2
|
||||
|
||||
/preact@10.18.1:
|
||||
resolution: {integrity: sha512-mKUD7RRkQQM6s7Rkmi7IFkoEHjuFqRQUaXamO61E6Nn7vqF/bo7EZCmSyrUnp2UWHw0O7XjZ2eeXis+m7tf4lg==}
|
||||
@@ -4161,9 +4175,9 @@ packages:
|
||||
sade: 1.8.1
|
||||
dev: true
|
||||
|
||||
/publish-browser-extension@2.1.1:
|
||||
resolution: {integrity: sha512-WqCnhHXbmiYn4vIbgcyKWVHiOvCoPjjpt3X+hfnU8rlOozU43TURpHUWZlQ0W49LhvJvBRsBfGRQxtRMGhqsxw==}
|
||||
engines: {node: '18', pnpm: '8'}
|
||||
/publish-browser-extension@2.1.2:
|
||||
resolution: {integrity: sha512-g6+mtdR4Z+GYHPIrfaAwC7Kbt1oQlpJ8r0x1PAytScy33OFdK+HVUeDDYorBpPAiQlmYJRQga7rY9QVTyTw34g==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
@@ -4885,8 +4899,8 @@ packages:
|
||||
/tinybench@2.5.1:
|
||||
resolution: {integrity: sha512-65NKvSuAVDP/n4CqH+a9w2kTlLReS9vhsAP06MWx+/89nMinJyB2icyl58RIcqCmIggpojIGeuJGhjU1aGMBSg==}
|
||||
|
||||
/tinypool@0.8.1:
|
||||
resolution: {integrity: sha512-zBTCK0cCgRROxvs9c0CGK838sPkeokNGdQVUUwHAbynHFlmyJYj825f/oRs528HaIJ97lo0pLIlDUzwN+IorWg==}
|
||||
/tinypool@0.8.2:
|
||||
resolution: {integrity: sha512-SUszKYe5wgsxnNOVlBYO6IC+8VGWdVGZWAqUxp3UErNBtptZvWbwyUOyzNL59zigz2rCA92QiL3wvG+JDSdJdQ==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
/tinyspy@2.2.0:
|
||||
@@ -5178,8 +5192,8 @@ packages:
|
||||
spdx-expression-parse: 3.0.1
|
||||
dev: true
|
||||
|
||||
/vite-node@1.1.0(@types/node@20.10.3)(sass@1.69.5):
|
||||
resolution: {integrity: sha512-jV48DDUxGLEBdHCQvxL1mEh7+naVy+nhUUUaPAZLd3FJgXuxQiewHcfeZebbJ6onDqNGkP4r3MhQ342PRlG81Q==}
|
||||
/vite-node@1.2.2(@types/node@20.10.3)(sass@1.69.5):
|
||||
resolution: {integrity: sha512-1as4rDTgVWJO3n1uHmUYqq7nsFgINQ9u+mRcXpjeOMJUmviqNKjcZB7UfRZrlM7MjYXMKpuWp5oGkjaFLnjawg==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
@@ -5187,7 +5201,7 @@ packages:
|
||||
debug: 4.3.4
|
||||
pathe: 1.1.1
|
||||
picocolors: 1.0.0
|
||||
vite: 5.0.12(@types/node@20.10.3)(sass@1.69.5)
|
||||
vite: 5.1.3(@types/node@20.10.3)(sass@1.69.5)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- less
|
||||
@@ -5198,8 +5212,8 @@ packages:
|
||||
- supports-color
|
||||
- terser
|
||||
|
||||
/vite@5.0.12(@types/node@20.10.3)(sass@1.69.5):
|
||||
resolution: {integrity: sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==}
|
||||
/vite@5.1.3(@types/node@20.10.3)(sass@1.69.5):
|
||||
resolution: {integrity: sha512-UfmUD36DKkqhi/F75RrxvPpry+9+tTkrXfMNZD+SboZqBCMsxKtO52XeGzzuh7ioz+Eo/SYDBbdb0Z7vgcDJew==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
@@ -5228,7 +5242,7 @@ packages:
|
||||
dependencies:
|
||||
'@types/node': 20.10.3
|
||||
esbuild: 0.19.11
|
||||
postcss: 8.4.32
|
||||
postcss: 8.4.35
|
||||
rollup: 4.6.1
|
||||
sass: 1.69.5
|
||||
optionalDependencies:
|
||||
@@ -5249,7 +5263,7 @@ packages:
|
||||
'@docsearch/css': 3.5.2
|
||||
'@docsearch/js': 3.5.2
|
||||
'@types/markdown-it': 13.0.7
|
||||
'@vitejs/plugin-vue': 5.0.2(vite@5.0.12)(vue@3.4.3)
|
||||
'@vitejs/plugin-vue': 5.0.2(vite@5.1.3)(vue@3.4.3)
|
||||
'@vue/devtools-api': 6.5.1
|
||||
'@vueuse/core': 10.7.1(vue@3.4.3)
|
||||
'@vueuse/integrations': 10.7.1(focus-trap@7.5.4)(vue@3.4.3)
|
||||
@@ -5260,7 +5274,7 @@ packages:
|
||||
shikiji: 0.9.16
|
||||
shikiji-core: 0.9.16
|
||||
shikiji-transformers: 0.9.16
|
||||
vite: 5.0.12(@types/node@20.10.3)(sass@1.69.5)
|
||||
vite: 5.1.3(@types/node@20.10.3)(sass@1.69.5)
|
||||
vue: 3.4.3(typescript@5.3.3)
|
||||
transitivePeerDependencies:
|
||||
- '@algolia/client-search'
|
||||
@@ -5290,7 +5304,7 @@ packages:
|
||||
- universal-cookie
|
||||
dev: true
|
||||
|
||||
/vitest-mock-extended@1.3.1(typescript@5.3.3)(vitest@1.1.0):
|
||||
/vitest-mock-extended@1.3.1(typescript@5.3.3)(vitest@1.2.2):
|
||||
resolution: {integrity: sha512-OpghYjh4BDuQ/Mzs3lFMQ1QRk9D8/2O9T47MLUA5eLn7K4RWIy+MfIivYOWEyxjTENjsBnzgMihDjyNalN/K0Q==}
|
||||
peerDependencies:
|
||||
typescript: 3.x || 4.x || 5.x
|
||||
@@ -5298,11 +5312,11 @@ packages:
|
||||
dependencies:
|
||||
ts-essentials: 9.3.2(typescript@5.3.3)
|
||||
typescript: 5.3.3
|
||||
vitest: 1.1.0(@types/node@20.10.3)(happy-dom@13.3.8)(sass@1.69.5)
|
||||
vitest: 1.2.2(@types/node@20.10.3)(happy-dom@13.3.8)(sass@1.69.5)
|
||||
dev: true
|
||||
|
||||
/vitest@1.1.0(@types/node@20.10.3)(happy-dom@13.3.8)(sass@1.69.5):
|
||||
resolution: {integrity: sha512-oDFiCrw7dd3Jf06HoMtSRARivvyjHJaTxikFxuqJjO76U436PqlVw1uLn7a8OSPrhSfMGVaRakKpA2lePdw79A==}
|
||||
/vitest@1.2.2(@types/node@20.10.3)(happy-dom@13.3.8)(sass@1.69.5):
|
||||
resolution: {integrity: sha512-d5Ouvrnms3GD9USIK36KG8OZ5bEvKEkITFtnGv56HFaSlbItJuYr7hv2Lkn903+AvRAgSixiamozUVfORUekjw==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
@@ -5327,12 +5341,12 @@ packages:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@types/node': 20.10.3
|
||||
'@vitest/expect': 1.1.0
|
||||
'@vitest/runner': 1.1.0
|
||||
'@vitest/snapshot': 1.1.0
|
||||
'@vitest/spy': 1.1.0
|
||||
'@vitest/utils': 1.1.0
|
||||
acorn-walk: 8.3.0
|
||||
'@vitest/expect': 1.2.2
|
||||
'@vitest/runner': 1.2.2
|
||||
'@vitest/snapshot': 1.2.2
|
||||
'@vitest/spy': 1.2.2
|
||||
'@vitest/utils': 1.2.2
|
||||
acorn-walk: 8.3.2
|
||||
cac: 6.7.14
|
||||
chai: 4.3.10
|
||||
debug: 4.3.4
|
||||
@@ -5345,9 +5359,9 @@ packages:
|
||||
std-env: 3.6.0
|
||||
strip-literal: 1.3.0
|
||||
tinybench: 2.5.1
|
||||
tinypool: 0.8.1
|
||||
vite: 5.0.12(@types/node@20.10.3)(sass@1.69.5)
|
||||
vite-node: 1.1.0(@types/node@20.10.3)(sass@1.69.5)
|
||||
tinypool: 0.8.2
|
||||
vite: 5.1.3(@types/node@20.10.3)(sass@1.69.5)
|
||||
vite-node: 1.2.2(@types/node@20.10.3)(sass@1.69.5)
|
||||
why-is-node-running: 2.2.2
|
||||
transitivePeerDependencies:
|
||||
- less
|
||||
|
||||
@@ -367,7 +367,7 @@ describe('Content Script UIs', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe.todo('after', () => {
|
||||
describe('after', () => {
|
||||
it('should append the UI after the anchor', () => {
|
||||
const ui = createIntegratedUi(ctx, {
|
||||
position: 'inline',
|
||||
|
||||
@@ -119,19 +119,18 @@ export async function createShadowRootUi<TMounted>(
|
||||
let mounted: TMounted;
|
||||
|
||||
const mount = () => {
|
||||
// Mount UI inside shadow root
|
||||
mounted = options.onMount(uiContainer, shadow, shadowHost);
|
||||
|
||||
// Add shadow root element to DOM
|
||||
mountUi(shadowHost, options);
|
||||
applyPosition(shadowHost, shadow.querySelector('html'), options);
|
||||
// Mount UI inside shadow root
|
||||
mounted = options.onMount(uiContainer, shadow, shadowHost);
|
||||
};
|
||||
|
||||
const remove = () => {
|
||||
// Detatch shadow root from DOM
|
||||
shadowHost.remove();
|
||||
// Cleanup mounted state
|
||||
options.onRemove?.(mounted);
|
||||
// Detatch shadow root from DOM
|
||||
shadowHost.remove();
|
||||
// Remove children from uiContainer
|
||||
while (uiContainer.lastChild)
|
||||
uiContainer.removeChild(uiContainer.lastChild);
|
||||
@@ -211,20 +210,16 @@ function mountUi(
|
||||
anchor.append(root);
|
||||
break;
|
||||
case 'first':
|
||||
if (anchor.firstChild) {
|
||||
anchor.insertBefore(root, anchor.firstChild);
|
||||
} else {
|
||||
anchor.append(root);
|
||||
}
|
||||
anchor.prepend(root);
|
||||
break;
|
||||
case 'replace':
|
||||
anchor.replaceWith(root);
|
||||
break;
|
||||
case 'after':
|
||||
anchor.replaceWith(anchor, root);
|
||||
anchor.parentElement?.insertBefore(root, anchor.nextElementSibling);
|
||||
break;
|
||||
case 'before':
|
||||
anchor.replaceWith(root, anchor);
|
||||
anchor.parentElement?.insertBefore(root, anchor);
|
||||
break;
|
||||
default:
|
||||
options.append(anchor, root);
|
||||
|
||||
@@ -10,6 +10,16 @@ export function bundleAnalysis(
|
||||
): vite.Plugin {
|
||||
return visualizer({
|
||||
template: 'raw-data',
|
||||
filename: path.resolve(config.outDir, `stats-${increment++}.json`),
|
||||
}) as vite.Plugin;
|
||||
filename: path.resolve(
|
||||
config.analysis.outputDir,
|
||||
`${config.analysis.outputName}-${increment++}.json`,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated FOR TESTING ONLY.
|
||||
*/
|
||||
export function resetBundleIncrement() {
|
||||
increment = 0;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { createUnimport } from 'unimport';
|
||||
import { ResolvedConfig } from '~/types';
|
||||
import { getUnimportOptions } from '~/core/utils/unimport';
|
||||
import type * as vite from 'vite';
|
||||
import { extname } from 'path';
|
||||
|
||||
@@ -19,7 +18,7 @@ const ENABLED_EXTENSIONS = new Set([
|
||||
export function unimport(
|
||||
config: Omit<ResolvedConfig, 'builder'>,
|
||||
): vite.PluginOption {
|
||||
const options = getUnimportOptions(config);
|
||||
const options = config.imports;
|
||||
if (options === false) return [];
|
||||
|
||||
const unimport = createUnimport(options);
|
||||
|
||||
+41
-23
@@ -5,10 +5,7 @@ import {
|
||||
ServerInfo,
|
||||
WxtDevServer,
|
||||
} from '~/types';
|
||||
import {
|
||||
getEntrypointBundlePath,
|
||||
resolvePerBrowserOption,
|
||||
} from '~/core/utils/entrypoints';
|
||||
import { getEntrypointBundlePath } from '~/core/utils/entrypoints';
|
||||
import {
|
||||
getContentScriptCssFiles,
|
||||
getContentScriptsCssMap,
|
||||
@@ -25,6 +22,8 @@ import { Mutex } from 'async-mutex';
|
||||
import pc from 'picocolors';
|
||||
import { relative } from 'node:path';
|
||||
import { registerWxt, wxt } from './wxt';
|
||||
import { unnormalizePath } from './utils/paths';
|
||||
import { mapWxtOptionsToRegisteredContentScript } from './utils/content-scripts';
|
||||
|
||||
/**
|
||||
* Creates a dev server and pre-builds all the files that need to exist before loading the extension.
|
||||
@@ -51,6 +50,14 @@ export async function createServer(
|
||||
// Build after starting the dev server so it can be used to transform HTML files
|
||||
server.currentOutput = await internalBuild();
|
||||
|
||||
// Add file watchers for files not loaded by the dev server. See
|
||||
// https://github.com/wxt-dev/wxt/issues/428#issuecomment-1944731870
|
||||
try {
|
||||
server.watcher.add(getExternalOutputDependencies(server));
|
||||
} catch (err) {
|
||||
wxt.config.logger.warn('Failed to register additional file paths:', err);
|
||||
}
|
||||
|
||||
// Open browser after everything is ready to go.
|
||||
await runner.openBrowser();
|
||||
};
|
||||
@@ -227,25 +234,9 @@ function reloadContentScripts(steps: BuildStepOutput[], server: WxtDevServer) {
|
||||
const cssMap = getContentScriptsCssMap(server.currentOutput, [entry]);
|
||||
const css = getContentScriptCssFiles([entry], cssMap);
|
||||
|
||||
server.reloadContentScript({
|
||||
allFrames: resolvePerBrowserOption(
|
||||
entry.options.allFrames,
|
||||
wxt.config.browser,
|
||||
),
|
||||
excludeMatches: resolvePerBrowserOption(
|
||||
entry.options.excludeMatches,
|
||||
wxt.config.browser,
|
||||
),
|
||||
matches: resolvePerBrowserOption(
|
||||
entry.options.matches,
|
||||
wxt.config.browser,
|
||||
),
|
||||
runAt: resolvePerBrowserOption(entry.options.runAt, wxt.config.browser),
|
||||
// @ts-expect-error: Chrome accepts this, not typed in webextension-polyfill (https://developer.chrome.com/docs/extensions/reference/scripting/#type-RegisteredContentScript)
|
||||
world: resolvePerBrowserOption(entry.options.world, wxt.config.browser),
|
||||
js,
|
||||
css,
|
||||
});
|
||||
server.reloadContentScript(
|
||||
mapWxtOptionsToRegisteredContentScript(entry.options, js, css),
|
||||
);
|
||||
});
|
||||
} else {
|
||||
server.reloadExtension();
|
||||
@@ -278,3 +269,30 @@ function getFilenameList(names: string[]): string {
|
||||
})
|
||||
.join(pc.dim(', '));
|
||||
}
|
||||
|
||||
/**
|
||||
* Based on the current build output, return a list of files that are:
|
||||
* 1. Not in node_modules
|
||||
* 2. Not inside project root
|
||||
*/
|
||||
function getExternalOutputDependencies(server: WxtDevServer) {
|
||||
return (
|
||||
server.currentOutput?.steps
|
||||
.flatMap((step, i) => {
|
||||
if (Array.isArray(step.entrypoints) && i === 0) {
|
||||
// Dev server is already watching all HTML/esm files
|
||||
return [];
|
||||
}
|
||||
|
||||
return step.chunks.flatMap((chunk) => {
|
||||
if (chunk.type === 'asset') return [];
|
||||
return chunk.moduleIds;
|
||||
});
|
||||
})
|
||||
.filter(
|
||||
(file) => !file.includes('node_modules') && !file.startsWith('\x00'),
|
||||
)
|
||||
.map(unnormalizePath)
|
||||
.filter((file) => !file.startsWith(wxt.config.root)) ?? []
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,10 +4,12 @@ import {
|
||||
fakeArray,
|
||||
fakeBackgroundEntrypoint,
|
||||
fakeBuildOutput,
|
||||
fakeContentScriptEntrypoint,
|
||||
fakeEntrypoint,
|
||||
fakeManifestCommand,
|
||||
fakeOptionsEntrypoint,
|
||||
fakePopupEntrypoint,
|
||||
fakeSidepanelEntrypoint,
|
||||
setFakeWxt,
|
||||
} from '../testing/fake-objects';
|
||||
import { Manifest } from 'webextension-polyfill';
|
||||
@@ -756,6 +758,136 @@ describe('Manifest Utils', () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('registration', () => {
|
||||
it('should throw an error when registration=runtime for MV2', async () => {
|
||||
const cs: ContentScriptEntrypoint = fakeContentScriptEntrypoint({
|
||||
options: {
|
||||
registration: 'runtime',
|
||||
},
|
||||
});
|
||||
|
||||
const entrypoints = [cs];
|
||||
const buildOutput: Omit<BuildOutput, 'manifest'> = {
|
||||
publicAssets: [],
|
||||
steps: [{ entrypoints: cs, chunks: [] }],
|
||||
};
|
||||
setFakeWxt({
|
||||
config: {
|
||||
manifestVersion: 2,
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
generateManifest(entrypoints, buildOutput),
|
||||
).rejects.toThrowError();
|
||||
});
|
||||
|
||||
it('should add host_permissions instead of content_scripts when registration=runtime', async () => {
|
||||
const cs: ContentScriptEntrypoint = {
|
||||
type: 'content-script',
|
||||
name: 'one',
|
||||
inputPath: 'entrypoints/one.content.ts',
|
||||
outputDir: contentScriptOutDir,
|
||||
options: {
|
||||
matches: ['*://google.com/*'],
|
||||
registration: 'runtime',
|
||||
},
|
||||
skipped: false,
|
||||
};
|
||||
const styles: OutputAsset = {
|
||||
type: 'asset',
|
||||
fileName: 'content-scripts/one.css',
|
||||
};
|
||||
|
||||
const entrypoints = [cs];
|
||||
const buildOutput: Omit<BuildOutput, 'manifest'> = {
|
||||
publicAssets: [],
|
||||
steps: [{ entrypoints: cs, chunks: [styles] }],
|
||||
};
|
||||
setFakeWxt({
|
||||
config: {
|
||||
manifestVersion: 3,
|
||||
outDir,
|
||||
command: 'build',
|
||||
},
|
||||
});
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
entrypoints,
|
||||
buildOutput,
|
||||
);
|
||||
|
||||
expect(actual.content_scripts).toEqual([]);
|
||||
expect(actual.host_permissions).toEqual(['*://google.com/*']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('sidepanel', () => {
|
||||
it.each(['chrome', 'safari', 'edge'])(
|
||||
'should include a side_panel ignoring all options for %s',
|
||||
async (browser) => {
|
||||
const sidepanel = fakeSidepanelEntrypoint({
|
||||
outputDir: outDir,
|
||||
});
|
||||
const buildOutput = fakeBuildOutput();
|
||||
|
||||
setFakeWxt({
|
||||
config: {
|
||||
manifestVersion: 3,
|
||||
browser,
|
||||
outDir,
|
||||
},
|
||||
});
|
||||
const expected = {
|
||||
side_panel: {
|
||||
default_path: 'sidepanel.html',
|
||||
},
|
||||
};
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
[sidepanel],
|
||||
buildOutput,
|
||||
);
|
||||
|
||||
expect(actual).toMatchObject(expected);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(['firefox'])(
|
||||
'should include a sidebar_action for %s',
|
||||
async (browser) => {
|
||||
const sidepanel = fakeSidepanelEntrypoint({
|
||||
outputDir: outDir,
|
||||
});
|
||||
const buildOutput = fakeBuildOutput();
|
||||
|
||||
setFakeWxt({
|
||||
config: {
|
||||
manifestVersion: 3,
|
||||
browser,
|
||||
outDir,
|
||||
},
|
||||
});
|
||||
const expected = {
|
||||
sidebar_action: {
|
||||
default_panel: 'sidepanel.html',
|
||||
open_at_install: sidepanel.options.openAtInstall,
|
||||
default_title: sidepanel.options.defaultTitle,
|
||||
default_icon: sidepanel.options.defaultIcon,
|
||||
browser_style: sidepanel.options.browserStyle,
|
||||
},
|
||||
};
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
[sidepanel],
|
||||
buildOutput,
|
||||
);
|
||||
|
||||
expect(actual).toMatchObject(expected);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('web_accessible_resources', () => {
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import {
|
||||
BackgroundEntrypoint,
|
||||
BackgroundEntrypointOptions,
|
||||
BaseEntrypointOptions,
|
||||
ContentScriptEntrypoint,
|
||||
GenericEntrypoint,
|
||||
OptionsEntrypoint,
|
||||
PopupEntrypoint,
|
||||
SidepanelEntrypoint,
|
||||
} from '~/types';
|
||||
import { resolve } from 'path';
|
||||
import { findEntrypoints } from '../find-entrypoints';
|
||||
@@ -242,7 +245,7 @@ describe('findEntrypoints', () => {
|
||||
])(
|
||||
'should find and load background entrypoint config from %s',
|
||||
async (path, expected) => {
|
||||
const options: BackgroundEntrypoint['options'] = {
|
||||
const options: BackgroundEntrypointOptions = {
|
||||
type: 'module',
|
||||
};
|
||||
globMock.mockResolvedValueOnce([path]);
|
||||
@@ -256,13 +259,87 @@ describe('findEntrypoints', () => {
|
||||
},
|
||||
);
|
||||
|
||||
it.each<[string, string, SidepanelEntrypoint]>([
|
||||
[
|
||||
'sidepanel.html',
|
||||
`
|
||||
<html>
|
||||
<head>
|
||||
<title>Default Title</title>
|
||||
<meta name="manifest.default_icon" content="{ '16': '/icon/16.png' }" />
|
||||
<meta name="manifest.open_at_install" content="true" />
|
||||
</head>
|
||||
</html>
|
||||
`,
|
||||
{
|
||||
type: 'sidepanel',
|
||||
name: 'sidepanel',
|
||||
inputPath: resolve(config.entrypointsDir, 'sidepanel.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {
|
||||
defaultTitle: 'Default Title',
|
||||
defaultIcon: { '16': '/icon/16.png' },
|
||||
openAtInstall: true,
|
||||
},
|
||||
skipped: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
'sidepanel/index.html',
|
||||
`<html></html>`,
|
||||
{
|
||||
type: 'sidepanel',
|
||||
name: 'sidepanel',
|
||||
inputPath: resolve(config.entrypointsDir, 'sidepanel/index.html'),
|
||||
options: {},
|
||||
outputDir: config.outDir,
|
||||
skipped: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
'named.sidepanel.html',
|
||||
`<html></html>`,
|
||||
{
|
||||
type: 'sidepanel',
|
||||
name: 'named',
|
||||
inputPath: resolve(config.entrypointsDir, 'named.sidepanel.html'),
|
||||
options: {},
|
||||
outputDir: config.outDir,
|
||||
skipped: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
'named.sidepanel/index.html',
|
||||
`<html></html>`,
|
||||
{
|
||||
type: 'sidepanel',
|
||||
name: 'named',
|
||||
inputPath: resolve(config.entrypointsDir, 'named.sidepanel/index.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
skipped: false,
|
||||
},
|
||||
],
|
||||
])(
|
||||
'should find and load sidepanel entrypoint config from %s',
|
||||
async (path, content, expected) => {
|
||||
globMock.mockResolvedValueOnce([path]);
|
||||
readFileMock.mockResolvedValueOnce(content);
|
||||
|
||||
const entrypoints = await findEntrypoints();
|
||||
|
||||
expect(entrypoints).toHaveLength(1);
|
||||
expect(entrypoints[0]).toEqual(expected);
|
||||
},
|
||||
);
|
||||
|
||||
it('should remove type=module from MV2 background scripts', async () => {
|
||||
setFakeWxt({
|
||||
config: {
|
||||
manifestVersion: 2,
|
||||
},
|
||||
});
|
||||
const options: BackgroundEntrypoint['options'] = {
|
||||
const options: BackgroundEntrypointOptions = {
|
||||
type: 'module',
|
||||
};
|
||||
globMock.mockResolvedValueOnce(['background.ts']);
|
||||
@@ -279,7 +356,7 @@ describe('findEntrypoints', () => {
|
||||
manifestVersion: 3,
|
||||
},
|
||||
});
|
||||
const options: BackgroundEntrypoint['options'] = {
|
||||
const options: BackgroundEntrypointOptions = {
|
||||
type: 'module',
|
||||
};
|
||||
globMock.mockResolvedValueOnce(['background.ts']);
|
||||
@@ -331,7 +408,7 @@ describe('findEntrypoints', () => {
|
||||
outputDir: config.outDir,
|
||||
skipped: false,
|
||||
};
|
||||
const options: GenericEntrypoint['options'] = {};
|
||||
const options: BaseEntrypointOptions = {};
|
||||
globMock.mockResolvedValueOnce([path]);
|
||||
importEntrypointFileMock.mockResolvedValue(options);
|
||||
|
||||
@@ -462,52 +539,6 @@ describe('findEntrypoints', () => {
|
||||
},
|
||||
],
|
||||
|
||||
// sidepanel
|
||||
[
|
||||
'sidepanel.html',
|
||||
{
|
||||
type: 'sidepanel',
|
||||
name: 'sidepanel',
|
||||
inputPath: resolve(config.entrypointsDir, 'sidepanel.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
skipped: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
'sidepanel/index.html',
|
||||
{
|
||||
type: 'sidepanel',
|
||||
name: 'sidepanel',
|
||||
inputPath: resolve(config.entrypointsDir, 'sidepanel/index.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
skipped: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
'named.sidepanel.html',
|
||||
{
|
||||
type: 'sidepanel',
|
||||
name: 'named',
|
||||
inputPath: resolve(config.entrypointsDir, 'named.sidepanel.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
skipped: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
'named.sidepanel/index.html',
|
||||
{
|
||||
type: 'sidepanel',
|
||||
name: 'named',
|
||||
inputPath: resolve(config.entrypointsDir, 'named.sidepanel/index.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
skipped: false,
|
||||
},
|
||||
],
|
||||
|
||||
// devtools
|
||||
[
|
||||
'devtools.html',
|
||||
|
||||
@@ -10,6 +10,10 @@ import {
|
||||
OptionsEntrypoint,
|
||||
PopupEntrypoint,
|
||||
UnlistedScriptDefinition,
|
||||
PopupEntrypointOptions,
|
||||
OptionsEntrypointOptions,
|
||||
SidepanelEntrypoint,
|
||||
SidepanelEntrypointOptions,
|
||||
} from '~/types';
|
||||
import fs from 'fs-extra';
|
||||
import { minimatch } from 'minimatch';
|
||||
@@ -19,7 +23,7 @@ import { importEntrypointFile } from '~/core/utils/building';
|
||||
import glob from 'fast-glob';
|
||||
import {
|
||||
getEntrypointName,
|
||||
resolvePerBrowserOption,
|
||||
resolvePerBrowserOptions,
|
||||
} from '~/core/utils/entrypoints';
|
||||
import { VIRTUAL_NOOP_BACKGROUND_MODULE_ID } from '~/core/utils/constants';
|
||||
import { CSS_EXTENSIONS_PATTERN } from '~/core/utils/paths';
|
||||
@@ -71,6 +75,8 @@ export async function findEntrypoints(): Promise<Entrypoint[]> {
|
||||
switch (type) {
|
||||
case 'popup':
|
||||
return await getPopupEntrypoint(info);
|
||||
case 'sidepanel':
|
||||
return await getSidepanelEntrypoint(info);
|
||||
case 'options':
|
||||
return await getOptionsEntrypoint(info);
|
||||
case 'background':
|
||||
@@ -199,153 +205,81 @@ function preventNoEntrypoints(files: EntrypointInfo[]) {
|
||||
}
|
||||
}
|
||||
|
||||
function getHtmlBaseOptions(document: Document): BaseEntrypointOptions {
|
||||
const options: BaseEntrypointOptions = {};
|
||||
|
||||
const includeContent = document
|
||||
.querySelector("meta[name='manifest.include']")
|
||||
?.getAttribute('content');
|
||||
if (includeContent) {
|
||||
options.include = JSON5.parse(includeContent);
|
||||
}
|
||||
|
||||
const excludeContent = document
|
||||
.querySelector("meta[name='manifest.exclude']")
|
||||
?.getAttribute('content');
|
||||
if (excludeContent) {
|
||||
options.exclude = JSON5.parse(excludeContent);
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path Absolute path to the popup HTML file.
|
||||
* @param content String contents of the file at the path.
|
||||
*/
|
||||
async function getPopupEntrypoint({
|
||||
inputPath,
|
||||
name,
|
||||
skipped,
|
||||
}: EntrypointInfo): Promise<PopupEntrypoint> {
|
||||
const content = await fs.readFile(inputPath, 'utf-8');
|
||||
const { document } = parseHTML(content);
|
||||
|
||||
const options: PopupEntrypoint['options'] = getHtmlBaseOptions(document);
|
||||
|
||||
const title = document.querySelector('title');
|
||||
if (title != null) options.defaultTitle = title.textContent ?? undefined;
|
||||
|
||||
const defaultIconContent = document
|
||||
.querySelector("meta[name='manifest.default_icon']")
|
||||
?.getAttribute('content');
|
||||
if (defaultIconContent) {
|
||||
try {
|
||||
options.defaultIcon = JSON5.parse(defaultIconContent);
|
||||
} catch (err) {
|
||||
wxt.logger.fatal(
|
||||
`Failed to parse default_icon meta tag content as JSON5. content=${defaultIconContent}`,
|
||||
err,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mv2TypeContent = document
|
||||
.querySelector("meta[name='manifest.type']")
|
||||
?.getAttribute('content');
|
||||
if (mv2TypeContent) {
|
||||
options.mv2Key =
|
||||
mv2TypeContent === 'page_action' ? 'page_action' : 'browser_action';
|
||||
}
|
||||
|
||||
const browserStyleContent = document
|
||||
.querySelector("meta[name='manifest.browser_style']")
|
||||
?.getAttribute('content');
|
||||
if (browserStyleContent) {
|
||||
options.browserStyle = browserStyleContent === 'true';
|
||||
}
|
||||
async function getPopupEntrypoint(
|
||||
info: EntrypointInfo,
|
||||
): Promise<PopupEntrypoint> {
|
||||
const options = await getHtmlEntrypointOptions<PopupEntrypointOptions>(
|
||||
info,
|
||||
{
|
||||
browserStyle: 'browse_style',
|
||||
exclude: 'exclude',
|
||||
include: 'include',
|
||||
defaultIcon: 'default_icon',
|
||||
defaultTitle: 'default_title',
|
||||
mv2Key: 'type',
|
||||
},
|
||||
{
|
||||
defaultTitle: (document) =>
|
||||
document.querySelector('title')?.textContent || undefined,
|
||||
},
|
||||
{
|
||||
defaultTitle: (content) => content,
|
||||
mv2Key: (content) =>
|
||||
content === 'page_action' ? 'page_action' : 'browser_action',
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
type: 'popup',
|
||||
name: 'popup',
|
||||
options,
|
||||
inputPath,
|
||||
options: resolvePerBrowserOptions(options, wxt.config.browser),
|
||||
inputPath: info.inputPath,
|
||||
outputDir: wxt.config.outDir,
|
||||
skipped,
|
||||
skipped: info.skipped,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path Absolute path to the options HTML file.
|
||||
* @param content String contents of the file at the path.
|
||||
*/
|
||||
async function getOptionsEntrypoint({
|
||||
inputPath,
|
||||
name,
|
||||
skipped,
|
||||
}: EntrypointInfo): Promise<OptionsEntrypoint> {
|
||||
const content = await fs.readFile(inputPath, 'utf-8');
|
||||
const { document } = parseHTML(content);
|
||||
|
||||
const options: OptionsEntrypoint['options'] = getHtmlBaseOptions(document);
|
||||
|
||||
const openInTabContent = document
|
||||
.querySelector("meta[name='manifest.open_in_tab']")
|
||||
?.getAttribute('content');
|
||||
if (openInTabContent) {
|
||||
options.openInTab = openInTabContent === 'true';
|
||||
}
|
||||
|
||||
const chromeStyleContent = document
|
||||
.querySelector("meta[name='manifest.chrome_style']")
|
||||
?.getAttribute('content');
|
||||
if (chromeStyleContent) {
|
||||
options.chromeStyle = chromeStyleContent === 'true';
|
||||
}
|
||||
|
||||
const browserStyleContent = document
|
||||
.querySelector("meta[name='manifest.browser_style']")
|
||||
?.getAttribute('content');
|
||||
if (browserStyleContent) {
|
||||
options.browserStyle = browserStyleContent === 'true';
|
||||
}
|
||||
|
||||
async function getOptionsEntrypoint(
|
||||
info: EntrypointInfo,
|
||||
): Promise<OptionsEntrypoint> {
|
||||
const options = await getHtmlEntrypointOptions<OptionsEntrypointOptions>(
|
||||
info,
|
||||
{
|
||||
browserStyle: 'browse_style',
|
||||
chromeStyle: 'chrome_style',
|
||||
exclude: 'exclude',
|
||||
include: 'include',
|
||||
openInTab: 'open_in_tab',
|
||||
},
|
||||
);
|
||||
return {
|
||||
type: 'options',
|
||||
name: 'options',
|
||||
options,
|
||||
inputPath,
|
||||
options: resolvePerBrowserOptions(options, wxt.config.browser),
|
||||
inputPath: info.inputPath,
|
||||
outputDir: wxt.config.outDir,
|
||||
skipped,
|
||||
skipped: info.skipped,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path Absolute path to the HTML file.
|
||||
* @param content String contents of the file at the path.
|
||||
*/
|
||||
async function getUnlistedPageEntrypoint({
|
||||
inputPath,
|
||||
name,
|
||||
skipped,
|
||||
}: EntrypointInfo): Promise<GenericEntrypoint> {
|
||||
const content = await fs.readFile(inputPath, 'utf-8');
|
||||
const { document } = parseHTML(content);
|
||||
async function getUnlistedPageEntrypoint(
|
||||
info: EntrypointInfo,
|
||||
): Promise<GenericEntrypoint> {
|
||||
const options = await getHtmlEntrypointOptions<BaseEntrypointOptions>(info, {
|
||||
exclude: 'exclude',
|
||||
include: 'include',
|
||||
});
|
||||
|
||||
return {
|
||||
type: 'unlisted-page',
|
||||
name: getEntrypointName(wxt.config.entrypointsDir, inputPath),
|
||||
inputPath,
|
||||
name: info.name,
|
||||
inputPath: info.inputPath,
|
||||
outputDir: wxt.config.outDir,
|
||||
options: getHtmlBaseOptions(document),
|
||||
skipped,
|
||||
options,
|
||||
skipped: info.skipped,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path Absolute path to the script's file.
|
||||
* @param content String contents of the file at the path.
|
||||
*/
|
||||
async function getUnlistedScriptEntrypoint({
|
||||
inputPath,
|
||||
name,
|
||||
@@ -358,21 +292,17 @@ async function getUnlistedScriptEntrypoint({
|
||||
`${name}: Default export not found, did you forget to call "export default defineUnlistedScript(...)"?`,
|
||||
);
|
||||
}
|
||||
const { main: _, ...moduleOptions } = defaultExport;
|
||||
const options: Omit<UnlistedScriptDefinition, 'main'> = moduleOptions;
|
||||
const { main: _, ...options } = defaultExport;
|
||||
return {
|
||||
type: 'unlisted-script',
|
||||
name,
|
||||
inputPath,
|
||||
outputDir: wxt.config.outDir,
|
||||
options,
|
||||
options: resolvePerBrowserOptions(options, wxt.config.browser),
|
||||
skipped,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path Absolute path to the background's TS file.
|
||||
*/
|
||||
async function getBackgroundEntrypoint({
|
||||
inputPath,
|
||||
name,
|
||||
@@ -400,21 +330,11 @@ async function getBackgroundEntrypoint({
|
||||
name,
|
||||
inputPath,
|
||||
outputDir: wxt.config.outDir,
|
||||
options: {
|
||||
...options,
|
||||
type: resolvePerBrowserOption(options.type, wxt.config.browser),
|
||||
persistent: resolvePerBrowserOption(
|
||||
options.persistent,
|
||||
wxt.config.browser,
|
||||
),
|
||||
},
|
||||
options: resolvePerBrowserOptions(options, wxt.config.browser),
|
||||
skipped,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path Absolute path to the content script's TS file.
|
||||
*/
|
||||
async function getContentScriptEntrypoint({
|
||||
inputPath,
|
||||
name,
|
||||
@@ -432,11 +352,87 @@ async function getContentScriptEntrypoint({
|
||||
name,
|
||||
inputPath,
|
||||
outputDir: resolve(wxt.config.outDir, CONTENT_SCRIPT_OUT_DIR),
|
||||
options,
|
||||
options: resolvePerBrowserOptions(options, wxt.config.browser),
|
||||
skipped,
|
||||
};
|
||||
}
|
||||
|
||||
async function getSidepanelEntrypoint(
|
||||
info: EntrypointInfo,
|
||||
): Promise<SidepanelEntrypoint> {
|
||||
const options = await getHtmlEntrypointOptions<SidepanelEntrypointOptions>(
|
||||
info,
|
||||
{
|
||||
browserStyle: 'browse_style',
|
||||
exclude: 'exclude',
|
||||
include: 'include',
|
||||
defaultIcon: 'default_icon',
|
||||
defaultTitle: 'default_title',
|
||||
openAtInstall: 'open_at_install',
|
||||
},
|
||||
{
|
||||
defaultTitle: (document) =>
|
||||
document.querySelector('title')?.textContent || undefined,
|
||||
},
|
||||
{
|
||||
defaultTitle: (content) => content,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
type: 'sidepanel',
|
||||
name: info.name,
|
||||
options: resolvePerBrowserOptions(options, wxt.config.browser),
|
||||
inputPath: info.inputPath,
|
||||
outputDir: wxt.config.outDir,
|
||||
skipped: info.skipped,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the HTML tags to extract options from them.
|
||||
*/
|
||||
async function getHtmlEntrypointOptions<T extends BaseEntrypointOptions>(
|
||||
info: EntrypointInfo,
|
||||
keyMap: Record<keyof T, string>,
|
||||
queries?: Partial<{
|
||||
[key in keyof T]: (
|
||||
document: Document,
|
||||
manifestKey: string,
|
||||
) => string | undefined;
|
||||
}>,
|
||||
parsers?: Partial<{ [key in keyof T]: (content: string) => T[key] }>,
|
||||
): Promise<T> {
|
||||
const content = await fs.readFile(info.inputPath, 'utf-8');
|
||||
const { document } = parseHTML(content);
|
||||
|
||||
const options = {} as T;
|
||||
|
||||
const defaultQuery = (manifestKey: string) =>
|
||||
document
|
||||
.querySelector(`meta[name='manifest.${manifestKey}']`)
|
||||
?.getAttribute('content');
|
||||
|
||||
Object.entries(keyMap).forEach(([_key, manifestKey]) => {
|
||||
const key = _key as keyof T;
|
||||
const content = queries?.[key]
|
||||
? queries[key]!(document, manifestKey)
|
||||
: defaultQuery(manifestKey);
|
||||
if (content) {
|
||||
try {
|
||||
options[key] = (parsers?.[key] ?? JSON5.parse)(content);
|
||||
} catch (err) {
|
||||
wxt.logger.fatal(
|
||||
`Failed to parse meta tag content. Usually this means you have invalid JSON5 content (content=${content})`,
|
||||
err,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
const PATH_GLOB_TO_TYPE_MAP: Record<string, Entrypoint['type']> = {
|
||||
'sandbox.html': 'sandbox',
|
||||
'sandbox/index.html': 'sandbox',
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { UnimportOptions, createUnimport } from 'unimport';
|
||||
import { Entrypoint } from '~/types';
|
||||
import { Unimport, createUnimport } from 'unimport';
|
||||
import {
|
||||
EslintGlobalsPropValue,
|
||||
Entrypoint,
|
||||
WxtResolvedUnimportOptions,
|
||||
} from '~/types';
|
||||
import fs from 'fs-extra';
|
||||
import { relative, resolve } from 'path';
|
||||
import { getEntrypointBundlePath } from '~/core/utils/entrypoints';
|
||||
import { getUnimportOptions } from '~/core/utils/unimport';
|
||||
import { getEntrypointGlobals, getGlobals } from '~/core/utils/globals';
|
||||
import { normalizePath } from '~/core/utils/paths';
|
||||
import path from 'node:path';
|
||||
@@ -21,9 +24,12 @@ export async function generateTypesDir(
|
||||
|
||||
const references: string[] = [];
|
||||
|
||||
const imports = getUnimportOptions(wxt.config);
|
||||
if (imports !== false) {
|
||||
references.push(await writeImportsDeclarationFile(imports));
|
||||
if (wxt.config.imports !== false) {
|
||||
const unimport = createUnimport(wxt.config.imports);
|
||||
references.push(await writeImportsDeclarationFile(unimport));
|
||||
if (wxt.config.imports.eslintrc.enabled) {
|
||||
await writeImportsEslintFile(unimport, wxt.config.imports);
|
||||
}
|
||||
}
|
||||
|
||||
references.push(await writePathsDeclarationFile(entrypoints));
|
||||
@@ -34,11 +40,8 @@ export async function generateTypesDir(
|
||||
await writeTsConfigFile(mainReference);
|
||||
}
|
||||
|
||||
async function writeImportsDeclarationFile(
|
||||
unimportOptions: Partial<UnimportOptions>,
|
||||
): Promise<string> {
|
||||
async function writeImportsDeclarationFile(unimport: Unimport) {
|
||||
const filePath = resolve(wxt.config.typesDir, 'imports.d.ts');
|
||||
const unimport = createUnimport(unimportOptions);
|
||||
|
||||
// Load project imports into unimport memory so they are output via generateTypeDeclarations
|
||||
await unimport.scanImportsFromDir(undefined, { cwd: wxt.config.srcDir });
|
||||
@@ -53,6 +56,23 @@ async function writeImportsDeclarationFile(
|
||||
return filePath;
|
||||
}
|
||||
|
||||
async function writeImportsEslintFile(
|
||||
unimport: Unimport,
|
||||
options: WxtResolvedUnimportOptions,
|
||||
) {
|
||||
const globals: Record<string, EslintGlobalsPropValue> = {};
|
||||
const eslintrc = { globals };
|
||||
|
||||
(await unimport.getImports())
|
||||
.map((i) => i.as ?? i.name)
|
||||
.filter(Boolean)
|
||||
.sort()
|
||||
.forEach((name) => {
|
||||
eslintrc.globals[name] = options.eslintrc.globalsPropValue;
|
||||
});
|
||||
await fs.writeJson(options.eslintrc.filePath, eslintrc, { spaces: 2 });
|
||||
}
|
||||
|
||||
async function writePathsDeclarationFile(
|
||||
entrypoints: Entrypoint[],
|
||||
): Promise<string> {
|
||||
|
||||
@@ -2,7 +2,6 @@ import createJITI, { TransformOptions as JitiTransformOptions } from 'jiti';
|
||||
import { createUnimport } from 'unimport';
|
||||
import fs from 'fs-extra';
|
||||
import { relative, resolve } from 'node:path';
|
||||
import { getUnimportOptions } from '~/core/utils/unimport';
|
||||
import { removeProjectImportStatements } from '~/core/utils/strings';
|
||||
import { normalizePath } from '~/core/utils/paths';
|
||||
import { TransformOptions, transformSync } from 'esbuild';
|
||||
@@ -30,7 +29,7 @@ export async function importEntrypointFile<T>(path: string): Promise<T> {
|
||||
const normalPath = normalizePath(path);
|
||||
|
||||
const unimport = createUnimport({
|
||||
...getUnimportOptions(wxt.config),
|
||||
...wxt.config.imports,
|
||||
// Only allow specific imports, not all from the project
|
||||
dirs: [],
|
||||
});
|
||||
@@ -106,6 +105,13 @@ function getEsbuildOptions(opts: JitiTransformOptions): TransformOptions {
|
||||
return {
|
||||
format: 'cjs',
|
||||
loader: isJsx ? 'tsx' : 'ts',
|
||||
jsx: isJsx ? 'automatic' : undefined,
|
||||
...(isJsx
|
||||
? {
|
||||
// `h` and `Fragment` are undefined, but that's OK because JSX is never evaluated while
|
||||
// grabbing the entrypoint's options.
|
||||
jsxFactory: 'h',
|
||||
jsxFragment: 'Fragment',
|
||||
}
|
||||
: undefined),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -87,8 +87,8 @@ export async function internalBuild(): Promise<BuildOutput> {
|
||||
}
|
||||
|
||||
async function combineAnalysisStats(): Promise<void> {
|
||||
const unixFiles = await glob(`stats-*.json`, {
|
||||
cwd: wxt.config.outDir,
|
||||
const unixFiles = await glob(`${wxt.config.analysis.outputName}-*.json`, {
|
||||
cwd: wxt.config.analysis.outputDir,
|
||||
absolute: true,
|
||||
});
|
||||
const absolutePaths = unixFiles.map(unnormalizePath);
|
||||
@@ -104,7 +104,10 @@ async function combineAnalysisStats(): Promise<void> {
|
||||
],
|
||||
{ cwd: wxt.config.root, stdio: 'inherit' },
|
||||
);
|
||||
await Promise.all(absolutePaths.map((statsFile) => fs.remove(statsFile)));
|
||||
|
||||
if (!wxt.config.analysis.keepArtifacts) {
|
||||
await Promise.all(absolutePaths.map((statsFile) => fs.remove(statsFile)));
|
||||
}
|
||||
}
|
||||
|
||||
function printValidationResults({
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
UserManifest,
|
||||
ExtensionRunnerConfig,
|
||||
WxtDevServer,
|
||||
WxtResolvedUnimportOptions,
|
||||
Logger,
|
||||
} from '~/types';
|
||||
import path from 'node:path';
|
||||
import { createFsCache } from '~/core/utils/cache';
|
||||
@@ -15,6 +17,7 @@ import consola, { LogLevels } from 'consola';
|
||||
import { createViteBuilder } from '~/core/builders/vite';
|
||||
import defu from 'defu';
|
||||
import { NullablyRequired } from '../types';
|
||||
import { isModuleInstalled } from '../package';
|
||||
|
||||
/**
|
||||
* Given an inline config, discover the config file if necessary, merge the results, resolve any
|
||||
@@ -34,6 +37,7 @@ export async function resolveConfig(
|
||||
let userConfigMetadata: ResolvedConfig['userConfigMetadata'] | undefined;
|
||||
if (inlineConfig.configFile !== false) {
|
||||
const { config: loadedConfig, ...metadata } = await loadConfig<UserConfig>({
|
||||
configFile: inlineConfig.configFile,
|
||||
name: 'wxt',
|
||||
cwd: inlineConfig.root ?? process.cwd(),
|
||||
rcFile: false,
|
||||
@@ -100,6 +104,13 @@ export async function resolveConfig(
|
||||
}).map(([key, value]) => [key, path.resolve(root, value)]),
|
||||
);
|
||||
|
||||
const analysisOutputFile = path.resolve(
|
||||
root,
|
||||
mergedConfig.analysis?.outputFile ?? 'stats.html',
|
||||
);
|
||||
const analysisOutputDir = path.dirname(analysisOutputFile);
|
||||
const analysisOutputName = path.parse(analysisOutputFile).name;
|
||||
|
||||
const finalConfig: Omit<ResolvedConfig, 'builder'> = {
|
||||
browser,
|
||||
command,
|
||||
@@ -108,7 +119,7 @@ export async function resolveConfig(
|
||||
filterEntrypoints,
|
||||
env,
|
||||
fsCache: createFsCache(wxtDir),
|
||||
imports: mergedConfig.imports ?? {},
|
||||
imports: await getUnimportOptions(wxtDir, logger, mergedConfig),
|
||||
logger,
|
||||
manifest: await resolveManifestConfig(env, mergedConfig.manifest),
|
||||
manifestVersion,
|
||||
@@ -129,10 +140,10 @@ export async function resolveConfig(
|
||||
analysis: {
|
||||
enabled: mergedConfig.analysis?.enabled ?? false,
|
||||
template: mergedConfig.analysis?.template ?? 'treemap',
|
||||
outputFile: path.resolve(
|
||||
root,
|
||||
mergedConfig.analysis?.outputFile ?? 'stats.html',
|
||||
),
|
||||
outputFile: analysisOutputFile,
|
||||
outputDir: analysisOutputDir,
|
||||
outputName: analysisOutputName,
|
||||
keepArtifacts: mergedConfig.analysis?.keepArtifacts ?? false,
|
||||
},
|
||||
userConfigMetadata: userConfigMetadata ?? {},
|
||||
alias,
|
||||
@@ -266,3 +277,48 @@ function resolveInternalZipConfig(
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
async function getUnimportOptions(
|
||||
wxtDir: string,
|
||||
logger: Logger,
|
||||
config: InlineConfig,
|
||||
): Promise<WxtResolvedUnimportOptions | false> {
|
||||
if (config.imports === false) return false;
|
||||
|
||||
const enabledConfig = config.imports?.eslintrc?.enabled;
|
||||
let enabled: boolean;
|
||||
switch (enabledConfig) {
|
||||
case undefined:
|
||||
case 'auto':
|
||||
enabled = await isModuleInstalled('eslint');
|
||||
break;
|
||||
default:
|
||||
enabled = enabledConfig;
|
||||
}
|
||||
|
||||
const defaultOptions: WxtResolvedUnimportOptions = {
|
||||
debugLog: logger.debug,
|
||||
imports: [
|
||||
{ name: 'defineConfig', from: 'wxt' },
|
||||
{ name: 'fakeBrowser', from: 'wxt/testing' },
|
||||
],
|
||||
presets: [
|
||||
{ package: 'wxt/client' },
|
||||
{ package: 'wxt/browser' },
|
||||
{ package: 'wxt/sandbox' },
|
||||
{ package: 'wxt/storage' },
|
||||
],
|
||||
warn: logger.warn,
|
||||
dirs: ['components', 'composables', 'hooks', 'utils'],
|
||||
eslintrc: {
|
||||
enabled,
|
||||
filePath: path.resolve(wxtDir, 'eslintrc-auto-import.json'),
|
||||
globalsPropValue: true,
|
||||
},
|
||||
};
|
||||
|
||||
return defu<WxtResolvedUnimportOptions, [WxtResolvedUnimportOptions]>(
|
||||
config.imports ?? {},
|
||||
defaultOptions,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import type { Manifest } from '~/browser';
|
||||
import type { Manifest, Scripting } from '~/browser';
|
||||
import { ContentScriptEntrypoint } from '~/types';
|
||||
import { resolvePerBrowserOption } from './entrypoints';
|
||||
import { wxt } from '../wxt';
|
||||
|
||||
/**
|
||||
* Returns a unique and consistent string hash based on a content scripts options.
|
||||
@@ -11,7 +9,11 @@ import { wxt } from '../wxt';
|
||||
export function hashContentScriptOptions(
|
||||
options: ContentScriptEntrypoint['options'],
|
||||
): string {
|
||||
const simplifiedOptions = mapWxtOptionsToContentScript(options);
|
||||
const simplifiedOptions = mapWxtOptionsToContentScript(
|
||||
options,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
|
||||
// Remove undefined fields and use defaults to generate hash
|
||||
Object.keys(simplifiedOptions).forEach((key) => {
|
||||
@@ -45,33 +47,39 @@ export function hashContentScriptOptions(
|
||||
|
||||
export function mapWxtOptionsToContentScript(
|
||||
options: ContentScriptEntrypoint['options'],
|
||||
): Omit<Manifest.ContentScript, 'js' | 'css'> {
|
||||
js: string[] | undefined,
|
||||
css: string[] | undefined,
|
||||
): Manifest.ContentScript {
|
||||
return {
|
||||
matches: resolvePerBrowserOption(options.matches, wxt.config.browser),
|
||||
all_frames: resolvePerBrowserOption(options.allFrames, wxt.config.browser),
|
||||
match_about_blank: resolvePerBrowserOption(
|
||||
options.matchAboutBlank,
|
||||
wxt.config.browser,
|
||||
),
|
||||
exclude_globs: resolvePerBrowserOption(
|
||||
options.excludeGlobs,
|
||||
wxt.config.browser,
|
||||
),
|
||||
exclude_matches: resolvePerBrowserOption(
|
||||
options.excludeMatches,
|
||||
wxt.config.browser,
|
||||
),
|
||||
include_globs: resolvePerBrowserOption(
|
||||
options.includeGlobs,
|
||||
wxt.config.browser,
|
||||
),
|
||||
run_at: resolvePerBrowserOption(options.runAt, wxt.config.browser),
|
||||
matches: options.matches,
|
||||
all_frames: options.allFrames,
|
||||
match_about_blank: options.matchAboutBlank,
|
||||
exclude_globs: options.excludeGlobs,
|
||||
exclude_matches: options.excludeMatches,
|
||||
include_globs: options.includeGlobs,
|
||||
run_at: options.runAt,
|
||||
css,
|
||||
js,
|
||||
|
||||
// @ts-expect-error: untyped chrome options
|
||||
match_origin_as_fallback: resolvePerBrowserOption(
|
||||
options.matchOriginAsFallback,
|
||||
wxt.config.browser,
|
||||
),
|
||||
match_origin_as_fallback: options.matchOriginAsFallback,
|
||||
world: options.world,
|
||||
};
|
||||
}
|
||||
|
||||
export function mapWxtOptionsToRegisteredContentScript(
|
||||
options: ContentScriptEntrypoint['options'],
|
||||
js: string[] | undefined,
|
||||
css: string[] | undefined,
|
||||
): Omit<Scripting.RegisteredContentScript, 'id'> {
|
||||
return {
|
||||
allFrames: options.allFrames,
|
||||
excludeMatches: options.excludeMatches,
|
||||
matches: options.matches,
|
||||
runAt: options.runAt,
|
||||
js,
|
||||
css,
|
||||
// @ts-expect-error: Chrome accepts this, not typed in webextension-polyfill (https://developer.chrome.com/docs/extensions/reference/scripting/#type-RegisteredContentScript)
|
||||
world: options.world,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { Entrypoint, PerBrowserOption, TargetBrowser } from '~/types';
|
||||
import {
|
||||
Entrypoint,
|
||||
PerBrowserOption,
|
||||
ResolvedPerBrowserOptions,
|
||||
TargetBrowser,
|
||||
} from '~/types';
|
||||
import path, { relative, resolve } from 'node:path';
|
||||
import { normalizePath } from './paths';
|
||||
|
||||
@@ -46,3 +51,22 @@ export function resolvePerBrowserOption<T>(
|
||||
return (option as any)[browser];
|
||||
return option;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an entrypoint option, resolve it's value based on a target browser.
|
||||
*
|
||||
* defaultIcon is special, it's the only key that's a record, which can confuse this function. So
|
||||
* it's been manually excluded from resolution.
|
||||
*/
|
||||
export function resolvePerBrowserOptions<
|
||||
T extends Record<string, any>,
|
||||
TKeys extends keyof T,
|
||||
>(options: T, browser: TargetBrowser): ResolvedPerBrowserOptions<T, TKeys> {
|
||||
// @ts-expect-error: Object.entries is untyped.
|
||||
return Object.fromEntries(
|
||||
Object.entries(options).map(([key, value]) => [
|
||||
key,
|
||||
key === 'defaultIcon' ? value : resolvePerBrowserOption(value, browser),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
+51
-36
@@ -6,13 +6,11 @@ import {
|
||||
ContentScriptEntrypoint,
|
||||
OptionsEntrypoint,
|
||||
PopupEntrypoint,
|
||||
SidepanelEntrypoint,
|
||||
} from '~/types';
|
||||
import fs from 'fs-extra';
|
||||
import { resolve } from 'path';
|
||||
import {
|
||||
getEntrypointBundlePath,
|
||||
resolvePerBrowserOption,
|
||||
} from './entrypoints';
|
||||
import { getEntrypointBundlePath } from './entrypoints';
|
||||
import { ContentSecurityPolicy } from './content-security-policy';
|
||||
import {
|
||||
hashContentScriptOptions,
|
||||
@@ -185,7 +183,9 @@ function addEntrypoints(
|
||||
| undefined;
|
||||
const popup = entriesByType['popup']?.[0] as PopupEntrypoint | undefined;
|
||||
const sandboxes = entriesByType['sandbox'];
|
||||
const sidepanels = entriesByType['sidepanel'];
|
||||
const sidepanels = entriesByType['sidepanel'] as
|
||||
| SidepanelEntrypoint[]
|
||||
| undefined;
|
||||
|
||||
if (background) {
|
||||
const script = getEntrypointBundlePath(
|
||||
@@ -331,9 +331,11 @@ function addEntrypoints(
|
||||
|
||||
if (wxt.config.browser === 'firefox') {
|
||||
manifest.sidebar_action = {
|
||||
// TODO: Add options to side panel
|
||||
// ...defaultSidepanel.options,
|
||||
default_panel: page,
|
||||
browser_style: defaultSidepanel.options.browserStyle,
|
||||
default_icon: defaultSidepanel.options.defaultIcon,
|
||||
default_title: defaultSidepanel.options.defaultTitle,
|
||||
open_at_install: defaultSidepanel.options.openAtInstall,
|
||||
};
|
||||
} else if (wxt.config.manifestVersion === 3) {
|
||||
// @ts-expect-error: Untyped
|
||||
@@ -353,40 +355,54 @@ function addEntrypoints(
|
||||
// Don't add content scripts to the manifest in dev mode for MV3 - they're managed and reloaded
|
||||
// at runtime
|
||||
if (wxt.config.command === 'serve' && wxt.config.manifestVersion === 3) {
|
||||
const hostPermissions = new Set<string>(manifest.host_permissions ?? []);
|
||||
contentScripts.forEach((script) => {
|
||||
const matches = resolvePerBrowserOption(
|
||||
script.options.matches,
|
||||
wxt.config.browser,
|
||||
);
|
||||
matches.forEach((matchPattern) => {
|
||||
hostPermissions.add(matchPattern);
|
||||
script.options.matches.forEach((matchPattern) => {
|
||||
addHostPermission(manifest, matchPattern);
|
||||
});
|
||||
});
|
||||
hostPermissions.forEach((permission) =>
|
||||
addHostPermission(manifest, permission),
|
||||
);
|
||||
} else {
|
||||
const hashToEntrypointsMap = contentScripts.reduce((map, script) => {
|
||||
const hash = hashContentScriptOptions(script.options);
|
||||
if (map.has(hash)) map.get(hash)?.push(script);
|
||||
else map.set(hash, [script]);
|
||||
return map;
|
||||
}, new Map<string, ContentScriptEntrypoint[]>());
|
||||
|
||||
const newContentScripts = Array.from(hashToEntrypointsMap.entries()).map(
|
||||
([, scripts]) => ({
|
||||
...mapWxtOptionsToContentScript(scripts[0].options),
|
||||
css: getContentScriptCssFiles(scripts, cssMap),
|
||||
js: scripts.map((entry) =>
|
||||
// Manifest scripts
|
||||
const hashToEntrypointsMap = contentScripts
|
||||
.filter((cs) => cs.options.registration !== 'runtime')
|
||||
.reduce((map, script) => {
|
||||
const hash = hashContentScriptOptions(script.options);
|
||||
if (map.has(hash)) map.get(hash)?.push(script);
|
||||
else map.set(hash, [script]);
|
||||
return map;
|
||||
}, new Map<string, ContentScriptEntrypoint[]>());
|
||||
const manifestContentScripts = Array.from(
|
||||
hashToEntrypointsMap.values(),
|
||||
).map((scripts) =>
|
||||
mapWxtOptionsToContentScript(
|
||||
scripts[0].options,
|
||||
scripts.map((entry) =>
|
||||
getEntrypointBundlePath(entry, wxt.config.outDir, '.js'),
|
||||
),
|
||||
}),
|
||||
getContentScriptCssFiles(scripts, cssMap),
|
||||
),
|
||||
);
|
||||
if (newContentScripts.length >= 0) {
|
||||
if (manifestContentScripts.length >= 0) {
|
||||
manifest.content_scripts ??= [];
|
||||
manifest.content_scripts.push(...newContentScripts);
|
||||
manifest.content_scripts.push(...manifestContentScripts);
|
||||
}
|
||||
|
||||
// Runtime content scripts
|
||||
const runtimeContentScripts = contentScripts.filter(
|
||||
(cs) => cs.options.registration === 'runtime',
|
||||
);
|
||||
if (
|
||||
runtimeContentScripts.length > 0 &&
|
||||
wxt.config.manifestVersion === 2
|
||||
) {
|
||||
throw Error(
|
||||
'Cannot use `registration: "runtime"` with MV2 content scripts, it is a MV3-only feature.',
|
||||
);
|
||||
}
|
||||
runtimeContentScripts.forEach((script) => {
|
||||
script.options.matches.forEach((matchPattern) => {
|
||||
addHostPermission(manifest, matchPattern);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const contentScriptCssResources = getContentScriptCssWebAccessibleResources(
|
||||
@@ -519,10 +535,9 @@ export function getContentScriptCssWebAccessibleResources(
|
||||
|
||||
resources.push({
|
||||
resources: [cssFile],
|
||||
matches: resolvePerBrowserOption(
|
||||
script.options.matches,
|
||||
wxt.config.browser,
|
||||
).map((matchPattern) => stripPathFromMatchPattern(matchPattern)),
|
||||
matches: script.options.matches.map((matchPattern) =>
|
||||
stripPathFromMatchPattern(matchPattern),
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -20,3 +20,7 @@ export async function getPackageJson(): Promise<
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
export function isModuleInstalled(name: string) {
|
||||
return import(name).then(() => true).catch(() => false);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
BuildStepOutput,
|
||||
UserManifest,
|
||||
Wxt,
|
||||
SidepanelEntrypoint,
|
||||
} from '~/types';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { vi } from 'vitest';
|
||||
@@ -148,6 +149,30 @@ export const fakePopupEntrypoint = fakeObjectCreator<PopupEntrypoint>(() => ({
|
||||
skipped: faker.datatype.boolean(),
|
||||
}));
|
||||
|
||||
export const fakeSidepanelEntrypoint = fakeObjectCreator<SidepanelEntrypoint>(
|
||||
() => ({
|
||||
type: 'sidepanel',
|
||||
inputPath: 'entrypoints/sidepanel.html',
|
||||
name: 'sidepanel',
|
||||
outputDir: fakeDir('.output'),
|
||||
options: {
|
||||
defaultTitle: faker.helpers.arrayElement([
|
||||
faker.person.fullName(),
|
||||
undefined,
|
||||
]),
|
||||
defaultIcon: faker.helpers.arrayElement([
|
||||
{
|
||||
'16': 'icon/16.png',
|
||||
'24': 'icon/24.png',
|
||||
'64': 'icon/64.png',
|
||||
},
|
||||
]),
|
||||
openAtInstall: faker.helpers.arrayElement([true, false, undefined]),
|
||||
},
|
||||
skipped: faker.datatype.boolean(),
|
||||
}),
|
||||
);
|
||||
|
||||
export const fakeGenericEntrypoint = fakeObjectCreator<GenericEntrypoint>(
|
||||
() => ({
|
||||
type: faker.helpers.arrayElement([
|
||||
@@ -155,7 +180,6 @@ export const fakeGenericEntrypoint = fakeObjectCreator<GenericEntrypoint>(
|
||||
'bookmarks',
|
||||
'history',
|
||||
'newtab',
|
||||
'sidepanel',
|
||||
'devtools',
|
||||
'unlisted-page',
|
||||
'unlisted-script',
|
||||
@@ -216,7 +240,20 @@ export const fakeResolvedConfig = fakeObjectCreator<ResolvedConfig>(() => {
|
||||
entrypointsDir: fakeDir(),
|
||||
env: { browser, command, manifestVersion, mode },
|
||||
fsCache: mock<FsCache>(),
|
||||
imports: {},
|
||||
imports: {
|
||||
eslintrc: {
|
||||
enabled: faker.datatype.boolean(),
|
||||
filePath: fakeFile(),
|
||||
globalsPropValue: faker.helpers.arrayElement([
|
||||
true,
|
||||
false,
|
||||
'readable',
|
||||
'readonly',
|
||||
'writable',
|
||||
'writeable',
|
||||
] as const),
|
||||
},
|
||||
},
|
||||
logger: mock(),
|
||||
manifest: fakeUserManifest(),
|
||||
manifestVersion,
|
||||
@@ -237,6 +274,9 @@ export const fakeResolvedConfig = fakeObjectCreator<ResolvedConfig>(() => {
|
||||
enabled: false,
|
||||
template: 'treemap',
|
||||
outputFile: fakeFile(),
|
||||
outputDir: fakeDir(),
|
||||
outputName: 'stats',
|
||||
keepArtifacts: false,
|
||||
},
|
||||
zip: {
|
||||
artifactTemplate: '{{name}}-{{version}}.zip',
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { UnimportOptions } from 'unimport';
|
||||
import { ResolvedConfig } from '~/types';
|
||||
import { defu } from 'defu';
|
||||
|
||||
export function getUnimportOptions(
|
||||
config: Omit<ResolvedConfig, 'builder'>,
|
||||
): Partial<UnimportOptions | false> {
|
||||
if (config.imports === false) return false;
|
||||
|
||||
const defaultOptions: Partial<UnimportOptions> = {
|
||||
debugLog: config.logger.debug,
|
||||
imports: [
|
||||
{ name: 'defineConfig', from: 'wxt' },
|
||||
{ name: 'fakeBrowser', from: 'wxt/testing' },
|
||||
],
|
||||
presets: [
|
||||
{ package: 'wxt/client' },
|
||||
{ package: 'wxt/browser' },
|
||||
{ package: 'wxt/sandbox' },
|
||||
{ package: 'wxt/storage' },
|
||||
],
|
||||
warn: config.logger.warn,
|
||||
dirs: ['components', 'composables', 'hooks', 'utils'],
|
||||
};
|
||||
|
||||
return defu(config.imports, defaultOptions);
|
||||
}
|
||||
+277
-142
@@ -77,7 +77,7 @@ export interface InlineConfig {
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
imports?: Partial<UnimportOptions> | false;
|
||||
imports?: WxtUnimportOptions | false;
|
||||
/**
|
||||
* Explicitly set a browser to build for. This will override the default browser for each command,
|
||||
* and can be overridden by the command line `--browser` option.
|
||||
@@ -212,9 +212,24 @@ export interface InlineConfig {
|
||||
/**
|
||||
* Name of the output HTML file. Relative to the project's root directory.
|
||||
*
|
||||
* Changing the filename of the outputFile also effects the names of the artifacts generated
|
||||
* when setting `keepArtifacts` to true:
|
||||
* - "stats.html" => "stats-*.json"
|
||||
* - "stats/bundle.html" => "bundle-*.json"
|
||||
* - ".analysis/index.html" => "index-*.json"
|
||||
*
|
||||
* @default "stats.html"
|
||||
*/
|
||||
outputFile?: string;
|
||||
/**
|
||||
* By default, the `stats-*.json` artifacts generated during bundle analysis are deleted. Set to
|
||||
* `true` to keep them.
|
||||
*
|
||||
* One stats file is output per build step.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
keepArtifacts?: boolean;
|
||||
};
|
||||
/**
|
||||
* Add additional paths to the `.wxt/tsconfig.json`. Use this instead of overwriting the `paths`
|
||||
@@ -409,131 +424,37 @@ export interface Logger {
|
||||
}
|
||||
|
||||
export interface BaseEntrypointOptions {
|
||||
/**
|
||||
* List of target browsers to include this entrypoint in. Defaults to being included in all
|
||||
* builds. Cannot be used with `exclude`. You must choose one of the two options.
|
||||
*
|
||||
* @default undefined
|
||||
*/
|
||||
include?: TargetBrowser[];
|
||||
/**
|
||||
* List of target browsers to exclude this entrypoint from. Cannot be used with `include`. You
|
||||
* must choose one of the two options.
|
||||
*
|
||||
* @default undefined
|
||||
*/
|
||||
exclude?: TargetBrowser[];
|
||||
}
|
||||
|
||||
export interface BaseEntrypoint {
|
||||
export interface BackgroundEntrypointOptions extends BaseEntrypointOptions {
|
||||
persistent?: PerBrowserOption<boolean>;
|
||||
/**
|
||||
* The entrypoint's name. This is the filename or dirname without the type suffix.
|
||||
* Set to `"module"` to output the background entrypoint as ESM. ESM outputs can share chunks and
|
||||
* reduce the overall size of the bundled extension.
|
||||
*
|
||||
* Examples:
|
||||
* - `popup.html` → `popup`
|
||||
* - `options/index.html` → `options`
|
||||
* - `named.sandbox.html` → `named`
|
||||
* - `named.sandbox/index.html` → `named`
|
||||
* - `sandbox.html` → `sandbox`
|
||||
* - `sandbox/index.html` → `sandbox`
|
||||
* - `overlay.content.ts` → `overlay`
|
||||
* - `overlay.content/index.ts` → `overlay`
|
||||
* When `undefined`, the background is bundled individually into an IIFE format.
|
||||
*
|
||||
* The name is used when generating an output file:
|
||||
* `<entrypoint.outputDir>/<entrypoint.name>.<ext>`
|
||||
* @default undefined
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Absolute path to the entrypoint's input file.
|
||||
*/
|
||||
inputPath: string;
|
||||
/**
|
||||
* Absolute path to the entrypoint's output directory. Can be the`InternalConfg.outDir` or a
|
||||
* subdirectory of it.
|
||||
*/
|
||||
outputDir: string;
|
||||
options: BaseEntrypointOptions;
|
||||
skipped: boolean;
|
||||
type?: PerBrowserOption<'module'>;
|
||||
}
|
||||
|
||||
export interface GenericEntrypoint extends BaseEntrypoint {
|
||||
type:
|
||||
| 'sandbox'
|
||||
| 'bookmarks'
|
||||
| 'history'
|
||||
| 'newtab'
|
||||
| 'sidepanel'
|
||||
| 'devtools'
|
||||
| 'unlisted-page'
|
||||
| 'unlisted-script'
|
||||
| 'unlisted-style'
|
||||
| 'content-script-style';
|
||||
}
|
||||
|
||||
export interface BackgroundEntrypoint extends BaseEntrypoint {
|
||||
type: 'background';
|
||||
options: {
|
||||
persistent?: boolean;
|
||||
type?: 'module';
|
||||
} & BaseEntrypointOptions;
|
||||
}
|
||||
|
||||
export interface ContentScriptEntrypoint extends BaseEntrypoint {
|
||||
type: 'content-script';
|
||||
options: Omit<ContentScriptDefinition, 'main'> & BaseEntrypointOptions;
|
||||
}
|
||||
|
||||
export interface PopupEntrypoint extends BaseEntrypoint {
|
||||
type: 'popup';
|
||||
options: {
|
||||
/**
|
||||
* Defaults to "browser_action" to be equivalent to MV3's "action" key
|
||||
*/
|
||||
mv2Key?: 'browser_action' | 'page_action';
|
||||
defaultIcon?: Record<string, string>;
|
||||
defaultTitle?: string;
|
||||
browserStyle?: boolean;
|
||||
} & BaseEntrypointOptions;
|
||||
}
|
||||
|
||||
export interface OptionsEntrypoint extends BaseEntrypoint {
|
||||
type: 'options';
|
||||
options: {
|
||||
openInTab?: boolean;
|
||||
browserStyle?: boolean;
|
||||
chromeStyle?: boolean;
|
||||
} & BaseEntrypointOptions;
|
||||
}
|
||||
|
||||
export type Entrypoint =
|
||||
| GenericEntrypoint
|
||||
| BackgroundEntrypoint
|
||||
| ContentScriptEntrypoint
|
||||
| PopupEntrypoint
|
||||
| OptionsEntrypoint;
|
||||
|
||||
export type EntrypointGroup = Entrypoint | Entrypoint[];
|
||||
|
||||
export type OnContentScriptStopped = (cb: () => void) => void;
|
||||
|
||||
export type ContentScriptDefinition =
|
||||
| ContentScriptIsolatedWorldDefinition
|
||||
| ContentScriptMainWorldDefinition;
|
||||
|
||||
export interface ContentScriptIsolatedWorldDefinition
|
||||
extends ContentScriptBaseDefinition {
|
||||
/**
|
||||
* See https://developer.chrome.com/docs/extensions/develop/concepts/content-scripts#isolated_world
|
||||
* @default "ISOLATED"
|
||||
*/
|
||||
world?: 'ISOLATED';
|
||||
/**
|
||||
* Main function executed when the content script is loaded.
|
||||
*/
|
||||
main(ctx: ContentScriptContext): void | Promise<void>;
|
||||
}
|
||||
|
||||
export interface ContentScriptMainWorldDefinition
|
||||
extends ContentScriptBaseDefinition {
|
||||
/**
|
||||
* See https://developer.chrome.com/docs/extensions/develop/concepts/content-scripts#isolated_world
|
||||
*/
|
||||
world: 'MAIN';
|
||||
/**
|
||||
* Main function executed when the content script is loaded.
|
||||
*/
|
||||
main(): void | Promise<void>;
|
||||
}
|
||||
|
||||
export interface ContentScriptBaseDefinition extends ExcludableEntrypoint {
|
||||
export interface BaseContentScriptEntrypointOptions
|
||||
extends BaseEntrypointOptions {
|
||||
matches: PerBrowserOption<Manifest.ContentScript['matches']>;
|
||||
/**
|
||||
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
|
||||
@@ -581,44 +502,207 @@ export interface ContentScriptBaseDefinition extends ExcludableEntrypoint {
|
||||
* onto the page. Use `browser.runtime.getURL("content-scripts/<name>.css")` to get the file's
|
||||
* URL
|
||||
* - `"ui"` - Exclude the CSS from the manifest. CSS will be automatically added to your UI when
|
||||
* calling `createContentScriptUi`
|
||||
* calling `createShadowRootUi`
|
||||
*
|
||||
* @default "manifest"
|
||||
*/
|
||||
cssInjectionMode?: PerBrowserOption<'manifest' | 'manual' | 'ui'>;
|
||||
/**
|
||||
* Specify how the content script is registered.
|
||||
*
|
||||
* - `"manifest"`: The content script will be added to the `content_scripts` entry in the
|
||||
* manifest. This is the normal and most well known way of registering a content script.
|
||||
* - `"runtime"`: The content script's `matches` is added to `host_permissions` and you are
|
||||
* responsible for using the scripting API to register the content script dynamically at
|
||||
* runtime.
|
||||
*
|
||||
* @default "manifest"
|
||||
*/
|
||||
registration?: PerBrowserOption<'manifest' | 'runtime'>;
|
||||
}
|
||||
|
||||
export interface BackgroundDefinition extends ExcludableEntrypoint {
|
||||
type?: PerBrowserOption<'module'>;
|
||||
persistent?: PerBrowserOption<boolean>;
|
||||
export interface MainWorldContentScriptEntrypointOptions
|
||||
extends BaseContentScriptEntrypointOptions {
|
||||
/**
|
||||
* See https://developer.chrome.com/docs/extensions/develop/concepts/content-scripts#isolated_world
|
||||
*/
|
||||
world: 'MAIN';
|
||||
}
|
||||
|
||||
export interface IsolatedWorldContentScriptEntrypointOptions
|
||||
extends BaseContentScriptEntrypointOptions {
|
||||
/**
|
||||
* See https://developer.chrome.com/docs/extensions/develop/concepts/content-scripts#isolated_world
|
||||
* @default "ISOLATED"
|
||||
*/
|
||||
world?: 'ISOLATED';
|
||||
}
|
||||
|
||||
export interface PopupEntrypointOptions extends BaseEntrypointOptions {
|
||||
/**
|
||||
* Defaults to "browser_action" to be equivalent to MV3's "action" key
|
||||
*/
|
||||
mv2Key?: PerBrowserOption<'browser_action' | 'page_action'>;
|
||||
defaultIcon?: Record<string, string>;
|
||||
defaultTitle?: PerBrowserOption<string>;
|
||||
browserStyle?: PerBrowserOption<boolean>;
|
||||
}
|
||||
|
||||
export interface OptionsEntrypointOptions extends BaseEntrypointOptions {
|
||||
openInTab?: PerBrowserOption<boolean>;
|
||||
browserStyle?: PerBrowserOption<boolean>;
|
||||
chromeStyle?: PerBrowserOption<boolean>;
|
||||
}
|
||||
|
||||
export interface SidepanelEntrypointOptions extends BaseEntrypointOptions {
|
||||
/**
|
||||
* Firefox only. See https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/sidebar_action#syntax
|
||||
* @default false
|
||||
*/
|
||||
openAtInstall?: PerBrowserOption<boolean>;
|
||||
/**
|
||||
* @deprecated See https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/sidebar_action#syntax
|
||||
*/
|
||||
browserStyle?: PerBrowserOption<boolean>;
|
||||
defaultIcon?: string | Record<string, string>;
|
||||
defaultTitle?: PerBrowserOption<string>;
|
||||
}
|
||||
|
||||
export interface BaseEntrypoint {
|
||||
/**
|
||||
* The entrypoint's name. This is the filename or dirname without the type suffix.
|
||||
*
|
||||
* Examples:
|
||||
* - `popup.html` → `popup`
|
||||
* - `options/index.html` → `options`
|
||||
* - `named.sandbox.html` → `named`
|
||||
* - `named.sandbox/index.html` → `named`
|
||||
* - `sandbox.html` → `sandbox`
|
||||
* - `sandbox/index.html` → `sandbox`
|
||||
* - `overlay.content.ts` → `overlay`
|
||||
* - `overlay.content/index.ts` → `overlay`
|
||||
*
|
||||
* The name is used when generating an output file:
|
||||
* `<entrypoint.outputDir>/<entrypoint.name>.<ext>`
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Absolute path to the entrypoint's input file.
|
||||
*/
|
||||
inputPath: string;
|
||||
/**
|
||||
* Absolute path to the entrypoint's output directory. Can be the`InternalConfg.outDir` or a
|
||||
* subdirectory of it.
|
||||
*/
|
||||
outputDir: string;
|
||||
skipped: boolean;
|
||||
}
|
||||
|
||||
export interface GenericEntrypoint extends BaseEntrypoint {
|
||||
type:
|
||||
| 'sandbox'
|
||||
| 'bookmarks'
|
||||
| 'history'
|
||||
| 'newtab'
|
||||
| 'devtools'
|
||||
| 'unlisted-page'
|
||||
| 'unlisted-script'
|
||||
| 'unlisted-style'
|
||||
| 'content-script-style';
|
||||
options: ResolvedPerBrowserOptions<BaseEntrypointOptions>;
|
||||
}
|
||||
|
||||
export interface BackgroundEntrypoint extends BaseEntrypoint {
|
||||
type: 'background';
|
||||
options: ResolvedPerBrowserOptions<BackgroundEntrypointOptions>;
|
||||
}
|
||||
|
||||
export interface ContentScriptEntrypoint extends BaseEntrypoint {
|
||||
type: 'content-script';
|
||||
options: ResolvedPerBrowserOptions<
|
||||
| MainWorldContentScriptEntrypointOptions
|
||||
| IsolatedWorldContentScriptEntrypointOptions
|
||||
>;
|
||||
}
|
||||
|
||||
export interface PopupEntrypoint extends BaseEntrypoint {
|
||||
type: 'popup';
|
||||
options: ResolvedPerBrowserOptions<PopupEntrypointOptions, 'defaultIcon'>;
|
||||
}
|
||||
|
||||
export interface OptionsEntrypoint extends BaseEntrypoint {
|
||||
type: 'options';
|
||||
options: ResolvedPerBrowserOptions<OptionsEntrypointOptions>;
|
||||
}
|
||||
|
||||
export interface SidepanelEntrypoint extends BaseEntrypoint {
|
||||
type: 'sidepanel';
|
||||
options: ResolvedPerBrowserOptions<SidepanelEntrypointOptions, 'defaultIcon'>;
|
||||
}
|
||||
|
||||
export type Entrypoint =
|
||||
| GenericEntrypoint
|
||||
| BackgroundEntrypoint
|
||||
| ContentScriptEntrypoint
|
||||
| PopupEntrypoint
|
||||
| OptionsEntrypoint
|
||||
| SidepanelEntrypoint;
|
||||
|
||||
export type EntrypointGroup = Entrypoint | Entrypoint[];
|
||||
|
||||
export type OnContentScriptStopped = (cb: () => void) => void;
|
||||
|
||||
export interface IsolatedWorldContentScriptDefinition
|
||||
extends IsolatedWorldContentScriptEntrypointOptions {
|
||||
/**
|
||||
* Main function executed when the content script is loaded.
|
||||
*/
|
||||
main(ctx: ContentScriptContext): void | Promise<void>;
|
||||
}
|
||||
|
||||
export interface MainWorldContentScriptDefinition
|
||||
extends MainWorldContentScriptEntrypointOptions {
|
||||
/**
|
||||
* Main function executed when the content script is loaded.
|
||||
*/
|
||||
main(): void | Promise<void>;
|
||||
}
|
||||
|
||||
export type ContentScriptDefinition =
|
||||
| IsolatedWorldContentScriptDefinition
|
||||
| MainWorldContentScriptDefinition;
|
||||
|
||||
export interface BackgroundDefinition extends BackgroundEntrypointOptions {
|
||||
/**
|
||||
* Main function executed when the background script is started. Cannot be async.
|
||||
*/
|
||||
main(): void;
|
||||
}
|
||||
|
||||
export interface UnlistedScriptDefinition extends ExcludableEntrypoint {
|
||||
export interface UnlistedScriptDefinition extends BaseEntrypointOptions {
|
||||
/**
|
||||
* Main function executed when the unlisted script is ran.
|
||||
*/
|
||||
main(): void | Promise<void>;
|
||||
}
|
||||
|
||||
export type PerBrowserOption<T> = T | { [browser: TargetBrowser]: T };
|
||||
/**
|
||||
* Either a single value or a map of different browsers to the value for that browser.
|
||||
*/
|
||||
export type PerBrowserOption<T> = T | PerBrowserMap<T>;
|
||||
export type PerBrowserMap<T> = { [browser: TargetBrowser]: T };
|
||||
|
||||
export interface ExcludableEntrypoint {
|
||||
/**
|
||||
* List of target browsers to include this entrypoint in. Defaults to being included in all
|
||||
* builds. Cannot be used with `exclude`. You must choose one of the two options.
|
||||
*
|
||||
* @default undefined
|
||||
*/
|
||||
include?: TargetBrowser[];
|
||||
/**
|
||||
* List of target browsers to exclude this entrypoint from. Cannot be used with `include`. You
|
||||
* must choose one of the two options.
|
||||
*
|
||||
* @default undefined
|
||||
*/
|
||||
exclude?: TargetBrowser[];
|
||||
}
|
||||
/**
|
||||
* Convert `{ key: PerBrowserOption<T> }` to just `{ key: T }`, stripping away the
|
||||
* `PerBrowserOption` type for all fields inside the object.
|
||||
*
|
||||
* A optional second list of keys can be passed if a field isn't compatible with `PerBrowserOption`, like `defaultIcon`.
|
||||
*/
|
||||
export type ResolvedPerBrowserOptions<T, TOmitted extends keyof T = never> = {
|
||||
[key in keyof Omit<T, TOmitted>]: T[key] extends PerBrowserOption<infer U>
|
||||
? U
|
||||
: T[key];
|
||||
} & { [key in TOmitted]: T[key] };
|
||||
|
||||
/**
|
||||
* Manifest customization available in the `wxt.config.ts` file. You cannot configure entrypoints
|
||||
@@ -634,8 +718,6 @@ export type UserManifest = Partial<
|
||||
| 'options_page'
|
||||
| 'options_ui'
|
||||
| 'sandbox'
|
||||
| 'sidepanel'
|
||||
| 'sidebar_action'
|
||||
>
|
||||
>;
|
||||
|
||||
@@ -884,7 +966,7 @@ export interface ResolvedConfig {
|
||||
manifestVersion: TargetManifestVersion;
|
||||
env: ConfigEnv;
|
||||
logger: Logger;
|
||||
imports: false | Partial<UnimportOptions>;
|
||||
imports: false | WxtResolvedUnimportOptions;
|
||||
manifest: UserManifest;
|
||||
fsCache: FsCache;
|
||||
server?: WxtDevServer;
|
||||
@@ -901,8 +983,13 @@ export interface ResolvedConfig {
|
||||
analysis: {
|
||||
enabled: boolean;
|
||||
template: NonNullable<PluginVisualizerOptions['template']>;
|
||||
/** Absolute file path */
|
||||
/** Absolute file path to the `stats.html` file */
|
||||
outputFile: string;
|
||||
/** The directory where the final `stats.html` file is located */
|
||||
outputDir: string;
|
||||
/** Name of the `stats.html` file, minus ".html" */
|
||||
outputName: string;
|
||||
keepArtifacts: boolean;
|
||||
};
|
||||
userConfigMetadata: Omit<C12ResolvedConfig<UserConfig>, 'config'>;
|
||||
/**
|
||||
@@ -934,3 +1021,51 @@ export type VirtualEntrypointType =
|
||||
| 'content-script-isolated-world'
|
||||
| 'background'
|
||||
| 'unlisted-script';
|
||||
|
||||
export type EslintGlobalsPropValue =
|
||||
| boolean
|
||||
| 'readonly'
|
||||
| 'readable'
|
||||
| 'writable'
|
||||
| 'writeable';
|
||||
|
||||
export interface Eslintrc {
|
||||
/**
|
||||
* When true, generates a file that can be used by ESLint to know which variables are valid globals.
|
||||
*
|
||||
* - `'auto'`: Check if eslint is installed, and if it is, generate the helper file
|
||||
* - `true`: Generate the helper file
|
||||
* - `false`: Don't generate the file
|
||||
*
|
||||
* @default 'auto'
|
||||
*/
|
||||
enabled?: boolean | 'auto';
|
||||
/**
|
||||
* File path to save the generated eslint config.
|
||||
*
|
||||
* @default './.wxt/eslintrc-auto-import.json'
|
||||
*/
|
||||
filePath?: string;
|
||||
/**
|
||||
* @default true
|
||||
*/
|
||||
globalsPropValue?: EslintGlobalsPropValue;
|
||||
}
|
||||
|
||||
export interface ResolvedEslintrc {
|
||||
enabled: boolean;
|
||||
/** Absolute path */
|
||||
filePath: string;
|
||||
globalsPropValue: EslintGlobalsPropValue;
|
||||
}
|
||||
|
||||
export type WxtUnimportOptions = Partial<UnimportOptions> & {
|
||||
/**
|
||||
* When eslint is installed,
|
||||
*/
|
||||
eslintrc?: Eslintrc;
|
||||
};
|
||||
|
||||
export type WxtResolvedUnimportOptions = Partial<UnimportOptions> & {
|
||||
eslintrc: ResolvedEslintrc;
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ lerna-debug.log*
|
||||
node_modules
|
||||
.output
|
||||
stats.html
|
||||
stats-*.json
|
||||
.wxt
|
||||
web-ext.config.ts
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ lerna-debug.log*
|
||||
node_modules
|
||||
.output
|
||||
stats.html
|
||||
stats-*.json
|
||||
.wxt
|
||||
web-ext.config.ts
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ lerna-debug.log*
|
||||
node_modules
|
||||
.output
|
||||
stats.html
|
||||
stats-*.json
|
||||
.wxt
|
||||
web-ext.config.ts
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ lerna-debug.log*
|
||||
node_modules
|
||||
.output
|
||||
stats.html
|
||||
stats-*.json
|
||||
.wxt
|
||||
web-ext.config.ts
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ lerna-debug.log*
|
||||
node_modules
|
||||
.output
|
||||
stats.html
|
||||
stats-*.json
|
||||
.wxt
|
||||
web-ext.config.ts
|
||||
|
||||
|
||||
Reference in New Issue
Block a user