Compare commits

...

23 Commits

Author SHA1 Message Date
GitHub Actions 0c5643f648 chore(release): v0.13.4 2024-01-07 16:51:31 +00:00
Aaron Klinker b7cdf1597c fix: Disable minification during development
This closes #328
2024-01-07 10:48:25 -06:00
Aaron Klinker 2770974261 chore: Use const instead of let 2024-01-07 10:46:49 -06:00
GitHub Actions ab6a34afb6 chore(release): v0.13.3 2024-01-05 05:14:03 +00:00
Aaron b0eb7da9b4 chore: Refactor manifest generation E2E tests to unit tests (#323) 2024-01-02 17:05:23 -06:00
Aaron 4f649fc872 feat(DX): Add ctrl+E/cmd+E shortcut to reload extension during development (#322) 2024-01-02 12:06:22 -06:00
dependabot[bot] 1af64aa02f chore(deps-dev): bump vitepress from 1.0.0-rc.31 to 1.0.0-rc.34 (#316)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-01 14:33:18 -06:00
dependabot[bot] c35acff259 chore(deps-dev): bump prettier from 3.1.0 to 3.1.1 (#318)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-01 14:31:32 -06:00
dependabot[bot] 995d4f8080 chore(deps-dev): bump tsx from 4.6.2 to 4.7.0 (#320)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-01 14:31:16 -06:00
GitHub Actions b7e2aa3d8f chore(release): v0.13.2 2024-01-01 16:06:12 +00:00
冯不游 e347f4529f feat: Add isolateEvents option to createContentScripUi (#313)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2024-01-01 10:01:33 -06:00
李志博 3d9683418d Upload a Chinese dubbing plugin for Youtube. (#314)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2024-01-01 09:45:44 -06:00
Aaron bdce363eed chore: Re-enable coverage (#312) 2023-12-29 17:28:35 -06:00
Aaron e7775f291a docs: Use example tags to automate relevant example lists (#311) 2023-12-29 17:25:46 -06:00
Aaron 59f6a28c65 chore: Upgrade template dependencies (#310) 2023-12-29 12:25:13 -06:00
Aaron f7534464bc chore: Update templates to ^0.13.0 (#309) 2023-12-29 12:14:05 -06:00
Aaron 01445fb1da docs: Add example for setting up custom panels/panes in devtools (#308) 2023-12-29 12:08:33 -06:00
Aaron Klinker 1360eb7c01 docs: Update content script entrypoint docs 2023-12-29 02:01:53 -06:00
Aaron Klinker c99a281169 docs: Update unlisted pages/scripts description 2023-12-29 01:54:39 -06:00
Aaron Klinker 76e63e24c0 docs: Remove duplicate entrypoints/ path 2023-12-29 01:51:26 -06:00
GitHub Actions f5ba1173da chore(release): v0.13.1 2023-12-29 07:39:32 +00:00
Aaron 12079e6ffc chore: Update scripts/build.ts show current build step in progress, not completed count (#306) 2023-12-29 01:21:18 -06:00
Aaron ff6740d429 fix: Ship vite/client types internally for proper resolution using PNPM (#304) 2023-12-29 01:21:05 -06:00
37 changed files with 1505 additions and 1151 deletions
+71
View File
@@ -1,5 +1,76 @@
# Changelog
## 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)
+2 -3
View File
@@ -9,9 +9,8 @@
<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>
<!-- 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> -->
<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">
@@ -0,0 +1,34 @@
<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,6 +5,10 @@ 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
@@ -12,6 +16,7 @@ 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);
@@ -24,6 +29,7 @@ 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,6 +2,7 @@ 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 {
@@ -10,5 +11,6 @@ export default {
ctx.app.component('Icon', Icon);
ctx.app.component('EntrypointPatterns', EntrypointPatterns);
ctx.app.component('UsingWxtSection', UsingWxtSection);
ctx.app.component('ExampleList', ExampleList);
},
};
+4 -2
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: ['*://google.com/*', '*://duckduckgo.com/*'],
matches: string[],
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,3 +137,5 @@ export default defineContentScript({
},
});
```
See [Content Script UI](/guide/content-script-ui) for more info on creating UIs and including CSS in content scripts.
+10 -3
View File
@@ -13,15 +13,12 @@
## 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', ...]" />
@@ -31,3 +28,13 @@ Plain old HTML file.
</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="[
['entrypoints/sandbox.html', 'sandbox.html'],
['entrypoints/sandbox/index.html', 'sandbox.html'],
['entrypoints/<name>.sandbox.html', '<name>.html` '],
['entrypoints/<name>.sandbox/index.html', '<name>.html` '],
['sandbox.html', 'sandbox.html'],
['sandbox/index.html', 'sandbox.html'],
['<name>.sandbox.html', '<name>.html` '],
['<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="[
['entrypoints/sidepanel.html', 'sidepanel.html'],
['entrypoints/sidepanel/index.html', 'sidepanel.html'],
['entrypoints/<name>.sidepanel.html', '<name>.html` '],
['entrypoints/<name>.sidepanel/index.html', '<name>.html` '],
['sidepanel.html', 'sidepanel.html'],
['sidepanel/index.html', 'sidepanel.html'],
['<name>.sidepanel.html', '<name>.html` '],
['<name>.sidepanel/index.html', '<name>.html` '],
]"
/>
+1 -1
View File
@@ -1,6 +1,6 @@
# Unlisted Pages
HTML pages that are built by Vite, but are not included in the manifest.
HTML pages that are bundled and shipped with the extension, but are not included in the manifest.
### Examples
+1 -1
View File
@@ -1,6 +1,6 @@
# Unlisted Scripts
TypeScript files that are built, but are not included in the manifest.
TypeScript files that are bundled and shipped with the extension, but are not included in the manifest.
You are responsible for loading/running these scripts where needed.
+1 -21
View File
@@ -2,26 +2,6 @@
Simple walkthroughs to accomplish common tasks or patterns with WXT.
<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>
<ExampleList />
> 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 | HMR | Use page's context |
| ---------- | :-------------: | :-: | :----------------: |
| Integrated | ❌ | ❌ | ✅ |
| ShadowRoot | ✅ | ❌ | ✅ |
| IFrame | ✅ | ✅ | ❌ |
| Method | Isolated Styles | Isolated Events | HMR | Use page's context |
| ---------- | :-------------: | :-----------------: | :-: | :----------------: |
| Integrated | ❌ | ❌ | ❌ | ✅ |
| ShadowRoot | ✅ | ✅ (off by default) | ❌ | ✅ |
| 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.
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.
To use `createContentScriptUi`, follow these steps:
+10
View File
@@ -57,3 +57,13 @@ 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.
:::
## 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`.
:::
+5 -4
View File
@@ -26,11 +26,13 @@ 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/) fields are based on your package.json's `version` property.
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`.
- `version_name` is the exact string listed in your package.json
- `version_name` is the exact string listed in your `package.json` or `wxt.config.ts` file
- `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
@@ -122,5 +124,4 @@ export default defineConfig({
See the official localization examples for more details:
- [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)
<ExampleList tag="i18n" />
+1 -3
View File
@@ -6,9 +6,7 @@ WXT officially supports [Vitest](https://vitest.dev/) for unit tests and either
For details setting up each testing framework, see the official examples:
- [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)
<ExampleList tag="testing" />
### Unofficial Frameworks
+2 -2
View File
@@ -44,7 +44,7 @@ describe('Auto Imports', () => {
".wxt/wxt.d.ts
----------------------------------------
// Generated by wxt
/// <reference types="vite/client" />
/// <reference types="wxt/vite-builder-env" />
/// <reference types="./types/imports.d.ts" />
/// <reference types="./types/paths.d.ts" />
/// <reference types="./types/i18n.d.ts" />
@@ -83,7 +83,7 @@ describe('Auto Imports', () => {
".wxt/wxt.d.ts
----------------------------------------
// Generated by wxt
/// <reference types="vite/client" />
/// <reference types="wxt/vite-builder-env" />
/// <reference types="./types/paths.d.ts" />
/// <reference types="./types/i18n.d.ts" />
/// <reference types="./types/globals.d.ts" />
-640
View File
@@ -1,556 +1,7 @@
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 },
@@ -579,95 +30,4 @@ 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",
}
`);
});
});
+1 -1
View File
@@ -227,7 +227,7 @@ describe('TypeScript Project', () => {
".wxt/wxt.d.ts
----------------------------------------
// Generated by wxt
/// <reference types="vite/client" />
/// <reference types="wxt/vite-builder-env" />
/// <reference types="./types/imports.d.ts" />
/// <reference types="./types/paths.d.ts" />
/// <reference types="./types/i18n.d.ts" />
+7 -4
View File
@@ -1,7 +1,7 @@
{
"name": "wxt",
"type": "module",
"version": "0.13.0",
"version": "0.13.4",
"description": "Next gen framework for developing web extensions",
"engines": {
"node": ">=18",
@@ -76,6 +76,9 @@
"types": "./dist/storage.d.cts",
"default": "./dist/storage.cjs"
}
},
"./vite-builder-env": {
"types": "./dist/vite-builder-env.d.ts"
}
},
"scripts": {
@@ -90,7 +93,7 @@
"compile:wxt": "tsc --noEmit",
"compile:virtual": "tsc --noEmit -p src/virtual",
"test": "vitest",
"test:coverage": "vitest run --coverage",
"test:coverage": "vitest run --coverage.enabled \"--coverage.include=src/**\" \"--coverage.exclude=src/core/utils/testing/**\" \"--coverage.exclude=**/*.d.ts\"",
"prepare": "simple-git-hooks",
"prepublish": "pnpm -s build",
"docs:gen": "typedoc --options docs/typedoc.json",
@@ -102,7 +105,7 @@
"dependencies": {
"@types/webextension-polyfill": "^0.10.5",
"@webext-core/fake-browser": "^1.3.1",
"@webext-core/isolated-element": "^1.0.4",
"@webext-core/isolated-element": "^1.1.1",
"@webext-core/match-patterns": "^1.0.3",
"async-mutex": "^0.4.0",
"c12": "^1.5.1",
@@ -157,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.31",
"vitepress": "1.0.0-rc.34",
"vitest": "^1.1.0",
"vitest-mock-extended": "^1.3.1",
"vue": "^3.3.10"
+280 -407
View File
File diff suppressed because it is too large Load Diff
+6 -3
View File
@@ -29,9 +29,9 @@ const preset = {
} satisfies tsup.Options;
function spinnerPMap(configs: tsup.Options[]) {
let completed = 0;
let progress = 1;
const updateSpinner = () => {
spinner.text = `${spinnerText} [${completed}/${configs.length}]`;
spinner.text = `${spinnerText} [${progress}/${configs.length}]`;
};
updateSpinner();
@@ -39,7 +39,7 @@ function spinnerPMap(configs: tsup.Options[]) {
config,
async (config) => {
const res = await tsup.build(config);
completed++;
progress++;
updateSpinner();
return res;
},
@@ -106,6 +106,9 @@ await spinnerPMap(config).catch((err) => {
process.exit(1);
});
// Copy "public" files that need shipped inside WXT
await fs.copyFile('src/vite-builder-env.d.ts', 'dist/vite-builder-env.d.ts');
spinner.clear().stop();
const duration = Date.now() - startTime;
@@ -5,6 +5,7 @@ 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');
@@ -332,4 +333,26 @@ 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. Automatically removed from the DOM
* when the content script's context is invalidated.
* 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.
*
* 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,6 +56,7 @@ export async function createContentScriptUi<TApp>(
textContent: css.join('\n').trim(),
},
mode: 'open',
isolateEvents: options.isolateEvents,
});
let mounted: TApp;
@@ -170,4 +171,13 @@ 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,6 +40,10 @@ 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(
+912
View File
@@ -0,0 +1,912 @@
import { describe, expect, it } from 'vitest';
import { generateManifest } 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',
]);
});
});
});
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();
});
});
});
});
+1 -1
View File
@@ -180,7 +180,7 @@ async function writeMainDeclarationFile(
filePath,
[
'// Generated by wxt',
`/// <reference types="vite/client" />`,
`/// <reference types="wxt/vite-builder-env" />`,
...references.map(
(ref) =>
`/// <reference types="./${normalizePath(relative(dir, ref))}" />`,
+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 { generateMainfest, writeManifest } from '~/core/utils/manifest';
import { generateManifest, 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 generateMainfest(
const newManifest = await generateManifest(
allEntrypoints,
mergedOutput,
config,
+28 -8
View File
@@ -50,29 +50,41 @@ export async function writeManifest(
/**
* Generates the manifest based on the config and entrypoints.
*/
export async function generateMainfest(
export async function generateManifest(
entrypoints: Entrypoint[],
buildOutput: Omit<BuildOutput, 'manifest'>,
config: InternalConfig,
): Promise<Manifest.WebExtensionManifest> {
const pkg = await getPackageJson(config);
const versionName = config.manifest.version_name ?? pkg?.version;
const version = config.manifest.version ?? simplifyVersion(pkg?.version);
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 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(
@@ -80,6 +92,14 @@ export async function generateMainfest(
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);
+36 -7
View File
@@ -17,6 +17,9 @@ import {
OutputChunk,
OutputFile,
OutputAsset,
BuildOutput,
BuildStepOutput,
UserManifest,
} from '~/types';
import { mock } from 'vitest-mock-extended';
@@ -39,6 +42,16 @@ 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',
@@ -65,8 +78,8 @@ export const fakeContentScriptEntrypoint =
export const fakeBackgroundEntrypoint = fakeObjectCreator<BackgroundEntrypoint>(
() => ({
type: 'background',
inputPath: fakeFile('src'),
name: faker.string.alpha(),
inputPath: 'entrypoints/background.ts',
name: 'background',
options: {
persistent: faker.helpers.arrayElement([true, false, undefined]),
type: faker.helpers.maybe(() => 'module'),
@@ -87,8 +100,8 @@ export const fakeUnlistedScriptEntrypoint =
export const fakeOptionsEntrypoint = fakeObjectCreator<OptionsEntrypoint>(
() => ({
type: 'options',
inputPath: fakeFile('src'),
name: faker.string.alpha(),
inputPath: 'entrypoints/options.html',
name: 'options',
outputDir: fakeDir('.output'),
options: {
browserStyle: faker.helpers.arrayElement([true, false, undefined]),
@@ -100,8 +113,8 @@ export const fakeOptionsEntrypoint = fakeObjectCreator<OptionsEntrypoint>(
export const fakePopupEntrypoint = fakeObjectCreator<PopupEntrypoint>(() => ({
type: 'popup',
inputPath: fakeFile('src'),
name: faker.string.alpha(),
inputPath: 'entrypoints/popup.html',
name: 'popup',
outputDir: fakeDir('.output'),
options: {
defaultTitle: faker.helpers.arrayElement([
@@ -165,6 +178,11 @@ 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++) {
@@ -187,7 +205,7 @@ export const fakeInternalConfig = fakeObjectCreator<InternalConfig>(() => {
fsCache: mock<FsCache>(),
imports: {},
logger: mock(),
manifest: fakeManifest(),
manifest: fakeUserManifest(),
manifestVersion,
mode,
outBaseDir: fakeDir(),
@@ -222,3 +240,14 @@ 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,6 +26,12 @@ 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 {
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
+4 -4
View File
@@ -19,10 +19,10 @@
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.2.14",
"@types/react-dom": "^18.2.6",
"@types/react": "^18.2.46",
"@types/react-dom": "^18.2.18",
"@vitejs/plugin-react": "^4.2.1",
"typescript": "^5.3.2",
"wxt": "^0.12.0"
"typescript": "^5.3.3",
"wxt": "^0.13.0"
}
}
+3 -3
View File
@@ -15,11 +15,11 @@
"postinstall": "wxt prepare"
},
"dependencies": {
"solid-js": "^1.7.8"
"solid-js": "^1.8.7"
},
"devDependencies": {
"typescript": "^5.3.2",
"typescript": "^5.3.3",
"vite-plugin-solid": "^2.8.0",
"wxt": "^0.12.0"
"wxt": "^0.13.0"
}
}
+7 -7
View File
@@ -14,12 +14,12 @@
"postinstall": "wxt prepare"
},
"devDependencies": {
"@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"
"@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"
}
}
+2 -2
View File
@@ -15,7 +15,7 @@
"postinstall": "wxt prepare"
},
"devDependencies": {
"typescript": "^5.3.2",
"wxt": "^0.12.0"
"typescript": "^5.3.3",
"wxt": "^0.13.0"
}
}
+5 -5
View File
@@ -15,12 +15,12 @@
"postinstall": "wxt prepare"
},
"dependencies": {
"vue": "^3.3.4"
"vue": "^3.4.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.5.1",
"typescript": "^5.3.2",
"vue-tsc": "^1.8.25",
"wxt": "^0.12.0"
"@vitejs/plugin-vue": "^5.0.1",
"typescript": "^5.3.3",
"vue-tsc": "^1.8.27",
"wxt": "^0.13.0"
}
}