Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1b1af245bd | |||
| 874a531a62 | |||
| a3d409f250 | |||
| b6758ca9fa | |||
| 2672308946 | |||
| 7ac171ed3b | |||
| 6a93f20bb3 | |||
| 921af6a5a1 | |||
| 21ebeacd20 | |||
| 527600031f | |||
| 91a804c492 | |||
| 54f3785063 | |||
| 44e4bc5295 | |||
| f464d7d33f | |||
| d0672739f0 | |||
| 44464f914f | |||
| 8940c41bdb | |||
| 446f265b6c | |||
| 7a465684c0 |
@@ -7,13 +7,9 @@ on:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
wxt:
|
||||
name: WXT
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
checks:
|
||||
name: Checks
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
@@ -46,6 +42,7 @@ jobs:
|
||||
pnpm build:all:firefox-mv3
|
||||
pnpm tsc --noEmit
|
||||
pnpm wxt zip
|
||||
pnpm vitest run
|
||||
working-directory: demo
|
||||
|
||||
- name: Tests
|
||||
@@ -56,6 +53,30 @@ jobs:
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
windows-tests:
|
||||
name: Windows Tests
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup PNPM
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
|
||||
- name: Setup NodeJS
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Tests
|
||||
run: pnpm test:coverage --reporter=default --reporter=hanging-process
|
||||
|
||||
project-templates:
|
||||
name: Project Templates
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
+2
-2
@@ -17,6 +17,6 @@ web-ext.config.ts
|
||||
templates/*/pnpm-lock.yaml
|
||||
templates/*/yarn.lock
|
||||
templates/*/package-lock.json
|
||||
docs/api/config.md
|
||||
docs/api/cli.md
|
||||
docs/api
|
||||
stats.html
|
||||
.tool-versions
|
||||
|
||||
@@ -1,5 +1,62 @@
|
||||
# Changelog
|
||||
|
||||
## v0.8.3
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.8.2...v0.8.3)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Add testing utils under `wxt/testing` ([#178](https://github.com/wxt-dev/wxt/pull/178))
|
||||
|
||||
## v0.8.2
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.8.1...v0.8.2)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- **firefox:** Stop extending `AbortController` to fix crash in content scripts ([#176](https://github.com/wxt-dev/wxt/pull/176))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Improve output consistency ([#175](https://github.com/wxt-dev/wxt/pull/175))
|
||||
|
||||
## v0.8.1
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.8.0...v0.8.1)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Output `action.browser_style` correctly ([6a93f20](https://github.com/wxt-dev/wxt/commit/6a93f20))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Generate full API docs with typedoc ([#174](https://github.com/wxt-dev/wxt/pull/174))
|
||||
|
||||
## v0.8.0
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.7.5...v0.8.0)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- ⚠️ Use `defineUnlistedScript` to define unlisted scripts ([#167](https://github.com/wxt-dev/wxt/pull/167))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Fix wrong links ([#166](https://github.com/wxt-dev/wxt/pull/166))
|
||||
|
||||
### 🌊 Types
|
||||
|
||||
- ⚠️ Rename `BackgroundScriptDefintition` to `BackgroundDefinition` ([446f265](https://github.com/wxt-dev/wxt/commit/446f265))
|
||||
|
||||
#### ⚠️ Breaking Changes
|
||||
|
||||
- ⚠️ Unlisted scripts must now `export default defineUnlistedScript(...)` ([#167](https://github.com/wxt-dev/wxt/pull/167))
|
||||
- ⚠️ Rename `BackgroundScriptDefintition` to `BackgroundDefinition` ([446f265](https://github.com/wxt-dev/wxt/commit/446f265))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- 渣渣120 <WOSHIZHAZHA120@qq.com>
|
||||
|
||||
## v0.7.5
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.7.4...v0.7.5)
|
||||
|
||||
+4
-2
@@ -11,14 +11,16 @@
|
||||
"build:all:chrome-mv2": "wxt build --mv2",
|
||||
"build:all:firefox-mv3": "wxt build -b firefox --mv3",
|
||||
"build:all:firefox-mv2": "wxt build -b firefox",
|
||||
"test": "pnpm -w build && vitest",
|
||||
"zip": "pnpm -w build && wxt zip",
|
||||
"compile": "pnpm -w build && tsc --noEmit",
|
||||
"postinstall": "pnpm -w build && wxt prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"webextension-polyfill": "^0.10.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"react-dom": "^18.2.0",
|
||||
"vitest": "^0.34.6",
|
||||
"webextension-polyfill": "^0.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.14",
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import background from '../background';
|
||||
|
||||
browser.i18n.getMessage = () => 'fake-message';
|
||||
|
||||
const logMock = vi.fn();
|
||||
console.log = logMock;
|
||||
|
||||
describe('Background Entrypoint', () => {
|
||||
it("should log the extenion's runtime ID", () => {
|
||||
const id = 'some-id';
|
||||
fakeBrowser.runtime.id = id;
|
||||
|
||||
background.main();
|
||||
|
||||
expect(logMock).toBeCalledWith(id);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
export default defineUnlistedScript(() => {
|
||||
console.log('injected');
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import { WxtVitest } from 'wxt/testing';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
mockReset: true,
|
||||
restoreMocks: true,
|
||||
},
|
||||
plugins: [WxtVitest()],
|
||||
});
|
||||
@@ -1,6 +1,19 @@
|
||||
import { defineConfig } from 'vitepress';
|
||||
import { generateConfigDocs } from './plugins/generate-config-docs';
|
||||
import { DefaultTheme, defineConfig } from 'vitepress';
|
||||
import { generateCliDocs } from './plugins/generate-cli-docs';
|
||||
import typedocSidebar from '../api/typedoc-sidebar.json';
|
||||
|
||||
const filteredTypedocSidebar = typedocSidebar.filter(
|
||||
(item) => item.text !== 'API',
|
||||
);
|
||||
// Typedoc's markdown theme adds collapse: true to all our items, event ones without any children,
|
||||
// so they need to be removed.
|
||||
function removeCollapsedWithNoItems(items: DefaultTheme.SidebarItem[]) {
|
||||
for (const item of items) {
|
||||
if (item.items) removeCollapsedWithNoItems(item.items);
|
||||
else delete item.collapsed;
|
||||
}
|
||||
}
|
||||
removeCollapsedWithNoItems(filteredTypedocSidebar);
|
||||
|
||||
const title = 'Next-gen Web Extension Framework';
|
||||
const titleSuffix = ' – WXT';
|
||||
@@ -18,7 +31,7 @@ export default defineConfig({
|
||||
description,
|
||||
vite: {
|
||||
clearScreen: false,
|
||||
plugins: [generateConfigDocs(), generateCliDocs()],
|
||||
plugins: [generateCliDocs()],
|
||||
},
|
||||
lastUpdated: true,
|
||||
sitemap: {
|
||||
@@ -120,10 +133,10 @@ export default defineConfig({
|
||||
{
|
||||
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' },
|
||||
{
|
||||
text: 'Modules',
|
||||
items: filteredTypedocSidebar,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -8,7 +8,7 @@ let hasGenerated = false;
|
||||
|
||||
const cliDir = resolve('src/cli/commands');
|
||||
const cliDirGlob = resolve(cliDir, '**');
|
||||
const cliTemplatePath = resolve('docs/api/cli.tpl.md');
|
||||
const cliTemplatePath = resolve('docs/.vitepress/plugins/cli.tpl.md');
|
||||
const cliPath = resolve('docs/api/cli.md');
|
||||
|
||||
const PREFACE = `<!--
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
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';
|
||||
import consola from 'consola';
|
||||
|
||||
let hasGenerated = false;
|
||||
|
||||
const externalTypesPath = resolve('src/core/types/external.ts');
|
||||
const configTemplatePath = resolve('docs/api/config.tpl.md');
|
||||
const configPath = resolve('docs/api/config.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\`.
|
||||
-->`;
|
||||
|
||||
/**
|
||||
* Custom property paths that should not be recursively inspected. Usually 3rd party types.
|
||||
*/
|
||||
const LEAF_PATHS = ['imports', 'vite', 'server'];
|
||||
|
||||
/**
|
||||
* Override any types that resolve to `import(...)` instead of their type names when calling
|
||||
* `type.getText()`.
|
||||
*
|
||||
* This also stops any further type inspection for objects, meaning the object will be documented,
|
||||
* not it's properties.
|
||||
*/
|
||||
const CUSTOM_TYPES = {
|
||||
manifest:
|
||||
'Manifest | Promise<Manifest> | () => Manifest | () => Promise<Manifest>',
|
||||
imports: "false | Partial<import('unimport').UnimportOptions>",
|
||||
vite: "Omit<import('vite').UserConfig, 'root' | 'configFile' | 'mode'>",
|
||||
};
|
||||
|
||||
export function generateConfigDocs() {
|
||||
writeFileSync(configPath, '');
|
||||
|
||||
const generateDocs = async () => {
|
||||
consola.info(`Generating ${relative(process.cwd(), configPath)}`);
|
||||
try {
|
||||
const project = new Project({
|
||||
tsConfigFilePath: resolve('tsconfig.json'),
|
||||
});
|
||||
|
||||
// Load file containing "UserConfig"
|
||||
const externalTypesFile = project.addSourceFileAtPath(externalTypesPath);
|
||||
project.resolveSourceFileDependencies();
|
||||
|
||||
const inlineConfigInterface =
|
||||
externalTypesFile.getInterfaceOrThrow('InlineConfig');
|
||||
|
||||
const getDocsFor = (path: string[], node: Node<ts.Node>): string[] => {
|
||||
const pathStr = path.join('.');
|
||||
|
||||
let type: Type<ts.Type>;
|
||||
if (node.isKind(ts.SyntaxKind.InterfaceDeclaration)) {
|
||||
type = node.getType();
|
||||
} else if (node.isKind(ts.SyntaxKind.PropertySignature)) {
|
||||
type = node.getTypeNodeOrThrow()?.getType();
|
||||
} else if (node.isKind(ts.SyntaxKind.MethodSignature)) {
|
||||
type = node.getType();
|
||||
} else {
|
||||
throw Error('Unsupported type node: ' + node.getKindName());
|
||||
}
|
||||
|
||||
if (type.isObject() && !type.isArray() && !CUSTOM_TYPES[pathStr]) {
|
||||
return type.getProperties().flatMap((property) => {
|
||||
const childPath = [...path, property.getName()];
|
||||
|
||||
return getDocsFor(childPath, property.getDeclarations()[0]);
|
||||
});
|
||||
}
|
||||
|
||||
if ('getJsDocs' in node) {
|
||||
const lines: string[] = [];
|
||||
const docs = (node as unknown as JSDocableNode).getJsDocs();
|
||||
let typeText: string;
|
||||
if (CUSTOM_TYPES[pathStr]) {
|
||||
typeText = CUSTOM_TYPES[pathStr];
|
||||
} else if (type.isUnion() && !type.isBoolean()) {
|
||||
typeText = type
|
||||
.getUnionTypes()
|
||||
.map((type) => type.getText())
|
||||
.join(' | ');
|
||||
} else {
|
||||
typeText = type.getText();
|
||||
}
|
||||
const defaultValue = docs
|
||||
.flatMap((doc) => doc.getTags())
|
||||
.find((tag) => tag.getTagName() === 'default')
|
||||
?.getCommentText();
|
||||
lines.push(
|
||||
'',
|
||||
`## ${pathStr}`,
|
||||
'',
|
||||
`- **Type**: \`${typeText}\``,
|
||||
`- **Default**: \`${defaultValue}\``,
|
||||
...docs.flatMap((doc) => doc.getDescription()),
|
||||
);
|
||||
return lines;
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
const lines = getDocsFor([], inlineConfigInterface);
|
||||
const text = await format(
|
||||
PREFACE +
|
||||
'\n\n' +
|
||||
readFileSync(configTemplatePath, 'utf-8').replace(
|
||||
'{{ DOCS }}',
|
||||
lines.join('\n'),
|
||||
),
|
||||
{ parser: 'markdown' },
|
||||
);
|
||||
|
||||
writeFileSync(configPath, text);
|
||||
consola.success(`Generated ${relative(process.cwd(), configPath)}`);
|
||||
} catch (err) {
|
||||
consola.fail(`Failed to generate ${relative(process.cwd(), configPath)}`);
|
||||
consola.error(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
name: 'docs:generate-config-docs',
|
||||
async config() {
|
||||
if (!hasGenerated) {
|
||||
hasGenerated = true;
|
||||
await generateDocs();
|
||||
}
|
||||
},
|
||||
configureServer(server: any) {
|
||||
server.watcher.add(externalTypesPath);
|
||||
},
|
||||
async handleHotUpdate(ctx: { file: string }) {
|
||||
if ([externalTypesPath, configTemplatePath].includes(ctx.file)) {
|
||||
await generateDocs();
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
# Config Reference
|
||||
|
||||
All the options you can use in your `wxt.config.ts` file.
|
||||
|
||||
{{ DOCS }}
|
||||
@@ -1,10 +0,0 @@
|
||||
# `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';
|
||||
```
|
||||
|
||||
:::
|
||||
@@ -1,14 +0,0 @@
|
||||
# `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';
|
||||
```
|
||||
|
||||
:::
|
||||
@@ -1,17 +0,0 @@
|
||||
# `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';
|
||||
```
|
||||
|
||||
:::
|
||||
@@ -5,7 +5,7 @@ WXT can build CSS entrypoints individually. CSS entrypoints are always unlisted.
|
||||
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](/api/config#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/wxt/interfaces/InlineConfig#transformmanifest) to manually add your CSS file to the manifest.
|
||||
:::
|
||||
|
||||
## Filenames
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
TypeScript files that are built, but are not included in the manifest.
|
||||
|
||||
You are responsible for loading/running these scripts where needed.
|
||||
|
||||
## Filenames
|
||||
|
||||
<EntrypointPatterns
|
||||
@@ -13,8 +15,23 @@ TypeScript files that are built, but are not included in the manifest.
|
||||
|
||||
## Definition
|
||||
|
||||
Unlike the background or content scripts, you can define this script's logic in the top level scope.
|
||||
```ts
|
||||
export default defineUnlistedScript(() => {
|
||||
// Executed when script is loaded
|
||||
});
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```ts
|
||||
// Code goes here
|
||||
export default defineUnlistedScript({
|
||||
// Set include/exclude if the script should be removed from some builds
|
||||
include: undefined | string[],
|
||||
exclude: undefined | string[],
|
||||
|
||||
// Executed when script is loaded
|
||||
main() {
|
||||
// ...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
@@ -10,12 +10,14 @@ To setup your test environment for auto-imports, see [Testing](/guide/testing).
|
||||
|
||||
Some WXT APIs can be used without importing them:
|
||||
|
||||
- [`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`
|
||||
- [`browser`](/api/wxt/browser/variables/browser) from `wxt/browser`, a small wrapper around `webextension-polyfill`
|
||||
- [`defineContentScript`](/api/wxt/client/functions/defineContentScript) from `wxt/client`
|
||||
- [`defineBackground`](/api/wxt/client/functions/defineBackground) from `wxt/client`
|
||||
- [`createContentScriptUi`](/api/wxt/client/functions/createContentScriptUi) from `wxt/client`
|
||||
- [`defineUnlistedScript`](/api/wxt/sandbox/functions/defineUnlistedScript) from `wxt/sandbox`
|
||||
- [`fakeBrowser`](/api/wxt/testing/variables/fakeBrowser) from `wxt/testing`
|
||||
|
||||
And more. All [`wxt/client`](/api/wxt-client) APIs can be used without imports.
|
||||
And more. All `wxt/*` APIs can be used without imports.
|
||||
|
||||
## Project Auto-imports
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ export default defineConfig({
|
||||
```
|
||||
|
||||
:::info
|
||||
See the [Config reference](/api/config) for a full list of options.
|
||||
See the [Config reference](/api/wxt/interfaces/InlineConfig) for a full list of options.
|
||||
:::
|
||||
|
||||
## Directory Config
|
||||
|
||||
@@ -15,7 +15,7 @@ WXT's main goal is providing the best DX it possibly can. When running your exte
|
||||
|
||||
## 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`.
|
||||
WXT uses [`web-ext` by Mozilla](https://github.com/mozilla/web-ext) to automatically open a browser with the extension installed. You can configure the runner's behavior via the [`runner`](/api/wxt/interfaces/InlineConfig#runner) option, or in a separate gitignored file, `web-ext.config.ts`.
|
||||
|
||||
:::code-group
|
||||
|
||||
|
||||
@@ -79,8 +79,8 @@ There are a number of message passing libraries you can use to improve the messa
|
||||
|
||||
Here are some that are compatible with WXT (because they are based off `webextension-polyfill` as well):
|
||||
|
||||
- [`@webext-core/messaging`](https://webext-core.aklinker1.io/guide/proxy-service/) - "A light-weight, type-safe wrapper around the `browser.runtime` messaging APIs"
|
||||
- [`@webext-core/proxy-service`](https://webext-core.aklinker1.io/guide/messaging/) - "Create TRPC-like services that can be called from anywhere but run in the background"
|
||||
- [`@webext-core/messaging`](https://webext-core.aklinker1.io/guide/messaging/) - "A light-weight, type-safe wrapper around the `browser.runtime` messaging APIs"
|
||||
- [`@webext-core/proxy-service`](https://webext-core.aklinker1.io/guide/proxy-service/) - "Create TRPC-like services that can be called from anywhere but run in the background"
|
||||
- [`webext-bridge`](https://github.com/zikaari/webext-bridge) - "Messaging in Web Extensions made super easy. Out of the box."
|
||||
|
||||
## Browser Differences
|
||||
|
||||
+132
-71
@@ -1,92 +1,153 @@
|
||||
# Testing
|
||||
|
||||
WXT provides a couple of utils for unit testing your extension.
|
||||
WXT provides several utils for writing tests.
|
||||
|
||||
[[toc]]
|
||||
## Unit tests
|
||||
|
||||
## Fake Browser
|
||||
If you're using auto-imports (enabled by default), [Vitest](https://vitest.dev/) is the only testing framework that supports them.
|
||||
|
||||
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/).
|
||||
If you want to use a different testing library/framework (like Jest, mocha, node:test, etc), you can keep using it, but you have two options:
|
||||
|
||||
Here's an example test using Vitest:
|
||||
1. Switch to Vitest (recommended)
|
||||
2. Configure the testing library manually
|
||||
- Disable auto-imports by setting `imports: false` in your `wxt.config.ts` file
|
||||
- Manually add globals normally provided by WXT (like `__BROWSER__`) that you consume to the global scope before accessing them (`globalThis.__BROWSER__ = "chrome"`)
|
||||
|
||||
```ts
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { browser } from 'wxt/browser';
|
||||
import { fakeBrowser } from 'wxt';
|
||||
### Vitest Setup
|
||||
|
||||
// Function we're testing
|
||||
function onHelloMessage(cb: () => void) {
|
||||
browser.runtime.onMessage.addEventListener((message) => {
|
||||
if (message.type === 'hello') return 'world';
|
||||
});
|
||||
}
|
||||
Install vitest and add the `WxtVitest` plugin to your `vitest.config.ts` file.
|
||||
|
||||
// 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();
|
||||
});
|
||||
});
|
||||
```sh
|
||||
pnpm i -D vitest
|
||||
```
|
||||
|
||||
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';
|
||||
// <root>/vitest.config.ts
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import { WxtVitest } from 'wxt/testing';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
autoImports({
|
||||
imports: [{ name: 'defineConfig', from: 'wxt' }],
|
||||
presets: [{ package: 'wxt/client' }, { package: 'wxt/browser' }],
|
||||
dirs: ['components', 'composables', 'hooks', 'utils'],
|
||||
}),
|
||||
],
|
||||
plugins: [WxtVitest()],
|
||||
});
|
||||
```
|
||||
|
||||
### Jest
|
||||
And that's it. You're ready to start writing tests.
|
||||
|
||||
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.
|
||||
### Writing Tests
|
||||
|
||||
I would recommend disabling auto-imports or migrating to Vitest if you want to use auto-imports.
|
||||
Here's a very basic test, written with a few different testing libraries, with a few different approaches for mocking the `browser` global.
|
||||
|
||||
### Mocha
|
||||
:::code-group
|
||||
|
||||
TODO: Is this possible? Maybe with `esbuild-mocha`? I would recommend moving to Vitest.
|
||||
```ts [Vitest]
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
|
||||
function logRuntimeId() {
|
||||
// Vitest automatically mocks "browser" with "fakeBrowser"
|
||||
console.log(browser.runtime.id);
|
||||
}
|
||||
|
||||
describe('logRuntimeId', () => {
|
||||
it("should log the extension's runtime ID", () => {
|
||||
// Set a known ID on fakeBrowser for the test
|
||||
const id = 'some-runtime-id';
|
||||
fakeBrowser.runtime.id = id;
|
||||
const logSpy = vi.spyOn(console, 'log');
|
||||
|
||||
logRuntimeId();
|
||||
|
||||
expect(logSpy).toBeCalledWith(id);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
```ts [Jest - Manual Mock]
|
||||
import { fakeBrowser } from 'wxt/testing';
|
||||
import { browser } from 'wxt/browser';
|
||||
|
||||
function logRuntimeId() {
|
||||
console.log(browser.runtime.id);
|
||||
}
|
||||
|
||||
// Manually mock
|
||||
jest.mock('wxt/browser', () => {
|
||||
const { fakeBrowser } = require('wxt/testing');
|
||||
return { browser: fakeBrowser };
|
||||
});
|
||||
|
||||
describe('logRuntimeId', () => {
|
||||
it("should log the extension's runtime ID", () => {
|
||||
// Set a known ID on fakeBrowser for the test
|
||||
const id = 'some-runtime-id';
|
||||
fakeBrowser.runtime.id = id;
|
||||
const logSpy = jest.spyOn(console, 'log');
|
||||
|
||||
logRuntimeId();
|
||||
|
||||
expect(logSpy).toBeCalledWith(id);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
```ts [node:test - Parameterized]
|
||||
import { describe, it, mock } from 'node:test';
|
||||
import { assert } from 'node:assert';
|
||||
import { fakeBrowser } from 'wxt/testing';
|
||||
import { browser } from 'wxt/browser';
|
||||
|
||||
// Add browser as a parameter so fakeBrowser can be passed instead of browser
|
||||
function logRuntimeId(browser = browser) {
|
||||
console.log(browser.runtime.id);
|
||||
}
|
||||
|
||||
describe('logRuntimeId', () => {
|
||||
it("should log the extension's runtime ID", () => {
|
||||
// Set a known ID on fakeBrowser for the test
|
||||
const id = 'some-runtime-id';
|
||||
fakeBrowser.runtime.id = id;
|
||||
console.log = mock.fn();
|
||||
|
||||
// pass in fakeBrowser during tests
|
||||
logRuntimeId(fakeBrowser);
|
||||
|
||||
assert.deepStrictEqual(console.log.mock.calls[0].arguments, [id]);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::warning
|
||||
Without mocking the `browser` variable, you'll see errors like this:
|
||||
|
||||
```
|
||||
This script should only be loaded in a browser extension.
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
WXT provides an in-memory, partial implementation of `browser`, [`fakeBrowser`](/api/wxt/testing/variables/fakeBrowser), from the [`@webext-core/fake-browser`](https://webext-core.aklinker1.io/guide/fake-browser/) package. `fakeBrowser` works with all testing frameworks/libraries. See their docs for a list of [implemented APIs](https://webext-core.aklinker1.io/guide/fake-browser/implemented-apis.html) and more example tests.
|
||||
|
||||
## E2E Tests
|
||||
|
||||
WXT does not provide any utils for running E2E tests. There are two libraries you can use to run E2E tests for any chrome extension.
|
||||
|
||||
- [`playwright`](https://playwright.dev/docs/chrome-extensions) (recommended) - "A high-level API to automate web browsers"
|
||||
- [`puppeteer`](https://pptr.dev/guides/chrome-extensions) - "A high-level API to control headless Chrome over the DevTools Protocol"
|
||||
|
||||
:::info
|
||||
Note that both only support running tests on Chrome.
|
||||
:::
|
||||
|
||||
Before running tests with either of these tools, you must build the extension with `wxt build` and then load the extension from the output directory in a new tab.
|
||||
|
||||
To test an extension's UI, like the popup or options page, you'll need to know the extension's ID to open the URL directly.
|
||||
|
||||
> _chrome-extension://`browser.runtime.id`/popup.html_
|
||||
|
||||
- Playwright provides an API to get your extension ID after it has been installed. [See their docs](https://playwright.dev/docs/chrome-extensions#testing).
|
||||
- Puppeteer requires you know the ID before installing the extension, so you can hard code it into the URLs you open. Follow [Chrome's guide](https://developer.chrome.com/docs/extensions/mv3/manifest/key/) to setup a consistent runtime id.
|
||||
|
||||
:::info
|
||||
You cannot test popups in their normal popup window, you have to open them in a tab.
|
||||
:::
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
# https://docs.netlify.com/routing/redirects/
|
||||
|
||||
# Old URLs -> New URLs
|
||||
/config.html /api/config.html
|
||||
/config.html /api/wxt/interfaces/InlineConfig.html
|
||||
/api/config.html /api/wxt/interfaces/InlineConfig.html
|
||||
/entrypoints /entrypoints/background.html
|
||||
/get-started/assets.html /guide/assets.html
|
||||
/get-started/build-targets.html /guide/multiple-browsers.html
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"entryPoints": [
|
||||
"../src",
|
||||
"../src/client",
|
||||
"../src/client/browser.ts",
|
||||
"../src/client/sandbox",
|
||||
"../src/testing"
|
||||
],
|
||||
"plugin": ["typedoc-plugin-markdown", "typedoc-vitepress-theme"],
|
||||
"out": "./api",
|
||||
"githubPages": false,
|
||||
"excludePrivate": true,
|
||||
"excludeProtected": true,
|
||||
"excludeInternal": true,
|
||||
"readme": "none"
|
||||
}
|
||||
@@ -22,6 +22,8 @@ describe('Auto Imports', () => {
|
||||
const defineBackground: typeof import('wxt/client')['defineBackground']
|
||||
const defineConfig: typeof import('wxt')['defineConfig']
|
||||
const defineContentScript: typeof import('wxt/client')['defineContentScript']
|
||||
const defineUnlistedScript: typeof import('wxt/sandbox')['defineUnlistedScript']
|
||||
const fakeBrowser: typeof import('wxt/testing')['fakeBrowser']
|
||||
}
|
||||
"
|
||||
`);
|
||||
|
||||
@@ -172,7 +172,10 @@ describe('Output Directory Structure', () => {
|
||||
'entrypoints/background.js',
|
||||
`export default defineBackground(() => {});`,
|
||||
);
|
||||
project.addFile('entrypoints/unlisted.js', ``);
|
||||
project.addFile(
|
||||
'entrypoints/unlisted.js',
|
||||
`export default defineUnlistedScript(() => {})`,
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/content.js',
|
||||
`export default defineContentScript({
|
||||
|
||||
@@ -5,7 +5,11 @@ describe('Remote Code', () => {
|
||||
it('should download "url:*" modules and include them in the final bundle', async () => {
|
||||
const url = 'https://code.jquery.com/jquery-3.7.1.slim.min.js';
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/popup.ts', `import "url:${url}"`);
|
||||
project.addFile(
|
||||
'entrypoints/popup.ts',
|
||||
`import "url:${url}"
|
||||
export default defineUnlistedScript(() => {})`,
|
||||
);
|
||||
|
||||
await project.build();
|
||||
|
||||
|
||||
+23
-11
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"name": "wxt",
|
||||
"type": "module",
|
||||
"version": "0.7.5",
|
||||
"version": "0.8.3",
|
||||
"description": "Next gen framework for developing web extensions",
|
||||
"engines": {
|
||||
"node": ">=18.16.0",
|
||||
"node": ">=18",
|
||||
"pnpm": ">=8"
|
||||
},
|
||||
"repository": {
|
||||
@@ -44,9 +44,18 @@
|
||||
"import": "./dist/client.js",
|
||||
"types": "./dist/client.d.ts"
|
||||
},
|
||||
"./sandbox": {
|
||||
"import": "./dist/sandbox.js",
|
||||
"types": "./dist/sandbox.d.ts"
|
||||
},
|
||||
"./browser": {
|
||||
"import": "./dist/browser.js",
|
||||
"types": "./dist/browser.d.ts"
|
||||
},
|
||||
"./testing": {
|
||||
"require": "./dist/testing.cjs",
|
||||
"import": "./dist/testing.js",
|
||||
"types": "./dist/testing.d.ts"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
@@ -59,9 +68,10 @@
|
||||
"test:coverage": "vitest run --coverage.enabled --coverage.exclude=e2e --coverage.exclude=src/testing",
|
||||
"prepare": "simple-git-hooks",
|
||||
"prepublish": "pnpm -s build",
|
||||
"docs:dev": "vitepress dev docs",
|
||||
"docs:build": "vitepress build docs",
|
||||
"docs:preview": "vitepress preview docs",
|
||||
"docs:gen": "typedoc --options docs/typedoc.json",
|
||||
"docs:dev": "pnpm -s docs:gen && vitepress dev docs",
|
||||
"docs:build": "pnpm -s docs:gen && vitepress build docs",
|
||||
"docs:preview": "pnpm -s docs:gen && vitepress preview docs",
|
||||
"sync-releases": "pnpx changelogen@latest gh release"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -90,7 +100,7 @@
|
||||
"prompts": "^2.4.2",
|
||||
"rollup-plugin-visualizer": "^5.9.2",
|
||||
"unimport": "^3.1.0",
|
||||
"vite": "^4.4.7",
|
||||
"vite": "^4.4.11",
|
||||
"web-ext-run": "^0.1.0",
|
||||
"webextension-polyfill": "^0.10.0",
|
||||
"zip-dir": "^2.0.0"
|
||||
@@ -99,9 +109,9 @@
|
||||
"@faker-js/faker": "^8.0.2",
|
||||
"@types/fs-extra": "^11.0.1",
|
||||
"@types/lodash.merge": "^4.6.7",
|
||||
"@types/node": "^20.4.5",
|
||||
"@types/node": "^20.8.6",
|
||||
"@types/prompts": "^2.4.4",
|
||||
"@vitest/coverage-v8": "^0.34.1",
|
||||
"@vitest/coverage-v8": "^0.34.6",
|
||||
"execa": "^7.2.0",
|
||||
"jsdom": "^22.1.0",
|
||||
"lint-staged": "^14.0.0",
|
||||
@@ -109,13 +119,15 @@
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^3.0.1",
|
||||
"simple-git-hooks": "^2.9.0",
|
||||
"ts-morph": "^20.0.0",
|
||||
"tsup": "^7.1.0",
|
||||
"tsx": "^3.12.7",
|
||||
"typedoc": "^0.25.2",
|
||||
"typedoc-plugin-markdown": "4.0.0-next.23",
|
||||
"typedoc-vitepress-theme": "1.0.0-next.3",
|
||||
"typescript": "^5.2.2",
|
||||
"vitepress": "1.0.0-rc.10",
|
||||
"vitest": "^0.34.1",
|
||||
"vitest-mock-extended": "^1.1.4",
|
||||
"vitest": "^0.34.6",
|
||||
"vitest-mock-extended": "^1.3.1",
|
||||
"vue": "^3.3.4",
|
||||
"webextension-polyfill": "^0.10.0"
|
||||
},
|
||||
|
||||
Generated
+220
-203
File diff suppressed because it is too large
Load Diff
+15
-1
@@ -10,7 +10,7 @@ const spinner = ora('Building WXT').start();
|
||||
|
||||
const startTime = Date.now();
|
||||
const outDir = 'dist';
|
||||
const virtualEntrypoints = ['background', 'content-script'];
|
||||
const virtualEntrypoints = ['background', 'content-script', 'unlisted-script'];
|
||||
|
||||
await fs.rm(outDir, { recursive: true, force: true });
|
||||
|
||||
@@ -48,6 +48,20 @@ await Promise.all([
|
||||
silent: true,
|
||||
external: ['vite'],
|
||||
}),
|
||||
tsup.build({
|
||||
entry: { sandbox: 'src/client/sandbox/index.ts' },
|
||||
format: ['esm'],
|
||||
sourcemap: 'inline',
|
||||
dts: true,
|
||||
silent: true,
|
||||
}),
|
||||
tsup.build({
|
||||
entry: { testing: 'src/testing/index.ts' },
|
||||
format: ['esm', 'cjs'],
|
||||
sourcemap: 'inline',
|
||||
dts: true,
|
||||
silent: true,
|
||||
}),
|
||||
...virtualEntrypoints.map((entryName) =>
|
||||
tsup.build({
|
||||
entry: {
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { defineBackground } from '../defineBackground';
|
||||
import { BackgroundDefinition } from '../../core/types';
|
||||
|
||||
describe('defineBackground', () => {
|
||||
it('should return the object definition when given an object', () => {
|
||||
const definition: BackgroundDefinition = {
|
||||
include: [''],
|
||||
persistent: false,
|
||||
main: vi.fn(),
|
||||
};
|
||||
|
||||
const actual = defineBackground(definition);
|
||||
|
||||
expect(actual).toEqual(definition);
|
||||
});
|
||||
|
||||
it('should return the object definition when given a main function', () => {
|
||||
const main = vi.fn();
|
||||
|
||||
const actual = defineBackground(main);
|
||||
|
||||
expect(actual).toEqual({ main });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { defineContentScript } from '../defineContentScript';
|
||||
import { ContentScriptDefinition } from '../../core/types';
|
||||
|
||||
describe('defineContentScript', () => {
|
||||
it('should return the object passed in', () => {
|
||||
const definition: ContentScriptDefinition = {
|
||||
matches: [],
|
||||
include: [''],
|
||||
main: vi.fn(),
|
||||
};
|
||||
|
||||
const actual = defineContentScript(definition);
|
||||
|
||||
expect(actual).toEqual(definition);
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* @module wxt/browser
|
||||
*/
|
||||
import originalBrowser, { Browser, Runtime, I18n } from 'webextension-polyfill';
|
||||
|
||||
export interface AugmentedBrowser extends Browser {
|
||||
|
||||
@@ -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/entrypoints/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
|
||||
@@ -31,10 +31,10 @@ import { ContentScriptContext } from '.';
|
||||
* }
|
||||
* })
|
||||
*/
|
||||
export async function createContentScriptUi<T>(
|
||||
export async function createContentScriptUi<TApp>(
|
||||
ctx: ContentScriptContext,
|
||||
options: ContentScriptUiOptions<T>,
|
||||
): Promise<ContentScriptUi<T>> {
|
||||
options: ContentScriptUiOptions<TApp>,
|
||||
): Promise<ContentScriptUi<TApp>> {
|
||||
const css = [options.css ?? ''];
|
||||
if (ctx.options?.cssInjectionMode === 'ui') {
|
||||
css.push(await loadCss());
|
||||
@@ -62,7 +62,7 @@ export async function createContentScriptUi<T>(
|
||||
return resolved ?? undefined;
|
||||
};
|
||||
|
||||
let mounted: T;
|
||||
let mounted: TApp;
|
||||
|
||||
const mount = () => {
|
||||
const anchor = getAnchor();
|
||||
@@ -173,7 +173,7 @@ async function loadCss(): Promise<string> {
|
||||
}
|
||||
}
|
||||
|
||||
export interface ContentScriptUi<T> {
|
||||
export interface ContentScriptUi<TApp> {
|
||||
/**
|
||||
* The `HTMLElement` hosting the shadow root used to isolate the UI's styles. This is the element
|
||||
* that get's added to the DOM. This element's style is not isolated from the webpage.
|
||||
@@ -191,7 +191,7 @@ export interface ContentScriptUi<T> {
|
||||
/**
|
||||
* Custom data returned from the `options.mount` function.
|
||||
*/
|
||||
mounted: T;
|
||||
mounted: TApp;
|
||||
/**
|
||||
* Function that mounts or remounts the UI on the page.
|
||||
*/
|
||||
@@ -202,7 +202,7 @@ export interface ContentScriptUi<T> {
|
||||
remove: () => void;
|
||||
}
|
||||
|
||||
interface BaseContentScriptUiOptions<T> {
|
||||
interface BaseContentScriptUiOptions<TApp> {
|
||||
/**
|
||||
* The name of the custom component used to host the ShadowRoot. Must be kebab-case.
|
||||
*/
|
||||
@@ -234,50 +234,53 @@ interface BaseContentScriptUiOptions<T> {
|
||||
*
|
||||
* Optionally return a value that can be accessed at `ui.mounted` or in the `onRemove` callback.
|
||||
*/
|
||||
mount: (container: Element) => T;
|
||||
mount: (container: Element) => TApp;
|
||||
/**
|
||||
* Callback called when the UI is removed from the webpage. Use to cleanup your UI, like
|
||||
* unmounting your vue or react apps.
|
||||
*/
|
||||
onRemove?: (mounted: T) => void;
|
||||
onRemove?: (mounted: TApp) => void;
|
||||
/**
|
||||
* Custom CSS text to apply to the UI. If your content script imports/generates CSS and you've
|
||||
* 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/entrypoints/content-scripts.html#ui> for more info.
|
||||
* See https://wxt.dev/entrypoints/content-scripts.html#ui for more info.
|
||||
*/
|
||||
css?: string;
|
||||
}
|
||||
|
||||
export type OverlayContentScriptUiOptions<T> = BaseContentScriptUiOptions<T> & {
|
||||
type: 'overlay';
|
||||
/**
|
||||
* When using `type: "overlay"`, the mounted element is 0px by 0px in size. Alignment specifies
|
||||
* which corner is aligned with that 0x0 pixel space.
|
||||
*
|
||||
* @default "top-left"
|
||||
*/
|
||||
alignment?: ContentScriptUiOverlayAlignment;
|
||||
/**
|
||||
* The `z-index` used on the `shadowHost`. Set to a positive number to show your UI over website
|
||||
* content.
|
||||
*/
|
||||
zIndex?: number;
|
||||
};
|
||||
export type OverlayContentScriptUiOptions<TApp> =
|
||||
BaseContentScriptUiOptions<TApp> & {
|
||||
type: 'overlay';
|
||||
/**
|
||||
* When using `type: "overlay"`, the mounted element is 0px by 0px in size. Alignment specifies
|
||||
* which corner is aligned with that 0x0 pixel space.
|
||||
*
|
||||
* @default "top-left"
|
||||
*/
|
||||
alignment?: ContentScriptUiOverlayAlignment;
|
||||
/**
|
||||
* The `z-index` used on the `shadowHost`. Set to a positive number to show your UI over website
|
||||
* content.
|
||||
*/
|
||||
zIndex?: number;
|
||||
};
|
||||
|
||||
export type ModalContentScriptUiOptions<T> = BaseContentScriptUiOptions<T> & {
|
||||
type: 'modal';
|
||||
/**
|
||||
* The `z-index` used on the `shadowHost`. Set to a positive number to show your UI over website
|
||||
* content.
|
||||
*/
|
||||
zIndex?: number;
|
||||
};
|
||||
export type ModalContentScriptUiOptions<TApp> =
|
||||
BaseContentScriptUiOptions<TApp> & {
|
||||
type: 'modal';
|
||||
/**
|
||||
* The `z-index` used on the `shadowHost`. Set to a positive number to show your UI over website
|
||||
* content.
|
||||
*/
|
||||
zIndex?: number;
|
||||
};
|
||||
|
||||
export type InlineContentScriptUiOptions<T> = BaseContentScriptUiOptions<T> & {
|
||||
type: 'inline';
|
||||
};
|
||||
export type InlineContentScriptUiOptions<TApp> =
|
||||
BaseContentScriptUiOptions<TApp> & {
|
||||
type: 'inline';
|
||||
};
|
||||
|
||||
export type ContentScriptUiOverlayAlignment =
|
||||
| 'top-left'
|
||||
@@ -292,7 +295,7 @@ export type ContentScriptAppendMode =
|
||||
| 'before'
|
||||
| 'after';
|
||||
|
||||
export type ContentScriptUiOptions<T> =
|
||||
| OverlayContentScriptUiOptions<T>
|
||||
| ModalContentScriptUiOptions<T>
|
||||
| InlineContentScriptUiOptions<T>;
|
||||
export type ContentScriptUiOptions<TApp> =
|
||||
| OverlayContentScriptUiOptions<TApp>
|
||||
| ModalContentScriptUiOptions<TApp>
|
||||
| InlineContentScriptUiOptions<TApp>;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { BackgroundScriptDefintition } from '..';
|
||||
import { BackgroundDefinition } from '..';
|
||||
|
||||
export function defineBackground(main: () => void): BackgroundScriptDefintition;
|
||||
export function defineBackground(main: () => void): BackgroundDefinition;
|
||||
export function defineBackground(
|
||||
definition: BackgroundScriptDefintition,
|
||||
): BackgroundScriptDefintition;
|
||||
definition: BackgroundDefinition,
|
||||
): BackgroundDefinition;
|
||||
export function defineBackground(
|
||||
arg: (() => void) | BackgroundScriptDefintition,
|
||||
): BackgroundScriptDefintition {
|
||||
arg: (() => void) | BackgroundDefinition,
|
||||
): BackgroundDefinition {
|
||||
if (typeof arg === 'function') return { main: arg };
|
||||
return arg;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* @module wxt/client
|
||||
*/
|
||||
export * from './defineContentScript';
|
||||
export * from './defineBackground';
|
||||
export * from './createContentScriptUi';
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { defineUnlistedScript } from '../defineUnlistedScript';
|
||||
import { UnlistedScriptDefinition } from '../../../core/types';
|
||||
|
||||
describe('defineUnlistedScript', () => {
|
||||
it('should return the object definition when given an object', () => {
|
||||
const definition: UnlistedScriptDefinition = {
|
||||
include: [''],
|
||||
main: vi.fn(),
|
||||
};
|
||||
|
||||
const actual = defineUnlistedScript(definition);
|
||||
|
||||
expect(actual).toEqual(definition);
|
||||
});
|
||||
|
||||
it('should return the object definition when given a main function', () => {
|
||||
const main = vi.fn();
|
||||
|
||||
const actual = defineUnlistedScript(main);
|
||||
|
||||
expect(actual).toEqual({ main });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import { UnlistedScriptDefinition } from '../../core/types';
|
||||
|
||||
export function defineUnlistedScript(
|
||||
main: () => void,
|
||||
): UnlistedScriptDefinition;
|
||||
export function defineUnlistedScript(
|
||||
definition: UnlistedScriptDefinition,
|
||||
): UnlistedScriptDefinition;
|
||||
export function defineUnlistedScript(
|
||||
arg: (() => void) | UnlistedScriptDefinition,
|
||||
): UnlistedScriptDefinition {
|
||||
if (typeof arg === 'function') return { main: arg };
|
||||
return arg;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* @module wxt/sandbox
|
||||
*/
|
||||
export * from './defineUnlistedScript';
|
||||
@@ -3,23 +3,23 @@ import { browser } from '../browser';
|
||||
import { logger } from './logger';
|
||||
|
||||
/**
|
||||
* Extends [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController).
|
||||
* Implements [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController).
|
||||
* Used to detect and stop content script code when the script is invalidated.
|
||||
*
|
||||
* It also provides several utilities like `ctx.setTimeout` and `ctx.setInterval` that should be used in
|
||||
* content scripts instead of `window.setTimeout` or `window.setInterval`.
|
||||
*/
|
||||
export class ContentScriptContext extends AbortController {
|
||||
static SCRIPT_STARTED_MESSAGE_TYPE = 'wxt:content-script-started';
|
||||
export class ContentScriptContext implements AbortController {
|
||||
private static SCRIPT_STARTED_MESSAGE_TYPE = 'wxt:content-script-started';
|
||||
|
||||
#isTopFrame = window.self === window.top;
|
||||
#abortController: AbortController;
|
||||
|
||||
constructor(
|
||||
private readonly contentScriptName: string,
|
||||
public readonly options?: Omit<ContentScriptDefinition, 'main'>,
|
||||
) {
|
||||
super();
|
||||
|
||||
this.#abortController = new AbortController();
|
||||
if (this.#isTopFrame) {
|
||||
this.#stopOldScripts();
|
||||
}
|
||||
@@ -29,6 +29,14 @@ export class ContentScriptContext extends AbortController {
|
||||
});
|
||||
}
|
||||
|
||||
get signal() {
|
||||
return this.#abortController.signal;
|
||||
}
|
||||
|
||||
abort(reason?: any): void {
|
||||
return this.#abortController.abort(reason);
|
||||
}
|
||||
|
||||
get isInvalid(): boolean {
|
||||
if (browser.runtime.id == null) {
|
||||
this.notifyInvalidated(); // Sets `signal.aborted` to true
|
||||
@@ -150,6 +158,7 @@ export class ContentScriptContext extends AbortController {
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* Abort the abort controller and execute all `onInvalidated` listeners.
|
||||
*/
|
||||
notifyInvalidated() {
|
||||
|
||||
@@ -37,6 +37,6 @@ try {
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('The background script crashed on startup!');
|
||||
logger.error('The background crashed on startup!');
|
||||
throw err;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,9 @@ import { ContentScriptContext } from '../utils/ContentScriptContext';
|
||||
|
||||
await main(ctx);
|
||||
} catch (err) {
|
||||
logger.error('The content script crashed on startup!', err);
|
||||
logger.error(
|
||||
`The content script "${__ENTRYPOINT__}" crashed on startup!`,
|
||||
err,
|
||||
);
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
import { fakeBrowser as browser } from '@webext-core/fake-browser';
|
||||
import { fakeBrowser as browser } from '../../testing/fake-browser';
|
||||
|
||||
export default browser;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import definition from 'virtual:user-unlisted-script';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
await definition.main();
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
`The unlisted script "${__ENTRYPOINT__}" crashed on startup!`,
|
||||
err,
|
||||
);
|
||||
}
|
||||
})();
|
||||
+6
-1
@@ -6,7 +6,7 @@ declare module '*?raw' {
|
||||
}
|
||||
|
||||
declare module 'virtual:user-background' {
|
||||
const definition: import('../../').BackgroundScriptDefintition;
|
||||
const definition: import('../../').BackgroundDefinition;
|
||||
export default definition;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,11 @@ declare module 'virtual:user-content-script' {
|
||||
export default definition;
|
||||
}
|
||||
|
||||
declare module 'virtual:user-unlisted-script' {
|
||||
const definition: import('../../').UnlistedScriptDefinition;
|
||||
export default definition;
|
||||
}
|
||||
|
||||
// Globals defined by the vite-plugins/devServerGlobals.ts and utils/globals.ts
|
||||
declare const __COMMAND__: 'build' | 'serve';
|
||||
declare const __DEV_SERVER_PROTOCOL__: string;
|
||||
|
||||
@@ -11,7 +11,7 @@ import { findEntrypoints } from '../findEntrypoints';
|
||||
import fs from 'fs-extra';
|
||||
import { importEntrypointFile } from '../../utils/importEntrypointFile';
|
||||
import glob from 'fast-glob';
|
||||
import { fakeInternalConfig } from '../../../testing/fake-objects';
|
||||
import { fakeInternalConfig } from '../../../testing-utils/fake-objects';
|
||||
import { unnormalizePath } from '../../utils/paths';
|
||||
|
||||
vi.mock('../../utils/importEntrypointFile');
|
||||
@@ -261,6 +261,43 @@ describe('findEntrypoints', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it.each<[string, Omit<GenericEntrypoint, 'options'>]>([
|
||||
[
|
||||
'injected.ts',
|
||||
{
|
||||
type: 'unlisted-script',
|
||||
name: 'injected',
|
||||
inputPath: resolve(config.entrypointsDir, 'injected.ts'),
|
||||
outputDir: config.outDir,
|
||||
},
|
||||
],
|
||||
[
|
||||
'injected/index.ts',
|
||||
{
|
||||
type: 'unlisted-script',
|
||||
name: 'injected',
|
||||
inputPath: resolve(config.entrypointsDir, 'injected/index.ts'),
|
||||
outputDir: config.outDir,
|
||||
},
|
||||
],
|
||||
])(
|
||||
'should find and load unlisted-script entrypoint config from %s',
|
||||
async (path, expected) => {
|
||||
const options: GenericEntrypoint['options'] = {};
|
||||
globMock.mockResolvedValueOnce([path]);
|
||||
importEntrypointFileMock.mockResolvedValue(options);
|
||||
|
||||
const entrypoints = await findEntrypoints(config);
|
||||
|
||||
expect(entrypoints).toHaveLength(1);
|
||||
expect(entrypoints[0]).toEqual({ ...expected, options });
|
||||
expect(importEntrypointFileMock).toBeCalledWith(
|
||||
expected.inputPath,
|
||||
config,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it.each<[string, GenericEntrypoint]>([
|
||||
// Sandbox
|
||||
[
|
||||
@@ -456,28 +493,6 @@ describe('findEntrypoints', () => {
|
||||
},
|
||||
],
|
||||
|
||||
// unlisted-script
|
||||
[
|
||||
'injected.ts',
|
||||
{
|
||||
type: 'unlisted-script',
|
||||
name: 'injected',
|
||||
inputPath: resolve(config.entrypointsDir, 'injected.ts'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
[
|
||||
'injected/index.ts',
|
||||
{
|
||||
type: 'unlisted-script',
|
||||
name: 'injected',
|
||||
inputPath: resolve(config.entrypointsDir, 'injected/index.ts'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
|
||||
// unlisted-style
|
||||
[
|
||||
'iframe.scss',
|
||||
|
||||
@@ -53,7 +53,11 @@ async function buildSingleEntrypoint(
|
||||
config: InternalConfig,
|
||||
): Promise<BuildStepOutput> {
|
||||
// Should this entrypoint be wrapped by the vite-plugins/virtualEntrypoint plugin?
|
||||
const isVirtual = ['background', 'content-script'].includes(entrypoint.type);
|
||||
const isVirtual = [
|
||||
'background',
|
||||
'content-script',
|
||||
'unlisted-script',
|
||||
].includes(entrypoint.type);
|
||||
const entry = isVirtual
|
||||
? `virtual:wxt-${entrypoint.type}?${entrypoint.inputPath}`
|
||||
: entrypoint.inputPath;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { relative, resolve } from 'path';
|
||||
import {
|
||||
BackgroundEntrypoint,
|
||||
BackgroundScriptDefintition,
|
||||
BackgroundDefinition,
|
||||
BaseEntrypointOptions,
|
||||
ContentScriptDefinition,
|
||||
ContentScriptEntrypoint,
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
InternalConfig,
|
||||
OptionsEntrypoint,
|
||||
PopupEntrypoint,
|
||||
UnlistedScriptDefinition,
|
||||
} from '../types';
|
||||
import fs from 'fs-extra';
|
||||
import { minimatch } from 'minimatch';
|
||||
@@ -37,11 +38,10 @@ export async function findEntrypoints(
|
||||
relativePaths.sort();
|
||||
|
||||
const pathGlobs = Object.keys(PATH_GLOB_TO_TYPE_MAP);
|
||||
const existingNames: Record<string, Entrypoint | undefined> = {};
|
||||
|
||||
const entrypoints: Entrypoint[] = [];
|
||||
let hasBackground = false;
|
||||
await Promise.all(
|
||||
// TODO: This parallelization is bad
|
||||
const possibleEntrypoints: Array<Entrypoint | undefined> = await Promise.all(
|
||||
relativePaths.map(async (relativePath) => {
|
||||
const path = resolve(config.entrypointsDir, relativePath);
|
||||
const matchingGlob = pathGlobs.find((glob) =>
|
||||
@@ -49,42 +49,35 @@ export async function findEntrypoints(
|
||||
);
|
||||
|
||||
if (matchingGlob == null) {
|
||||
return config.logger.warn(
|
||||
config.logger.warn(
|
||||
`${relativePath} does not match any known entrypoint. Known entrypoints:\n${JSON.stringify(
|
||||
PATH_GLOB_TO_TYPE_MAP,
|
||||
null,
|
||||
2,
|
||||
)}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const type = PATH_GLOB_TO_TYPE_MAP[matchingGlob];
|
||||
if (type === 'ignored') return;
|
||||
|
||||
let entrypoint: Entrypoint;
|
||||
switch (type) {
|
||||
case 'popup':
|
||||
entrypoint = await getPopupEntrypoint(config, path);
|
||||
break;
|
||||
return await getPopupEntrypoint(config, path);
|
||||
case 'options':
|
||||
entrypoint = await getOptionsEntrypoint(config, path);
|
||||
break;
|
||||
return await getOptionsEntrypoint(config, path);
|
||||
case 'background':
|
||||
entrypoint = await getBackgroundEntrypoint(config, path);
|
||||
hasBackground = true;
|
||||
break;
|
||||
return await getBackgroundEntrypoint(config, path);
|
||||
case 'content-script':
|
||||
entrypoint = await getContentScriptEntrypoint(
|
||||
config,
|
||||
getEntrypointName(config.entrypointsDir, path),
|
||||
path,
|
||||
);
|
||||
break;
|
||||
return await getContentScriptEntrypoint(config, path);
|
||||
case 'unlisted-page':
|
||||
entrypoint = await getUnlistedPageEntrypoint(config, path);
|
||||
break;
|
||||
return await getUnlistedPageEntrypoint(config, path);
|
||||
case 'unlisted-script':
|
||||
return await getUnlistedScriptEntrypoint(config, path);
|
||||
case 'content-script-style':
|
||||
entrypoint = {
|
||||
return {
|
||||
type,
|
||||
name: getEntrypointName(config.entrypointsDir, path),
|
||||
inputPath: path,
|
||||
@@ -94,9 +87,8 @@ export async function findEntrypoints(
|
||||
exclude: undefined,
|
||||
},
|
||||
};
|
||||
break;
|
||||
default:
|
||||
entrypoint = {
|
||||
return {
|
||||
type,
|
||||
name: getEntrypointName(config.entrypointsDir, path),
|
||||
inputPath: path,
|
||||
@@ -107,22 +99,30 @@ export async function findEntrypoints(
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const withSameName = existingNames[entrypoint.name];
|
||||
if (withSameName) {
|
||||
throw Error(
|
||||
`Multiple entrypoints with the name "${
|
||||
entrypoint.name
|
||||
}" detected, but only one is allowed: ${[
|
||||
relative(config.root, withSameName.inputPath),
|
||||
relative(config.root, entrypoint.inputPath),
|
||||
].join(', ')}`,
|
||||
);
|
||||
}
|
||||
entrypoints.push(entrypoint);
|
||||
existingNames[entrypoint.name] = entrypoint;
|
||||
}),
|
||||
);
|
||||
|
||||
const entrypoints = possibleEntrypoints.filter(
|
||||
(entry) => !!entry,
|
||||
) as Entrypoint[];
|
||||
|
||||
// Report duplicate entrypoint names
|
||||
const existingNames: Record<string, Entrypoint | undefined> = {};
|
||||
entrypoints.forEach((entrypoint) => {
|
||||
const withSameName = existingNames[entrypoint.name];
|
||||
if (withSameName) {
|
||||
throw Error(
|
||||
`Multiple entrypoints with the name "${
|
||||
entrypoint.name
|
||||
}" detected, but only one is allowed: ${[
|
||||
relative(config.root, withSameName.inputPath),
|
||||
relative(config.root, entrypoint.inputPath),
|
||||
].join(', ')}`,
|
||||
);
|
||||
}
|
||||
existingNames[entrypoint.name] = entrypoint;
|
||||
});
|
||||
|
||||
if (config.command === 'serve' && !hasBackground) {
|
||||
entrypoints.push(
|
||||
await getBackgroundEntrypoint(config, VIRTUAL_NOOP_BACKGROUND_MODULE_ID),
|
||||
@@ -288,6 +288,35 @@ async function getUnlistedPageEntrypoint(
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path Absolute path to the script's file.
|
||||
* @param content String contents of the file at the path.
|
||||
*/
|
||||
async function getUnlistedScriptEntrypoint(
|
||||
config: InternalConfig,
|
||||
path: string,
|
||||
): Promise<GenericEntrypoint> {
|
||||
const name = getEntrypointName(config.entrypointsDir, path);
|
||||
const defaultExport = await importEntrypointFile<UnlistedScriptDefinition>(
|
||||
path,
|
||||
config,
|
||||
);
|
||||
if (defaultExport == null) {
|
||||
throw Error(
|
||||
`${name}: Default export not found, did you forget to call "export default defineUnlistedScript(...)"?`,
|
||||
);
|
||||
}
|
||||
const { main: _, ...moduleOptions } = defaultExport;
|
||||
const options: Omit<UnlistedScriptDefinition, 'main'> = moduleOptions;
|
||||
return {
|
||||
type: 'unlisted-script',
|
||||
name,
|
||||
inputPath: path,
|
||||
outputDir: config.outDir,
|
||||
options,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path Absolute path to the background's TS file.
|
||||
*/
|
||||
@@ -295,19 +324,24 @@ async function getBackgroundEntrypoint(
|
||||
config: InternalConfig,
|
||||
path: string,
|
||||
): Promise<BackgroundEntrypoint> {
|
||||
let options: Omit<BackgroundScriptDefintition, 'main'> = {};
|
||||
const name = 'background';
|
||||
let options: Omit<BackgroundDefinition, 'main'> = {};
|
||||
if (path !== VIRTUAL_NOOP_BACKGROUND_MODULE_ID) {
|
||||
const defaultExport =
|
||||
await importEntrypointFile<BackgroundScriptDefintition>(path, config);
|
||||
const defaultExport = await importEntrypointFile<BackgroundDefinition>(
|
||||
path,
|
||||
config,
|
||||
);
|
||||
if (defaultExport == null) {
|
||||
throw Error('Background script does not have a default export');
|
||||
throw Error(
|
||||
`${name}: Default export not found, did you forget to call "export default defineBackground(...)"?`,
|
||||
);
|
||||
}
|
||||
const { main: _, ...moduleOptions } = defaultExport;
|
||||
options = moduleOptions;
|
||||
}
|
||||
return {
|
||||
type: 'background',
|
||||
name: 'background',
|
||||
name,
|
||||
inputPath: path,
|
||||
outputDir: config.outDir,
|
||||
options: {
|
||||
@@ -323,17 +357,19 @@ async function getBackgroundEntrypoint(
|
||||
*/
|
||||
async function getContentScriptEntrypoint(
|
||||
config: InternalConfig,
|
||||
name: string,
|
||||
path: string,
|
||||
): Promise<ContentScriptEntrypoint> {
|
||||
const name = getEntrypointName(config.entrypointsDir, path);
|
||||
const { main: _, ...options } =
|
||||
await importEntrypointFile<ContentScriptDefinition>(path, config);
|
||||
if (options == null) {
|
||||
throw Error(`Content script ${name} does not have a default export`);
|
||||
throw Error(
|
||||
`${name}: Default export not found, did you forget to call "export default defineContentScript(...)"?`,
|
||||
);
|
||||
}
|
||||
return {
|
||||
type: 'content-script',
|
||||
name: getEntrypointName(config.entrypointsDir, path),
|
||||
name,
|
||||
inputPath: path,
|
||||
outputDir: resolve(config.outDir, CONTENT_SCRIPT_OUT_DIR),
|
||||
options,
|
||||
|
||||
@@ -8,6 +8,9 @@ import pc from 'picocolors';
|
||||
* Remove generated/temp files from the directory.
|
||||
*
|
||||
* @param root The directory to look for generated/temp files in. Defaults to `process.cwd()`. Can be relative to `process.cwd()` or absolute.
|
||||
*
|
||||
* @example
|
||||
* await clean();
|
||||
*/
|
||||
export async function clean(root = process.cwd()) {
|
||||
consola.info('Cleaning Project');
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { createExtensionRunner } from '..';
|
||||
import { fakeInternalConfig } from '../../../testing/fake-objects';
|
||||
import { fakeInternalConfig } from '../../../testing-utils/fake-objects';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { createSafariRunner } from '../safari';
|
||||
import { ExtensionRunner } from '../extension-runner';
|
||||
|
||||
+18
-11
@@ -110,12 +110,12 @@ export interface InlineConfig {
|
||||
*
|
||||
* Available template variables:
|
||||
*
|
||||
* - `{name}` - The project's name converted to kebab-case
|
||||
* - `{version}` - The version_name or version from the manifest
|
||||
* - `{browser}` - The target browser from the `--browser` CLI flag
|
||||
* - `{manifestVersion}` - Either "2" or "3"
|
||||
* - `{{name}}` - The project's name converted to kebab-case
|
||||
* - `{{version}}` - The version_name or version from the manifest
|
||||
* - `{{browser}}` - The target browser from the `--browser` CLI flag
|
||||
* - `{{manifestVersion}}` - Either "2" or "3"
|
||||
*
|
||||
* @default "{name}-{version}-{browser}.zip"
|
||||
* @default "{{name}}-{{version}}-{{browser}}.zip"
|
||||
*/
|
||||
artifactTemplate?: string;
|
||||
/**
|
||||
@@ -123,12 +123,12 @@ export interface InlineConfig {
|
||||
*
|
||||
* Available template variables:
|
||||
*
|
||||
* - `{name}` - The project's name converted to kebab-case
|
||||
* - `{version}` - The version_name or version from the manifest
|
||||
* - `{browser}` - The target browser from the `--browser` CLI flag
|
||||
* - `{manifestVersion}` - Either "2" or "3"
|
||||
* - `{{name}}` - The project's name converted to kebab-case
|
||||
* - `{{version}}` - The version_name or version from the manifest
|
||||
* - `{{browser}}` - The target browser from the `--browser` CLI flag
|
||||
* - `{{manifestVersion}}` - Either "2" or "3"
|
||||
*
|
||||
* @default "{name}-{version}-sources.zip"
|
||||
* @default "{{name}}-{{version}}-sources.zip"
|
||||
*/
|
||||
sourcesTemplate?: string;
|
||||
/**
|
||||
@@ -429,12 +429,19 @@ export interface ContentScriptDefinition extends ExcludableEntrypoint {
|
||||
main(ctx: ContentScriptContext): void | Promise<void>;
|
||||
}
|
||||
|
||||
export interface BackgroundScriptDefintition extends ExcludableEntrypoint {
|
||||
export interface BackgroundDefinition extends ExcludableEntrypoint {
|
||||
type?: PerBrowserOption<'module'>;
|
||||
persistent?: PerBrowserOption<boolean>;
|
||||
main(): void;
|
||||
}
|
||||
|
||||
export interface UnlistedScriptDefinition extends ExcludableEntrypoint {
|
||||
/**
|
||||
* Main function executed when the unlisted script is ran.
|
||||
*/
|
||||
main(): void | Promise<void>;
|
||||
}
|
||||
|
||||
export type PerBrowserOption<T> = T | { [browser: TargetBrowser]: T };
|
||||
|
||||
export interface ExcludableEntrypoint {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { hashContentScriptOptions } from '../content-scripts';
|
||||
import { fakeInternalConfig } from '../../../testing/fake-objects';
|
||||
import { fakeInternalConfig } from '../../../testing-utils/fake-objects';
|
||||
|
||||
describe('Content Script Utils', () => {
|
||||
describe('hashContentScriptOptions', () => {
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
fakePopupEntrypoint,
|
||||
fakeRollupOutputAsset,
|
||||
fakeRollupOutputChunk,
|
||||
} from '../../../testing/fake-objects';
|
||||
} from '../../../testing-utils/fake-objects';
|
||||
import { BuildOutput, BuildStepOutput } from '../../types';
|
||||
|
||||
describe('Detect Dev Changes', () => {
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { importEntrypointFile } from '../importEntrypointFile';
|
||||
import { fakeInternalConfig } from '../../../testing-utils/fake-objects';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
const entrypointPath = (filename: string) =>
|
||||
resolve('src/core/utils/__tests__/test-entrypoints', filename);
|
||||
|
||||
const config = fakeInternalConfig({
|
||||
imports: false,
|
||||
debug: false,
|
||||
// Run inside the demo folder so that wxt is in the node_modules
|
||||
// WXT must also be built for these tests to pass
|
||||
root: 'demo',
|
||||
});
|
||||
|
||||
describe('importEntrypointFile', () => {
|
||||
it.each([
|
||||
['background.ts', { main: expect.any(Function) }],
|
||||
['content.ts', { main: expect.any(Function), matches: ['<all_urls>'] }],
|
||||
['unlisted.ts', { main: expect.any(Function) }],
|
||||
['react.tsx', { main: expect.any(Function) }],
|
||||
['with-named.ts', { main: expect.any(Function) }],
|
||||
])(
|
||||
'should return the default export of test-entrypoints/%s',
|
||||
async (file, expected) => {
|
||||
const actual = await importEntrypointFile(entrypointPath(file), config);
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
},
|
||||
);
|
||||
|
||||
it('should return undefined when there is no default export', async () => {
|
||||
const actual = await importEntrypointFile(
|
||||
entrypointPath('no-default-export.ts'),
|
||||
config,
|
||||
);
|
||||
|
||||
expect(actual).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
import { defineBackground } from '../../../../client';
|
||||
|
||||
export default defineBackground({
|
||||
main() {},
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
import { defineContentScript } from '../../../../client';
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ['<all_urls>'],
|
||||
main() {},
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
import { defineUnlistedScript } from '../../../../client/sandbox';
|
||||
|
||||
export default defineUnlistedScript(() => {});
|
||||
@@ -0,0 +1,3 @@
|
||||
import { defineUnlistedScript } from '../../../../client/sandbox';
|
||||
|
||||
export default defineUnlistedScript(() => {});
|
||||
@@ -0,0 +1,5 @@
|
||||
import { defineBackground } from '../../../../client';
|
||||
|
||||
export const a = {};
|
||||
|
||||
export default defineBackground(() => {});
|
||||
@@ -9,8 +9,15 @@ export function getUnimportOptions(
|
||||
|
||||
const defaultOptions: Partial<UnimportOptions> = {
|
||||
debugLog: config.logger.debug,
|
||||
imports: [{ name: 'defineConfig', from: 'wxt' }],
|
||||
presets: [{ package: 'wxt/client' }, { package: 'wxt/browser' }],
|
||||
imports: [
|
||||
{ name: 'defineConfig', from: 'wxt' },
|
||||
{ name: 'fakeBrowser', from: 'wxt/testing' },
|
||||
],
|
||||
presets: [
|
||||
{ package: 'wxt/client' },
|
||||
{ package: 'wxt/browser' },
|
||||
{ package: 'wxt/sandbox' },
|
||||
],
|
||||
warn: config.logger.warn,
|
||||
dirs: ['components', 'composables', 'hooks', 'utils'],
|
||||
};
|
||||
|
||||
@@ -14,7 +14,6 @@ import * as vite from 'vite';
|
||||
import { createFsCache } from './createFsCache';
|
||||
import consola, { LogLevels } from 'consola';
|
||||
import * as plugins from '../vite-plugins';
|
||||
import { getGlobals } from './globals';
|
||||
|
||||
/**
|
||||
* Given an inline config, discover the config file if necessary, merge the results, resolve any
|
||||
@@ -237,10 +236,13 @@ async function resolveInternalViteConfig(
|
||||
internalVite.plugins.push(plugins.devHtmlPrerender(finalConfig));
|
||||
internalVite.plugins.push(plugins.unimport(finalConfig));
|
||||
internalVite.plugins.push(
|
||||
plugins.virtualEntrypoin('background', finalConfig),
|
||||
plugins.virtualEntrypoint('background', finalConfig),
|
||||
);
|
||||
internalVite.plugins.push(
|
||||
plugins.virtualEntrypoin('content-script', finalConfig),
|
||||
plugins.virtualEntrypoint('content-script', finalConfig),
|
||||
);
|
||||
internalVite.plugins.push(
|
||||
plugins.virtualEntrypoint('unlisted-script', finalConfig),
|
||||
);
|
||||
internalVite.plugins.push(plugins.devServerGlobals(finalConfig));
|
||||
internalVite.plugins.push(plugins.tsconfigPaths(finalConfig));
|
||||
@@ -248,10 +250,7 @@ async function resolveInternalViteConfig(
|
||||
if (finalConfig.analysis.enabled) {
|
||||
internalVite.plugins.push(plugins.bundleAnalysis());
|
||||
}
|
||||
internalVite.plugins.push(plugins.globals(finalConfig));
|
||||
|
||||
internalVite.define ??= {};
|
||||
for (const global of getGlobals(finalConfig)) {
|
||||
internalVite.define[global.name] = JSON.stringify(global.value);
|
||||
}
|
||||
return internalVite;
|
||||
}
|
||||
|
||||
@@ -5,42 +5,42 @@ export function getGlobals(
|
||||
): Array<{ name: string; value: any; type: string }> {
|
||||
return [
|
||||
{
|
||||
name: '__MANIFEST_VERSION__',
|
||||
name: surroundInUnderscore('MANIFEST_VERSION'),
|
||||
value: config.manifestVersion,
|
||||
type: `2 | 3`,
|
||||
},
|
||||
{
|
||||
name: '__BROWSER__',
|
||||
name: surroundInUnderscore('BROWSER'),
|
||||
value: config.browser,
|
||||
type: `string`,
|
||||
},
|
||||
{
|
||||
name: '__IS_CHROME__',
|
||||
name: surroundInUnderscore('IS_CHROME'),
|
||||
value: config.browser === 'chrome',
|
||||
type: `boolean`,
|
||||
},
|
||||
{
|
||||
name: '__IS_FIREFOX__',
|
||||
name: surroundInUnderscore('IS_FIREFOX'),
|
||||
value: config.browser === 'firefox',
|
||||
type: `boolean`,
|
||||
},
|
||||
{
|
||||
name: '__IS_SAFARI__',
|
||||
name: surroundInUnderscore('IS_SAFARI'),
|
||||
value: config.browser === 'safari',
|
||||
type: `boolean`,
|
||||
},
|
||||
{
|
||||
name: '__IS_EDGE__',
|
||||
name: surroundInUnderscore('IS_EDGE'),
|
||||
value: config.browser === 'edge',
|
||||
type: `boolean`,
|
||||
},
|
||||
{
|
||||
name: '__IS_OPERA__',
|
||||
name: surroundInUnderscore('IS_OPERA'),
|
||||
value: config.browser === 'opera',
|
||||
type: `boolean`,
|
||||
},
|
||||
{
|
||||
name: '__COMMAND__',
|
||||
name: surroundInUnderscore('COMMAND'),
|
||||
value: config.command,
|
||||
type: `"build" | "serve"`,
|
||||
},
|
||||
@@ -53,9 +53,17 @@ export function getEntrypointGlobals(
|
||||
) {
|
||||
return [
|
||||
{
|
||||
name: '__ENTRYPOINT__',
|
||||
name: surroundInUnderscore('ENTRYPOINT'),
|
||||
value: entrypointName,
|
||||
type: `string`,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Don't hardcode the complete name so that the string litterals in this file aren't replaced during
|
||||
* tests (which causes syntax errors), only during builds.
|
||||
*/
|
||||
function surroundInUnderscore(name: string): string {
|
||||
return `__${name}__`;
|
||||
}
|
||||
|
||||
@@ -49,7 +49,6 @@ export async function importEntrypointFile<T>(
|
||||
cache: false,
|
||||
debug: config.debug,
|
||||
esmResolve: true,
|
||||
interopDefault: true,
|
||||
alias: {
|
||||
'webextension-polyfill': resolve(
|
||||
config.root,
|
||||
@@ -69,7 +68,8 @@ export async function importEntrypointFile<T>(
|
||||
});
|
||||
|
||||
try {
|
||||
return await jiti(path);
|
||||
const res = await jiti(path);
|
||||
return res.default;
|
||||
} catch (err) {
|
||||
config.logger.error(err);
|
||||
throw err;
|
||||
|
||||
@@ -217,6 +217,7 @@ function addEntrypoints(
|
||||
const options: Manifest.ActionManifest = {
|
||||
default_icon: popup.options.defaultIcon,
|
||||
default_title: popup.options.defaultTitle,
|
||||
browser_style: popup.options.browserStyle,
|
||||
};
|
||||
if (manifest.manifest_version === 3) {
|
||||
manifest.action = {
|
||||
@@ -323,14 +324,10 @@ function addEntrypoints(
|
||||
const newContentScripts = Array.from(hashToEntrypointsMap.entries()).map(
|
||||
([, scripts]) => ({
|
||||
...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(),
|
||||
js: scripts
|
||||
.map((entry) =>
|
||||
getEntrypointBundlePath(entry, config.outDir, '.js'),
|
||||
)
|
||||
.sort(),
|
||||
css: getContentScriptCssFiles(scripts, cssMap),
|
||||
js: scripts.map((entry) =>
|
||||
getEntrypointBundlePath(entry, config.outDir, '.js'),
|
||||
),
|
||||
}),
|
||||
);
|
||||
if (newContentScripts.length >= 0) {
|
||||
|
||||
@@ -23,6 +23,7 @@ export function removeProjectImportStatements(text: string): string {
|
||||
const noImports = removeImportStatements(text);
|
||||
|
||||
return `import { defineContentScript, defineBackground } from 'wxt/client';
|
||||
import { defineUnlistedScript } from 'wxt/sandbox';
|
||||
|
||||
${noImports}`;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import * as vite from 'vite';
|
||||
import { InternalConfig } from '../types';
|
||||
import { getGlobals } from '../utils/globals';
|
||||
|
||||
export function globals(config: InternalConfig): vite.PluginOption {
|
||||
return {
|
||||
name: 'wxt:globals',
|
||||
config() {
|
||||
const define: vite.InlineConfig['define'] = {};
|
||||
for (const global of getGlobals(config)) {
|
||||
define[global.name] = JSON.stringify(global.value);
|
||||
}
|
||||
return {
|
||||
define,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -8,3 +8,5 @@ export * from './tsconfigPaths';
|
||||
export * from './noopBackground';
|
||||
export * from './cssEntrypoints';
|
||||
export * from './bundleAnalysis';
|
||||
export * from './globals';
|
||||
export * from './webextensionPolyfillAlias';
|
||||
|
||||
@@ -7,7 +7,7 @@ import { normalizePath } from '../utils/paths';
|
||||
/**
|
||||
* Wraps a user's entrypoint with a vitual version with additional logic.
|
||||
*/
|
||||
export function virtualEntrypoin(
|
||||
export function virtualEntrypoint(
|
||||
type: Entrypoint['type'],
|
||||
config: InternalConfig,
|
||||
): Plugin {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import path from 'node:path';
|
||||
import * as vite from 'vite';
|
||||
import { InternalConfig } from '../types';
|
||||
|
||||
/**
|
||||
* Creates an alias to redirect "webextension-polyfill" imports to WXT's `fakeBrowser`.
|
||||
*
|
||||
* This should only be used during tests.
|
||||
*/
|
||||
export function webextensionPolyfillAlias(
|
||||
config: InternalConfig,
|
||||
): vite.PluginOption {
|
||||
return {
|
||||
name: 'wxt:webextension-polyfill-test-alias',
|
||||
config() {
|
||||
return {
|
||||
resolve: {
|
||||
alias: {
|
||||
'webextension-polyfill': path.resolve(
|
||||
config.root,
|
||||
'node_modules/wxt/dist/virtual-modules/fake-browser',
|
||||
),
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
+6
-1
@@ -12,7 +12,12 @@ import { printFileList } from './log/printFileList';
|
||||
* Zip the extension for distribution. Does not build, just zips the output that should exist for
|
||||
* the given config.
|
||||
*
|
||||
* Returns a list of ZIP files that were created.
|
||||
* @returns A list of ZIP files that were created.
|
||||
*
|
||||
* @example
|
||||
* const config: InlineConfig = {};
|
||||
* const result = await build(config);
|
||||
* await zipExtension(config, result);
|
||||
*/
|
||||
export async function zipExtension(
|
||||
config: InternalConfig,
|
||||
|
||||
+15
-2
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* @module wxt
|
||||
*/
|
||||
import { BuildOutput, WxtDevServer, InlineConfig } from './core/types';
|
||||
import { getInternalConfig } from './core/utils/getInternalConfig';
|
||||
import pc from 'picocolors';
|
||||
@@ -22,6 +25,11 @@ export * from './core/utils/defineRunnerConfig';
|
||||
|
||||
/**
|
||||
* Bundles the extension for production. Returns a promise of the build result.
|
||||
*
|
||||
* @example
|
||||
* const res = await build({
|
||||
* // Enter config...
|
||||
* })
|
||||
*/
|
||||
export async function build(config: InlineConfig): Promise<BuildOutput> {
|
||||
const internalConfig = await getInternalConfig(config, 'build');
|
||||
@@ -29,8 +37,13 @@ export async function build(config: InlineConfig): Promise<BuildOutput> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a dev server, pre-builds all the files that need to exist to load the extension, and open
|
||||
* the browser with the extension installed.
|
||||
* Creates a dev server and pre-builds all the files that need to exist before loading the extension.
|
||||
*
|
||||
* @example
|
||||
* const server = await wxt.createServer({
|
||||
* // Enter config...
|
||||
* });
|
||||
* await server.start();
|
||||
*/
|
||||
export async function createServer(
|
||||
config?: InlineConfig,
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { fakeBrowser, type FakeBrowser } from '@webext-core/fake-browser';
|
||||
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* @module wxt/testing
|
||||
*/
|
||||
export * from './fake-browser';
|
||||
export * from './wxt-vitest-plugin';
|
||||
@@ -0,0 +1,35 @@
|
||||
import * as vite from 'vite';
|
||||
import {
|
||||
unimport,
|
||||
download,
|
||||
tsconfigPaths,
|
||||
globals,
|
||||
webextensionPolyfillAlias,
|
||||
} from '../core/vite-plugins';
|
||||
import { getInternalConfig } from '../core/utils/getInternalConfig';
|
||||
import { InlineConfig } from '../core/types';
|
||||
|
||||
/**
|
||||
* Vite plugin that configures Vitest with everything required to test a WXT extension, based on the `<root>/wxt.config.ts`
|
||||
*
|
||||
* ```ts
|
||||
* // vitest.config.ts
|
||||
* import { defineConfig } from 'vitest/config';
|
||||
* import { AutoImport } from 'wxt/testing';
|
||||
*
|
||||
* export default defineConfig({
|
||||
* plugins: [AutoImport()],
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @param inlineConfig Customize WXT's config for testing. Any config specified here overrides the config from your `wxt.config.ts` file.
|
||||
*/
|
||||
export function WxtVitest(inlineConfig?: InlineConfig): vite.PluginOption {
|
||||
return getInternalConfig(inlineConfig ?? {}, 'serve').then((config) => [
|
||||
webextensionPolyfillAlias(config),
|
||||
unimport(config),
|
||||
globals(config),
|
||||
download(config),
|
||||
tsconfigPaths(config),
|
||||
]);
|
||||
}
|
||||
@@ -23,6 +23,6 @@
|
||||
"@types/react-dom": "^18.2.6",
|
||||
"@vitejs/plugin-react": "^4.0.3",
|
||||
"typescript": "^5.1.6",
|
||||
"wxt": "^0.7.0"
|
||||
"wxt": "^0.8.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,6 @@
|
||||
"devDependencies": {
|
||||
"typescript": "^5.1.6",
|
||||
"vite-plugin-solid": "^2.7.0",
|
||||
"wxt": "^0.7.0"
|
||||
"wxt": "^0.8.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,6 @@
|
||||
"svelte-check": "^3.4.6",
|
||||
"tslib": "^2.6.0",
|
||||
"typescript": "^5.1.6",
|
||||
"wxt": "^0.7.0"
|
||||
"wxt": "^0.8.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.1.6",
|
||||
"wxt": "^0.7.0"
|
||||
"wxt": "^0.8.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,6 @@
|
||||
"@vitejs/plugin-vue": "^4.2.3",
|
||||
"typescript": "^5.1.6",
|
||||
"vue-tsc": "^1.8.4",
|
||||
"wxt": "^0.7.0"
|
||||
"wxt": "^0.8.0"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user