Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0369316463 | |||
| 8b74291d18 | |||
| 739b738100 | |||
| 95442dd39b | |||
| 4f82645c08 | |||
| d200c376f8 | |||
| b58fb02016 | |||
| e1bab6c746 | |||
| d10c22fbd9 | |||
| da1f7f3ea5 | |||
| 2659272f8a | |||
| 868fd27804 | |||
| d9fdcb5b33 | |||
| 580793158f | |||
| f58d69dc5f | |||
| c9028dd335 | |||
| 6342f59c5e | |||
| 2d91898452 | |||
| 4acefd601c | |||
| 9a2e71b481 | |||
| 6e578f3156 | |||
| 474de83e28 | |||
| 65fcfc0064 |
+4
-2
@@ -2,5 +2,7 @@ coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
target: 80%
|
||||
threshold: 1%
|
||||
informational: true
|
||||
patch:
|
||||
default:
|
||||
informational: true
|
||||
|
||||
+2
-1
@@ -17,5 +17,6 @@ web-ext.config.ts
|
||||
templates/*/pnpm-lock.yaml
|
||||
templates/*/yarn.lock
|
||||
templates/*/package-lock.json
|
||||
docs/config.md
|
||||
docs/api/config.md
|
||||
docs/api/cli.md
|
||||
stats.html
|
||||
|
||||
@@ -12,3 +12,5 @@ CHANGELOG.md
|
||||
*.svg
|
||||
*.txt
|
||||
_gitignore
|
||||
_redirects
|
||||
*.svelte
|
||||
|
||||
@@ -1,5 +1,81 @@
|
||||
# Changelog
|
||||
|
||||
## v0.7.5
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.7.4...v0.7.5)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- More consistent `version_name` generation between browsers ([#163](https://github.com/wxt-dev/wxt/pull/163))
|
||||
- Ignore non-manifest fields when merging content script entries ([#164](https://github.com/wxt-dev/wxt/pull/164))
|
||||
- Add `browser_style` to popup options ([#165](https://github.com/wxt-dev/wxt/pull/165))
|
||||
|
||||
## v0.7.4
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.7.3...v0.7.4)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Support `react-refresh` when pre-rendering HTML pages in dev mode ([#158](https://github.com/wxt-dev/wxt/pull/158))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Add migration guides ([b58fb02](https://github.com/wxt-dev/wxt/commit/b58fb02))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Upgrade templates to v0.7 ([#156](https://github.com/wxt-dev/wxt/pull/156))
|
||||
|
||||
## v0.7.3
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.7.2...v0.7.3)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Support JS entrypoints ([#155](https://github.com/wxt-dev/wxt/pull/155))
|
||||
|
||||
## v0.7.2
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.7.1...v0.7.2)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Allow customizing entrypoint options per browser ([#154](https://github.com/wxt-dev/wxt/pull/154))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Default safari to MV2 ([5807931](https://github.com/wxt-dev/wxt/commit/5807931))
|
||||
- Add missing `persistent` type to `defineBackgroundScript` ([d9fdcb5](https://github.com/wxt-dev/wxt/commit/d9fdcb5))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Restructure website to improve UX ([#149](https://github.com/wxt-dev/wxt/pull/149))
|
||||
- Add docs for development and testing ([f58d69d](https://github.com/wxt-dev/wxt/commit/f58d69d))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- **deps-dev:** Bump @types/fs-extra from 11.0.1 to 11.0.2 ([#144](https://github.com/wxt-dev/wxt/pull/144))
|
||||
- **deps-dev:** Bump @faker-js/faker from 8.0.2 to 8.1.0 ([#146](https://github.com/wxt-dev/wxt/pull/146))
|
||||
- **deps-dev:** Bump vitest-mock-extended from 1.2.1 to 1.3.0 ([#147](https://github.com/wxt-dev/wxt/pull/147))
|
||||
- **deps-dev:** Bump vitest from 0.34.3 to 0.34.6 ([#145](https://github.com/wxt-dev/wxt/pull/145))
|
||||
- **deps-dev:** Bump typescript from 5.1 to 5.2 ([#148](https://github.com/wxt-dev/wxt/pull/148))
|
||||
|
||||
## v0.7.1
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.7.0...v0.7.1)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- `createContentScriptUi` helper ([#143](https://github.com/wxt-dev/wxt/pull/143))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Add docs for `createContentScriptUi` ([65fcfc0](https://github.com/wxt-dev/wxt/commit/65fcfc0))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- **release:** V0.7.1-alpha1 ([2d4983e](https://github.com/wxt-dev/wxt/commit/2d4983e))
|
||||
|
||||
## v0.7.0
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.6.6...v0.7.0)
|
||||
|
||||
@@ -28,11 +28,11 @@
|
||||
<p align="center">
|
||||
<a href="https://wxt.dev" target="_blank">Get Started</a>
|
||||
•
|
||||
<a href="https://wxt.dev/get-started/installation.html" target="_blank">Installation</a>
|
||||
<a href="https://wxt.dev/guide/installation.html" target="_blank">Installation</a>
|
||||
•
|
||||
<a href="https://wxt.dev/guide/background.html" target="_blank">Entrypoints</a>
|
||||
<a href="https://wxt.dev/entrypoints/background.html" target="_blank">Entrypoints</a>
|
||||
•
|
||||
<a href="https://wxt.dev/config.html" target="_blank">Configuration</a>
|
||||
<a href="https://wxt.dev/api/config.html" target="_blank">Configuration</a>
|
||||
</p>
|
||||
|
||||

|
||||
@@ -45,7 +45,7 @@ Bootstrap a new project:
|
||||
pnpx wxt@latest init <project-name>
|
||||
```
|
||||
|
||||
Or see the [installation guide](https://wxt.dev/get-started/installation.html) to get started with WXT.
|
||||
Or see the [installation guide](https://wxt.dev/guide/installation.html) to get started with WXT.
|
||||
|
||||
## Features
|
||||
|
||||
|
||||
+53
-34
@@ -1,5 +1,6 @@
|
||||
import { defineConfig } from 'vitepress';
|
||||
import { generateConfigDocs } from './plugins/generate-config-docs';
|
||||
import { generateCliDocs } from './plugins/generate-cli-docs';
|
||||
|
||||
const title = 'Next-gen Web Extension Framework';
|
||||
const titleSuffix = ' – WXT';
|
||||
@@ -17,7 +18,7 @@ export default defineConfig({
|
||||
description,
|
||||
vite: {
|
||||
clearScreen: false,
|
||||
plugins: [generateConfigDocs()],
|
||||
plugins: [generateConfigDocs(), generateCliDocs()],
|
||||
},
|
||||
lastUpdated: true,
|
||||
sitemap: {
|
||||
@@ -55,56 +56,74 @@ export default defineConfig({
|
||||
},
|
||||
|
||||
nav: [
|
||||
{ text: 'Get Started', link: '/get-started/installation.md' },
|
||||
{ text: 'Guide', link: '/guide/auto-imports.md' },
|
||||
{ text: 'Config', link: '/config.md' },
|
||||
{ text: 'API', link: '/api.md' },
|
||||
{ text: 'Guide', link: '/guide/installation.md' },
|
||||
{ text: 'Entrypoints', link: '/entrypoints/background.md' },
|
||||
{ text: 'API', link: '/api/cli.md' },
|
||||
],
|
||||
|
||||
sidebar: {
|
||||
'/get-started/': [
|
||||
{
|
||||
text: 'Get Started',
|
||||
items: [
|
||||
{ text: 'Introduction', link: '/get-started/introduction.md' },
|
||||
{ text: 'Installation', link: '/get-started/installation.md' },
|
||||
{ text: 'Configuration', link: '/get-started/configuration.md' },
|
||||
{ text: 'Entrypoints', link: '/get-started/entrypoints.md' },
|
||||
{ text: 'Assets', link: '/get-started/assets.md' },
|
||||
{ text: 'Build Targets', link: '/get-started/build-targets.md' },
|
||||
{ text: 'Publishing', link: '/get-started/publishing.md' },
|
||||
{ text: 'Testing', link: '/get-started/testing.md' },
|
||||
{ text: 'Compare', link: '/get-started/compare.md' },
|
||||
],
|
||||
},
|
||||
],
|
||||
'/guide/': [
|
||||
{
|
||||
text: 'Guide',
|
||||
items: [
|
||||
{ text: 'Introduction', link: '/guide/introduction.md' },
|
||||
{ text: 'Installation', link: '/guide/installation.md' },
|
||||
{ text: 'Configuration', link: '/guide/configuration.md' },
|
||||
{ text: 'Entrypoints', link: '/guide/entrypoints.md' },
|
||||
{ text: 'Assets', link: '/guide/assets.md' },
|
||||
{ text: 'Multiple Browsers', link: '/guide/multiple-browsers.md' },
|
||||
{ text: 'Publishing', link: '/guide/publishing.md' },
|
||||
{ text: 'Auto-imports', link: '/guide/auto-imports.md' },
|
||||
{ text: 'Manifest.json', link: '/guide/manifest.md' },
|
||||
{ text: 'Extension APIs', link: '/guide/extension-apis.md' },
|
||||
{ text: 'Remote Code', link: '/guide/remote-code.md' },
|
||||
{ text: 'Development', link: '/guide/development.md' },
|
||||
{ text: 'Testing', link: '/guide/testing.md' },
|
||||
{ text: 'Vite', link: '/guide/vite.md' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Other',
|
||||
items: [
|
||||
{ text: 'Migrate to WXT', link: '/guide/migrate-to-wxt.md' },
|
||||
{ text: 'Compare', link: '/guide/compare.md' },
|
||||
],
|
||||
},
|
||||
],
|
||||
'/entrypoints/': [
|
||||
{
|
||||
text: 'Entrypoints',
|
||||
items: [
|
||||
{ text: 'Background', link: '/guide/background.md' },
|
||||
{ text: 'Bookmarks', link: '/guide/bookmarks.md' },
|
||||
{ text: 'Content Scripts', link: '/guide/content-scripts.md' },
|
||||
{ text: 'CSS', link: '/guide/css.md' },
|
||||
{ text: 'Devtools', link: '/guide/devtools.md' },
|
||||
{ text: 'History', link: '/guide/history.md' },
|
||||
{ text: 'Newtab', link: '/guide/newtab.md' },
|
||||
{ text: 'Options', link: '/guide/options.md' },
|
||||
{ text: 'Popup', link: '/guide/popup.md' },
|
||||
{ text: 'Sandbox', link: '/guide/sandbox.md' },
|
||||
{ text: 'Sidepanel', link: '/guide/sidepanel.md' },
|
||||
{ text: 'Unlisted Pages', link: '/guide/unlisted-pages.md' },
|
||||
{ text: 'Unlisted Scripts', link: '/guide/unlisted-scripts.md' },
|
||||
{ text: 'Background', link: '/entrypoints/background.md' },
|
||||
{ text: 'Bookmarks', link: '/entrypoints/bookmarks.md' },
|
||||
{
|
||||
text: 'Content Scripts',
|
||||
link: '/entrypoints/content-scripts.md',
|
||||
},
|
||||
{ text: 'CSS', link: '/entrypoints/css.md' },
|
||||
{ text: 'Devtools', link: '/entrypoints/devtools.md' },
|
||||
{ text: 'History', link: '/entrypoints/history.md' },
|
||||
{ text: 'Newtab', link: '/entrypoints/newtab.md' },
|
||||
{ text: 'Options', link: '/entrypoints/options.md' },
|
||||
{ text: 'Popup', link: '/entrypoints/popup.md' },
|
||||
{ text: 'Sandbox', link: '/entrypoints/sandbox.md' },
|
||||
{ text: 'Sidepanel', link: '/entrypoints/sidepanel.md' },
|
||||
{ text: 'Unlisted Pages', link: '/entrypoints/unlisted-pages.md' },
|
||||
{
|
||||
text: 'Unlisted Scripts',
|
||||
link: '/entrypoints/unlisted-scripts.md',
|
||||
},
|
||||
].sort((l, r) => l.text.localeCompare(r.text)),
|
||||
},
|
||||
],
|
||||
'/api/': [
|
||||
{
|
||||
items: [
|
||||
{ text: 'CLI', link: '/api/cli.md' },
|
||||
{ text: 'Config', link: '/api/config.md' },
|
||||
{ text: 'wxt', link: '/api/wxt.md' },
|
||||
{ text: 'wxt/browser', link: '/api/wxt-browser.md' },
|
||||
{ text: 'wxt/client', link: '/api/wxt-client.md' },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import { relative, resolve } from 'node:path';
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import { format } from 'prettier';
|
||||
import consola from 'consola';
|
||||
import { execaCommand } from 'execa';
|
||||
|
||||
let hasGenerated = false;
|
||||
|
||||
const cliDir = resolve('src/cli/commands');
|
||||
const cliDirGlob = resolve(cliDir, '**');
|
||||
const cliTemplatePath = resolve('docs/api/cli.tpl.md');
|
||||
const cliPath = resolve('docs/api/cli.md');
|
||||
|
||||
const PREFACE = `<!--
|
||||
DO NOT EDIT
|
||||
Generated by \`wxt/docs/.vitepress/plugins/generate-config-docs.ts\`
|
||||
To make changes to the config reference, update the JSDoc in \`src/core/types/external.ts\`.
|
||||
-->`;
|
||||
|
||||
export function generateCliDocs() {
|
||||
writeFileSync(cliPath, '');
|
||||
|
||||
const generateDocs = async () => {
|
||||
consola.info(`Generating ${relative(process.cwd(), cliPath)}`);
|
||||
try {
|
||||
const res = await execaCommand('pnpm -s wxt --help');
|
||||
const dev = splitInfo(res.stdout);
|
||||
const lines: Array<string | string[]> = [
|
||||
`## \`wxt\``,
|
||||
'```sh',
|
||||
dev.rest,
|
||||
'```',
|
||||
];
|
||||
|
||||
const commands = await Promise.all(
|
||||
extractCommands(dev.info).map(async (command) => {
|
||||
const res = await execaCommand(`pnpm -s wxt ${command} --help`);
|
||||
const { rest: docs } = splitInfo(res.stdout);
|
||||
return [`## \`wxt ${command}\``, '```sh', docs, '```'];
|
||||
}),
|
||||
);
|
||||
lines.push(...commands);
|
||||
|
||||
const text = await format(
|
||||
PREFACE +
|
||||
'\n\n' +
|
||||
readFileSync(cliTemplatePath, 'utf-8').replace(
|
||||
'{{ DOCS }}',
|
||||
lines.flat().join('\n'),
|
||||
),
|
||||
{ parser: 'markdown' },
|
||||
);
|
||||
|
||||
writeFileSync(cliPath, text);
|
||||
consola.success(`Generated ${relative(process.cwd(), cliPath)}`);
|
||||
} catch (err) {
|
||||
consola.fail(`Failed to generate ${relative(process.cwd(), cliPath)}`);
|
||||
consola.error(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
name: 'docs:generate-cli-docs',
|
||||
async config() {
|
||||
if (!hasGenerated) {
|
||||
hasGenerated = true;
|
||||
await generateDocs();
|
||||
}
|
||||
},
|
||||
configureServer(server: any) {
|
||||
server.watcher.add(cliDirGlob);
|
||||
},
|
||||
async handleHotUpdate(ctx: { file: string }) {
|
||||
if (ctx.file === cliTemplatePath || ctx.file.includes(cliDir)) {
|
||||
await generateDocs();
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function splitInfo(text: string): { info: string; rest: string } {
|
||||
const infoStart = text.indexOf('For more info,');
|
||||
const infoEnd = text.indexOf('\n\nOptions:');
|
||||
|
||||
if (infoStart === -1 || infoEnd === -1) {
|
||||
return { info: '', rest: text };
|
||||
}
|
||||
|
||||
const info = text.substring(infoStart, infoEnd).trim();
|
||||
const rest = text.replace(info, '').trim().replace('\n\n\n\n', '\n\n');
|
||||
|
||||
return { info, rest };
|
||||
}
|
||||
|
||||
function extractCommands(info: string): string[] {
|
||||
const commands: string[] = [];
|
||||
|
||||
// Split the info by line and iterate through each line
|
||||
info.split('\n').forEach((line) => {
|
||||
// Use regex to capture the command after "$ wxt " and before "--help"
|
||||
const match = line.match(/\$ wxt (\w+) --help/);
|
||||
if (match && match[1]) {
|
||||
commands.push(match[1]);
|
||||
}
|
||||
});
|
||||
|
||||
return commands;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { resolve } from 'node:path';
|
||||
import { relative, resolve } from 'node:path';
|
||||
import { Project, ts, Type, Node, JSDocableNode } from 'ts-morph';
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import { format } from 'prettier';
|
||||
@@ -7,8 +7,8 @@ import consola from 'consola';
|
||||
let hasGenerated = false;
|
||||
|
||||
const externalTypesPath = resolve('src/core/types/external.ts');
|
||||
const configTemplatePath = resolve('docs/config.tpl.md');
|
||||
const configPath = resolve('docs/config.md');
|
||||
const configTemplatePath = resolve('docs/api/config.tpl.md');
|
||||
const configPath = resolve('docs/api/config.md');
|
||||
|
||||
const PREFACE = `<!--
|
||||
DO NOT EDIT
|
||||
@@ -39,7 +39,7 @@ export function generateConfigDocs() {
|
||||
writeFileSync(configPath, '');
|
||||
|
||||
const generateDocs = async () => {
|
||||
consola.info('Generating /config.md');
|
||||
consola.info(`Generating ${relative(process.cwd(), configPath)}`);
|
||||
try {
|
||||
const project = new Project({
|
||||
tsConfigFilePath: resolve('tsconfig.json'),
|
||||
@@ -118,9 +118,9 @@ export function generateConfigDocs() {
|
||||
);
|
||||
|
||||
writeFileSync(configPath, text);
|
||||
consola.success('Generated /config.md');
|
||||
consola.success(`Generated ${relative(process.cwd(), configPath)}`);
|
||||
} catch (err) {
|
||||
consola.fail('Failed to generate /config.md');
|
||||
consola.fail(`Failed to generate ${relative(process.cwd(), configPath)}`);
|
||||
consola.error(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
# API Reference
|
||||
|
||||
:::warning 🚧 Under construction
|
||||
This documentation does not exist yet.
|
||||
:::
|
||||
@@ -0,0 +1,5 @@
|
||||
# CLI Reference
|
||||
|
||||
> Reference generated from `wxt <command> --help`
|
||||
|
||||
{{ DOCS }}
|
||||
@@ -0,0 +1,5 @@
|
||||
# Config Reference
|
||||
|
||||
All the options you can use in your `wxt.config.ts` file.
|
||||
|
||||
{{ DOCS }}
|
||||
@@ -0,0 +1,10 @@
|
||||
# `wxt/browser` Reference
|
||||
|
||||
:::warning 🚧 Under construction
|
||||
This documentation does not exist yet. All APIs are documented with JSDoc, so for now, you can view the documentation in your editor.
|
||||
|
||||
```ts
|
||||
import { browser } from 'wxt/browser';
|
||||
```
|
||||
|
||||
:::
|
||||
@@ -0,0 +1,14 @@
|
||||
# `wxt/client` Reference
|
||||
|
||||
:::warning 🚧 Under construction
|
||||
This documentation does not exist yet. All APIs are documented with JSDoc, so for now, you can view the documentation in your editor.
|
||||
|
||||
```ts
|
||||
import {
|
||||
createContentScriptUi,
|
||||
defineBackground,
|
||||
defineContentScript,
|
||||
} from 'wxt/client';
|
||||
```
|
||||
|
||||
:::
|
||||
@@ -0,0 +1,17 @@
|
||||
# `wxt` Reference
|
||||
|
||||
:::warning 🚧 Under construction
|
||||
This documentation does not exist yet. All APIs are documented with JSDoc, so for now, you can view the documentation in your editor.
|
||||
|
||||
```ts
|
||||
import {
|
||||
build,
|
||||
clean,
|
||||
createServer,
|
||||
defineConfig,
|
||||
defineRunnerConfig,
|
||||
version,
|
||||
} from 'wxt';
|
||||
```
|
||||
|
||||
:::
|
||||
@@ -1,5 +0,0 @@
|
||||
# Config Reference
|
||||
|
||||
Discover all the options you can use in your `wxt.config.ts` file.
|
||||
|
||||
{{ DOCS }}
|
||||
@@ -8,8 +8,8 @@ For MV2, the background is added as a script to the background page. For MV3, th
|
||||
|
||||
<EntrypointPatterns
|
||||
:patterns="[
|
||||
['background.ts', 'background.js'],
|
||||
['background/index.ts', 'background.js'],
|
||||
['background.[jt]s', 'background.js'],
|
||||
['background/index.[jt]s', 'background.js'],
|
||||
]"
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
# Content Scripts
|
||||
|
||||
[Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/content_scripts/) • [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Content_scripts)
|
||||
|
||||
When creating content script entrypoints, they are automatically included in the `manifest.json` along with any CSS files they import.
|
||||
|
||||
## Filenames
|
||||
|
||||
<EntrypointPatterns
|
||||
:patterns="[
|
||||
['content.[jt]sx?', 'content-scripts/content.js'],
|
||||
['content/index.[jt]sx?', 'content-scripts/content.js'],
|
||||
['<name>.content.[jt]sx?', 'content-scripts/<name>.js'],
|
||||
['<name>.content/index.[jt]sx?', 'content-scripts/<name>.js'],
|
||||
]"
|
||||
/>
|
||||
|
||||
## Definition
|
||||
|
||||
```ts
|
||||
export default defineContentScript({
|
||||
// Set manifest options
|
||||
matches: ['*://google.com/*', '*://duckduckgo.com/*'],
|
||||
excludeMatches: undefined | [],
|
||||
includeGlobs: undefined | [],
|
||||
excludeGlobs: undefined | [],
|
||||
allFrames: undefined | [],
|
||||
runAt: undefined | 'document_start' | 'document_end' | 'document_idle',
|
||||
matchAboutBlank: undefined | true | false,
|
||||
matchOriginAsFallback: undefined | true | false,
|
||||
world: undefined | 'ISOLATED' | 'MAIN',
|
||||
|
||||
// Set include/exclude if the background should be removed from some builds
|
||||
include: undefined | string[],
|
||||
exclude: undefined | string[],
|
||||
|
||||
// Configure how CSS is injected onto the page
|
||||
cssInjectionMode: undefined | "manifest" | "manual" | "ui",
|
||||
|
||||
main(ctx) {
|
||||
// Executed when content script is loaded
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
> All manifest options default to `undefined`.
|
||||
|
||||
When defining multiple content scripts, content script entrypoints that have the same set of options will be merged into a single `content_script` item in the manifest.
|
||||
|
||||
## Context
|
||||
|
||||
Old content scripts are not automatically stopped when an extension updates and reloads. Often, this leads to "Invalidated context" errors in production when a content script from an old version of your extension tries to use a extension API.
|
||||
|
||||
WXT provides a utility for managing this process: `ContentScriptContext`. An instance of this class is provided to you automatically inside the `main` function of your content script.
|
||||
|
||||
```ts
|
||||
export default defineContentScript({
|
||||
// ...
|
||||
main(ctx: ContentScriptContext) {
|
||||
// Add custom listeners for stopping work
|
||||
ctx.onInvalidated(() => {
|
||||
// ...
|
||||
});
|
||||
|
||||
// Stop fetch requests
|
||||
fetch('...url', { signal: ctx.signal });
|
||||
|
||||
// Timeout utilities
|
||||
ctx.setTimeout(() => {
|
||||
// ...
|
||||
}, 5e3);
|
||||
ctx.setInterval(() => {
|
||||
// ...
|
||||
}, 60e3);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
The class extends [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) and provides other utilities for stopping a content script's logic once it becomes invalidated.
|
||||
|
||||
:::tip
|
||||
When working with content scripts, **you should always use the `ctx` object to stop any async or future work.**
|
||||
|
||||
This prevents old content scripts from interfering with new content scripts, and prevents error messages from the console in production.
|
||||
:::
|
||||
|
||||
## CSS
|
||||
|
||||
To include CSS with your content script, import the CSS file at the top of your entrypoint.
|
||||
|
||||
```
|
||||
|
||||
<srcDir>
|
||||
└─ entrypoints/
|
||||
└─ overlay.content/
|
||||
├─ index.ts
|
||||
└─ style.css
|
||||
```
|
||||
|
||||
```ts
|
||||
// entrypoints/overlay.content/index.ts
|
||||
import './style.css';
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ['*://google.com/*', '*://duckduckgo.com/*'],
|
||||
|
||||
main(ctx) {
|
||||
// ...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Any styles imported in your content script will be added to that content script's `css` array in your `manifest.json`:
|
||||
|
||||
```json
|
||||
// .output/chrome-mv3/manifest.json
|
||||
{
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["*://google.com/*", "*://duckduckgo.com/*"],
|
||||
"js": ["content-scripts/overlay.js"],
|
||||
"css": ["content-scripts/overlay.css"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
To disable this behavior, set `cssInjectionMode` to `"manual"` or `"ui"`.
|
||||
|
||||
```ts
|
||||
export default defineContentScript({
|
||||
matches: ['*://google.com/*', '*://duckduckgo.com/*'],
|
||||
cssInjectionMode: 'manual',
|
||||
|
||||
main(ctx) {
|
||||
// ...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## UI
|
||||
|
||||
WXT provides a utility function, `createContentScriptUi` to simplify mounting a UI from a content script. Internally, it uses the `ShadowRoot` API to isolate your CSS from the webpages.
|
||||
|
||||
`createContentScriptUi` requires a `ContentScriptContext` so that when the context is invalidated, the UI is automatically removed from the webpage.
|
||||
|
||||
:::details When to use `createContentScriptUi`
|
||||
You should only use `createContentScriptUi` if you want your UI's styles isolated from the webpages. If you want to create a more "integrated" UI that uses the page's styles, you can just use the regular JS API's to append your UI to the page.
|
||||
|
||||
```ts
|
||||
const ui = document.createElement('div');
|
||||
const anchor = document.querySelector('#anchor-selector');
|
||||
anchor.append(ui);
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
### Usage
|
||||
|
||||
To use `createContentScriptUi`, follow these steps:
|
||||
|
||||
1. Import your CSS file at the top of your content script
|
||||
2. Set `cssInjectionMode: "ui"` inside `defineContentScript`
|
||||
3. Call `createContentScriptUi`
|
||||
4. Call `ui.mount()` to add the UI to the webpage
|
||||
|
||||
Here's a basic example:
|
||||
|
||||
```ts
|
||||
// entrypoints/ui.content/index.ts
|
||||
import './style.css';
|
||||
|
||||
export default defineContentScript({
|
||||
// ...
|
||||
cssInjectionMode: 'ui',
|
||||
|
||||
async main(ctx) {
|
||||
const ui = await createContentScriptUi(ctx, {
|
||||
name: 'example-ui',
|
||||
type: 'inline',
|
||||
anchor: '#some-element',
|
||||
append: 'after',
|
||||
mount(container) {
|
||||
// Mount UI inside `container`...
|
||||
},
|
||||
});
|
||||
|
||||
// Yoy must call `mount` to add the UI to the page.
|
||||
ui.mount();
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
If you're using a frontend framework, you'll also need to include an `onRemoved` callback:
|
||||
|
||||
:::code-group
|
||||
|
||||
```ts [Vue]
|
||||
import { createApp } from 'vue';
|
||||
|
||||
createContentScriptUi(ctx, {
|
||||
// ...
|
||||
mount(container) {
|
||||
// Create a new app and mount it inside the container
|
||||
const app = createApp(...);
|
||||
app.mount(container);
|
||||
return app;
|
||||
},
|
||||
onRemove(app) {
|
||||
// When the UI is removed from the DOM, call unmount to stop the app
|
||||
app.unmount();
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```ts [React]
|
||||
import ReactDOM from 'react-dom/client';
|
||||
|
||||
createContentScriptUi(ctx, {
|
||||
// ...
|
||||
mount(container) {
|
||||
// Create a root using the container and render your app
|
||||
const root = ReactDOM.createRoot(container);
|
||||
root.render(...);
|
||||
return root;
|
||||
},
|
||||
onRemove(root) {
|
||||
// When the UI is removed from the DOM, call unmount to stop the app
|
||||
root.unmount();
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```ts [Svelte]
|
||||
import App from './App.svelte';
|
||||
|
||||
createContentScriptUi(ctx, {
|
||||
// ...
|
||||
mount(container) {
|
||||
// Mount your app component inside the container
|
||||
return new App({
|
||||
target: container,
|
||||
});
|
||||
},
|
||||
onRemove(app) {
|
||||
// When the UI is removed from the DOM, call $destroy to stop the app
|
||||
app.$destroy();
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```ts [Solid]
|
||||
import { render } from 'solid-js/web';
|
||||
|
||||
createContentScriptUi(ctx, {
|
||||
// ...
|
||||
mount(container) {
|
||||
// Render your app component into the container
|
||||
return render(() => ..., container)
|
||||
},
|
||||
onRemove(unmount) {
|
||||
// When the UI is removed from the DOM, call unmount to stop the app
|
||||
unmount();
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
### `anchor`
|
||||
|
||||
The anchor dictates where the UI will be mounted.
|
||||
|
||||
### `append`
|
||||
|
||||
Customize where the UI get's appended to the DOM, relative to the `anchor` element.
|
||||
|
||||
### `type`
|
||||
|
||||
There are 3 types of UI's you can mount.
|
||||
|
||||
- `inline`: Shows up inline based on the `anchor` and `append` options
|
||||
- `overlay`: Shows up inline, but styled to be 0px by 0px, with overflow visible. This causes the UI to overlay on top of the webpage's content
|
||||
- `modal`: A fullscreen overlay that covers the entire screen, regardless of where it's anchored.
|
||||
|
||||
> TODO: Add visualization of the different UI types.
|
||||
|
||||
### Overlay `alignment`
|
||||
|
||||
Because the overlay UI type results in a 0px by 0px container being added to the webpage, the `alignment` option allows you to configure which corner of your UI is aligned with the 0x0 element.
|
||||
|
||||
> TODO: Add visualization of the different alignments.
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
WXT can build CSS entrypoints individually. CSS entrypoints are always unlisted.
|
||||
|
||||
See [Content Script CSS](/guide/content-scripts.md#css) documentation for the recomended approach to include CSS with a content script.
|
||||
See [Content Script CSS](/entrypoints/content-scripts#css) documentation for the recomended approach to include CSS with a content script.
|
||||
|
||||
:::info
|
||||
If the recommended approach doesn't work for your use case, you can use any of the filename patterns below to build the styles separate from the JS and use the [`transformManifest` hook](/config.md#transformmanifest) to manually add your CSS file to the manifest.
|
||||
If the recommended approach doesn't work for your use case, you can use any of the filename patterns below to build the styles separate from the JS and use the [`transformManifest` hook](/api/config#transformmanifest) to manually add your CSS file to the manifest.
|
||||
:::
|
||||
|
||||
## Filenames
|
||||
@@ -29,6 +29,7 @@
|
||||
}"
|
||||
/>
|
||||
<meta name="manifest.type" content="page_action|browser_action" />
|
||||
<meta name="manifest.browser_style" content="true|false" />
|
||||
<!-- Set include/exclude if the page should be removed from some builds -->
|
||||
<meta name="manifest.include" content="['chrome', ...]" />
|
||||
<meta name="manifest.exclude" content="['chrome', ...]" />
|
||||
@@ -6,8 +6,8 @@ TypeScript files that are built, but are not included in the manifest.
|
||||
|
||||
<EntrypointPatterns
|
||||
:patterns="[
|
||||
['<name>.(ts|tsx)', '<name>.js'],
|
||||
['<name>/index.(ts|tsx)', '<name>.js'],
|
||||
['<name>.[jt]sx?', '<name>.js'],
|
||||
['<name>/index.[jt]sx?', '<name>.js'],
|
||||
]"
|
||||
/>
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
# Build Targets
|
||||
|
||||
You can build an extension for any combination of browser and manifest verison. Different browsers and manifest versions support different APIs and entrypoints, so be sure to check that your extension functions as expected for each target.
|
||||
|
||||
Separate build targets are written to their own output directories:
|
||||
|
||||
```
|
||||
<rootDir>
|
||||
└─ .output
|
||||
├─ chrome-mv3
|
||||
├─ firefox-mv2
|
||||
├─ edge-mv3
|
||||
└─ ...
|
||||
```
|
||||
|
||||
## Target Browser
|
||||
|
||||
To build for a specific browser, pass the `-b --browser` flag from the CLI:
|
||||
|
||||
```
|
||||
|
||||
wxt --browser firefox
|
||||
wxt build --browser firefox
|
||||
|
||||
```
|
||||
|
||||
By default, it will build for `chrome`. When excluding the [`--mv2` or `--mv3` flags](#target-manifest-version), it will default to the commonly accepted manifest version used with that browser.
|
||||
|
||||
| Browser | Default Manifest Version |
|
||||
| ---------------- | :----------------------: |
|
||||
| `chrome` | 3 |
|
||||
| `firefox` | 2 |
|
||||
| `safari` | 2 |
|
||||
| `edge` | 3 |
|
||||
| Any other string | 3 |
|
||||
|
||||
## Target Manifest Version
|
||||
|
||||
To build for a specific manifest version, pass either the `--mv2` flag or `--mv3` flag from the CLI.
|
||||
|
||||
```sh
|
||||
wxt --mv2
|
||||
wxt build --mv2
|
||||
```
|
||||
|
||||
When the `-b --browser` flag is not passed, it defaults to `chrome`. So here, we're targetting MV2 for Chrome.
|
||||
@@ -1,5 +0,0 @@
|
||||
# Testing
|
||||
|
||||
:::warning 🚧 Testing utils are not implemented yet!
|
||||
Eventually, the plan is to have an integration with Vitest.
|
||||
:::
|
||||
@@ -3,18 +3,19 @@
|
||||
WXT uses the same tool as Nuxt for auto-imports, [`unimport`](https://github.com/unjs/unimport).
|
||||
|
||||
:::info Testing
|
||||
To setup your test environment for auto-imports, see [Testing](/get-started/testing.md).
|
||||
To setup your test environment for auto-imports, see [Testing](/guide/testing).
|
||||
:::
|
||||
|
||||
## WXT Auto-imports
|
||||
|
||||
Some WXT APIs can be used without importing them:
|
||||
|
||||
- [`browser`](/config.md#browser) from `wxt/browser`, a small wrapper around `webextension-polyfill`
|
||||
- [`defineContentScript`](/config.md#defiencontentscript) from `wxt/client`
|
||||
- [`defineBackground`](/config.md#definebackgroundscript) from `wxt/client`
|
||||
- [`browser`](/api/wxt-browser#browser) from `wxt/browser`, a small wrapper around `webextension-polyfill`
|
||||
- [`defineContentScript`](/api/wxt-client#defiencontentscript) from `wxt/client`
|
||||
- [`defineBackground`](/api/wxt-client#definebackgroundscript) from `wxt/client`
|
||||
- [`createContentScriptUi`](/api/wxt-client#createcontentscriptui) from `wxt/client`
|
||||
|
||||
And more. All [`wxt/client`](/config.md#wxtclient) APIs can be used without imports.
|
||||
And more. All [`wxt/client`](/api/wxt-client) APIs can be used without imports.
|
||||
|
||||
## Project Auto-imports
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ export default defineConfig({
|
||||
```
|
||||
|
||||
:::info
|
||||
See the [Config reference](/config.md) for a full list of options.
|
||||
See the [Config reference](/api/config) for a full list of options.
|
||||
:::
|
||||
|
||||
## Directory Config
|
||||
@@ -25,7 +25,7 @@ WXT allows you to edit several directories to your liking:
|
||||
- `root` (default: `process.cwd()`) - Root of the WXT project
|
||||
- `srcDir` (default: `<rootDir>`) - Location of all your source code
|
||||
- `entrypointsDir` (default: `<srcDir>/entrypoints`) - Folder containing all the entrypoints.
|
||||
- `publicDir` (default: `<srcDir>/public`) - Folder containing [public assets](/get-started/assets.md)
|
||||
- `publicDir` (default: `<srcDir>/public`) - Folder containing [public assets](/guide/assets)
|
||||
|
||||
### Example
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
# Content Scripts
|
||||
|
||||
[Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/content_scripts/) • [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Content_scripts)
|
||||
|
||||
When creating content script entrypoints, they are automatically included in the `manifest.json` along with any CSS files they import.
|
||||
|
||||
## Filenames
|
||||
|
||||
<EntrypointPatterns
|
||||
:patterns="[
|
||||
['content.(ts|tsx)', 'content-scripts/content.js'],
|
||||
['content/index.(ts|tsx)', 'content-scripts/content.js'],
|
||||
['<name>.content.(ts|tsx)', 'content-scripts/<name>.js'],
|
||||
['<name>.content/index.(ts|tsx)', 'content-scripts/<name>.js'],
|
||||
]"
|
||||
/>
|
||||
|
||||
## Definition
|
||||
|
||||
```ts
|
||||
export default defineContentScript({
|
||||
// Set manifest options
|
||||
matches: ['*://google.com/*', '*://duckduckgo.com/*'],
|
||||
excludeMatches: undefined | [],
|
||||
includeGlobs: undefined | [],
|
||||
excludeGlobs: undefined | [],
|
||||
allFrames: undefined | [],
|
||||
runAt: undefined | 'document_start' | 'document_end' | 'document_idle',
|
||||
matchAboutBlank: undefined | true | false,
|
||||
matchOriginAsFallback: undefined | true | false,
|
||||
world: undefined | 'ISOLATED' | 'MAIN',
|
||||
|
||||
// Set include/exclude if the background should be removed from some builds
|
||||
include: undefined | string[],
|
||||
exclude: undefined | string[],
|
||||
|
||||
// Configure how CSS is injected onto the page
|
||||
cssInjectionMode: undefined | "manifest" | "manual" | "ui",
|
||||
|
||||
main(ctx) {
|
||||
// Executed when content script is loaded
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
> All manifest options default to `undefined`.
|
||||
|
||||
When defining multiple content scripts, content script entrypoints that have the same set of options will be merged into a single `content_script` item in the manifest.
|
||||
|
||||
## CSS
|
||||
|
||||
To include CSS with your content script, import the CSS file at the top of your entrypoint.
|
||||
|
||||
```
|
||||
|
||||
<srcDir>
|
||||
└─ entrypoints/
|
||||
└─ overlay.content/
|
||||
├─ index.ts
|
||||
└─ style.css
|
||||
```
|
||||
|
||||
```ts
|
||||
// entrypoints/overlay.content/index.ts
|
||||
import './style.css';
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ['*://google.com/*', '*://duckduckgo.com/*'],
|
||||
main(ctx) {
|
||||
// ...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Any styles imported in your content script will be added to that content script's `css` array in your `manifest.json`:
|
||||
|
||||
```json
|
||||
// .output/chrome-mv3/manifest.json
|
||||
{
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["*://google.com/*", "*://duckduckgo.com/*"],
|
||||
"js": ["content-scripts/overlay.js"],
|
||||
"css": ["content-scripts/overlay.css"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Context
|
||||
|
||||
Old content scripts are not automatically stopped when an extension updates and restarts. Often, this leads to "Invalidated context" errors in production when a content script from an old version of your extension tries to use a web extension API. Since it's not connected to the latest version of your extension, the browser decides to throw an error.
|
||||
|
||||
WXT provides a utility for managing this process: `ContentScriptContext`. An instance of this class is provided to you automatically inside the `main` function of your content script.
|
||||
|
||||
```ts
|
||||
export default defineContentScript({
|
||||
// ...
|
||||
main(ctx: ContentScriptContext) {
|
||||
// Add custom listeners for stopping work
|
||||
ctx.onInvalidated(() => {
|
||||
// ...
|
||||
});
|
||||
|
||||
// Stop fetch requests
|
||||
fetch('...url', { signal: ctx.signal });
|
||||
|
||||
// Timeout utilities
|
||||
ctx.setTimeout(() => {
|
||||
// ...
|
||||
}, 5e3);
|
||||
ctx.setInterval(() => {
|
||||
// ...
|
||||
}, 60e3);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
The class extends [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) and provides other utilities for stopping a content script's logic once it becomes invalidated.
|
||||
|
||||
:::tip
|
||||
When working with content scripts, **you should always use the `ctx` object to stop any async or future work.**
|
||||
|
||||
This prevents old content scripts from interfering with new content scripts, and prevents error messages from the console in production.
|
||||
:::
|
||||
@@ -0,0 +1,53 @@
|
||||
# Development
|
||||
|
||||
WXT's main goal is providing the best DX it possibly can. When running your extension in dev mode, each part of your extension is reloaded separately when possible.
|
||||
|
||||
| | HMR | Reloaded individually | Reload extension | Restart browser |
|
||||
| ------------------- | :-: | :-------------------: | :--------------: | :----------------------------------------------------: |
|
||||
| HTML File | | ✅ |
|
||||
| HTML Dependency | ✅ |
|
||||
| MV3 Content Script | | ✅ |
|
||||
| MV2 Content Script | | | ✅ |
|
||||
| Background | | | ✅ |
|
||||
| manifest.json | | | | 🟡 See [#16](https://github.com/wxt-dev/wxt/issues/16) |
|
||||
| `wxt.config.ts` | | | | 🟡 See [#10](https://github.com/wxt-dev/wxt/issues/10) |
|
||||
| `web-ext.config.ts` | | | | 🟡 See [#10](https://github.com/wxt-dev/wxt/issues/10) |
|
||||
|
||||
## Configure Browser Startup
|
||||
|
||||
WXT uses [`web-ext` by Mozilla](https://github.com/mozilla/web-ext) to automatically open a browser with the extension installed. You can configure the runner's behavior via the [`runner`](/api/config#runner.disabled) option, or in a separate gitignored file, `web-ext.config.ts`.
|
||||
|
||||
:::code-group
|
||||
|
||||
```ts [wxt.config.ts]
|
||||
import { defineConfig } from 'wxt';
|
||||
|
||||
export default defineConfig({
|
||||
runner: {
|
||||
// Runner config
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```ts [web-ext.config.ts]
|
||||
import { defineRunnerConfig } from 'wxt';
|
||||
|
||||
export default defineRunnerConfig({
|
||||
// Runner config
|
||||
});
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
You may also setup default for your entire computer by creating a `web-ext.config.ts` file in your home directory. This is useful if you want to specify config for all project on your computer, like that you want to use Chrome Beta instead of Chrome.
|
||||
|
||||
```ts
|
||||
// ~/web-ext.config.ts
|
||||
import { defineRunnerConfig } from 'wxt';
|
||||
|
||||
export default defineRunnerConfig({
|
||||
binaries: {
|
||||
chrome: '/path/to/chrome-beta',
|
||||
},
|
||||
});
|
||||
```
|
||||
@@ -42,7 +42,7 @@ would result in the following `manifest.json`:
|
||||
}
|
||||
```
|
||||
|
||||
If a file uses a [special name recognized by WXT](/get-started/entrypoints.md), it will be added to the manifest. In this case:
|
||||
If a file uses a special name recognized by WXT, it will be added to the manifest. In this case:
|
||||
|
||||
- `popup.html` → `action.default_popup`
|
||||
- `content.ts` → `content_scripts.0.js.0`
|
||||
@@ -53,7 +53,7 @@ But not all entrypoints are added to the `manifest.json`. If the filename is not
|
||||
In this case, `injected.ts` gets output to `<outdir>/injected.js` and is accessible via `browser.runtime.getURL("/injected.js")`.
|
||||
|
||||
:::info
|
||||
See [`/entrypoints` folder](/guide/background.md) documentation for a full list of recognized entrypoint filenames.
|
||||
See [`/entrypoints` folder](/entrypoints/background) documentation for a full list of recognized entrypoint filenames.
|
||||
:::
|
||||
|
||||
## Entrypoint Options
|
||||
@@ -84,5 +84,5 @@ export default defineContentScript({
|
||||
```
|
||||
|
||||
:::info
|
||||
For a full list of entrypoints and each of their options, see the [`/entrypoints` folder](/guide/background.md) documentation.
|
||||
For a full list of entrypoints and each of their options, see the [`/entrypoints` folder](/entrypoints/background) documentation.
|
||||
:::
|
||||
@@ -11,7 +11,7 @@ And that's it! Your extension now supports Chrome, Firefox, Safari, Edge, and ot
|
||||
|
||||
## Basic Usage
|
||||
|
||||
The `browser` variable is available globally via [auto-imports](/guide/auto-imports.md), or it can be imported manually.
|
||||
The `browser` variable is available globally via [auto-imports](/guide/auto-imports), or it can be imported manually.
|
||||
|
||||
```ts
|
||||
import { browser } from 'wxt/browser';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Installation
|
||||
|
||||
Bootstrap a new project, start from scratch, or migrate an existing project.
|
||||
Bootstrap a new project, start from scratch, or [migrate an existing project](/guide/migrate-to-wxt).
|
||||
|
||||
## Bootstrap Project
|
||||
|
||||
@@ -26,7 +26,9 @@ There are several starting templates available.
|
||||
| <Icon name="Svelte" /> [`svelte`](https://github.com/wxt-dev/wxt/tree/main/templates/svelte) |
|
||||
| <Icon name="Solid" icon="https://www.solidjs.com/img/favicons/favicon-32x32.png" /> [`solid`](https://github.com/wxt-dev/wxt/tree/main/templates/solid) |
|
||||
|
||||
> All templates are in TypeScript. WXT does not support JS at this time.
|
||||
:::info
|
||||
All templates default to TypeScript. Rename the file extensions to `.js` to use JavaScript instead.
|
||||
:::
|
||||
|
||||
## From Scratch
|
||||
|
||||
@@ -142,6 +144,6 @@ If you're an experienced web extension developer and think the dev manifest look
|
||||
|
||||
You're ready to build your web extension!
|
||||
|
||||
- Learn how to [add entrypoints](./entrypoints.md) like the popup, options page, or content scripts
|
||||
- [Configure WXT](./configuration.md) by creating a `wxt.config.ts` file
|
||||
- Learn how to [add entrypoints](./entrypoints) like the popup, options page, or content scripts
|
||||
- [Configure WXT](./configuration) by creating a `wxt.config.ts` file
|
||||
- Checkout [example projects](https://github.com/wxt-dev/wxt-examples) to see how to perfom common tasks with WXT
|
||||
@@ -35,5 +35,5 @@ Production builds are optimized for store review, changing as few files as possi
|
||||
In addition, WXT fully supports Firefox's source code requirements when using a bundler. It will automatically create and upload a ZIP file of your source code.
|
||||
|
||||
:::info
|
||||
See [Publishing](./publishing.md) for more info around production builds.
|
||||
See [Publishing](./publishing) for more info around production builds.
|
||||
:::
|
||||
@@ -49,7 +49,7 @@ The [manifest's `version` and `version_name`](https://developer.chrome.com/docs/
|
||||
|
||||
## `icons`
|
||||
|
||||
By default, WXT will discover icons in your [`public` directory](/get-started/assets#public-directory) and use them for the [manifest's `icons`](https://developer.chrome.com/docs/extensions/mv3/manifest/icons/).
|
||||
By default, WXT will discover icons in your [`public` directory](/guide/assets#public-directory) and use them for the [manifest's `icons`](https://developer.chrome.com/docs/extensions/mv3/manifest/icons/).
|
||||
|
||||
```
|
||||
public/
|
||||
@@ -94,7 +94,7 @@ export default defineConfig({
|
||||
|
||||
## Localization
|
||||
|
||||
Similar to the icon, the [`_locales` directory](https://developer.chrome.com/docs/extensions/reference/i18n/) should be placed inside the the WXT's [`public` directory](/get-started/assets#public-directory).
|
||||
Similar to the icon, the [`_locales` directory](https://developer.chrome.com/docs/extensions/reference/i18n/) should be placed inside the the WXT's [`public` directory](/guide/assets#public-directory).
|
||||
|
||||
```
|
||||
public/
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# Migrate to WXT
|
||||
|
||||
> If you have problems migrating to WXT, feel free to ask for help in GitHub by [starting a discussion](https://github.com/wxt-dev/wxt/discussions/new?category=q-a)!
|
||||
|
||||
## Overview
|
||||
|
||||
Always start by generating a new vanilla project and merging it into your project one file at a time.
|
||||
|
||||
```sh
|
||||
cd path/to/your/project
|
||||
pnpx wxt@latest init example-wxt --template vanilla
|
||||
```
|
||||
|
||||
In general, you'll need to:
|
||||
|
||||
<input type="checkbox" /> Install `wxt`<br />
|
||||
<input type="checkbox" /> Update/create `package.json` scripts to use `wxt` (don't forget about `postinstall`)<br />
|
||||
<input type="checkbox" /> Move entrypoints into `entrypoints/` directory<br />
|
||||
<input type="checkbox" /> Move assets into either the `assets/` or `public/` directories<br />
|
||||
<input type="checkbox" /> Move manifest.json content into `wxt.config.ts`<br />
|
||||
<input type="checkbox" /> Convert custom import syntax to be compatible with Vite<br />
|
||||
<input type="checkbox" /> Add a default export to JS entrypoints<br />
|
||||
<input type="checkbox" /> Use the `browser` global instead of `chrome`<br />
|
||||
<input type="checkbox" /> Compare final `manifest.json` files, making sure permissions and host permissions are unchanged<br />
|
||||
<input type="checkbox" /> Extension output by `wxt build` works the same way as before the migration<br />
|
||||
|
||||
Every project is different, so there's no one-solution-fits-all to migrating your project. Just make sure `wxt dev` runs, `wxt build` results in a working extension, and the list of permissions in the `manifest.json` hasn't changed. If all that looks good, you've finished migrating your extension!
|
||||
|
||||
## Popular Tools/Frameworks
|
||||
|
||||
Here's specific steps for other popuplar frameworks/build tools.
|
||||
|
||||
### `vite-plugin-web-extension`
|
||||
|
||||
Since you're already using Vite, it's a simple refactor.
|
||||
|
||||
1. Install `wxt`
|
||||
2. Move and refactor your entrypoints to WXT's style (with a default export)
|
||||
3. Update package.json scripts to use `wxt`
|
||||
4. Add `"postinstall": "wxt prepare"` script
|
||||
5. Move the `manifest.json` into `wxt.config.ts`
|
||||
6. Move any custom settings from `vite.config.ts` into `wxt.config.ts`'s
|
||||
7. Compare `dist/manifest.json` to `.output/*/manifest.json`, they should have the same content as before. If not, tweak your entrypoints and config to get as close as possible.
|
||||
|
||||
### `plasmo`
|
||||
|
||||
1. Install `wxt`
|
||||
2. Move entrypoints into `entrypoints/` directory, merging the named exports used to configure your JS entrypoints into WXT's default export
|
||||
3. Move public `assets/*` into the `public/` directory
|
||||
4. If you use CSUI, migrate to WXT's `createContentScriptUi`
|
||||
5. Convert Plasmo's custom import resolutions to Vite's
|
||||
6. If importing remote code via a URL, add a `url:` prefix so it works with WXT
|
||||
7. Compare your output `manifest.json` files from before the migration to after the migration. They should have the same content. If not, tweak your entrypoints and config to get as close as possible.
|
||||
@@ -0,0 +1,110 @@
|
||||
# Multiple Browsers
|
||||
|
||||
You can build an extension for any combination of browser and manifest verison. Different browsers and manifest versions support different APIs and entrypoints, so be sure to check that your extension functions as expected for each target.
|
||||
|
||||
Separate build targets are written to their own output directories:
|
||||
|
||||
```
|
||||
<rootDir>
|
||||
└─ .output
|
||||
├─ chrome-mv3
|
||||
├─ firefox-mv2
|
||||
├─ edge-mv3
|
||||
└─ ...
|
||||
```
|
||||
|
||||
## Target Browser
|
||||
|
||||
To build for a specific browser, pass the `-b --browser` flag from the CLI:
|
||||
|
||||
```
|
||||
|
||||
wxt --browser firefox
|
||||
wxt build --browser firefox
|
||||
|
||||
```
|
||||
|
||||
By default, it will build for `chrome`. When excluding the [`--mv2` or `--mv3` flags](#target-manifest-version), it will default to the commonly accepted manifest version used with that browser.
|
||||
|
||||
| Browser | Default Manifest Version |
|
||||
| ---------------- | :----------------------: |
|
||||
| `chrome` | 3 |
|
||||
| `firefox` | 2 |
|
||||
| `safari` | 2 |
|
||||
| `edge` | 3 |
|
||||
| Any other string | 3 |
|
||||
|
||||
## Target Manifest Version
|
||||
|
||||
To build for a specific manifest version, pass either the `--mv2` flag or `--mv3` flag from the CLI.
|
||||
|
||||
```sh
|
||||
wxt --mv2
|
||||
wxt build --mv2
|
||||
```
|
||||
|
||||
When the `-b --browser` flag is not passed, it defaults to `chrome`. So here, we're targetting MV2 for Chrome.
|
||||
|
||||
## Customizing Entrypoints
|
||||
|
||||
There are several ways to customize entrypoint definitions per browser.
|
||||
|
||||
First, you can use either the `include` or `exclude` option to include or exclude the entrypoint from specific browsers. Here are some examples
|
||||
|
||||
:::code-group
|
||||
|
||||
```ts [Background]
|
||||
export default defineBackground({
|
||||
// Only include a background script when targeting chrome
|
||||
include: ['chrome'],
|
||||
});
|
||||
```
|
||||
|
||||
```ts [Content Script]
|
||||
export default defineContentScript({
|
||||
// Do not add this content script to the manifest when targeting firefox
|
||||
exclude: ['firefox'],
|
||||
});
|
||||
```
|
||||
|
||||
```html [HTML page]
|
||||
<!-- entrypoints/options.html -->
|
||||
<html>
|
||||
<head>
|
||||
<!-- Don't include the options page for safari -->
|
||||
<meta name="manifest.exclude" content="['safari']" />
|
||||
</head>
|
||||
</html>
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
Second, you can change individual options per-browser:
|
||||
|
||||
:::code-group
|
||||
|
||||
```ts [Background]
|
||||
export default defineBackground({
|
||||
persistent: {
|
||||
// Use a non-persistent background script for just safari
|
||||
safari: false,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```ts [Content Script]
|
||||
export default defineContentScript({
|
||||
matches: {
|
||||
// Run the content script on different pages for each browser
|
||||
chrome: ['*://*.google.com/*'],
|
||||
firefox: ['*://*.duckduckgo.com/*'],
|
||||
edge: ['*://*.bing.com/*'],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::warning
|
||||
Only `defineBackground` and `defineContentScript` support per-browser options right now.
|
||||
:::
|
||||
@@ -0,0 +1,92 @@
|
||||
# Testing
|
||||
|
||||
WXT provides a couple of utils for unit testing your extension.
|
||||
|
||||
[[toc]]
|
||||
|
||||
## Fake Browser
|
||||
|
||||
The `wxt/fake-browser` package includes an in-memory implementation of the `browser` variable you can use for testing. WXT simply re-exports the `fakeBrowser` variable from [`@webext-core/fake-browser`](https://webext-core.aklinker1.io/guide/fake-browser/).
|
||||
|
||||
Here's an example test using Vitest:
|
||||
|
||||
```ts
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { browser } from 'wxt/browser';
|
||||
import { fakeBrowser } from 'wxt';
|
||||
|
||||
// Function we're testing
|
||||
function onHelloMessage(cb: () => void) {
|
||||
browser.runtime.onMessage.addEventListener((message) => {
|
||||
if (message.type === 'hello') return 'world';
|
||||
});
|
||||
}
|
||||
|
||||
// Mock the real `browser` object with a fake one
|
||||
vi.mock('wxt/browser', () => import('wxt/fake-browser'));
|
||||
|
||||
describe('onHelloMessage', () => {
|
||||
it("should call the callback when the message type is 'hello'", () => {
|
||||
const cb = vi.fn();
|
||||
const expected = 'world';
|
||||
|
||||
onHelloMessage(cb);
|
||||
const actual = await fakeBrowser.runtime.sendMessage({ type: 'hello' });
|
||||
|
||||
expect(cb).toBeCalledTimes(1);
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
|
||||
it("should ignore the message when the message type is not 'hello'", () => {
|
||||
const cb = vi.fn();
|
||||
|
||||
onHelloMessage(cb);
|
||||
await fakeBrowser.runtime.sendMessage({ type: 'not-hello' }).catch();
|
||||
|
||||
expect(cb).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
See [`@webext-core/fake-browser`](https://webext-core.aklinker1.io/guide/fake-browser/) for setup, implemented APIs, and example tests.
|
||||
|
||||
## Handling Auto-imports
|
||||
|
||||
By default, WXT uses auto-imports. For tests, this can cause issues if your test environment is not setup to handle them correctly.
|
||||
|
||||
:::warning 🚧 Testing utils are not implemented yet!
|
||||
Eventually, WXT will provide utilities for setting up these auto-imports. For now, you'll need to set them up manually.
|
||||
:::
|
||||
|
||||
Not all testing frameworks can handle auto-imports. If your framework or setup is not listed below, it may be easiest to disable auto-imports.
|
||||
|
||||
To setup auto-imports manually, use [`unplugin-auto-import`](https://www.npmjs.com/package/unplugin-auto-import). It uses the same tool, `unimport`, as WXT and will result in compatiple auto-imports. `unplugin-auto-import` supports lots of different tools (vite, webpack, esbuild, rollup, etc). You can try and integrate it into your build process.
|
||||
|
||||
### Vitest (Recommended)
|
||||
|
||||
Vitest is easy, simply add `uplugin-auto-import` to your project.
|
||||
|
||||
```ts
|
||||
// vitest.config.ts
|
||||
import autoImports from 'unplugin-auto-import/vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
autoImports({
|
||||
imports: [{ name: 'defineConfig', from: 'wxt' }],
|
||||
presets: [{ package: 'wxt/client' }, { package: 'wxt/browser' }],
|
||||
dirs: ['components', 'composables', 'hooks', 'utils'],
|
||||
}),
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### Jest
|
||||
|
||||
Don't use jest and auto-imports. You could try and configure jest to be transpiled by one of `unplugin-auto-import`'s supported built tools, but I don't know of a way to configure this. See [unplugin/unplugin-auto-import#33](https://github.com/unplugin/unplugin-auto-import/issues/33) if you want to try and set it up.
|
||||
|
||||
I would recommend disabling auto-imports or migrating to Vitest if you want to use auto-imports.
|
||||
|
||||
### Mocha
|
||||
|
||||
TODO: Is this possible? Maybe with `esbuild-mocha`? I would recommend moving to Vitest.
|
||||
+9
-9
@@ -13,31 +13,31 @@ hero:
|
||||
actions:
|
||||
- theme: brand
|
||||
text: Get Started
|
||||
link: /get-started/installation
|
||||
link: /guide/installation
|
||||
- theme: alt
|
||||
text: Learn More
|
||||
link: /get-started/introduction
|
||||
link: /guide/introduction
|
||||
|
||||
features:
|
||||
- icon: 🌐
|
||||
title: Supported Browsers
|
||||
details: WXT will build extensions for Chrome, Firefox, Edge, Safari, and any Chromium based browser.
|
||||
link: /get-started/build-targets
|
||||
link: /guide/multiple-browsers
|
||||
linkText: Read docs
|
||||
- icon: ✅
|
||||
title: MV2 and MV3
|
||||
details: Build Manifest V2 or V3 extensions for any browser using the same codebase.
|
||||
link: /get-started/build-targets#target-manifest-version
|
||||
link: /guide/multiple-browsers#target-manifest-version
|
||||
linkText: Read docs
|
||||
- icon: ⚡
|
||||
title: Fast Dev Mode
|
||||
details: Lighting fast HMR for UI development and fast reloads for content/background scripts enables faster iterations.
|
||||
link: /get-started/introduction.html#development
|
||||
link: /guide/development.html
|
||||
linkText: Learn more
|
||||
- icon: 📂
|
||||
title: File Based Entrypoints
|
||||
details: Manifest is generated based on files in the project with inline configuration.
|
||||
link: /get-started/entrypoints
|
||||
link: /guide/entrypoints
|
||||
linkText: See project structure
|
||||
- icon: 🚔
|
||||
title: TypeScript
|
||||
@@ -50,17 +50,17 @@ features:
|
||||
- icon: ⬇️
|
||||
title: Bundle Remote Code
|
||||
details: Downloads and bundles remote code imported from URLs.
|
||||
link: guide/remote-code
|
||||
link: /guide/remote-code
|
||||
linkText: Read docs
|
||||
- icon: 🎨
|
||||
title: Frontend Framework Agnostic
|
||||
details: Works with any front-end framework with a Vite plugin.
|
||||
link: /get-started/configuration#frontend-frameworks
|
||||
link: /guide/configuration#frontend-frameworks
|
||||
linkText: Add a framework
|
||||
- icon: 🖍️
|
||||
title: Bootstrap a New Project
|
||||
details: Get started quickly with several awesome project templates.
|
||||
link: /get-started/installation#bootstrap-project
|
||||
link: /guide/installation#bootstrap-project
|
||||
linkText: See templates
|
||||
- icon: 📏
|
||||
title: Bundle Analysis
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# Netlify Redirects File
|
||||
# https://docs.netlify.com/routing/redirects/
|
||||
|
||||
# Old URLs -> New URLs
|
||||
/config.html /api/config.html
|
||||
/entrypoints /entrypoints/background.html
|
||||
/get-started/assets.html /guide/assets.html
|
||||
/get-started/build-targets.html /guide/multiple-browsers.html
|
||||
/get-started/compare.html /guide/compare.html
|
||||
/get-started/configuration.html /guide/configuration.html
|
||||
/get-started/entrypoints.html /guide/entrypoints.html
|
||||
/get-started/installation.html /guide/installation.html
|
||||
/get-started/introduction.html /guide/introduction.html
|
||||
/get-started/publishing.html /guide/publishing.html
|
||||
/get-started/testing.html /guide/testing.html
|
||||
/guide/background.html /entrypoints/background.html
|
||||
/guide/bookmarks.html /entrypoints/bookmarks.html
|
||||
/guide/content-scripts.html /entrypoints/content-scripts.html
|
||||
/guide/css.html /entrypoints/css.html
|
||||
/guide/devtools.html /entrypoints/devtools.html
|
||||
/guide/history.html /entrypoints/history.html
|
||||
/guide/manifest.html /entrypoints/manifest.html
|
||||
/guide/newtab.html /entrypoints/newtab.html
|
||||
/guide/options.html /entrypoints/options.html
|
||||
/guide/popup.html /entrypoints/popup.html
|
||||
/guide/sandbox.html /entrypoints/sandbox.html
|
||||
/guide/sidepanel.html /entrypoints/sidepanel.html
|
||||
/guide/unlisted-pages.html /entrypoints/unlisted-pages.html
|
||||
/guide/unlisted-scripts.html /entrypoints/unlisted-scripts.html
|
||||
/guide/build-targets.html /guide/multiple-browsers.html
|
||||
@@ -194,15 +194,15 @@ describe('Manifest Content', () => {
|
||||
});
|
||||
|
||||
describe('content_scripts', () => {
|
||||
it('should group content scripts and styles together based on their matches and run_at', async () => {
|
||||
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: () => {},
|
||||
})`,
|
||||
export default defineContentScript({
|
||||
matches: ["*://google.com/*"],
|
||||
main: () => {},
|
||||
})`,
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/one.content/style.css',
|
||||
@@ -211,11 +211,11 @@ describe('Manifest Content', () => {
|
||||
project.addFile(
|
||||
'entrypoints/two.content/index.ts',
|
||||
`import "./style.css";
|
||||
export default defineContentScript({
|
||||
matches: ["*://google.com/*"],
|
||||
runAt: "document_end",
|
||||
main: () => {},
|
||||
})`,
|
||||
export default defineContentScript({
|
||||
matches: ["*://google.com/*"],
|
||||
runAt: "document_end",
|
||||
main: () => {},
|
||||
})`,
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/two.content/style.css',
|
||||
@@ -224,11 +224,11 @@ describe('Manifest Content', () => {
|
||||
project.addFile(
|
||||
'entrypoints/three.content/index.ts',
|
||||
`import "./style.css";
|
||||
export default defineContentScript({
|
||||
matches: ["*://google.com/*"],
|
||||
runAt: "document_end",
|
||||
main: () => {},
|
||||
})`,
|
||||
export default defineContentScript({
|
||||
matches: ["*://google.com/*"],
|
||||
runAt: "document_end",
|
||||
main: () => {},
|
||||
})`,
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/three.content/style.css',
|
||||
@@ -237,11 +237,11 @@ describe('Manifest Content', () => {
|
||||
project.addFile(
|
||||
'entrypoints/four.content/index.ts',
|
||||
`import "./style.css";
|
||||
export default defineContentScript({
|
||||
matches: ["*://duckduckgo.com/*"],
|
||||
runAt: "document_end",
|
||||
main: () => {},
|
||||
})`,
|
||||
export default defineContentScript({
|
||||
matches: ["*://duckduckgo.com/*"],
|
||||
runAt: "document_end",
|
||||
main: () => {},
|
||||
})`,
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/four.content/style.css',
|
||||
@@ -276,15 +276,15 @@ describe('Manifest Content', () => {
|
||||
project.addFile(
|
||||
'entrypoints/one.content/index.ts',
|
||||
`export default defineContentScript({
|
||||
matches: ["*://google.com/*"],
|
||||
main: () => {},
|
||||
})`,
|
||||
matches: ["*://google.com/*"],
|
||||
main: () => {},
|
||||
})`,
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/two.content/style.css',
|
||||
`body {
|
||||
background-color: red;
|
||||
}`,
|
||||
background-color: red;
|
||||
}`,
|
||||
);
|
||||
project.setConfigFileConfig({
|
||||
manifest: {
|
||||
@@ -333,7 +333,7 @@ describe('Manifest Content', () => {
|
||||
.toMatchInlineSnapshot(`
|
||||
".output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"content_scripts\\":[{\\"matches\\":[\\"https://*.google.com/*\\"],\\"css\\":[\\"content-scripts/content.css\\"],\\"js\\":[\\"content-scripts/content.js\\"]}]}"
|
||||
{\\"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\\"]}]}"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -360,7 +360,7 @@ describe('Manifest Content', () => {
|
||||
.toMatchInlineSnapshot(`
|
||||
".output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"content_scripts\\":[{\\"matches\\":[\\"https://*.google.com/*\\"],\\"css\\":[\\"content-scripts/content.css\\"],\\"js\\":[\\"content-scripts/content.js\\"]}]}"
|
||||
{\\"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\\"]}]}"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -385,10 +385,10 @@ describe('Manifest Content', () => {
|
||||
|
||||
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\\",\\"version_name\\":\\"0.0.0-test\\",\\"content_scripts\\":[{\\"matches\\":[\\"https://*.google.com/*\\"],\\"js\\":[\\"content-scripts/content.js\\"]}]}"
|
||||
`);
|
||||
".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 () => {
|
||||
@@ -416,7 +416,7 @@ describe('Manifest Content', () => {
|
||||
.toMatchInlineSnapshot(`
|
||||
".output/chrome-mv2/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":2,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"content_scripts\\":[{\\"matches\\":[\\"https://*.google.com/*\\"],\\"js\\":[\\"content-scripts/content.js\\"]}],\\"web_accessible_resources\\":[\\"content-scripts/content.css\\"]}"
|
||||
{\\"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\\"]}"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -445,7 +445,7 @@ describe('Manifest Content', () => {
|
||||
.toMatchInlineSnapshot(`
|
||||
".output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"content_scripts\\":[{\\"matches\\":[\\"https://*.google.com/*\\"],\\"js\\":[\\"content-scripts/content.js\\"]}],\\"web_accessible_resources\\":[{\\"resources\\":[\\"content-scripts/content.css\\"],\\"matches\\":[\\"https://*.google.com/*\\"]}]}"
|
||||
{\\"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/*\\"]}]}"
|
||||
`);
|
||||
});
|
||||
});
|
||||
@@ -480,7 +480,7 @@ describe('Manifest Content', () => {
|
||||
.toMatchInlineSnapshot(`
|
||||
".output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"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\\"]}]}"
|
||||
{\\"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\\"]}]}"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -505,7 +505,98 @@ describe('Manifest Content', () => {
|
||||
expect(output).toMatchInlineSnapshot(`
|
||||
".output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"author\\":\\"Custom Author\\"}"
|
||||
{\\"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 },
|
||||
{ browser: 'firefox', outDir: 'firefox-mv2', expected: true },
|
||||
{ browser: 'safari', outDir: 'safari-mv2', expected: false },
|
||||
])(
|
||||
'should respect the per-browser entrypoint option with %j',
|
||||
async ({ browser, expected, outDir }) => {
|
||||
const project = new TestProject();
|
||||
|
||||
project.addFile(
|
||||
'entrypoints/background.ts',
|
||||
`export default defineBackground({
|
||||
persistent: {
|
||||
firefox: true,
|
||||
safari: false,
|
||||
},
|
||||
main: () => {},
|
||||
})`,
|
||||
);
|
||||
await project.build({ browser });
|
||||
|
||||
const safariManifest = await project.getOutputManifest(
|
||||
`.output/${outDir}/manifest.json`,
|
||||
);
|
||||
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',
|
||||
});
|
||||
|
||||
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',
|
||||
});
|
||||
|
||||
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',
|
||||
});
|
||||
|
||||
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();
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ describe('Output Directory Structure', () => {
|
||||
expect(await project.serializeOutput()).toMatchInlineSnapshot(`
|
||||
".output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\"}"
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\"}"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -69,7 +69,7 @@ describe('Output Directory Structure', () => {
|
||||
================================================================================
|
||||
.output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"content_scripts\\":[{\\"matches\\":[\\"*://*/*\\"],\\"css\\":[\\"content-scripts/one.css\\",\\"content-scripts/two.css\\"],\\"js\\":[\\"content-scripts/one.js\\",\\"content-scripts/two.js\\"]}]}"
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"content_scripts\\":[{\\"matches\\":[\\"*://*/*\\"],\\"css\\":[\\"content-scripts/one.css\\",\\"content-scripts/two.css\\"],\\"js\\":[\\"content-scripts/one.js\\",\\"content-scripts/two.js\\"]}]}"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -96,7 +96,7 @@ describe('Output Directory Structure', () => {
|
||||
================================================================================
|
||||
.output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"content_scripts\\":[{\\"matches\\":[\\"*://*/*\\"],\\"js\\":[\\"content-scripts/overlay-one.js\\"]}]}"
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"content_scripts\\":[{\\"matches\\":[\\"*://*/*\\"],\\"js\\":[\\"content-scripts/overlay-one.js\\"]}]}"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -165,4 +165,44 @@ describe('Output Directory Structure', () => {
|
||||
|
||||
expect(await project.fileExists('stats.html')).toBe(true);
|
||||
});
|
||||
|
||||
it('should support JavaScript entrypoints', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile(
|
||||
'entrypoints/background.js',
|
||||
`export default defineBackground(() => {});`,
|
||||
);
|
||||
project.addFile('entrypoints/unlisted.js', ``);
|
||||
project.addFile(
|
||||
'entrypoints/content.js',
|
||||
`export default defineContentScript({
|
||||
matches: ["*://*.google.com/*"],
|
||||
main() {},
|
||||
})`,
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/named.content.jsx',
|
||||
`export default defineContentScript({
|
||||
matches: ["*://*.duckduckgo.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\\",\\"background\\":{\\"service_worker\\":\\"background.js\\"},\\"content_scripts\\":[{\\"matches\\":[\\"*://*.google.com/*\\"],\\"js\\":[\\"content-scripts/content.js\\"]},{\\"matches\\":[\\"*://*.duckduckgo.com/*\\"],\\"js\\":[\\"content-scripts/named.js\\"]}]}"
|
||||
`);
|
||||
expect(await project.fileExists('.output/chrome-mv3/background.js'));
|
||||
expect(
|
||||
await project.fileExists('.output/chrome-mv3/content-scripts/content.js'),
|
||||
);
|
||||
expect(
|
||||
await project.fileExists('.output/chrome-mv3/content-scripts/named.js'),
|
||||
);
|
||||
expect(await project.fileExists('.output/chrome-mv3/unlisted.js'));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -35,9 +35,9 @@ describe('React', () => {
|
||||
).toBe(true);
|
||||
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\\",\\"version_name\\":\\"0.0.0-test\\",\\"content_scripts\\":[{\\"matches\\":\\"<all_urls>\\",\\"js\\":[\\"content-scripts/demo.js\\"]}]}"
|
||||
`);
|
||||
".output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"content_scripts\\":[{\\"matches\\":\\"<all_urls>\\",\\"js\\":[\\"content-scripts/demo.js\\"]}]}"
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,7 +28,7 @@ describe('User Config', () => {
|
||||
================================================================================
|
||||
.output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"background\\":{\\"service_worker\\":\\"background.js\\"}}"
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"background\\":{\\"service_worker\\":\\"background.js\\"}}"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -54,7 +54,7 @@ describe('User Config', () => {
|
||||
================================================================================
|
||||
.output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"background\\":{\\"service_worker\\":\\"background.js\\"}}"
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"background\\":{\\"service_worker\\":\\"background.js\\"}}"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -82,7 +82,7 @@ describe('User Config', () => {
|
||||
expect(output).toMatchInlineSnapshot(`
|
||||
".output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"example_customization\\":[\\"production\\",\\"chrome\\",\\"3\\",\\"build\\"]}"
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"example_customization\\":[\\"production\\",\\"chrome\\",\\"3\\",\\"build\\"]}"
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ export class TestProject {
|
||||
{
|
||||
name: 'E2E Extension',
|
||||
description: 'Example description',
|
||||
version: '0.0.0-test',
|
||||
version: '0.0.0',
|
||||
dependencies: {
|
||||
wxt: '../../..',
|
||||
},
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "wxt",
|
||||
"type": "module",
|
||||
"version": "0.7.1-alpha1",
|
||||
"version": "0.7.5",
|
||||
"description": "Next gen framework for developing web extensions",
|
||||
"engines": {
|
||||
"node": ">=18.16.0",
|
||||
@@ -109,10 +109,10 @@
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^3.0.1",
|
||||
"simple-git-hooks": "^2.9.0",
|
||||
"ts-morph": "^19.0.0",
|
||||
"ts-morph": "^20.0.0",
|
||||
"tsup": "^7.1.0",
|
||||
"tsx": "^3.12.7",
|
||||
"typescript": "^5.1.6",
|
||||
"typescript": "^5.2.2",
|
||||
"vitepress": "1.0.0-rc.10",
|
||||
"vitest": "^0.34.1",
|
||||
"vitest-mock-extended": "^1.1.4",
|
||||
|
||||
Generated
+27
-27
@@ -98,10 +98,10 @@ importers:
|
||||
devDependencies:
|
||||
'@faker-js/faker':
|
||||
specifier: ^8.0.2
|
||||
version: 8.0.2
|
||||
version: 8.1.0
|
||||
'@types/fs-extra':
|
||||
specifier: ^11.0.1
|
||||
version: 11.0.1
|
||||
version: 11.0.2
|
||||
'@types/lodash.merge':
|
||||
specifier: ^4.6.7
|
||||
version: 4.6.7
|
||||
@@ -136,17 +136,17 @@ importers:
|
||||
specifier: ^2.9.0
|
||||
version: 2.9.0
|
||||
ts-morph:
|
||||
specifier: ^19.0.0
|
||||
version: 19.0.0
|
||||
specifier: ^20.0.0
|
||||
version: 20.0.0
|
||||
tsup:
|
||||
specifier: ^7.1.0
|
||||
version: 7.2.0(typescript@5.1.6)
|
||||
version: 7.2.0(typescript@5.2.2)
|
||||
tsx:
|
||||
specifier: ^3.12.7
|
||||
version: 3.12.8
|
||||
typescript:
|
||||
specifier: ^5.1.6
|
||||
version: 5.1.6
|
||||
specifier: ^5.2.2
|
||||
version: 5.2.2
|
||||
vitepress:
|
||||
specifier: 1.0.0-rc.10
|
||||
version: 1.0.0-rc.10(@types/node@20.5.9)
|
||||
@@ -155,7 +155,7 @@ importers:
|
||||
version: 0.34.3(jsdom@22.1.0)
|
||||
vitest-mock-extended:
|
||||
specifier: ^1.1.4
|
||||
version: 1.2.1(typescript@5.1.6)(vitest@0.34.3)
|
||||
version: 1.3.0(typescript@5.2.2)(vitest@0.34.3)
|
||||
vue:
|
||||
specifier: ^3.3.4
|
||||
version: 3.3.4
|
||||
@@ -854,8 +854,8 @@ packages:
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@faker-js/faker@8.0.2:
|
||||
resolution: {integrity: sha512-Uo3pGspElQW91PCvKSIAXoEgAUlRnH29sX2/p89kg7sP1m2PzCufHINd0FhTXQf6DYGiUlVncdSPa2F9wxed2A==}
|
||||
/@faker-js/faker@8.1.0:
|
||||
resolution: {integrity: sha512-38DT60rumHfBYynif3lmtxMqMqmsOQIxQgEuPZxCk2yUYN0eqWpTACgxi0VpidvsJB8CRxCpvP7B3anK85FjtQ==}
|
||||
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0, npm: '>=6.14.13'}
|
||||
dev: true
|
||||
|
||||
@@ -978,8 +978,8 @@ packages:
|
||||
engines: {node: '>= 10'}
|
||||
dev: true
|
||||
|
||||
/@ts-morph/common@0.20.0:
|
||||
resolution: {integrity: sha512-7uKjByfbPpwuzkstL3L5MQyuXPSKdoNG93Fmi2JoDcTf3pEP731JdRFAduRVkOs8oqxPsXKA+ScrWkdQ8t/I+Q==}
|
||||
/@ts-morph/common@0.21.0:
|
||||
resolution: {integrity: sha512-ES110Mmne5Vi4ypUKrtVQfXFDtCsDXiUiGxF6ILVlE90dDD4fdpC1LSjydl/ml7xJWKSDZwUYD2zkOePMSrPBA==}
|
||||
dependencies:
|
||||
fast-glob: 3.3.1
|
||||
minimatch: 7.4.6
|
||||
@@ -1001,8 +1001,8 @@ packages:
|
||||
resolution: {integrity: sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==}
|
||||
dev: false
|
||||
|
||||
/@types/fs-extra@11.0.1:
|
||||
resolution: {integrity: sha512-MxObHvNl4A69ofaTRU8DFqvgzzv8s9yRtaPPm5gud9HDNvpB3GPQFvNuTWAI59B9huVGV5jXYJwbCsmBsOGYWA==}
|
||||
/@types/fs-extra@11.0.2:
|
||||
resolution: {integrity: sha512-c0hrgAOVYr21EX8J0jBMXGLMgJqVf/v6yxi0dLaJboW9aQPh16Id+z6w2Tx1hm+piJOLv8xPfVKZCLfjPw/IMQ==}
|
||||
dependencies:
|
||||
'@types/jsonfile': 6.1.1
|
||||
'@types/node': 20.5.9
|
||||
@@ -4545,22 +4545,22 @@ packages:
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/ts-essentials@9.3.2(typescript@5.1.6):
|
||||
/ts-essentials@9.3.2(typescript@5.2.2):
|
||||
resolution: {integrity: sha512-JxKJzuWqH1MmH4ZFHtJzGEhkfN3QvVR3C3w+4BIoWeoY68UVVoA2Np/Bca9z0IPSErVCWhv439aT0We4Dks8kQ==}
|
||||
peerDependencies:
|
||||
typescript: '>=4.1.0'
|
||||
dependencies:
|
||||
typescript: 5.1.6
|
||||
typescript: 5.2.2
|
||||
dev: true
|
||||
|
||||
/ts-interface-checker@0.1.13:
|
||||
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
|
||||
dev: true
|
||||
|
||||
/ts-morph@19.0.0:
|
||||
resolution: {integrity: sha512-D6qcpiJdn46tUqV45vr5UGM2dnIEuTGNxVhg0sk5NX11orcouwj6i1bMqZIz2mZTZB1Hcgy7C3oEVhAT+f6mbQ==}
|
||||
/ts-morph@20.0.0:
|
||||
resolution: {integrity: sha512-JVmEJy2Wow5n/84I3igthL9sudQ8qzjh/6i4tmYCm6IqYyKFlNbJZi7oBdjyqcWSWYRu3CtL0xbT6fS03ESZIg==}
|
||||
dependencies:
|
||||
'@ts-morph/common': 0.20.0
|
||||
'@ts-morph/common': 0.21.0
|
||||
code-block-writer: 12.0.0
|
||||
dev: true
|
||||
|
||||
@@ -4568,7 +4568,7 @@ packages:
|
||||
resolution: {integrity: sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==}
|
||||
dev: false
|
||||
|
||||
/tsup@7.2.0(typescript@5.1.6):
|
||||
/tsup@7.2.0(typescript@5.2.2):
|
||||
resolution: {integrity: sha512-vDHlczXbgUvY3rWvqFEbSqmC1L7woozbzngMqTtL2PGBODTtWlRwGDDawhvWzr5c1QjKe4OAKqJGfE1xeXUvtQ==}
|
||||
engines: {node: '>=16.14'}
|
||||
hasBin: true
|
||||
@@ -4598,7 +4598,7 @@ packages:
|
||||
source-map: 0.8.0-beta.0
|
||||
sucrase: 3.32.0
|
||||
tree-kill: 1.2.2
|
||||
typescript: 5.1.6
|
||||
typescript: 5.2.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- ts-node
|
||||
@@ -4652,8 +4652,8 @@ packages:
|
||||
resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==}
|
||||
dev: false
|
||||
|
||||
/typescript@5.1.6:
|
||||
resolution: {integrity: sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==}
|
||||
/typescript@5.2.2:
|
||||
resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==}
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
dev: true
|
||||
@@ -4874,14 +4874,14 @@ packages:
|
||||
- universal-cookie
|
||||
dev: true
|
||||
|
||||
/vitest-mock-extended@1.2.1(typescript@5.1.6)(vitest@0.34.3):
|
||||
resolution: {integrity: sha512-zRiSAR4d44S0XEWgfhnyYkU9Z2a7bo9FiEoh7Qe9qW/Qm+j0Uq9i8paOONpSDwzOe1fxwff8Au5PS5+uBZ06bw==}
|
||||
/vitest-mock-extended@1.3.0(typescript@5.2.2)(vitest@0.34.3):
|
||||
resolution: {integrity: sha512-qolbgansCq7huhmUUlHXOUYlejL86+jFkfyeoxGR9IWKhWcIjI3Ek2J9eTwIFfVzNkaTCpuJizEg9EqDJuuOkg==}
|
||||
peerDependencies:
|
||||
typescript: 3.x || 4.x || 5.x
|
||||
vitest: '>=0.31.1'
|
||||
dependencies:
|
||||
ts-essentials: 9.3.2(typescript@5.1.6)
|
||||
typescript: 5.1.6
|
||||
ts-essentials: 9.3.2(typescript@5.2.2)
|
||||
typescript: 5.2.2
|
||||
vitest: 0.34.3(jsdom@22.1.0)
|
||||
dev: true
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { ContentScriptContext } from '.';
|
||||
* 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-scripts.html#ui> for full documentation.
|
||||
* See <https://wxt.dev/entrypoints/content-scripts.html#ui> for full documentation.
|
||||
*
|
||||
* @example
|
||||
* // entrypoints/example-ui.content/index.ts
|
||||
@@ -245,7 +245,7 @@ interface BaseContentScriptUiOptions<T> {
|
||||
* set `cssInjectionMode: "ui"`, the imported CSS will be included automatically. You do not need
|
||||
* to pass those styles in here. This is for any additional styles not in the imported CSS.
|
||||
*
|
||||
* See <https://wxt.dev/guide/content-scripts.html#ui> for more info.
|
||||
* See <https://wxt.dev/entrypoints/content-scripts.html#ui> for more info.
|
||||
*/
|
||||
css?: string;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,10 @@ import { parseHTML } from 'linkedom';
|
||||
import JSON5 from 'json5';
|
||||
import { importEntrypointFile } from '../utils/importEntrypointFile';
|
||||
import glob from 'fast-glob';
|
||||
import { getEntrypointName } from '../utils/entrypoints';
|
||||
import {
|
||||
getEntrypointName,
|
||||
resolvePerBrowserOption,
|
||||
} from '../utils/entrypoints';
|
||||
import { VIRTUAL_NOOP_BACKGROUND_MODULE_ID } from '../vite-plugins/noopBackground';
|
||||
import { CSS_EXTENSIONS_PATTERN } from '../utils/paths';
|
||||
|
||||
@@ -198,12 +201,19 @@ async function getPopupEntrypoint(
|
||||
}
|
||||
}
|
||||
|
||||
const mv2KeyContent = document
|
||||
const mv2TypeContent = document
|
||||
.querySelector("meta[name='manifest.type']")
|
||||
?.getAttribute('content');
|
||||
if (mv2KeyContent) {
|
||||
if (mv2TypeContent) {
|
||||
options.mv2Key =
|
||||
mv2KeyContent === 'page_action' ? 'page_action' : 'browser_action';
|
||||
mv2TypeContent === 'page_action' ? 'page_action' : 'browser_action';
|
||||
}
|
||||
|
||||
const browserStyleContent = document
|
||||
.querySelector("meta[name='manifest.browser_style']")
|
||||
?.getAttribute('content');
|
||||
if (browserStyleContent) {
|
||||
options.browserStyle = browserStyleContent === 'true';
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -300,7 +310,11 @@ async function getBackgroundEntrypoint(
|
||||
name: 'background',
|
||||
inputPath: path,
|
||||
outputDir: config.outDir,
|
||||
options: options,
|
||||
options: {
|
||||
...options,
|
||||
type: resolvePerBrowserOption(options.type, config.browser),
|
||||
persistent: resolvePerBrowserOption(options.persistent, config.browser),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -349,14 +363,14 @@ const PATH_GLOB_TO_TYPE_MAP: Record<string, Entrypoint['type'] | 'ignored'> = {
|
||||
'devtools.html': 'devtools',
|
||||
'devtools/index.html': 'devtools',
|
||||
|
||||
'background.ts': 'background',
|
||||
'background/index.ts': 'background',
|
||||
'background.[jt]s': 'background',
|
||||
'background/index.[jt]s': 'background',
|
||||
[VIRTUAL_NOOP_BACKGROUND_MODULE_ID]: 'background',
|
||||
|
||||
'content.ts?(x)': 'content-script',
|
||||
'content/index.ts?(x)': 'content-script',
|
||||
'*.content.ts?(x)': 'content-script',
|
||||
'*.content/index.ts?(x)': 'content-script',
|
||||
'content.[jt]s?(x)': 'content-script',
|
||||
'content/index.[jt]s?(x)': 'content-script',
|
||||
'*.content.[jt]s?(x)': 'content-script',
|
||||
'*.content/index.[jt]s?(x)': 'content-script',
|
||||
[`content.${CSS_EXTENSIONS_PATTERN}`]: 'content-script-style',
|
||||
[`*.content.${CSS_EXTENSIONS_PATTERN}`]: 'content-script-style',
|
||||
[`content/index.${CSS_EXTENSIONS_PATTERN}`]: 'content-script-style',
|
||||
@@ -370,7 +384,7 @@ const PATH_GLOB_TO_TYPE_MAP: Record<string, Entrypoint['type'] | 'ignored'> = {
|
||||
|
||||
'*.html': 'unlisted-page',
|
||||
'*/index.html': 'unlisted-page',
|
||||
'*.ts': 'unlisted-script',
|
||||
'*.[jt]s': 'unlisted-script',
|
||||
'*/index.ts': 'unlisted-script',
|
||||
[`*.${CSS_EXTENSIONS_PATTERN}`]: 'unlisted-style',
|
||||
[`*/index.${CSS_EXTENSIONS_PATTERN}`]: 'unlisted-style',
|
||||
|
||||
+15
-6
@@ -6,7 +6,10 @@ import {
|
||||
} from './types';
|
||||
import * as vite from 'vite';
|
||||
import { Scripting } from 'webextension-polyfill';
|
||||
import { getEntrypointBundlePath } from './utils/entrypoints';
|
||||
import {
|
||||
getEntrypointBundlePath,
|
||||
resolvePerBrowserOption,
|
||||
} from './utils/entrypoints';
|
||||
import {
|
||||
getContentScriptCssFiles,
|
||||
getContentScriptsCssMap,
|
||||
@@ -106,12 +109,18 @@ export function reloadContentScripts(
|
||||
const css = getContentScriptCssFiles([entry], cssMap);
|
||||
|
||||
server.reloadContentScript({
|
||||
allFrames: entry.options.allFrames,
|
||||
excludeMatches: entry.options.excludeMatches,
|
||||
matches: entry.options.matches,
|
||||
runAt: entry.options.runAt,
|
||||
allFrames: resolvePerBrowserOption(
|
||||
entry.options.allFrames,
|
||||
config.browser,
|
||||
),
|
||||
excludeMatches: resolvePerBrowserOption(
|
||||
entry.options.excludeMatches,
|
||||
config.browser,
|
||||
),
|
||||
matches: resolvePerBrowserOption(entry.options.matches, config.browser),
|
||||
runAt: resolvePerBrowserOption(entry.options.runAt, config.browser),
|
||||
// @ts-expect-error: Chrome accepts this, not typed in webextension-polyfill (https://developer.chrome.com/docs/extensions/reference/scripting/#type-RegisteredContentScript)
|
||||
world: entry.options.world,
|
||||
world: resolvePerBrowserOption(entry.options.world, config.browser),
|
||||
js,
|
||||
css,
|
||||
});
|
||||
|
||||
+19
-11
@@ -343,6 +343,7 @@ export interface PopupEntrypoint extends BaseEntrypoint {
|
||||
mv2Key?: 'browser_action' | 'page_action';
|
||||
defaultIcon?: Record<string, string>;
|
||||
defaultTitle?: string;
|
||||
browserStyle?: boolean;
|
||||
} & BaseEntrypointOptions;
|
||||
}
|
||||
|
||||
@@ -365,47 +366,49 @@ export type Entrypoint =
|
||||
export type OnContentScriptStopped = (cb: () => void) => void;
|
||||
|
||||
export interface ContentScriptDefinition extends ExcludableEntrypoint {
|
||||
matches: Manifest.ContentScript['matches'];
|
||||
matches: PerBrowserOption<Manifest.ContentScript['matches']>;
|
||||
/**
|
||||
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
|
||||
* @default "documentIdle"
|
||||
*/
|
||||
runAt?: Manifest.ContentScript['run_at'];
|
||||
runAt?: PerBrowserOption<Manifest.ContentScript['run_at']>;
|
||||
/**
|
||||
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
|
||||
* @default false
|
||||
*/
|
||||
matchAboutBlank?: Manifest.ContentScript['match_about_blank'];
|
||||
matchAboutBlank?: PerBrowserOption<
|
||||
Manifest.ContentScript['match_about_blank']
|
||||
>;
|
||||
/**
|
||||
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
|
||||
* @default []
|
||||
*/
|
||||
excludeMatches?: Manifest.ContentScript['exclude_matches'];
|
||||
excludeMatches?: PerBrowserOption<Manifest.ContentScript['exclude_matches']>;
|
||||
/**
|
||||
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
|
||||
* @default []
|
||||
*/
|
||||
includeGlobs?: Manifest.ContentScript['include_globs'];
|
||||
includeGlobs?: PerBrowserOption<Manifest.ContentScript['include_globs']>;
|
||||
/**
|
||||
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
|
||||
* @default []
|
||||
*/
|
||||
excludeGlobs?: Manifest.ContentScript['exclude_globs'];
|
||||
excludeGlobs?: PerBrowserOption<Manifest.ContentScript['exclude_globs']>;
|
||||
/**
|
||||
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
|
||||
* @default false
|
||||
*/
|
||||
allFrames?: Manifest.ContentScript['all_frames'];
|
||||
allFrames?: PerBrowserOption<Manifest.ContentScript['all_frames']>;
|
||||
/**
|
||||
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
|
||||
* @default false
|
||||
*/
|
||||
matchOriginAsFallback?: boolean;
|
||||
matchOriginAsFallback?: PerBrowserOption<boolean>;
|
||||
/**
|
||||
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
|
||||
* @default "ISOLATED"
|
||||
*/
|
||||
world?: 'ISOLATED' | 'MAIN';
|
||||
world?: PerBrowserOption<'ISOLATED' | 'MAIN'>;
|
||||
/**
|
||||
* Customize how imported/generated styles are injected with the content script. Regardless of the
|
||||
* mode selected, CSS will always be built and included in the output directory.
|
||||
@@ -414,10 +417,12 @@ export interface ContentScriptDefinition extends ExcludableEntrypoint {
|
||||
* - `"manual"` - Exclude the CSS from the manifest. You are responsible for manually loading it
|
||||
* onto the page. Use `browser.runtime.getURL("content-scripts/<name>.css")` to get the file's
|
||||
* URL
|
||||
* - `"ui"` - Exclude the CSS from the manifest. CSS will be automatically added to your UI when
|
||||
* calling `createContentScriptUi`
|
||||
*
|
||||
* @default "manifest"
|
||||
*/
|
||||
cssInjectionMode?: 'manifest' | 'manual' | 'ui';
|
||||
cssInjectionMode?: PerBrowserOption<'manifest' | 'manual' | 'ui'>;
|
||||
/**
|
||||
* Main function executed when the content script is loaded.
|
||||
*/
|
||||
@@ -425,10 +430,13 @@ export interface ContentScriptDefinition extends ExcludableEntrypoint {
|
||||
}
|
||||
|
||||
export interface BackgroundScriptDefintition extends ExcludableEntrypoint {
|
||||
type?: 'module';
|
||||
type?: PerBrowserOption<'module'>;
|
||||
persistent?: PerBrowserOption<boolean>;
|
||||
main(): void;
|
||||
}
|
||||
|
||||
export type PerBrowserOption<T> = T | { [browser: TargetBrowser]: T };
|
||||
|
||||
export interface ExcludableEntrypoint {
|
||||
/**
|
||||
* List of target browsers to include this entrypoint in. Defaults to being included in all
|
||||
|
||||
@@ -1,26 +1,36 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { hashContentScriptOptions } from '../content-scripts';
|
||||
import { fakeInternalConfig } from '../../../testing/fake-objects';
|
||||
|
||||
describe('Content Script Utils', () => {
|
||||
describe('hashContentScriptOptions', () => {
|
||||
it('should return a string containing all the options with defaults applied', () => {
|
||||
const hash = hashContentScriptOptions({ matches: [] });
|
||||
const hash = hashContentScriptOptions(
|
||||
{ matches: [] },
|
||||
fakeInternalConfig(),
|
||||
);
|
||||
|
||||
expect(hash).toMatchInlineSnapshot(
|
||||
'"[[\\"allFrames\\",false],[\\"excludeGlobs\\",[]],[\\"excludeMatches\\",[]],[\\"includeGlobs\\",[]],[\\"matchAboutBlank\\",false],[\\"matches\\",[]],[\\"matchOriginAsFallback\\",false],[\\"runAt\\",\\"document_idle\\"],[\\"world\\",\\"ISOLATED\\"]]"',
|
||||
'"[[\\"all_frames\\",false],[\\"exclude_globs\\",[]],[\\"exclude_matches\\",[]],[\\"include_globs\\",[]],[\\"match_about_blank\\",false],[\\"match_origin_as_fallback\\",false],[\\"matches\\",[]],[\\"run_at\\",\\"document_idle\\"],[\\"world\\",\\"ISOLATED\\"]]"',
|
||||
);
|
||||
});
|
||||
|
||||
it('should be consistent regardless of the object ordering and default values', () => {
|
||||
const hash1 = hashContentScriptOptions({
|
||||
allFrames: true,
|
||||
matches: ['*://google.com/*', '*://duckduckgo.com/*'],
|
||||
matchAboutBlank: false,
|
||||
});
|
||||
const hash2 = hashContentScriptOptions({
|
||||
matches: ['*://duckduckgo.com/*', '*://google.com/*'],
|
||||
allFrames: true,
|
||||
});
|
||||
const hash1 = hashContentScriptOptions(
|
||||
{
|
||||
allFrames: true,
|
||||
matches: ['*://google.com/*', '*://duckduckgo.com/*'],
|
||||
matchAboutBlank: false,
|
||||
},
|
||||
fakeInternalConfig(),
|
||||
);
|
||||
const hash2 = hashContentScriptOptions(
|
||||
{
|
||||
matches: ['*://duckduckgo.com/*', '*://google.com/*'],
|
||||
allFrames: true,
|
||||
},
|
||||
fakeInternalConfig(),
|
||||
);
|
||||
|
||||
expect(hash1).toBe(hash2);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { getEntrypointName, getEntrypointOutputFile } from '../entrypoints';
|
||||
import {
|
||||
getEntrypointName,
|
||||
getEntrypointOutputFile,
|
||||
resolvePerBrowserOption,
|
||||
} from '../entrypoints';
|
||||
import { Entrypoint } from '../../types';
|
||||
import { resolve } from 'path';
|
||||
|
||||
@@ -49,4 +53,25 @@ describe('Entrypoint Utils', () => {
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolvePerBrowserOption', () => {
|
||||
it('should return the value directly', () => {
|
||||
expect(resolvePerBrowserOption('some-string', '')).toEqual('some-string');
|
||||
expect(resolvePerBrowserOption(false, '')).toEqual(false);
|
||||
expect(resolvePerBrowserOption([1], '')).toEqual([1]);
|
||||
expect(resolvePerBrowserOption(['string'], '')).toEqual(['string']);
|
||||
});
|
||||
|
||||
it('should return the value for the specific browser', () => {
|
||||
expect(resolvePerBrowserOption({ a: 'one', b: 'two' }, 'a')).toEqual(
|
||||
'one',
|
||||
);
|
||||
expect(resolvePerBrowserOption({ c: ['one'], d: ['two'] }, 'c')).toEqual([
|
||||
'one',
|
||||
]);
|
||||
expect(resolvePerBrowserOption({ c: false, d: true }, 'e')).toEqual(
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Manifest } from 'webextension-polyfill';
|
||||
import { ContentScriptEntrypoint } from '../types';
|
||||
import { ContentScriptEntrypoint, InternalConfig } from '../types';
|
||||
import { resolvePerBrowserOption } from './entrypoints';
|
||||
|
||||
/**
|
||||
* Returns a unique and consistent string hash based on a content scripts options.
|
||||
@@ -8,18 +9,27 @@ import { ContentScriptEntrypoint } from '../types';
|
||||
*/
|
||||
export function hashContentScriptOptions(
|
||||
options: ContentScriptEntrypoint['options'],
|
||||
config: InternalConfig,
|
||||
): string {
|
||||
const withDefaults: ContentScriptEntrypoint['options'] = {
|
||||
excludeGlobs: [],
|
||||
excludeMatches: [],
|
||||
includeGlobs: [],
|
||||
matchAboutBlank: false,
|
||||
matchOriginAsFallback: false,
|
||||
runAt: 'document_idle',
|
||||
allFrames: false,
|
||||
const simplifiedOptions = mapWxtOptionsToContentScript(options, config);
|
||||
|
||||
// Remove undefined fields and use defaults to generate hash
|
||||
Object.keys(simplifiedOptions).forEach((key) => {
|
||||
// @ts-expect-error: key not typed as keyof ...
|
||||
if (simplifiedOptions[key] == null) delete simplifiedOptions[key];
|
||||
});
|
||||
|
||||
const withDefaults: Manifest.ContentScript = {
|
||||
exclude_globs: [],
|
||||
exclude_matches: [],
|
||||
include_globs: [],
|
||||
match_about_blank: false,
|
||||
run_at: 'document_idle',
|
||||
all_frames: false,
|
||||
// @ts-expect-error - not in type
|
||||
match_origin_as_fallback: false,
|
||||
world: 'ISOLATED',
|
||||
// TODO: strip undefined fields from options object to improve content script grouping.
|
||||
...options,
|
||||
...simplifiedOptions,
|
||||
};
|
||||
return JSON.stringify(
|
||||
Object.entries(withDefaults)
|
||||
@@ -35,18 +45,34 @@ export function hashContentScriptOptions(
|
||||
|
||||
export function mapWxtOptionsToContentScript(
|
||||
options: ContentScriptEntrypoint['options'],
|
||||
config: InternalConfig,
|
||||
): Omit<Manifest.ContentScript, 'js' | 'css'> {
|
||||
return {
|
||||
matches: options.matches,
|
||||
all_frames: options.allFrames,
|
||||
match_about_blank: options.matchAboutBlank,
|
||||
exclude_globs: options.excludeGlobs,
|
||||
exclude_matches: options.excludeMatches,
|
||||
include_globs: options.includeGlobs,
|
||||
run_at: options.runAt,
|
||||
matches: resolvePerBrowserOption(options.matches, config.browser),
|
||||
all_frames: resolvePerBrowserOption(options.allFrames, config.browser),
|
||||
match_about_blank: resolvePerBrowserOption(
|
||||
options.matchAboutBlank,
|
||||
config.browser,
|
||||
),
|
||||
exclude_globs: resolvePerBrowserOption(
|
||||
options.excludeGlobs,
|
||||
config.browser,
|
||||
),
|
||||
exclude_matches: resolvePerBrowserOption(
|
||||
options.excludeMatches,
|
||||
config.browser,
|
||||
),
|
||||
include_globs: resolvePerBrowserOption(
|
||||
options.includeGlobs,
|
||||
config.browser,
|
||||
),
|
||||
run_at: resolvePerBrowserOption(options.runAt, config.browser),
|
||||
|
||||
// @ts-expect-error: untyped chrome options
|
||||
match_origin_as_fallback: options.matchOriginAsFallback,
|
||||
match_origin_as_fallback: resolvePerBrowserOption(
|
||||
options.matchOriginAsFallback,
|
||||
config.browser,
|
||||
),
|
||||
world: options.world,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Entrypoint } from '../types';
|
||||
import { Entrypoint, PerBrowserOption, TargetBrowser } from '../types';
|
||||
import path, { relative, resolve } from 'node:path';
|
||||
import { normalizePath } from './paths';
|
||||
|
||||
@@ -34,3 +34,15 @@ export function getEntrypointBundlePath(
|
||||
relative(outDir, getEntrypointOutputFile(entrypoint, ext)),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an entrypoint option, resolve it's value based on a target browser.
|
||||
*/
|
||||
export function resolvePerBrowserOption<T>(
|
||||
option: PerBrowserOption<T>,
|
||||
browser: TargetBrowser,
|
||||
): T {
|
||||
if (typeof option === 'object' && !Array.isArray(option))
|
||||
return (option as any)[browser];
|
||||
return option;
|
||||
}
|
||||
|
||||
@@ -53,7 +53,8 @@ export async function getInternalConfig(
|
||||
|
||||
const browser = mergedConfig.browser ?? 'chrome';
|
||||
const manifestVersion =
|
||||
mergedConfig.manifestVersion ?? (browser == 'firefox' ? 2 : 3);
|
||||
mergedConfig.manifestVersion ??
|
||||
(browser === 'firefox' || browser === 'safari' ? 2 : 3);
|
||||
const mode =
|
||||
mergedConfig.mode ?? (command === 'build' ? 'production' : 'development');
|
||||
const env: ConfigEnv = { browser, command, manifestVersion, mode };
|
||||
|
||||
@@ -56,7 +56,8 @@ export async function importEntrypointFile<T>(
|
||||
'node_modules/wxt/dist/virtual-modules/fake-browser.js',
|
||||
),
|
||||
},
|
||||
extensions: ['.ts', '.tsx', '.cjs', '.js', '.mjs'],
|
||||
// List of extensions to transform with esbuild
|
||||
extensions: ['.ts', '.cts', '.mts', '.tsx', '.js', '.cjs', '.mjs', '.jsx'],
|
||||
transform(opts) {
|
||||
const isEntrypoint = opts.filename === normalPath;
|
||||
return transformSync(
|
||||
|
||||
@@ -10,7 +10,10 @@ import {
|
||||
} from '../types';
|
||||
import fs from 'fs-extra';
|
||||
import { resolve } from 'path';
|
||||
import { getEntrypointBundlePath } from './entrypoints';
|
||||
import {
|
||||
getEntrypointBundlePath,
|
||||
resolvePerBrowserOption,
|
||||
} from './entrypoints';
|
||||
import { ContentSecurityPolicy } from './ContentSecurityPolicy';
|
||||
import {
|
||||
hashContentScriptOptions,
|
||||
@@ -57,16 +60,19 @@ export async function generateMainfest(
|
||||
): Promise<Manifest.WebExtensionManifest> {
|
||||
const pkg = await getPackageJson(config);
|
||||
|
||||
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: pkg?.version && simplifyVersion(pkg.version),
|
||||
// Only add the version name to chromium and if the user hasn't specified a custom version.
|
||||
version,
|
||||
version_name:
|
||||
config.browser !== 'firefox' && !config.manifest.version
|
||||
? pkg?.version
|
||||
: undefined,
|
||||
// Firefox doesn't support version_name
|
||||
config.browser === 'firefox' || versionName === version
|
||||
? undefined
|
||||
: versionName,
|
||||
short_name: pkg?.shortName,
|
||||
icons: discoverIcons(buildOutput),
|
||||
};
|
||||
@@ -295,7 +301,11 @@ function addEntrypoints(
|
||||
if (config.command === 'serve' && config.manifestVersion === 3) {
|
||||
const hostPermissions = new Set<string>(manifest.host_permissions ?? []);
|
||||
contentScripts.forEach((script) => {
|
||||
script.options.matches.forEach((matchPattern) => {
|
||||
const matches = resolvePerBrowserOption(
|
||||
script.options.matches,
|
||||
config.browser,
|
||||
);
|
||||
matches.forEach((matchPattern) => {
|
||||
hostPermissions.add(matchPattern);
|
||||
});
|
||||
});
|
||||
@@ -304,7 +314,7 @@ function addEntrypoints(
|
||||
);
|
||||
} else {
|
||||
const hashToEntrypointsMap = contentScripts.reduce((map, script) => {
|
||||
const hash = hashContentScriptOptions(script.options);
|
||||
const hash = hashContentScriptOptions(script.options, config);
|
||||
if (map.has(hash)) map.get(hash)?.push(script);
|
||||
else map.set(hash, [script]);
|
||||
return map;
|
||||
@@ -312,7 +322,7 @@ function addEntrypoints(
|
||||
|
||||
const newContentScripts = Array.from(hashToEntrypointsMap.entries()).map(
|
||||
([, scripts]) => ({
|
||||
...mapWxtOptionsToContentScript(scripts[0].options),
|
||||
...mapWxtOptionsToContentScript(scripts[0].options, config),
|
||||
// TOOD: Sorting css and js arrays here so we get consistent test results... but we
|
||||
// shouldn't have to. Where is the inconsistency coming from?
|
||||
css: getContentScriptCssFiles(scripts, cssMap)?.sort(),
|
||||
|
||||
@@ -4,68 +4,147 @@ import { getEntrypointName } from '../utils/entrypoints';
|
||||
import { parseHTML } from 'linkedom';
|
||||
import { dirname, isAbsolute, relative, resolve } from 'path';
|
||||
|
||||
// Cache the preamble script for all devHtmlPrerender plugins, not just one
|
||||
let reactRefreshPreamble = '';
|
||||
|
||||
/**
|
||||
* Pre-renders the HTML entrypoints when building the extension to connect to the dev server.
|
||||
*/
|
||||
export function devHtmlPrerender(config: InternalConfig): vite.Plugin {
|
||||
return {
|
||||
apply: 'build',
|
||||
name: 'wxt:dev-html-prerender',
|
||||
config() {
|
||||
return {
|
||||
resolve: {
|
||||
alias: {
|
||||
'@wxt/reload-html': resolve(
|
||||
config.root,
|
||||
'node_modules/wxt/dist/virtual-modules/reload-html.js',
|
||||
),
|
||||
export function devHtmlPrerender(config: InternalConfig): vite.PluginOption {
|
||||
const htmlReloadId = '@wxt/reload-html';
|
||||
const resolvedHtmlReloadId = resolve(
|
||||
config.root,
|
||||
'node_modules/wxt/dist/virtual-modules/reload-html.js',
|
||||
);
|
||||
const virtualReactRefreshId = '@wxt/virtual-react-refresh';
|
||||
const resolvedVirtualReactRefreshId = '\0' + virtualReactRefreshId;
|
||||
|
||||
return [
|
||||
{
|
||||
apply: 'build',
|
||||
name: 'wxt:dev-html-prerender',
|
||||
config() {
|
||||
return {
|
||||
resolve: {
|
||||
alias: {
|
||||
[htmlReloadId]: resolvedHtmlReloadId,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
},
|
||||
// Convert scripts like src="./main.tsx" -> src="http://localhost:3000/entrypoints/popup/main.tsx"
|
||||
// before the paths are replaced with their bundled path
|
||||
transform(code, id) {
|
||||
const server = config.server;
|
||||
if (
|
||||
config.command !== 'serve' ||
|
||||
server == null ||
|
||||
!id.endsWith('.html')
|
||||
)
|
||||
return;
|
||||
|
||||
const { document } = parseHTML(code);
|
||||
|
||||
const pointToDevServer = (
|
||||
querySelector: string,
|
||||
attr: string,
|
||||
): void => {
|
||||
document.querySelectorAll(querySelector).forEach((element) => {
|
||||
const src = element.getAttribute(attr);
|
||||
if (!src) return;
|
||||
|
||||
if (isAbsolute(src)) {
|
||||
element.setAttribute(attr, server.origin + src);
|
||||
} else if (src.startsWith('.')) {
|
||||
const abs = resolve(dirname(id), src);
|
||||
const pathname = relative(config.root, abs);
|
||||
element.setAttribute(attr, `${server.origin}/${pathname}`);
|
||||
}
|
||||
});
|
||||
};
|
||||
pointToDevServer('script[type=module]', 'src');
|
||||
pointToDevServer('link[rel=stylesheet]', 'href');
|
||||
|
||||
// Add a script to add page reloading
|
||||
const reloader = document.createElement('script');
|
||||
reloader.src = htmlReloadId;
|
||||
reloader.type = 'module';
|
||||
document.head.appendChild(reloader);
|
||||
|
||||
const newHtml = document.toString();
|
||||
config.logger.debug('transform ' + id);
|
||||
config.logger.debug('Old HTML:\n' + code);
|
||||
config.logger.debug('New HTML:\n' + newHtml);
|
||||
return newHtml;
|
||||
},
|
||||
|
||||
// Pass the HTML through the dev server to add dev-mode specific code
|
||||
async transformIndexHtml(html, ctx) {
|
||||
const server = config.server;
|
||||
if (config.command !== 'serve' || server == null) return;
|
||||
|
||||
const originalUrl = `${server.origin}${ctx.path}`;
|
||||
const name = getEntrypointName(config.entrypointsDir, ctx.filename);
|
||||
const url = `${server.origin}/${name}.html`;
|
||||
const serverHtml = await server.transformIndexHtml(
|
||||
url,
|
||||
html,
|
||||
originalUrl,
|
||||
);
|
||||
const { document } = parseHTML(serverHtml);
|
||||
|
||||
// React pages include a preamble as an unsafe-inline type="module" script to enable fast refresh, as shown here:
|
||||
// https://github.com/wxt-dev/wxt/issues/157#issuecomment-1756497616
|
||||
// Since unsafe-inline scripts are blocked by MV3 CSPs, we need to virtualize it.
|
||||
const reactRefreshScript = Array.from(
|
||||
document.querySelectorAll('script[type=module]'),
|
||||
).find((script) => script.innerHTML.includes('@react-refresh'));
|
||||
if (reactRefreshScript) {
|
||||
// Save preamble to serve from server
|
||||
reactRefreshPreamble = reactRefreshScript.innerHTML;
|
||||
|
||||
// Replace unsafe inline script
|
||||
const virtualScript = document.createElement('script');
|
||||
virtualScript.type = 'module';
|
||||
virtualScript.src = `${server.origin}/${virtualReactRefreshId}`;
|
||||
reactRefreshScript.replaceWith(virtualScript);
|
||||
}
|
||||
|
||||
// Change /@vite/client -> http://localhost:3000/@vite/client
|
||||
const viteClientScript = document.querySelector<HTMLScriptElement>(
|
||||
"script[src='/@vite/client']",
|
||||
);
|
||||
if (viteClientScript) {
|
||||
viteClientScript.src = `${server.origin}${viteClientScript.src}`;
|
||||
}
|
||||
|
||||
const newHtml = document.toString();
|
||||
config.logger.debug('transformIndexHtml ' + ctx.filename);
|
||||
config.logger.debug('Old HTML:\n' + html);
|
||||
config.logger.debug('New HTML:\n' + newHtml);
|
||||
return newHtml;
|
||||
},
|
||||
},
|
||||
async transform(html, id) {
|
||||
const server = config.server;
|
||||
if (config.command !== 'serve' || server == null || !id.endsWith('.html'))
|
||||
return;
|
||||
|
||||
const originalUrl = `${server.origin}${id}`;
|
||||
const name = getEntrypointName(config.entrypointsDir, id);
|
||||
const url = `${server.origin}/${name}.html`;
|
||||
const serverHtml = await server.transformIndexHtml(
|
||||
url,
|
||||
html,
|
||||
originalUrl,
|
||||
);
|
||||
const { document } = parseHTML(serverHtml);
|
||||
|
||||
const pointToDevServer = (querySelector: string, attr: string): void => {
|
||||
document.querySelectorAll(querySelector).forEach((element) => {
|
||||
const src = element.getAttribute(attr);
|
||||
if (!src) return;
|
||||
|
||||
if (isAbsolute(src)) {
|
||||
element.setAttribute(attr, server.origin + src);
|
||||
} else if (src.startsWith('.')) {
|
||||
const abs = resolve(dirname(id), src);
|
||||
const pathname = relative(config.root, abs);
|
||||
element.setAttribute(attr, `${server.origin}/${pathname}`);
|
||||
}
|
||||
});
|
||||
};
|
||||
pointToDevServer('script[type=module]', 'src');
|
||||
pointToDevServer('link[rel=stylesheet]', 'href');
|
||||
|
||||
// Add a script to add page reloading
|
||||
const reloader = document.createElement('script');
|
||||
reloader.src = '@wxt/reload-html';
|
||||
reloader.type = 'module';
|
||||
document.head.appendChild(reloader);
|
||||
|
||||
const newHtml = document.toString();
|
||||
config.logger.debug('Transformed ' + id);
|
||||
config.logger.debug('Old HTML:\n' + html);
|
||||
config.logger.debug('New HTML:\n' + newHtml);
|
||||
return newHtml;
|
||||
{
|
||||
name: 'wxt:virtualize-react-refresh',
|
||||
apply: 'serve',
|
||||
resolveId(id) {
|
||||
if (id === `/${virtualReactRefreshId}`) {
|
||||
return resolvedVirtualReactRefreshId;
|
||||
}
|
||||
// Ignore chunk contents when pre-rendering
|
||||
if (id.startsWith('/chunks/')) {
|
||||
return '\0noop';
|
||||
}
|
||||
},
|
||||
load(id) {
|
||||
if (id === resolvedVirtualReactRefreshId) {
|
||||
return reactRefreshPreamble;
|
||||
}
|
||||
if (id === '\0noop') {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
];
|
||||
}
|
||||
|
||||
@@ -23,6 +23,6 @@
|
||||
"@types/react-dom": "^18.2.6",
|
||||
"@vitejs/plugin-react": "^4.0.3",
|
||||
"typescript": "^5.1.6",
|
||||
"wxt": "^0.5.0"
|
||||
"wxt": "^0.7.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { defineConfig } from 'wxt';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
// See https://wxt.dev/config.html
|
||||
// See https://wxt.dev/api/config.html
|
||||
export default defineConfig({
|
||||
vite: () => ({
|
||||
plugins: [react()],
|
||||
|
||||
@@ -20,6 +20,6 @@
|
||||
"devDependencies": {
|
||||
"typescript": "^5.1.6",
|
||||
"vite-plugin-solid": "^2.7.0",
|
||||
"wxt": "^0.5.0"
|
||||
"wxt": "^0.7.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { defineConfig } from 'wxt';
|
||||
import Solid from 'vite-plugin-solid';
|
||||
|
||||
// See https://wxt.dev/config.html
|
||||
// See https://wxt.dev/api/config.html
|
||||
export default defineConfig({
|
||||
vite: () => ({
|
||||
build: {
|
||||
|
||||
@@ -20,6 +20,6 @@
|
||||
"svelte-check": "^3.4.6",
|
||||
"tslib": "^2.6.0",
|
||||
"typescript": "^5.1.6",
|
||||
"wxt": "^0.5.0"
|
||||
"wxt": "^0.7.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<img src={svelteLogo} class="logo svelte" alt="Svelte Logo" />
|
||||
</a>
|
||||
</div>
|
||||
<h1>WXT 2 + Svelte</h1>
|
||||
<h1>WXT + Svelte</h1>
|
||||
|
||||
<div class="card">
|
||||
<Counter />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { defineConfig } from 'wxt';
|
||||
import { svelte, vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
// See https://wxt.dev/config.html
|
||||
// See https://wxt.dev/api/config.html
|
||||
export default defineConfig({
|
||||
srcDir: 'src',
|
||||
vite: () => ({
|
||||
|
||||
@@ -16,6 +16,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.1.6",
|
||||
"wxt": "^0.5.0"
|
||||
"wxt": "^0.7.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defineConfig } from 'wxt';
|
||||
|
||||
// See https://wxt.dev/config.html
|
||||
// See https://wxt.dev/api/config.html
|
||||
export default defineConfig({});
|
||||
|
||||
@@ -21,6 +21,6 @@
|
||||
"@vitejs/plugin-vue": "^4.2.3",
|
||||
"typescript": "^5.1.6",
|
||||
"vue-tsc": "^1.8.4",
|
||||
"wxt": "^0.5.0"
|
||||
"wxt": "^0.7.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { defineConfig } from 'wxt';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
|
||||
// See https://wxt.dev/config.html
|
||||
// See https://wxt.dev/api/config.html
|
||||
export default defineConfig({
|
||||
vite: () => ({
|
||||
plugins: [vue()],
|
||||
|
||||
Reference in New Issue
Block a user