Compare commits

..

2 Commits

Author SHA1 Message Date
Aaron Klinker fa335c4cea Bump version for testing 2023-12-29 00:51:21 -06:00
Aaron Klinker 84e136a54d fix: Ship vite/client types internally for proper import.meta.env types 2023-12-29 00:50:57 -06:00
34 changed files with 1153 additions and 1611 deletions
+6 -14
View File
@@ -12,22 +12,14 @@ A clear and concise description of what the bug is.
### To Reproduce
Share minimal reproduction. Examples of a minimal reproduction:
Upload a ZIP or share a link to a repo representing the minimal reproduction. **_If you don't upload a minimal reproduction, you bug report will be closed._**
- PR with a failing test case
- ZIP file containing a minimal WXT project (be sure to include your lockfile!!)
Steps to reproduce the behavior:
> **_⚠️ If you don't upload a minimal reproduction, your issue will be closed until a reproduction is added._**
>
> Why? https://antfu.me/posts/why-reproductions-are-required
Steps to reproduce the bug using the reproduction:
1. Install dependencies: `pnpm i`
2. Start dev mode: `pnpm dev`
3. Click this...
4. Do that...
5. Etc...
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
### Expected behavior
-87
View File
@@ -1,92 +1,5 @@
# Changelog
## v0.13.5
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.13.4...v0.13.5)
### 🩹 Fixes
- Strip path from `web_accessible_resources[0].matches` ([#332](https://github.com/wxt-dev/wxt/pull/332))
### 📖 Documentation
- Add section about customizing other browser options during development ([8683bd4](https://github.com/wxt-dev/wxt/commit/8683bd4))
### 🏡 Chore
- Update bug report template ([9a2cc18](https://github.com/wxt-dev/wxt/commit/9a2cc18))
## v0.13.4
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.13.3...v0.13.4)
### 🩹 Fixes
- Disable minification during development ([b7cdf15](https://github.com/wxt-dev/wxt/commit/b7cdf15))
### 🏡 Chore
- Use `const` instead of `let` ([2770974](https://github.com/wxt-dev/wxt/commit/2770974))
## v0.13.3
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.13.2...v0.13.3)
### 🚀 Enhancements
- **DX:** Add `ctrl+E`/`cmd+E` shortcut to reload extension during development ([#322](https://github.com/wxt-dev/wxt/pull/322))
### 🏡 Chore
- **deps-dev:** Bump tsx from 4.6.2 to 4.7.0 ([#320](https://github.com/wxt-dev/wxt/pull/320))
- **deps-dev:** Bump prettier from 3.1.0 to 3.1.1 ([#318](https://github.com/wxt-dev/wxt/pull/318))
- **deps-dev:** Bump vitepress from 1.0.0-rc.31 to 1.0.0-rc.34 ([#316](https://github.com/wxt-dev/wxt/pull/316))
- Refactor manifest generation E2E tests to unit tests ([#323](https://github.com/wxt-dev/wxt/pull/323))
## v0.13.2
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.13.1...v0.13.2)
### 🚀 Enhancements
- Add `isolateEvents` option to `createContentScripUi` ([#313](https://github.com/wxt-dev/wxt/pull/313))
### 📖 Documentation
- Remove duplicate `entrypoints/` path ([76e63e2](https://github.com/wxt-dev/wxt/commit/76e63e2))
- Update unlisted pages/scripts description ([c99a281](https://github.com/wxt-dev/wxt/commit/c99a281))
- Update content script entrypoint docs ([1360eb7](https://github.com/wxt-dev/wxt/commit/1360eb7))
- Add example for setting up custom panels/panes in devtools ([#308](https://github.com/wxt-dev/wxt/pull/308))
- Use example tags to automate relevant example lists ([#311](https://github.com/wxt-dev/wxt/pull/311))
### 🏡 Chore
- Update templates to `^0.13.0` ([#309](https://github.com/wxt-dev/wxt/pull/309))
- Upgrade template dependencies ([#310](https://github.com/wxt-dev/wxt/pull/310))
- Re-enable coverage ([#312](https://github.com/wxt-dev/wxt/pull/312))
### ❤️ Contributors
- 冯不游
## v0.13.1
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.13.0...v0.13.1)
### 🩹 Fixes
- **storage:** Support multiple `:` characters in storage keys ([#303](https://github.com/wxt-dev/wxt/pull/303))
- Ship `vite/client` types internally for proper resolution using PNPM ([#304](https://github.com/wxt-dev/wxt/pull/304))
### 📖 Documentation
- Reorder guide ([6421ab3](https://github.com/wxt-dev/wxt/commit/6421ab3))
- General fixes and improvements ([2ad099b](https://github.com/wxt-dev/wxt/commit/2ad099b))
### 🏡 Chore
- Update `scripts/build.ts` show current build step in progress, not completed count ([#306](https://github.com/wxt-dev/wxt/pull/306))
## v0.13.0
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.12.5...v0.13.0)
+3 -2
View File
@@ -9,8 +9,9 @@
<a href="https://www.npmjs.com/package/wxt" target="_blank"><img alt="npm" src="https://img.shields.io/npm/dm/wxt?labelColor=black&color=%234fa048"></a>
<span> </span>
<a href="https://github.com/wxt-dev/wxt/blob/main/LICENSE" target="_blank"><img alt="NPM" src="https://img.shields.io/npm/l/wxt?labelColor=black&color=%234fa048"></a>
<span> </span>
<a href="https://codecov.io/github/wxt-dev/wxt" target="_blank"><img alt="Codecov" src="https://img.shields.io/codecov/c/github/wxt-dev/wxt?labelColor=black&color=%234fa048"></a>
<!-- Hide code coverage while it's broken -->
<!-- <span> </span>
<a href="https://codecov.io/github/wxt-dev/wxt" target="_blank"><img alt="Codecov" src="https://img.shields.io/codecov/c/github/wxt-dev/wxt?labelColor=black&color=%234fa048"></a> -->
</p>
<p align="center">
@@ -1,34 +0,0 @@
<script lang="ts" setup>
import { ref, onMounted, computed } from 'vue';
const props = defineProps<{
tag?: string;
}>();
const examples = ref();
onMounted(async () => {
const res = await fetch(
'https://raw.githubusercontent.com/wxt-dev/wxt-examples/main/examples.json',
);
examples.value = await res.json();
});
const filteredExamples = computed(() => {
if (props.tag == null) return examples.value;
return examples.value.filter((example) => {
return example.tags?.includes(props.tag);
});
});
</script>
<template>
<ul>
<li v-if="examples == null">Loading...</li>
<template v-else>
<li v-for="example of filteredExamples">
<a :href="example.url" target="_blank">{{ example.name }}</a>
</li>
</template>
</ul>
</template>
@@ -5,10 +5,6 @@ import useListExtensionDetails, {
} from '../composables/useListExtensionDetails';
// Add extension IDs here. Order doesn't matter, will be sorted by weekly active users
// During the transition from chrome.google.com/webstore to
// chromewebstore.google.com, queue.wxt.dev might return null for your
// extension. If it does, use "<slug>/<id>" instead of just the ID. The slug
// can be retrieved from the URL of the item on chromewebstore.google.com
const chromeExtensionIds = [
'ocfdgncpifmegplaglcnglhioflaimkd', // GitHub: Better Line Counts
'mgmdkjcljneegjfajchedjpdhbadklcf', // Anime Skip Player
@@ -16,7 +12,6 @@ const chromeExtensionIds = [
'elfaihghhjjoknimpccccmkioofjjfkf', // StayFree - Website Blocker & Web Analytics
'okifoaikfmpfcamplcfjkpdnhfodpkil', // Doozy: Ai Made Easy
'lknmjhcajhfbbglglccadlfdjbaiifig', // tl;dv - Record, Transcribe & ChatGPT for Google Meet
'youtube中文配音/oglffgiaiekgeicdgkdlnlkhliajdlja', // Youtube中文配音
];
const { data } = useListExtensionDetails(chromeExtensionIds);
@@ -29,7 +24,6 @@ const sortedExtensions = computed(() => {
// Sort based on the user count weighted by the rating
sortKey: ((item.rating ?? 5) / 5) * item.weeklyActiveUsers,
}))
.filter((item) => !!item)
.sort((l, r) => r.sortKey - l.sortKey);
});
-2
View File
@@ -2,7 +2,6 @@ import DefaultTheme from 'vitepress/theme';
import Icon from '../components/Icon.vue';
import EntrypointPatterns from '../components/EntrypointPatterns.vue';
import UsingWxtSection from '../components/UsingWxtSection.vue';
import ExampleList from '../components/ExampleList.vue';
import './custom.css';
export default {
@@ -11,6 +10,5 @@ export default {
ctx.app.component('Icon', Icon);
ctx.app.component('EntrypointPatterns', EntrypointPatterns);
ctx.app.component('UsingWxtSection', UsingWxtSection);
ctx.app.component('ExampleList', ExampleList);
},
};
+2 -4
View File
@@ -20,7 +20,7 @@ When creating content script entrypoints, they are automatically included in the
```ts
export default defineContentScript({
// Set manifest options
matches: string[],
matches: ['*://google.com/*', '*://duckduckgo.com/*'],
excludeMatches: undefined | [],
includeGlobs: undefined | [],
excludeGlobs: undefined | [],
@@ -90,7 +90,7 @@ To include CSS with your content script, import the CSS file at the top of your
```
<srcDir>/
<srcDir>
└─ entrypoints/
└─ overlay.content/
├─ index.ts
@@ -137,5 +137,3 @@ export default defineContentScript({
},
});
```
See [Content Script UI](/guide/content-script-ui) for more info on creating UIs and including CSS in content scripts.
+3 -10
View File
@@ -13,12 +13,15 @@
## Definition
Plain old HTML file.
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Title</title>
<!-- Set include/exclude if the page should be removed from some builds -->
<meta name="manifest.include" content="['chrome', ...]" />
<meta name="manifest.exclude" content="['chrome', ...]" />
@@ -28,13 +31,3 @@
</body>
</html>
```
## Adding UI Elements
Chrome extensions allow you to add panels and side panes to the devtools window.
![DevTools window showing Elements panel and Styles sidebar pane.](https://developer.chrome.com/static/docs/extensions/how-to/devtools/extend-devtools/image/devtools-window-showing-e-9051f7f0347cd_1920.png)
See the WXT's examples for a full walkthrough of extending the devtools window:
<ExampleList tag="devtools" />
+4 -4
View File
@@ -10,10 +10,10 @@ Firefox does not support sandboxed pages.
<EntrypointPatterns
:patterns="[
['sandbox.html', 'sandbox.html'],
['sandbox/index.html', 'sandbox.html'],
['<name>.sandbox.html', '<name>.html` '],
['<name>.sandbox/index.html', '<name>.html` '],
['entrypoints/sandbox.html', 'sandbox.html'],
['entrypoints/sandbox/index.html', 'sandbox.html'],
['entrypoints/<name>.sandbox.html', '<name>.html` '],
['entrypoints/<name>.sandbox/index.html', '<name>.html` '],
]"
/>
+4 -4
View File
@@ -10,10 +10,10 @@ Chrome added support for sidepanels in Manifest V3, they are not available in Ma
<EntrypointPatterns
:patterns="[
['sidepanel.html', 'sidepanel.html'],
['sidepanel/index.html', 'sidepanel.html'],
['<name>.sidepanel.html', '<name>.html` '],
['<name>.sidepanel/index.html', '<name>.html` '],
['entrypoints/sidepanel.html', 'sidepanel.html'],
['entrypoints/sidepanel/index.html', 'sidepanel.html'],
['entrypoints/<name>.sidepanel.html', '<name>.html` '],
['entrypoints/<name>.sidepanel/index.html', '<name>.html` '],
]"
/>
+1 -1
View File
@@ -1,6 +1,6 @@
# Unlisted Pages
HTML pages that are bundled and shipped with the extension, but are not included in the manifest.
HTML pages that are built by Vite, but are not included in the manifest.
### Examples
+1 -1
View File
@@ -1,6 +1,6 @@
# Unlisted Scripts
TypeScript files that are bundled and shipped with the extension, but are not included in the manifest.
TypeScript files that are built, but are not included in the manifest.
You are responsible for loading/running these scripts where needed.
+21 -1
View File
@@ -2,6 +2,26 @@
Simple walkthroughs to accomplish common tasks or patterns with WXT.
<ExampleList />
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
const examples = ref()
onMounted(async () => {
const res = await fetch("https://raw.githubusercontent.com/wxt-dev/wxt-examples/main/examples.json");
examples.value = await res.json();
})
</script>
<ul>
<li v-if="examples == null">
Loading...
</li>
<template v-else>
<li v-for="example of examples">
<a :href="example.url" target="_blank">{{ example.name }}</a>
</li>
</template>
</ul>
> Full code available at [`wxt-dev/wxt-examples`](https://github.com/wxt-dev/wxt-examples)
+6 -6
View File
@@ -6,11 +6,11 @@ There are three ways to mount a UI inside a content script:
Each has their own set of advantages and disadvantages.
| Method | Isolated Styles | Isolated Events | HMR | Use page's context |
| ---------- | :-------------: | :-----------------: | :-: | :----------------: |
| Integrated | ❌ | ❌ | ❌ | ✅ |
| ShadowRoot | ✅ | ✅ (off by default) | ❌ | ✅ |
| IFrame | ✅ | ✅ | ✅ | ❌ |
| Method | Isolated Styles | HMR | Use page's context |
| ---------- | :-------------: | :-: | :----------------: |
| Integrated | ❌ | ❌ | ✅ |
| ShadowRoot | ✅ | ❌ | ✅ |
| IFrame | ✅ | ✅ | ❌ |
## Integrated
@@ -149,7 +149,7 @@ export default defineContentScript({
Often in web extensions, you don't want your content script's CSS affecting the page, or vise-versa. The [`ShadowRoot`](https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot) API is ideal for this.
WXT provides a helper function, [`createContentScriptUi`](/api/wxt/client/functions/createContentScriptUi), that abstracts all the `ShadowRoot` setup away, making it easy to create UIs with isolated CSS. It also supports an optional `isolateEvents` parameter to further isolate user interactions.
WXT provides a helper function, [`createContentScriptUi`](/api/wxt/client/functions/createContentScriptUi), that abstracts all the `ShadowRoot` setup away, making it easy to create UIs with isolated CSS.
To use `createContentScriptUi`, follow these steps:
-29
View File
@@ -39,8 +39,6 @@ export default defineRunnerConfig({
:::
### Browser Binaries
`web-ext`'s browser discovery is very limitted. By default, it only guesses at where Chrome and Firefox are installed. If you've customized your install locations, you may need to tell `web-ext` where the binaries/executables are located using the [`binaries` option](/api/wxt/interfaces/ExtensionRunnerConfig#binaries). For other Chromium based browsers, like Edge or Opera, you'll need to explicitly list them in the `binaries` option as well, otherwise they will open in Chrome by default.
```ts
@@ -59,30 +57,3 @@ export default defineRunnerConfig({
:::tip
When configuring browser binaries, it's helpful to put them in `~/web-ext.config.ts` instead of the project directory's `web-ext.config.ts` file. When placed in your home directory (`~/`), this config will be used by all WXT projects, so you only need to configure the binaries once.
:::
### Other options
You can customize other options as well, like startup URLs, profiles, or additional command line arguments:
```ts
// web-ext.config.ts
import { defineRunnerConfig } from 'wxt';
export default defineRunnerConfig({
startUrls: ['https://google.com', 'https://duckduckgo.com'],
chromiumProfile: '/path/to/profile/to/use',
chromiumArgs: ['--window-size=400x300'],
});
```
For a full list of options, see the [API Reference](/api/wxt/interfaces/ExtensionRunnerConfig).
## Reload the Extension
Normally, to manually reload an extension, you have to visit `chrome://extensions` and click the reload button for your 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
This shortcut is only available during development, and is not be added to your extension when running `wxt build` or `wxt-zip`.
:::
+4 -5
View File
@@ -26,13 +26,11 @@ If not provided via the `manifest` config, the [manifest's `name`](https://devel
## `version` and `version_name`
The [manifest's `version` and `version_name`](https://developer.chrome.com/docs/extensions/mv3/manifest/version/) properties are based on the `version` field listed in your `package.json` or `wxt.config.ts`.
The [manifest's `version` and `version_name`](https://developer.chrome.com/docs/extensions/mv3/manifest/version/) fields are based on your package.json's `version` property.
- `version_name` is the exact string listed in your `package.json` or `wxt.config.ts` file
- `version_name` is the exact string listed in your package.json
- `version` is the string cleaned up, with any invalid suffixes removed
If a version is not found, a warning is logged and the version defaults to `"0.0.0"`.
### Example
```json
@@ -124,4 +122,5 @@ export default defineConfig({
See the official localization examples for more details:
<ExampleList tag="i18n" />
- [I18n](https://github.com/wxt-dev/wxt-examples/tree/main/examples/vanilla-i18n#readme)
- [Vue I18n](https://github.com/wxt-dev/wxt-examples/tree/main/examples/vue-i18n#readme)
+3 -1
View File
@@ -6,7 +6,9 @@ WXT officially supports [Vitest](https://vitest.dev/) for unit tests and either
For details setting up each testing framework, see the official examples:
<ExampleList tag="testing" />
- [Vitest example](https://github.com/wxt-dev/wxt-examples/tree/main/examples/vanilla-vitest#readme)
- [Playwright example](https://github.com/wxt-dev/wxt-examples/tree/main/examples/vanilla-playwright#readme)
- [Puppeteer example](https://github.com/wxt-dev/wxt-examples/tree/main/examples/vanilla-puppeteer#readme)
### Unofficial Frameworks
+640
View File
@@ -1,7 +1,556 @@
import { describe, it, expect } from 'vitest';
import { TestProject } from '../utils';
// TODO: move to unit tests to speed this up - this doesn't have to be in E2E tests
describe('Manifest Content', () => {
describe('popup', () => {
const popupContent = (type?: 'browser_action' | 'page_action') => `
<html>
<head>
${type == null ? '' : `<meta name="manifest.type" content="${type}">`}
<meta name="manifest.default_icon" content="{ '16': '/icon/16.png' }">
<title>Default Title</title>
</head>
</html>
`;
it('should include an action for mv3', async () => {
const project = new TestProject();
project.addFile('entrypoints/popup.html', popupContent());
await project.build();
const manifest = await project.getOutputManifest();
expect(manifest.action).toEqual({
default_icon: { '16': '/icon/16.png' },
default_title: 'Default Title',
default_popup: 'popup.html',
});
});
it.each([
[undefined, 'browser_action'],
['browser_action', 'browser_action'],
['page_action', 'page_action'],
] as const)(
'should include a browser_action for mv2',
async (type, expectedType) => {
const project = new TestProject();
project.addFile('entrypoints/popup.html', popupContent(type));
await project.build({ manifestVersion: 2 });
const manifest = await project.getOutputManifest(
'.output/chrome-mv2/manifest.json',
);
expect(manifest[expectedType]).toEqual({
default_icon: { '16': '/icon/16.png' },
default_title: 'Default Title',
default_popup: 'popup.html',
});
},
);
});
describe('options', () => {
const optionsContent = `
<html>
<head>
<meta name="manifest.open_in_tab" content="false">
<meta name="manifest.chrome_style" content="true">
<meta name="manifest.browser_style" content="true">
</head>
</html>
`;
it('should include a options_ui and chrome_style for chrome', async () => {
const project = new TestProject();
project.addFile('entrypoints/options.html', optionsContent);
await project.build();
const manifest = await project.getOutputManifest();
expect(manifest.options_ui).toEqual({
open_in_tab: false,
chrome_style: true,
page: 'options.html',
});
});
it('should include a options_ui and browser_style for firefox', async () => {
const project = new TestProject();
project.addFile('entrypoints/options.html', optionsContent);
await project.build({ browser: 'firefox' });
const manifest = await project.getOutputManifest(
'.output/firefox-mv2/manifest.json',
);
expect(manifest.options_ui).toEqual({
open_in_tab: false,
browser_style: true,
page: 'options.html',
});
});
});
describe('background', () => {
const backgroundContent = `
export default defineBackground({
persistent: true,
type: "module",
main: () => {},
})
`;
it.each(['chrome', 'safari'])(
'should include scripts and persistent for %s mv2',
async (browser) => {
const project = new TestProject();
project.addFile('entrypoints/background.ts', backgroundContent);
await project.build({ browser, manifestVersion: 2 });
const manifest = await project.getOutputManifest(
`.output/${browser}-mv2/manifest.json`,
);
expect(manifest.background).toEqual({
persistent: true,
scripts: ['background.js'],
});
},
);
it.each(['chrome', 'safari'])(
'should include a service worker and type for %s mv3',
async (browser) => {
const project = new TestProject();
project.addFile('entrypoints/background.ts', backgroundContent);
await project.build({ browser, manifestVersion: 3 });
const manifest = await project.getOutputManifest(
`.output/${browser}-mv3/manifest.json`,
);
expect(manifest.background).toEqual({
type: 'module',
service_worker: 'background.js',
});
},
);
it('should include a background script and type for firefox mv3', async () => {
const project = new TestProject();
project.addFile('entrypoints/background.ts', backgroundContent);
await project.build({ browser: 'firefox', manifestVersion: 3 });
const manifest = await project.getOutputManifest(
'.output/firefox-mv3/manifest.json',
);
expect(manifest.background).toEqual({
type: 'module',
scripts: ['background.js'],
});
});
it('should include a background script and persistent for firefox mv2', async () => {
const project = new TestProject();
project.addFile('entrypoints/background.ts', backgroundContent);
await project.build({ browser: 'firefox', manifestVersion: 2 });
const manifest = await project.getOutputManifest(
'.output/firefox-mv2/manifest.json',
);
expect(manifest.background).toEqual({
persistent: true,
scripts: ['background.js'],
});
});
});
describe('icons', () => {
it('should auto-discover icons with the correct name', async () => {
const project = new TestProject();
project.addFile('entrypoints/unlisted.html');
project.addFile('public/icon-16.png');
project.addFile('public/icon/32.png');
project.addFile('public/icon@48w.png');
project.addFile('public/icon-64x64.png');
project.addFile('public/icon@96.png');
project.addFile('public/icons/128x128.png');
await project.build();
const manifest = await project.getOutputManifest();
expect(manifest.icons).toEqual({
'16': 'icon-16.png',
'32': 'icon/32.png',
'48': 'icon@48w.png',
'64': 'icon-64x64.png',
'96': 'icon@96.png',
'128': 'icons/128x128.png',
});
});
it('should return undefined when no icons are found', async () => {
const project = new TestProject();
project.addFile('entrypoints/unlisted.html');
project.addFile('public/logo.png');
project.addFile('public/icon.jpeg');
await project.build();
const manifest = await project.getOutputManifest();
expect(manifest.icons).toBeUndefined();
});
it('should allow icons to be overwritten from the wxt.config.ts file', async () => {
const project = new TestProject();
project.addFile('entrypoints/unlisted.html');
project.addFile('public/icon-16.png');
project.addFile('public/icon-32.png');
project.addFile('public/logo-16.png');
project.addFile('public/logo-32.png');
project.addFile('public/logo-48.png');
const icons = {
'16': 'logo-16.png',
'32': 'logo-32.png',
'48': 'logo-48.png',
};
project.setConfigFileConfig({
manifest: {
icons,
},
});
await project.build();
const manifest = await project.getOutputManifest();
expect(manifest.icons).toEqual(icons);
});
});
describe('content_scripts', () => {
it('should group content scripts and styles together based on their manifest properties', async () => {
const project = new TestProject();
project.addFile(
'entrypoints/one.content/index.ts',
`import "./style.css";
export default defineContentScript({
matches: ["*://google.com/*"],
main: () => {},
})`,
);
project.addFile(
'entrypoints/one.content/style.css',
`body { color: red }`,
);
project.addFile(
'entrypoints/two.content/index.ts',
`import "./style.css";
export default defineContentScript({
matches: ["*://google.com/*"],
runAt: "document_end",
main: () => {},
})`,
);
project.addFile(
'entrypoints/two.content/style.css',
`body { color: green }`,
);
project.addFile(
'entrypoints/three.content/index.ts',
`import "./style.css";
export default defineContentScript({
matches: ["*://google.com/*"],
runAt: "document_end",
main: () => {},
})`,
);
project.addFile(
'entrypoints/three.content/style.css',
`body { color: blue }`,
);
project.addFile(
'entrypoints/four.content/index.ts',
`import "./style.css";
export default defineContentScript({
matches: ["*://duckduckgo.com/*"],
runAt: "document_end",
main: () => {},
})`,
);
project.addFile(
'entrypoints/four.content/style.css',
`body { color: yellow }`,
);
await project.build();
const manifest = await project.getOutputManifest();
expect(manifest.content_scripts).toContainEqual({
matches: ['*://duckduckgo.com/*'],
run_at: 'document_end',
css: ['content-scripts/four.css'],
js: ['content-scripts/four.js'],
});
expect(manifest.content_scripts).toContainEqual({
matches: ['*://google.com/*'],
run_at: 'document_end',
css: ['content-scripts/three.css', 'content-scripts/two.css'],
js: ['content-scripts/three.js', 'content-scripts/two.js'],
});
expect(manifest.content_scripts).toContainEqual({
matches: ['*://google.com/*'],
css: ['content-scripts/one.css'],
js: ['content-scripts/one.js'],
});
});
it('should add to any content scripts declared in wxt.config.ts', async () => {
const project = new TestProject();
project.addFile(
'entrypoints/one.content/index.ts',
`export default defineContentScript({
matches: ["*://google.com/*"],
main: () => {},
})`,
);
project.addFile(
'entrypoints/two.content/style.css',
`body {
background-color: red;
}`,
);
project.setConfigFileConfig({
manifest: {
content_scripts: [
{
css: ['content-scripts/two.css'],
matches: ['*://*.google.com/*'],
},
],
},
});
await project.build();
const manifest = await project.getOutputManifest();
expect(manifest.content_scripts).toContainEqual({
css: ['content-scripts/two.css'],
matches: ['*://*.google.com/*'],
});
expect(manifest.content_scripts).toContainEqual({
matches: ['*://google.com/*'],
js: ['content-scripts/one.js'],
});
});
it('should add a CSS entry when cssInjectionMode is undefined', async () => {
const project = new TestProject();
project.addFile(
'entrypoints/content/style.css',
'body { background-color: red; }',
);
project.addFile(
'entrypoints/content/index.ts',
`import "./style.css";
export default defineContentScript({
matches: ["https://*.google.com/*"],
main() {},
});`,
);
await project.build();
expect(await project.serializeFile('.output/chrome-mv3/manifest.json'))
.toMatchInlineSnapshot(`
".output/chrome-mv3/manifest.json
----------------------------------------
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","content_scripts":[{"matches":["https://*.google.com/*"],"css":["content-scripts/content.css"],"js":["content-scripts/content.js"]}]}"
`);
});
it('should add a CSS entry when cssInjectionMode is "manifest"', async () => {
const project = new TestProject();
project.addFile(
'entrypoints/content/style.css',
'body { background-color: red; }',
);
project.addFile(
'entrypoints/content/index.ts',
`import "./style.css";
export default defineContentScript({
matches: ["https://*.google.com/*"],
cssInjectionMode: "manifest",
main() {},
});`,
);
await project.build();
expect(await project.serializeFile('.output/chrome-mv3/manifest.json'))
.toMatchInlineSnapshot(`
".output/chrome-mv3/manifest.json
----------------------------------------
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","content_scripts":[{"matches":["https://*.google.com/*"],"css":["content-scripts/content.css"],"js":["content-scripts/content.js"]}]}"
`);
});
it('should not add an entry for CSS when cssInjectionMode is "manual"', async () => {
const project = new TestProject();
project.addFile(
'entrypoints/content/style.css',
'body { background-color: red; }',
);
project.addFile(
'entrypoints/content/index.ts',
`import "./style.css";
export default defineContentScript({
matches: ["https://*.google.com/*"],
cssInjectionMode: "manual",
main() {},
});`,
);
await project.build();
expect(await project.serializeFile('.output/chrome-mv3/manifest.json'))
.toMatchInlineSnapshot(`
".output/chrome-mv3/manifest.json
----------------------------------------
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","content_scripts":[{"matches":["https://*.google.com/*"],"js":["content-scripts/content.js"]}]}"
`);
});
it('should not add a content script entry for CSS when cssInjectionMode is "ui", but add a web_accessible_resources entry for MV2', async () => {
const project = new TestProject();
project.addFile(
'entrypoints/content/style.css',
'body { background-color: red; }',
);
project.addFile(
'entrypoints/content/index.ts',
`import "./style.css";
export default defineContentScript({
matches: ["https://*.google.com/*"],
cssInjectionMode: "ui",
main() {},
});`,
);
await project.build({
manifestVersion: 2,
});
expect(await project.serializeFile('.output/chrome-mv2/manifest.json'))
.toMatchInlineSnapshot(`
".output/chrome-mv2/manifest.json
----------------------------------------
{"manifest_version":2,"name":"E2E Extension","description":"Example description","version":"0.0.0","content_scripts":[{"matches":["https://*.google.com/*"],"js":["content-scripts/content.js"]}],"web_accessible_resources":["content-scripts/content.css"]}"
`);
});
it('should not add a content script entry for CSS when cssInjectionMode is "ui", but add a web_accessible_resources entry for MV3', async () => {
const project = new TestProject();
project.addFile(
'entrypoints/content/style.css',
'body { background-color: red; }',
);
project.addFile(
'entrypoints/content/index.ts',
`import "./style.css";
export default defineContentScript({
matches: ["https://*.google.com/*"],
cssInjectionMode: "ui",
main() {},
});`,
);
await project.build({
manifestVersion: 3,
});
expect(await project.serializeFile('.output/chrome-mv3/manifest.json'))
.toMatchInlineSnapshot(`
".output/chrome-mv3/manifest.json
----------------------------------------
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","content_scripts":[{"matches":["https://*.google.com/*"],"js":["content-scripts/content.js"]}],"web_accessible_resources":[{"resources":["content-scripts/content.css"],"matches":["https://*.google.com/*"]}]}"
`);
});
});
it('should combine web accessible resources', async () => {
const project = new TestProject();
project.addFile(
'entrypoints/content/style.css',
'body { background-color: red; }',
);
project.addFile(
'entrypoints/content/index.ts',
`import "./style.css";
export default defineContentScript({
matches: ["https://*.google.com/*"],
cssInjectionMode: "ui",
main() {},
});`,
);
project.setConfigFileConfig({
manifest: {
web_accessible_resources: [
{ resources: ['one.png'], matches: ['https://one.com/*'] },
],
},
});
await project.build();
expect(await project.serializeFile('.output/chrome-mv3/manifest.json'))
.toMatchInlineSnapshot(`
".output/chrome-mv3/manifest.json
----------------------------------------
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","web_accessible_resources":[{"resources":["one.png"],"matches":["https://one.com/*"]},{"resources":["content-scripts/content.css"],"matches":["https://*.google.com/*"]}],"content_scripts":[{"matches":["https://*.google.com/*"],"js":["content-scripts/content.js"]}]}"
`);
});
it('should respect the transformManifest option', async () => {
const project = new TestProject();
project.addFile('entrypoints/unlisted.html');
project.addFile(
'wxt.config.ts',
`import { defineConfig } from 'wxt';
export default defineConfig({
transformManifest(manifest) {
manifest.author = "Custom Author"
}
})`,
);
await project.build();
const output = await project.serializeFile(
'.output/chrome-mv3/manifest.json',
);
expect(output).toMatchInlineSnapshot(`
".output/chrome-mv3/manifest.json
----------------------------------------
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","author":"Custom Author"}"
`);
});
it.each([
{ browser: undefined, outDir: 'chrome-mv3', expected: undefined },
{ browser: 'chrome', outDir: 'chrome-mv3', expected: undefined },
@@ -30,4 +579,95 @@ describe('Manifest Content', () => {
expect(safariManifest.background.persistent).toBe(expected);
},
);
describe('versions', () => {
it.each([
['chrome', 3] as const,
['safari', 2] as const,
['edge', 3] as const,
])(
'should include version_name on %s when it needs simplified',
async (browser, manifestVersion) => {
const project = new TestProject({
version: '1.0.0-alpha1',
});
project.addFile('entrypoints/unlisted.html');
await project.build({ browser, manifestVersion });
const manifest = await project.getOutputManifest(
`.output/${browser}-mv${manifestVersion}/manifest.json`,
);
expect(manifest.version).toBe('1.0.0');
expect(manifest.version_name).toBe('1.0.0-alpha1');
},
);
it.each([['firefox', 2] as const])(
"should not include a version_name on %s because the browser doesn't support it",
async (browser, manifestVersion) => {
const project = new TestProject({
version: '1.0.0-alpha1',
});
project.addFile('entrypoints/unlisted.html');
await project.build({ browser, manifestVersion });
const manifest = await project.getOutputManifest(
`.output/${browser}-mv${manifestVersion}/manifest.json`,
);
expect(manifest.version).toBe('1.0.0');
expect(manifest.version_name).toBeUndefined();
},
);
it.each([
['chrome', 3] as const,
['firefox', 2] as const,
['safari', 3] as const,
['edge', 3] as const,
])(
'should not include the version_name if it is equal to version',
async (browser, manifestVersion) => {
const project = new TestProject({
version: '1.0.0.1',
});
project.addFile('entrypoints/unlisted.html');
await project.build({ browser, manifestVersion });
const manifest = await project.getOutputManifest(
`.output/${browser}-mv${manifestVersion}/manifest.json`,
);
expect(manifest.version).toBe('1.0.0.1');
expect(manifest.version_name).toBeUndefined();
},
);
});
it('should respect the action field in the manifest without a popup', async () => {
const project = new TestProject();
project.addFile('entrypoints/unlisted.html');
project.setConfigFileConfig({
manifest: {
action: {
default_title: 'Hello world',
},
},
});
await project.build();
expect(await project.getOutputManifest()).toMatchInlineSnapshot(`
{
"action": {
"default_title": "Hello world",
},
"description": "Example description",
"manifest_version": 3,
"name": "E2E Extension",
"version": "0.0.0",
}
`);
});
});
+4 -4
View File
@@ -1,7 +1,7 @@
{
"name": "wxt",
"type": "module",
"version": "0.13.5",
"version": "0.13.1-alpha1",
"description": "Next gen framework for developing web extensions",
"engines": {
"node": ">=18",
@@ -93,7 +93,7 @@
"compile:wxt": "tsc --noEmit",
"compile:virtual": "tsc --noEmit -p src/virtual",
"test": "vitest",
"test:coverage": "vitest run --coverage.enabled \"--coverage.include=src/**\" \"--coverage.exclude=src/core/utils/testing/**\" \"--coverage.exclude=**/*.d.ts\"",
"test:coverage": "vitest run --coverage",
"prepare": "simple-git-hooks",
"prepublish": "pnpm -s build",
"docs:gen": "typedoc --options docs/typedoc.json",
@@ -105,7 +105,7 @@
"dependencies": {
"@types/webextension-polyfill": "^0.10.5",
"@webext-core/fake-browser": "^1.3.1",
"@webext-core/isolated-element": "^1.1.1",
"@webext-core/isolated-element": "^1.0.4",
"@webext-core/match-patterns": "^1.0.3",
"async-mutex": "^0.4.0",
"c12": "^1.5.1",
@@ -160,7 +160,7 @@
"typedoc-plugin-markdown": "4.0.0-next.23",
"typedoc-vitepress-theme": "1.0.0-next.3",
"typescript": "^5.3.2",
"vitepress": "1.0.0-rc.34",
"vitepress": "1.0.0-rc.31",
"vitest": "^1.1.0",
"vitest-mock-extended": "^1.3.1",
"vue": "^3.3.10"
+406 -279
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -29,9 +29,9 @@ const preset = {
} satisfies tsup.Options;
function spinnerPMap(configs: tsup.Options[]) {
let progress = 1;
let completed = 0;
const updateSpinner = () => {
spinner.text = `${spinnerText} [${progress}/${configs.length}]`;
spinner.text = `${spinnerText} [${completed}/${configs.length}]`;
};
updateSpinner();
@@ -39,7 +39,7 @@ function spinnerPMap(configs: tsup.Options[]) {
config,
async (config) => {
const res = await tsup.build(config);
progress++;
completed++;
updateSpinner();
return res;
},
@@ -5,7 +5,6 @@ import { ContentScriptContext } from '~/client/content-scripts/content-script-co
import { createIsolatedElement } from '@webext-core/isolated-element';
import { mock } from 'vitest-mock-extended';
import { browser } from '~/browser';
import { faker } from '@faker-js/faker';
vi.mock('@webext-core/isolated-element', async () => {
const { vi } = await import('vitest');
@@ -333,26 +332,4 @@ describe('createContentScriptUi', () => {
expect(document.querySelector('test-app')).toBeDefined();
});
});
it('should forward isolateEvents into createIsolatedElement', async () => {
const isolateEvents = faker.helpers.arrayElement([
undefined,
true,
false,
['click', 'keydown'],
]);
await createContentScriptUi(createCtx(), {
name: 'test-app',
type: 'inline',
mount: testApp,
isolateEvents,
});
expect(createIsolatedElementMock).toBeCalledTimes(1);
expect(createIsolatedElementMock).toBeCalledWith(
expect.objectContaining({
isolateEvents,
}),
);
});
});
@@ -10,8 +10,8 @@ import {
} from '../utils/content-script-ui';
/**
* Utility for mounting content script UI's with isolated styles and controlled event bubbling.
* Automatically removed from the DOM when the content script's context is invalidated.
* Utility for mounting content script UI's with isolated styles. Automatically removed from the DOM
* when the content script's context is invalidated.
*
* See https://wxt.dev/guide/content-script-ui.html for full documentation.
*
@@ -31,7 +31,7 @@ import {
* const app = document.createElement("div");
* app.textContent = "Content Script UI";
* container.append(app);
* },
* }
* })
* ui.mount();
* }
@@ -56,7 +56,6 @@ export async function createContentScriptUi<TApp>(
textContent: css.join('\n').trim(),
},
mode: 'open',
isolateEvents: options.isolateEvents,
});
let mounted: TApp;
@@ -171,13 +170,4 @@ export type ContentScriptUiOptions<TApp> = ContentScriptPositioningOptions &
* See https://wxt.dev/guide/content-script-ui.html for more info.
*/
css?: string;
/**
* When enabled, `event.stopPropagation` will be called on events trying to bubble out of the
* shadow root.
*
* - Set to `true` to stop the propagation of a default set of events,
* `["keyup", "keydown", "keypress"]`
* - Set to an array of event names to stop the propagation of a custom list of events
*/
isolateEvents?: boolean | string[];
};
-4
View File
@@ -40,10 +40,6 @@ export async function craeteViteBuilder(
config.build ??= {};
config.build.outDir = wxtConfig.outDir;
config.build.emptyOutDir = false;
// Disable minification for the dev command
if (config.build.minify == null && wxtConfig.command === 'serve') {
config.build.minify = false;
}
config.plugins ??= [];
config.plugins.push(
-965
View File
@@ -1,965 +0,0 @@
import { describe, expect, it } from 'vitest';
import { generateManifest, stripPathFromMatchPattern } from '../manifest';
import {
fakeArray,
fakeBackgroundEntrypoint,
fakeBuildOutput,
fakeEntrypoint,
fakeInternalConfig,
fakeOptionsEntrypoint,
fakePopupEntrypoint,
} from '../testing/fake-objects';
import { Manifest } from 'webextension-polyfill';
import {
BuildOutput,
ContentScriptEntrypoint,
Entrypoint,
OutputAsset,
} from '~/types';
const outDir = '/output';
const contentScriptOutDir = '/output/content-scripts';
describe('Manifest Utils', () => {
describe('generateManifest', () => {
describe('popup', () => {
type ActionType = 'browser_action' | 'page_action';
const popupEntrypoint = (type?: ActionType) =>
fakePopupEntrypoint({
options: {
// @ts-expect-error: Force this to be undefined instead of inheriting the random value
mv2Key: type ?? null,
defaultIcon: {
'16': '/icon/16.png',
},
defaultTitle: 'Default Iitle',
},
outputDir: outDir,
});
it('should include an action for mv3', async () => {
const popup = popupEntrypoint();
const buildOutput = fakeBuildOutput();
const config = fakeInternalConfig({
manifestVersion: 3,
outDir,
});
const expected: Partial<Manifest.WebExtensionManifest> = {
action: {
default_icon: popup.options.defaultIcon,
default_title: popup.options.defaultTitle,
default_popup: 'popup.html',
},
};
const actual = await generateManifest([popup], buildOutput, config);
expect(actual).toMatchObject(expected);
});
it.each<{
inputType: ActionType | undefined;
expectedType: ActionType;
}>([
{ inputType: undefined, expectedType: 'browser_action' },
{ inputType: 'browser_action', expectedType: 'browser_action' },
{ inputType: 'page_action', expectedType: 'page_action' },
])(
'should use the correct action for mv2: %j',
async ({ inputType, expectedType }) => {
const popup = popupEntrypoint(inputType);
const buildOutput = fakeBuildOutput();
const config = fakeInternalConfig({
manifestVersion: 2,
outDir,
});
const expected = {
default_icon: popup.options.defaultIcon,
default_title: popup.options.defaultTitle,
default_popup: 'popup.html',
};
const actual = await generateManifest([popup], buildOutput, config);
expect(actual[expectedType]).toEqual(expected);
},
);
});
describe('action without popup', () => {
it('should respect the action field in the manifest without a popup', async () => {
const buildOutput = fakeBuildOutput();
const config = fakeInternalConfig({
outDir,
manifest: {
action: {
default_icon: 'icon-16.png',
default_title: 'Example title',
},
},
});
const expected: Partial<Manifest.WebExtensionManifest> = {
action: config.manifest.action,
};
const actual = await generateManifest([], buildOutput, config);
expect(actual).toMatchObject(expected);
});
});
describe('options', () => {
const options = fakeOptionsEntrypoint({
outputDir: outDir,
options: {
openInTab: false,
chromeStyle: true,
browserStyle: true,
},
});
it('should include a options_ui and chrome_style for chrome', async () => {
const config = fakeInternalConfig({
manifestVersion: 3,
outDir,
browser: 'chrome',
});
const buildOutput = fakeBuildOutput();
const expected = {
open_in_tab: false,
chrome_style: true,
page: 'options.html',
};
const actual = await generateManifest([options], buildOutput, config);
expect(actual.options_ui).toEqual(expected);
});
it('should include a options_ui and browser_style for firefox', async () => {
const config = fakeInternalConfig({
manifestVersion: 3,
browser: 'firefox',
outDir,
});
const buildOutput = fakeBuildOutput();
const expected = {
open_in_tab: false,
browser_style: true,
page: 'options.html',
};
const actual = await generateManifest([options], buildOutput, config);
expect(actual.options_ui).toEqual(expected);
});
});
describe('background', () => {
const background = fakeBackgroundEntrypoint({
outputDir: outDir,
options: {
persistent: true,
type: 'module',
},
});
describe('MV3', () => {
it.each(['chrome', 'safari'])(
'should include a service worker and type for %s',
async (browser) => {
const config = fakeInternalConfig({
outDir,
manifestVersion: 3,
browser,
});
const buildOutput = fakeBuildOutput();
const expected = {
type: 'module',
service_worker: 'background.js',
};
const actual = await generateManifest(
[background],
buildOutput,
config,
);
expect(actual.background).toEqual(expected);
},
);
it('should include a background script and type for firefox', async () => {
const config = fakeInternalConfig({
outDir,
manifestVersion: 3,
browser: 'firefox',
});
const buildOutput = fakeBuildOutput();
const expected = {
type: 'module',
scripts: ['background.js'],
};
const actual = await generateManifest(
[background],
buildOutput,
config,
);
expect(actual.background).toEqual(expected);
});
});
describe('MV2', () => {
it.each(['chrome', 'safari'])(
'should include scripts and persistent for %s',
async (browser) => {
const config = fakeInternalConfig({
outDir,
manifestVersion: 2,
browser,
});
const buildOutput = fakeBuildOutput();
const expected = {
persistent: true,
scripts: ['background.js'],
};
const actual = await generateManifest(
[background],
buildOutput,
config,
);
expect(actual.background).toEqual(expected);
},
);
it('should include a background script and persistent for firefox mv2', async () => {
const config = fakeInternalConfig({
outDir,
manifestVersion: 2,
browser: 'firefox',
});
const buildOutput = fakeBuildOutput();
const expected = {
persistent: true,
scripts: ['background.js'],
};
const actual = await generateManifest(
[background],
buildOutput,
config,
);
expect(actual.background).toEqual(expected);
});
});
});
describe('icons', () => {
it('should auto-discover icons with the correct name', async () => {
const entrypoints = fakeArray(fakeEntrypoint);
const buildOutput = fakeBuildOutput({
publicAssets: [
{ type: 'asset', fileName: 'icon-16.png' },
{ type: 'asset', fileName: 'icon/32.png' },
{ type: 'asset', fileName: 'icon@48w.png' },
{ type: 'asset', fileName: 'icon-64x64.png' },
{ type: 'asset', fileName: 'icon@96.png' },
{ type: 'asset', fileName: 'icons/128x128.png' },
],
});
const config = fakeInternalConfig();
const actual = await generateManifest(entrypoints, buildOutput, config);
expect(actual.icons).toEqual({
16: 'icon-16.png',
32: 'icon/32.png',
48: 'icon@48w.png',
64: 'icon-64x64.png',
96: 'icon@96.png',
128: 'icons/128x128.png',
});
});
it('should return undefined when no icons are found', async () => {
const entrypoints = fakeArray(fakeEntrypoint);
const buildOutput = fakeBuildOutput({
publicAssets: [
{ type: 'asset', fileName: 'logo.png' },
{ type: 'asset', fileName: 'icon-16.jpeg' },
],
});
const config = fakeInternalConfig();
const actual = await generateManifest(entrypoints, buildOutput, config);
expect(actual.icons).toBeUndefined();
});
it('should allow icons to be overwritten from the wxt.config.ts file', async () => {
const entrypoints = fakeArray(fakeEntrypoint);
const buildOutput = fakeBuildOutput({
publicAssets: [
{ type: 'asset', fileName: 'icon-16.png' },
{ type: 'asset', fileName: 'icon-32.png' },
{ type: 'asset', fileName: 'logo-16.png' },
{ type: 'asset', fileName: 'logo-32.png' },
{ type: 'asset', fileName: 'logo-48.png' },
],
});
const expected = {
16: 'logo-16.png',
32: 'logo-32.png',
48: 'logo-48.png',
};
const config = fakeInternalConfig({
manifest: {
icons: expected,
},
});
const actual = await generateManifest(entrypoints, buildOutput, config);
expect(actual.icons).toEqual(expected);
});
});
describe('content_scripts', () => {
it('should group content scripts and styles together based on their manifest properties', async () => {
const cs1: ContentScriptEntrypoint = {
type: 'content-script',
name: 'one',
inputPath: 'entrypoints/one.content/index.ts',
outputDir: contentScriptOutDir,
options: {
matches: ['*://google.com/*'],
},
};
const cs1Styles: OutputAsset = {
type: 'asset',
fileName: 'content-scripts/one.css',
};
const cs2: ContentScriptEntrypoint = {
type: 'content-script',
name: 'two',
inputPath: 'entrypoints/two.content/index.ts',
outputDir: contentScriptOutDir,
options: {
matches: ['*://google.com/*'],
runAt: 'document_end',
},
};
const cs2Styles: OutputAsset = {
type: 'asset',
fileName: 'content-scripts/two.css',
};
const cs3: ContentScriptEntrypoint = {
type: 'content-script',
name: 'three',
inputPath: 'entrypoints/three.content/index.ts',
outputDir: contentScriptOutDir,
options: {
matches: ['*://google.com/*'],
runAt: 'document_end',
},
};
const cs3Styles: OutputAsset = {
type: 'asset',
fileName: 'content-scripts/three.css',
};
const cs4: ContentScriptEntrypoint = {
type: 'content-script',
name: 'four',
inputPath: 'entrypoints/four.content/index.ts',
outputDir: contentScriptOutDir,
options: {
matches: ['*://duckduckgo.com/*'],
runAt: 'document_end',
},
};
const cs4Styles: OutputAsset = {
type: 'asset',
fileName: 'content-scripts/four.css',
};
const cs5: ContentScriptEntrypoint = {
type: 'content-script',
name: 'five',
inputPath: 'entrypoints/five.content/index.ts',
outputDir: contentScriptOutDir,
options: {
matches: ['*://google.com/*'],
world: 'MAIN',
},
};
const cs5Styles: OutputAsset = {
type: 'asset',
fileName: 'content-scripts/five.css',
};
const entrypoints = [cs1, cs2, cs3, cs4, cs5];
const config = fakeInternalConfig({
command: 'build',
outDir,
manifestVersion: 3,
});
const buildOutput: Omit<BuildOutput, 'manifest'> = {
publicAssets: [],
steps: [
{ entrypoints: cs1, chunks: [cs1Styles] },
{ entrypoints: cs2, chunks: [cs2Styles] },
{ entrypoints: cs3, chunks: [cs3Styles] },
{ entrypoints: cs4, chunks: [cs4Styles] },
{ entrypoints: cs5, chunks: [cs5Styles] },
],
};
const actual = await generateManifest(entrypoints, buildOutput, config);
expect(actual.content_scripts).toContainEqual({
matches: ['*://google.com/*'],
css: ['content-scripts/one.css'],
js: ['content-scripts/one.js'],
});
expect(actual.content_scripts).toContainEqual({
matches: ['*://google.com/*'],
run_at: 'document_end',
css: ['content-scripts/two.css', 'content-scripts/three.css'],
js: ['content-scripts/two.js', 'content-scripts/three.js'],
});
expect(actual.content_scripts).toContainEqual({
matches: ['*://duckduckgo.com/*'],
run_at: 'document_end',
css: ['content-scripts/four.css'],
js: ['content-scripts/four.js'],
});
expect(actual.content_scripts).toContainEqual({
matches: ['*://google.com/*'],
css: ['content-scripts/five.css'],
js: ['content-scripts/five.js'],
world: 'MAIN',
});
});
it('should merge any content scripts declared in wxt.config.ts', async () => {
const cs: ContentScriptEntrypoint = {
type: 'content-script',
name: 'one',
inputPath: 'entrypoints/one.content.ts',
outputDir: contentScriptOutDir,
options: {
matches: ['*://google.com/*'],
},
};
const generatedContentScript = {
matches: ['*://google.com/*'],
js: ['content-scripts/one.js'],
};
const userContentScript = {
css: ['content-scripts/two.css'],
matches: ['*://*.google.com/*'],
};
const entrypoints = [cs];
const buildOutput = fakeBuildOutput();
const config = fakeInternalConfig({
outDir,
command: 'build',
manifest: {
content_scripts: [userContentScript],
},
});
const actual = await generateManifest(entrypoints, buildOutput, config);
expect(actual.content_scripts).toContainEqual(userContentScript);
expect(actual.content_scripts).toContainEqual(generatedContentScript);
});
describe('cssInjectionMode', () => {
it.each([undefined, 'manifest'] as const)(
'should add a CSS entry when cssInjectionMode is %s',
async (cssInjectionMode) => {
const cs: ContentScriptEntrypoint = {
type: 'content-script',
name: 'one',
inputPath: 'entrypoints/one.content.ts',
outputDir: contentScriptOutDir,
options: {
matches: ['*://google.com/*'],
cssInjectionMode,
},
};
const styles: OutputAsset = {
type: 'asset',
fileName: 'content-scripts/one.css',
};
const entrypoints = [cs];
const buildOutput: Omit<BuildOutput, 'manifest'> = {
publicAssets: [],
steps: [{ entrypoints: cs, chunks: [styles] }],
};
const config = fakeInternalConfig({
outDir,
command: 'build',
});
const actual = await generateManifest(
entrypoints,
buildOutput,
config,
);
expect(actual.content_scripts).toEqual([
{
js: ['content-scripts/one.js'],
css: ['content-scripts/one.css'],
matches: ['*://google.com/*'],
},
]);
},
);
it.each(['manual', 'ui'] as const)(
'should not add an entry for CSS when cssInjectionMode is %s',
async (cssInjectionMode) => {
const cs: ContentScriptEntrypoint = {
type: 'content-script',
name: 'one',
inputPath: 'entrypoints/one.content.ts',
outputDir: contentScriptOutDir,
options: {
matches: ['*://google.com/*'],
cssInjectionMode,
},
};
const styles: OutputAsset = {
type: 'asset',
fileName: 'content-scripts/one.css',
};
const entrypoints = [cs];
const buildOutput: Omit<BuildOutput, 'manifest'> = {
publicAssets: [],
steps: [{ entrypoints: cs, chunks: [styles] }],
};
const config = fakeInternalConfig({
outDir,
command: 'build',
});
const actual = await generateManifest(
entrypoints,
buildOutput,
config,
);
expect(actual.content_scripts).toEqual([
{
js: ['content-scripts/one.js'],
matches: ['*://google.com/*'],
},
]);
},
);
it('should add CSS file to `web_accessible_resources` when cssInjectionMode is "ui" for MV3', async () => {
const cs: ContentScriptEntrypoint = {
type: 'content-script',
name: 'one',
inputPath: 'entrypoints/one.content.ts',
outputDir: contentScriptOutDir,
options: {
matches: ['*://google.com/*'],
cssInjectionMode: 'ui',
},
};
const styles: OutputAsset = {
type: 'asset',
fileName: 'content-scripts/one.css',
};
const entrypoints = [cs];
const buildOutput: Omit<BuildOutput, 'manifest'> = {
publicAssets: [],
steps: [{ entrypoints: cs, chunks: [styles] }],
};
const config = fakeInternalConfig({
outDir,
command: 'build',
manifestVersion: 3,
});
const actual = await generateManifest(
entrypoints,
buildOutput,
config,
);
expect(actual.web_accessible_resources).toEqual([
{
matches: ['*://google.com/*'],
resources: ['content-scripts/one.css'],
},
]);
});
it('should add CSS file to `web_accessible_resources` when cssInjectionMode is "ui" for MV2', async () => {
const cs: ContentScriptEntrypoint = {
type: 'content-script',
name: 'one',
inputPath: 'entrypoints/one.content.ts',
outputDir: contentScriptOutDir,
options: {
matches: ['*://google.com/*'],
cssInjectionMode: 'ui',
},
};
const styles: OutputAsset = {
type: 'asset',
fileName: 'content-scripts/one.css',
};
const entrypoints = [cs];
const buildOutput: Omit<BuildOutput, 'manifest'> = {
publicAssets: [],
steps: [{ entrypoints: cs, chunks: [styles] }],
};
const config = fakeInternalConfig({
outDir,
command: 'build',
manifestVersion: 2,
});
const actual = await generateManifest(
entrypoints,
buildOutput,
config,
);
expect(actual.web_accessible_resources).toEqual([
'content-scripts/one.css',
]);
});
it('should strip the path off the match pattern so the pattern is valid for `web_accessible_resources`', async () => {
const cs: ContentScriptEntrypoint = {
type: 'content-script',
name: 'one',
inputPath: 'entrypoints/one.content.ts',
outputDir: contentScriptOutDir,
options: {
matches: ['*://play.google.com/books/*'],
cssInjectionMode: 'ui',
},
};
const styles: OutputAsset = {
type: 'asset',
fileName: 'content-scripts/one.css',
};
const entrypoints = [cs];
const buildOutput: Omit<BuildOutput, 'manifest'> = {
publicAssets: [],
steps: [{ entrypoints: cs, chunks: [styles] }],
};
const config = fakeInternalConfig({
outDir,
command: 'build',
manifestVersion: 3,
});
const actual = await generateManifest(
entrypoints,
buildOutput,
config,
);
expect(actual.web_accessible_resources).toEqual([
{
matches: ['*://play.google.com/*'],
resources: ['content-scripts/one.css'],
},
]);
});
});
});
describe('web_accessible_resources', () => {
it('should combine user defined resources and generated resources for MV3', async () => {
const cs: ContentScriptEntrypoint = {
type: 'content-script',
name: 'one',
inputPath: 'entrypoints/one.content.ts',
outputDir: contentScriptOutDir,
options: {
matches: ['*://google.com/*'],
cssInjectionMode: 'ui',
},
};
const styles: OutputAsset = {
type: 'asset',
fileName: 'content-scripts/one.css',
};
const entrypoints = [cs];
const buildOutput: Omit<BuildOutput, 'manifest'> = {
publicAssets: [],
steps: [{ entrypoints: cs, chunks: [styles] }],
};
const config = fakeInternalConfig({
outDir,
command: 'build',
manifestVersion: 3,
manifest: {
web_accessible_resources: [
{ resources: ['one.png'], matches: ['*://one.com/*'] },
],
},
});
const actual = await generateManifest(entrypoints, buildOutput, config);
expect(actual.web_accessible_resources).toEqual([
{ resources: ['one.png'], matches: ['*://one.com/*'] },
{
resources: ['content-scripts/one.css'],
matches: ['*://google.com/*'],
},
]);
});
it('should combine user defined resources and generated resources for MV2', async () => {
const cs: ContentScriptEntrypoint = {
type: 'content-script',
name: 'one',
inputPath: 'entrypoints/one.content.ts',
outputDir: contentScriptOutDir,
options: {
matches: ['*://google.com/*'],
cssInjectionMode: 'ui',
},
};
const styles: OutputAsset = {
type: 'asset',
fileName: 'content-scripts/one.css',
};
const entrypoints = [cs];
const buildOutput: Omit<BuildOutput, 'manifest'> = {
publicAssets: [],
steps: [{ entrypoints: cs, chunks: [styles] }],
};
const config = fakeInternalConfig({
outDir,
command: 'build',
manifestVersion: 2,
manifest: {
web_accessible_resources: ['one.png'],
},
});
const actual = await generateManifest(entrypoints, buildOutput, config);
expect(actual.web_accessible_resources).toEqual([
'one.png',
'content-scripts/one.css',
]);
});
});
describe('transformManifest option', () => {
it("should call the transformManifest option after the manifest is generated, but before it's returned", async () => {
const entrypoints: Entrypoint[] = [];
const buildOutput = fakeBuildOutput();
const newAuthor = 'Custom Author';
const config = fakeInternalConfig({
transformManifest(manifest: any) {
manifest.author = newAuthor;
},
});
const expected = {
author: newAuthor,
};
const actual = await generateManifest(entrypoints, buildOutput, config);
expect(actual).toMatchObject(expected);
});
});
describe('version', () => {
it.each(['chrome', 'safari', 'edge'] as const)(
'should include version and version_name as is on %s',
async (browser) => {
const version = '1.0.0';
const versionName = '1.0.0-alpha1';
const entrypoints: Entrypoint[] = [];
const buildOutput = fakeBuildOutput();
const config = fakeInternalConfig({
browser,
manifest: {
version,
version_name: versionName,
},
});
const actual = await generateManifest(
entrypoints,
buildOutput,
config,
);
expect(actual.version).toBe(version);
expect(actual.version_name).toBe(versionName);
},
);
it.each(['firefox'] as const)(
'should not include a version_name on %s because it is unsupported',
async (browser) => {
const version = '1.0.0';
const versionName = '1.0.0-alpha1';
const entrypoints: Entrypoint[] = [];
const buildOutput = fakeBuildOutput();
const config = fakeInternalConfig({
browser,
manifest: {
version,
version_name: versionName,
},
});
const actual = await generateManifest(
entrypoints,
buildOutput,
config,
);
expect(actual.version).toBe(version);
expect(actual.version_name).toBeUndefined();
},
);
it.each(['chrome', 'firefox', 'safari', 'edge'])(
'should not include the version_name if it is equal to version',
async (browser) => {
const version = '1.0.0';
const entrypoints: Entrypoint[] = [];
const buildOutput = fakeBuildOutput();
const config = fakeInternalConfig({
browser,
manifest: {
version,
version_name: version,
},
});
const actual = await generateManifest(
entrypoints,
buildOutput,
config,
);
expect(actual.version).toBe(version);
expect(actual.version_name).toBeUndefined();
},
);
it('should log a warning if the version could not be detected', async () => {
const entrypoints: Entrypoint[] = [];
const buildOutput = fakeBuildOutput();
const config = fakeInternalConfig({
manifest: {
// @ts-ignore: Purposefully removing version from fake object
version: null,
},
});
const actual = await generateManifest(entrypoints, buildOutput, config);
expect(actual.version).toBe('0.0.0');
expect(actual.version_name).toBeUndefined();
expect(config.logger.warn).toBeCalledTimes(1);
expect(config.logger.warn).toBeCalledWith(
expect.stringContaining('Extension version not found'),
);
});
});
describe('commands', () => {
const reloadCommandName = 'wxt:reload-extension';
const reloadCommand = {
suggested_key: {
default: 'Ctrl+E',
},
};
it('should include a command for reloading the extension during development', async () => {
const config = fakeInternalConfig({ command: 'serve' });
const output = fakeBuildOutput();
const entrypoints = fakeArray(fakeEntrypoint);
const actual = await generateManifest(entrypoints, output, config);
expect(actual.commands).toMatchObject({
[reloadCommandName]: reloadCommand,
});
});
it('should not override any existing commands when adding the one to reload the extension', async () => {
const customCommandName = 'custom-command';
const customCommand = {
description: 'Some other command',
suggested_key: {
default: 'Ctrl+H',
},
};
const config = fakeInternalConfig({
command: 'serve',
manifest: {
commands: {
[customCommandName]: customCommand,
},
},
});
const output = fakeBuildOutput();
const entrypoints = fakeArray(fakeEntrypoint);
const actual = await generateManifest(entrypoints, output, config);
expect(actual.commands).toMatchObject({
[reloadCommandName]: reloadCommand,
[customCommandName]: customCommand,
});
});
it('should not include the command when building an extension', async () => {
const config = fakeInternalConfig({ command: 'build' });
const output = fakeBuildOutput();
const entrypoints = fakeArray(fakeEntrypoint);
const actual = await generateManifest(entrypoints, output, config);
expect(actual.commands).toBeUndefined();
});
});
});
describe('stripPathFromMatchPattern', () => {
it.each([
['<all_urls>', '<all_urls>'],
['*://play.google.com/books/*', '*://play.google.com/*'],
['*://*/*', '*://*/*'],
['https://github.com/wxt-dev/*', 'https://github.com/*'],
])('should convert "%s" to "%s"', (input, expected) => {
const actual = stripPathFromMatchPattern(input);
expect(actual).toEqual(expected);
});
});
});
+2 -2
View File
@@ -3,7 +3,7 @@ import { BuildOutput, EntrypointGroup, InternalConfig } from '~/types';
import { findEntrypoints } from './find-entrypoints';
import { generateTypesDir } from './generate-wxt-dir';
import { buildEntrypoints } from './build-entrypoints';
import { generateManifest, writeManifest } from '~/core/utils/manifest';
import { generateMainfest, writeManifest } from '~/core/utils/manifest';
/**
* Given a configuration, list of entrypoints, and an existing, partial output, build the
@@ -41,7 +41,7 @@ export async function rebuild(
publicAssets: [...existingOutput.publicAssets, ...newOutput.publicAssets],
};
const newManifest = await generateManifest(
const newManifest = await generateMainfest(
allEntrypoints,
mergedOutput,
config,
+9 -44
View File
@@ -50,41 +50,29 @@ export async function writeManifest(
/**
* Generates the manifest based on the config and entrypoints.
*/
export async function generateManifest(
export async function generateMainfest(
entrypoints: Entrypoint[],
buildOutput: Omit<BuildOutput, 'manifest'>,
config: InternalConfig,
): Promise<Manifest.WebExtensionManifest> {
const pkg = await getPackageJson(config);
let versionName =
config.manifest.version_name ?? config.manifest.version ?? pkg?.version;
if (versionName == null) {
versionName = '0.0.0';
config.logger.warn(
'Extension version not found, defaulting to "0.0.0". Add a version to your `package.json` or `wxt.config.ts` file. For more details, see: https://wxt.dev/guide/manifest.html#version-and-version-name',
);
}
const version = config.manifest.version ?? simplifyVersion(versionName);
const versionName = config.manifest.version_name ?? pkg?.version;
const version = config.manifest.version ?? simplifyVersion(pkg?.version);
const baseManifest: Manifest.WebExtensionManifest = {
manifest_version: config.manifestVersion,
name: pkg?.name,
description: pkg?.description,
version,
version_name:
// Firefox doesn't support version_name
config.browser === 'firefox' || versionName === version
? undefined
: versionName,
short_name: pkg?.shortName,
icons: discoverIcons(buildOutput),
};
if (config.command === 'serve') {
baseManifest.commands = {
'wxt:reload-extension': {
description: 'Reload the extension during development',
suggested_key: {
default: 'Ctrl+E',
},
},
};
}
const userManifest = config.manifest;
const manifest = defu(
@@ -92,14 +80,6 @@ export async function generateManifest(
baseManifest,
) as Manifest.WebExtensionManifest;
// Apply the final version fields after merging the user manifest
manifest.version = version;
manifest.version_name =
// Firefox doesn't support version_name
config.browser === 'firefox' || versionName === version
? undefined
: versionName;
addEntrypoints(manifest, entrypoints, buildOutput, config);
if (config.command === 'serve') addDevModeCsp(manifest, config);
@@ -503,10 +483,7 @@ export function getContentScriptCssWebAccessibleResources(
} else {
resources.push({
resources: [cssFile],
matches: resolvePerBrowserOption(
script.options.matches,
config.browser,
).map((matchPattern) => stripPathFromMatchPattern(matchPattern)),
matches: script.options.matches,
});
}
});
@@ -550,15 +527,3 @@ function addHostPermission(
if (manifest.host_permissions.includes(hostPermission)) return;
manifest.host_permissions.push(hostPermission);
}
/**
* - "<all_urls>" &rarr; "<all_urls>"
* - "*://play.google.com/books/*" &rarr; "*://play.google.com/*"
*/
export function stripPathFromMatchPattern(pattern: string) {
const protocolSepIndex = pattern.indexOf('://');
if (protocolSepIndex === -1) return pattern;
const startOfPath = pattern.indexOf('/', protocolSepIndex + 3);
return pattern.substring(0, startOfPath) + '/*';
}
+7 -36
View File
@@ -17,9 +17,6 @@ import {
OutputChunk,
OutputFile,
OutputAsset,
BuildOutput,
BuildStepOutput,
UserManifest,
} from '~/types';
import { mock } from 'vitest-mock-extended';
@@ -42,16 +39,6 @@ export function fakeDir(root = process.cwd()): string {
return resolve(root, faker.string.alphanumeric());
}
export const fakeEntrypoint = () =>
faker.helpers.arrayElement([
fakePopupEntrypoint,
fakeGenericEntrypoint,
fakeOptionsEntrypoint,
fakeBackgroundEntrypoint,
fakeContentScriptEntrypoint,
fakeUnlistedScriptEntrypoint,
])();
export const fakeContentScriptEntrypoint =
fakeObjectCreator<ContentScriptEntrypoint>(() => ({
type: 'content-script',
@@ -78,8 +65,8 @@ export const fakeContentScriptEntrypoint =
export const fakeBackgroundEntrypoint = fakeObjectCreator<BackgroundEntrypoint>(
() => ({
type: 'background',
inputPath: 'entrypoints/background.ts',
name: 'background',
inputPath: fakeFile('src'),
name: faker.string.alpha(),
options: {
persistent: faker.helpers.arrayElement([true, false, undefined]),
type: faker.helpers.maybe(() => 'module'),
@@ -100,8 +87,8 @@ export const fakeUnlistedScriptEntrypoint =
export const fakeOptionsEntrypoint = fakeObjectCreator<OptionsEntrypoint>(
() => ({
type: 'options',
inputPath: 'entrypoints/options.html',
name: 'options',
inputPath: fakeFile('src'),
name: faker.string.alpha(),
outputDir: fakeDir('.output'),
options: {
browserStyle: faker.helpers.arrayElement([true, false, undefined]),
@@ -113,8 +100,8 @@ export const fakeOptionsEntrypoint = fakeObjectCreator<OptionsEntrypoint>(
export const fakePopupEntrypoint = fakeObjectCreator<PopupEntrypoint>(() => ({
type: 'popup',
inputPath: 'entrypoints/popup.html',
name: 'popup',
inputPath: fakeFile('src'),
name: faker.string.alpha(),
outputDir: fakeDir('.output'),
options: {
defaultTitle: faker.helpers.arrayElement([
@@ -178,11 +165,6 @@ export const fakeManifest = fakeObjectCreator<Manifest.WebExtensionManifest>(
}),
);
export const fakeUserManifest = fakeObjectCreator<UserManifest>(() => ({
name: faker.string.alphanumeric(),
version: `${faker.number.int()}.${faker.number.int()}.${faker.number.int()}`,
}));
export function fakeArray<T>(createItem: () => T, count = 3): T[] {
const array: T[] = [];
for (let i = 0; i < count; i++) {
@@ -205,7 +187,7 @@ export const fakeInternalConfig = fakeObjectCreator<InternalConfig>(() => {
fsCache: mock<FsCache>(),
imports: {},
logger: mock(),
manifest: fakeUserManifest(),
manifest: fakeManifest(),
manifestVersion,
mode,
outBaseDir: fakeDir(),
@@ -240,14 +222,3 @@ export const fakeInternalConfig = fakeObjectCreator<InternalConfig>(() => {
builder: mock(),
};
});
export const fakeBuildOutput = fakeObjectCreator<BuildOutput>(() => ({
manifest: fakeManifest(),
publicAssets: fakeArray(fakeOutputAsset),
steps: fakeArray(fakeBuildStepOutput),
}));
export const fakeBuildStepOutput = fakeObjectCreator<BuildStepOutput>(() => ({
chunks: fakeArray(fakeOutputChunk),
entrypoints: fakeArray(fakeEntrypoint),
}));
-6
View File
@@ -26,12 +26,6 @@ if (__COMMAND__ === 'serve') {
} catch (err) {
logger.error('Failed to setup web socket connection with dev server', err);
}
browser.commands.onCommand.addListener((command) => {
if (command === 'wxt:reload-extension') {
browser.runtime.reload();
}
});
}
try {
+4 -4
View File
@@ -19,10 +19,10 @@
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.2.46",
"@types/react-dom": "^18.2.18",
"@types/react": "^18.2.14",
"@types/react-dom": "^18.2.6",
"@vitejs/plugin-react": "^4.2.1",
"typescript": "^5.3.3",
"wxt": "^0.13.0"
"typescript": "^5.3.2",
"wxt": "^0.12.0"
}
}
+3 -3
View File
@@ -15,11 +15,11 @@
"postinstall": "wxt prepare"
},
"dependencies": {
"solid-js": "^1.8.7"
"solid-js": "^1.7.8"
},
"devDependencies": {
"typescript": "^5.3.3",
"typescript": "^5.3.2",
"vite-plugin-solid": "^2.8.0",
"wxt": "^0.13.0"
"wxt": "^0.12.0"
}
}
+7 -7
View File
@@ -14,12 +14,12 @@
"postinstall": "wxt prepare"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^3.0.1",
"@tsconfig/svelte": "^5.0.2",
"svelte": "^4.2.8",
"svelte-check": "^3.6.2",
"tslib": "^2.6.2",
"typescript": "^5.3.3",
"wxt": "^0.13.0"
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"@tsconfig/svelte": "^5.0.0",
"svelte": "^4.0.5",
"svelte-check": "^3.4.6",
"tslib": "^2.6.0",
"typescript": "^5.3.2",
"wxt": "^0.12.0"
}
}
+2 -2
View File
@@ -15,7 +15,7 @@
"postinstall": "wxt prepare"
},
"devDependencies": {
"typescript": "^5.3.3",
"wxt": "^0.13.0"
"typescript": "^5.3.2",
"wxt": "^0.12.0"
}
}
+5 -5
View File
@@ -15,12 +15,12 @@
"postinstall": "wxt prepare"
},
"dependencies": {
"vue": "^3.4.0"
"vue": "^3.3.4"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.1",
"typescript": "^5.3.3",
"vue-tsc": "^1.8.27",
"wxt": "^0.13.0"
"@vitejs/plugin-vue": "^4.5.1",
"typescript": "^5.3.2",
"vue-tsc": "^1.8.25",
"wxt": "^0.12.0"
}
}