Compare commits

..

16 Commits

Author SHA1 Message Date
GitHub Actions 64f42064ae chore(release): v0.14.5 2024-01-19 14:36:07 +00:00
Aaron a525d786f1 feat: Add dev.reloadCommand config (#362) 2024-01-19 08:33:03 -06:00
Aaron 948ee810da fix: Disable reload dev command when 4 commands are already registered (#361) 2024-01-19 07:47:04 -06:00
GitHub Actions 8687b2980e chore(release): v0.14.4 2024-01-18 20:54:29 +00:00
Aaron 6f0814bc97 fix: Allow requiring built-in node modules from ESM CLI (#356) 2024-01-18 14:51:35 -06:00
Aaron 148c0da51f chore: Add unit tests for passing flags via the CLI (#354) 2024-01-18 14:49:35 -06:00
GitHub Actions ba09f3e0f1 chore(release): v0.14.3 2024-01-17 05:20:19 +00:00
Yuns 54e24652da fix: Make getArrayFromFlags result can be undefined (#352) 2024-01-16 23:15:18 -06:00
GitHub Actions d6516142d2 chore(release): v0.14.2 2024-01-16 19:57:03 +00:00
Yuns 460667e3c0 feat: Add filterEntrypoints option to speed up development (#344)
Co-authored-by: Aaron Klinker <aaronklinker1@gmail.com>
2024-01-16 13:52:58 -06:00
Armin 6ed61d0766 docs: Fix typos and outdated ui function usage (#347) 2024-01-16 13:01:12 -06:00
Aaron 971daf2784 fix: Allow browser.runtime.getURL to include hashes and query params for HTML paths (#350) 2024-01-16 12:59:38 -06:00
Aaron 89d8ef9a50 fix: Improve error message and document use of imported variables outside an entrypoint's main function (#346) 2024-01-14 09:36:22 -06:00
Aaron Klinker a329e24818 chore: Fix typo in function name 2024-01-12 13:03:43 -06:00
Aaron 249cf63da8 perf: Only call findEntrypoint once per build (#342) 2024-01-12 13:02:44 -06:00
Aaron Klinker 70a496108a chore: Update templates to ^0.14.0 2024-01-12 08:53:32 -06:00
32 changed files with 1004 additions and 93 deletions
+67
View File
@@ -1,5 +1,72 @@
# Changelog
## v0.14.5
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.14.4...v0.14.5)
### 🚀 Enhancements
- Add `dev.reloadCommand` config ([#362](https://github.com/wxt-dev/wxt/pull/362))
### 🩹 Fixes
- Disable reload dev command when 4 commands are already registered ([#361](https://github.com/wxt-dev/wxt/pull/361))
## v0.14.4
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.14.3...v0.14.4)
### 🩹 Fixes
- Allow requiring built-in node modules from ESM CLI ([#356](https://github.com/wxt-dev/wxt/pull/356))
### 🏡 Chore
- Add unit tests for passing flags via the CLI ([#354](https://github.com/wxt-dev/wxt/pull/354))
## v0.14.3
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.14.2...v0.14.3)
### 🩹 Fixes
- Make `getArrayFromFlags` result can be undefined ([#352](https://github.com/wxt-dev/wxt/pull/352))
### ❤️ Contributors
- Yuns ([@yunsii](http://github.com/yunsii))
## v0.14.2
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.14.1...v0.14.2)
### 🚀 Enhancements
- Add `filterEntrypoints` option to speed up development ([#344](https://github.com/wxt-dev/wxt/pull/344))
### 🔥 Performance
- Only call `findEntrypoint` once per build ([#342](https://github.com/wxt-dev/wxt/pull/342))
### 🩹 Fixes
- Improve error message and document use of imported variables outside an entrypoint's `main` function ([#346](https://github.com/wxt-dev/wxt/pull/346))
- Allow `browser.runtime.getURL` to include hashes and query params for HTML paths ([#350](https://github.com/wxt-dev/wxt/pull/350))
### 📖 Documentation
- Fix typos and outdated ui function usage ([#347](https://github.com/wxt-dev/wxt/pull/347))
### 🏡 Chore
- Update templates to `^0.14.0` ([70a4961](https://github.com/wxt-dev/wxt/commit/70a4961))
- Fix typo in function name ([a329e24](https://github.com/wxt-dev/wxt/commit/a329e24))
### ❤️ Contributors
- Yuns ([@yunsii](http://github.com/yunsii))
- Armin
## v0.14.1
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.14.0...v0.14.1)
+4
View File
@@ -15,6 +15,10 @@ export default defineBackground(() => {
browser.runtime.getURL('/');
browser.runtime.getURL('/background.js');
browser.runtime.getURL('/icon/128.png');
browser.runtime.getURL('/example.html#hash');
browser.runtime.getURL('/example.html?query=param');
// @ts-expect-error: should only allow hashes/query params on HTML files
browser.runtime.getURL('/icon-128.png?query=param');
// @ts-expect-error: should only accept known message names
browser.i18n.getMessage('test');
+8 -5
View File
@@ -95,7 +95,7 @@ export default defineContentScript({
});
```
```ts [Svelete]
```ts [Svelte]
// entrypoints/example-ui.content/index.ts
import App from './App.svelte';
@@ -243,7 +243,8 @@ export default defineContentScript({
main(ctx) {
// 3. Define your UI
const ui = createIntegratedUi(ctx, {
const ui = createShadowRootUi(ctx, {
name: 'example-ui',
position: 'inline',
anchor: '#anchor',
onMount: (container) => {
@@ -264,7 +265,7 @@ export default defineContentScript({
});
```
```ts [Svelete]
```ts [Svelte]
// 1. Import the style
import './style.css';
import App from './App.svelte';
@@ -275,7 +276,8 @@ export default defineContentScript({
main(ctx) {
// 3. Define your UI
const ui = createIntegratedUi(ctx, {
const ui = createShadowRootUi(ctx, {
name: 'example-ui',
position: 'inline',
anchor: '#anchor',
onMount: (container) => {
@@ -308,7 +310,8 @@ export default defineContentScript({
main(ctx) {
// 3. Define your UI
const ui = createIntegratedUi(ctx, {
const ui = createShadowRootUi(ctx, {
name: 'example-ui',
position: 'inline',
anchor: '#anchor',
onMount: (container) => {
+53
View File
@@ -86,3 +86,56 @@ export default defineContentScript({
:::info
For a full list of entrypoints and each of their options, see the [`/entrypoints` folder](/entrypoints/background) documentation.
:::
### Side Effects
You cannot use imported variables outside the `main` function is JS entrypoints. This includes options, as shown below:
```ts
// entrypoints/content.ts
import { GOOGLE_MATCHES } from '~/utils/match-patterns';
export default defineContentScript({
matches: GOOGLE_MATCHES,
main() {
// ...
},
});
```
```
$ wxt build
wxt build
WXT 0.14.1
Building chrome-mv3 for production with Vite 5.0.5
✖ Command failed after 360 ms
[8:55:54 AM] ERROR entrypoints/content.ts: Cannot use imported variable "GOOGLE_MATCHES" before main function. See https://wxt.dev/guide/entrypoints.html#side-effects
```
This throws an error because WXT needs to import each entrypoint during the build process to extract its definition (containing the `match`, `run_at`, `include`/`exclude`, etc.) to render the `manifest.json` correctly. Before loading an entrypoint, a transformation is applied to remove all imports. This prevents imported modules (local or NPM) with side-effects from running during the build process, potentially throwing an error.
:::details Why?
When importing your entrypoint to get its definition, the file is imported in a **_node environement_**, and doesn't have access to the `window`, `chrome`, or `browser` globals a web extension ususally has access to. If WXT doesn't remove all the imports from the file, the imported modules could try and access one of these variables, throwing an error.
:::
:::warning
See [`wxt-dev/wxt#336`](https://github.com/wxt-dev/wxt/issues/336) to track the status of this bug.
:::
Usually, this error occurs when you try to extract options into a shared file or try to run code outside the `main` function. To fix the example from above, use litteral values when defining an entrypoint instead of importing them:
```ts
import { GOOGLE_MATCHES } from '~/utils/match-patterns'; // [!code --]
export default defineContentScript({
matches: GOOGLE_MATCHES, // [!code --]
matches: ['*//*.google.com/*'], // [!code ++]
main() {
// ...
},
});
```
+2
View File
@@ -49,8 +49,10 @@ describe('TypeScript Project', () => {
| "/options.html"
| "/popup.html"
| "/sandbox.html"
type HtmlPublicPath = Extract<PublicPath, \`\${string}.html\`>
export interface WxtRuntime extends Runtime.Static {
getURL(path: PublicPath): string;
getURL(path: \`\${HtmlPublicPath}\${string}\`): string;
}
}
"
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "wxt",
"type": "module",
"version": "0.14.1",
"version": "0.14.5",
"description": "Next gen framework for developing web extensions",
"engines": {
"node": ">=18",
+5
View File
@@ -97,6 +97,11 @@ const config: tsup.Options[] = [
cli: 'src/cli.ts',
},
format: ['esm'],
banner: {
// Fixes dynamic require of nodejs modules. See https://github.com/wxt-dev/wxt/issues/355
// https://github.com/evanw/esbuild/issues/1921#issuecomment-1152991694
js: "import { createRequire } from 'module';const require = createRequire(import.meta.url);",
},
},
];
+376
View File
@@ -0,0 +1,376 @@
import { describe, it, vi, beforeEach, expect } from 'vitest';
import { build } from '../core/build';
import { createServer } from '../core/create-server';
import { zip } from '../core/zip';
import { prepare } from '../core/prepare';
import { clean } from '../core/clean';
import { initialize } from '../core/initialize';
import { mock } from 'vitest-mock-extended';
import consola from 'consola';
vi.mock('../core/build');
const buildMock = vi.mocked(build);
vi.mock('../core/create-server');
const createServerMock = vi.mocked(createServer);
vi.mock('../core/zip');
const zipMock = vi.mocked(zip);
vi.mock('../core/prepare');
const prepareMock = vi.mocked(prepare);
vi.mock('../core/clean');
const cleanMock = vi.mocked(clean);
vi.mock('../core/initialize');
const initializeMock = vi.mocked(initialize);
consola.wrapConsole();
const ogArgv = process.argv;
function mockArgv(...args: string[]) {
process.argv = ['/bin/node', 'bin/wxt.mjs', ...args];
}
describe('CLI', () => {
beforeEach(() => {
vi.resetModules();
process.argv = ogArgv;
createServerMock.mockResolvedValue(mock());
});
describe('dev', () => {
it('should not pass any config when no flags are passed', async () => {
mockArgv();
await import('../cli');
expect(createServerMock).toBeCalledWith({});
});
it('should respect passing a custom root', async () => {
mockArgv('path/to/root');
await import('../cli');
expect(createServerMock).toBeCalledWith({
root: 'path/to/root',
});
});
it('should respect a custom config file', async () => {
mockArgv('-c', './path/to/config.ts');
await import('../cli');
expect(createServerMock).toBeCalledWith({
configFile: './path/to/config.ts',
});
});
it('should respect passing a custom mode', async () => {
mockArgv('-m', 'development');
await import('../cli');
expect(createServerMock).toBeCalledWith({
mode: 'development',
});
});
it('should respect passing a custom browser', async () => {
mockArgv('-b', 'firefox');
await import('../cli');
expect(createServerMock).toBeCalledWith({
browser: 'firefox',
});
});
it('should pass correct filtered entrypoints', async () => {
mockArgv('-e', 'popup', '-e', 'options');
await import('../cli');
expect(createServerMock).toBeCalledWith({
filterEntrypoints: ['popup', 'options'],
});
});
it('should respect passing --mv2', async () => {
mockArgv('--mv2');
await import('../cli');
expect(createServerMock).toBeCalledWith({
manifestVersion: 2,
});
});
it('should respect passing --mv3', async () => {
mockArgv('--mv3');
await import('../cli');
expect(createServerMock).toBeCalledWith({
manifestVersion: 3,
});
});
it('should respect passing --debug', async () => {
mockArgv('--debug');
await import('../cli');
expect(createServerMock).toBeCalledWith({
debug: true,
});
});
});
describe('build', () => {
it('should not pass any config when no flags are passed', async () => {
mockArgv('build');
await import('../cli');
expect(buildMock).toBeCalledWith({});
});
it('should respect passing a custom root', async () => {
mockArgv('build', 'path/to/root');
await import('../cli');
expect(buildMock).toBeCalledWith({
root: 'path/to/root',
});
});
it('should respect a custom config file', async () => {
mockArgv('build', '-c', './path/to/config.ts');
await import('../cli');
expect(buildMock).toBeCalledWith({
configFile: './path/to/config.ts',
});
});
it('should respect passing a custom mode', async () => {
mockArgv('build', '-m', 'development');
await import('../cli');
expect(buildMock).toBeCalledWith({
mode: 'development',
});
});
it('should respect passing a custom browser', async () => {
mockArgv('build', '-b', 'firefox');
await import('../cli');
expect(buildMock).toBeCalledWith({
browser: 'firefox',
});
});
it('should pass correct filtered entrypoints', async () => {
mockArgv('build', '-e', 'popup', '-e', 'options');
await import('../cli');
expect(buildMock).toBeCalledWith({
filterEntrypoints: ['popup', 'options'],
});
});
it('should respect passing --mv2', async () => {
mockArgv('build', '--mv2');
await import('../cli');
expect(buildMock).toBeCalledWith({
manifestVersion: 2,
});
});
it('should respect passing --mv3', async () => {
mockArgv('build', '--mv3');
await import('../cli');
expect(buildMock).toBeCalledWith({
manifestVersion: 3,
});
});
it('should include analysis in the build', async () => {
mockArgv('build', '--analyze');
await import('../cli');
expect(buildMock).toBeCalledWith({
analysis: {
enabled: true,
},
});
});
it('should respect passing --debug', async () => {
mockArgv('build', '--debug');
await import('../cli');
expect(buildMock).toBeCalledWith({
debug: true,
});
});
});
describe('zip', () => {
it('should not pass any config when no flags are passed', async () => {
mockArgv('zip');
await import('../cli');
expect(zipMock).toBeCalledWith({});
});
it('should respect passing a custom root', async () => {
mockArgv('zip', 'path/to/root');
await import('../cli');
expect(zipMock).toBeCalledWith({
root: 'path/to/root',
});
});
it('should respect a custom config file', async () => {
mockArgv('zip', '-c', './path/to/config.ts');
await import('../cli');
expect(zipMock).toBeCalledWith({
configFile: './path/to/config.ts',
});
});
it('should respect passing a custom mode', async () => {
mockArgv('zip', '-m', 'development');
await import('../cli');
expect(zipMock).toBeCalledWith({
mode: 'development',
});
});
it('should respect passing a custom browser', async () => {
mockArgv('zip', '-b', 'firefox');
await import('../cli');
expect(zipMock).toBeCalledWith({
browser: 'firefox',
});
});
it('should respect passing --mv2', async () => {
mockArgv('zip', '--mv2');
await import('../cli');
expect(zipMock).toBeCalledWith({
manifestVersion: 2,
});
});
it('should respect passing --mv3', async () => {
mockArgv('zip', '--mv3');
await import('../cli');
expect(zipMock).toBeCalledWith({
manifestVersion: 3,
});
});
it('should respect passing --debug', async () => {
mockArgv('zip', '--debug');
await import('../cli');
expect(zipMock).toBeCalledWith({
debug: true,
});
});
});
describe('prepare', () => {
it('should not pass any config when no flags are passed', async () => {
mockArgv('prepare');
await import('../cli');
expect(prepareMock).toBeCalledWith({});
});
it('should respect passing a custom root', async () => {
mockArgv('prepare', 'path/to/root');
await import('../cli');
expect(prepareMock).toBeCalledWith({
root: 'path/to/root',
});
});
it('should respect a custom config file', async () => {
mockArgv('prepare', '-c', './path/to/config.ts');
await import('../cli');
expect(prepareMock).toBeCalledWith({
configFile: './path/to/config.ts',
});
});
it('should respect passing --debug', async () => {
mockArgv('prepare', '--debug');
await import('../cli');
expect(prepareMock).toBeCalledWith({
debug: true,
});
});
});
describe('clean', () => {
it('should not pass any config when no flags are passed', async () => {
mockArgv('clean');
await import('../cli');
expect(cleanMock).toBeCalledWith(undefined);
});
it('should respect passing a custom root', async () => {
mockArgv('clean', 'path/to/root');
await import('../cli');
expect(cleanMock).toBeCalledWith('path/to/root');
});
});
describe('init', () => {
it('should not pass any options when no flags are passed', async () => {
mockArgv('init');
await import('../cli');
expect(initializeMock).toBeCalledWith({});
});
it('should respect the provided folder', async () => {
mockArgv('init', 'path/to/folder');
await import('../cli');
expect(initializeMock).toBeCalledWith({
directory: 'path/to/folder',
});
});
it('should respect passing --template', async () => {
mockArgv('init', '-t', 'vue');
await import('../cli');
expect(initializeMock).toBeCalledWith({
template: 'vue',
});
});
it('should respect passing --pm', async () => {
mockArgv('init', '--pm', 'pnpm');
await import('../cli');
expect(initializeMock).toBeCalledWith({
packageManager: 'pnpm',
});
});
});
});
+28 -4
View File
@@ -20,6 +20,13 @@ cli
.option('-c, --config <file>', 'use specified config file')
.option('-m, --mode <mode>', 'set env mode')
.option('-b, --browser <browser>', 'specify a browser')
.option(
'-e, --filter-entrypoint <entrypoint>',
'only build specific entrypoints',
{
type: [],
},
)
.option('--mv3', 'target manifest v3')
.option('--mv2', 'target manifest v2')
.action(
@@ -31,6 +38,7 @@ cli
manifestVersion: flags.mv3 ? 3 : flags.mv2 ? 2 : undefined,
configFile: flags.config,
debug: flags.debug,
filterEntrypoints: getArrayFromFlags(flags, 'filterEntrypoint'),
});
await server.start();
return { isOngoing: true };
@@ -43,6 +51,13 @@ cli
.option('-c, --config <file>', 'use specified config file')
.option('-m, --mode <mode>', 'set env mode')
.option('-b, --browser <browser>', 'specify a browser')
.option(
'-e, --filter-entrypoint <entrypoint>',
'only build specific entrypoints',
{
type: [],
},
)
.option('--mv3', 'target manifest v3')
.option('--mv2', 'target manifest v2')
.option('--analyze', 'visualize extension bundle')
@@ -55,9 +70,8 @@ cli
manifestVersion: flags.mv3 ? 3 : flags.mv2 ? 2 : undefined,
configFile: flags.config,
debug: flags.debug,
analysis: {
enabled: flags.analyze,
},
analysis: flags.analyze ? { enabled: true } : undefined,
filterEntrypoints: getArrayFromFlags(flags, 'filterEntrypoint'),
});
}),
);
@@ -125,7 +139,7 @@ cli
),
);
cli.parse();
cli.parse(process.argv);
/**
* Wrap an action handler to add a timer, error handling, and maybe enable debug mode.
@@ -165,3 +179,13 @@ function wrapAction(
}
};
}
/**
* Array flags, when not passed, are either `undefined` or `[undefined]`. This function filters out
* the
*/
function getArrayFromFlags<T>(flags: any, name: string): T[] | undefined {
const array = [flags[name]].flat() as Array<T | undefined>;
const result = array.filter((item) => item != null) as T[];
return result.length ? result : undefined;
}
+1 -1
View File
@@ -12,7 +12,7 @@ import {
import * as wxtPlugins from './plugins';
import { getEntrypointBundlePath } from '~/core/utils/entrypoints';
export async function craeteViteBuilder(
export async function createViteBuilder(
inlineConfig: InlineConfig,
userConfig: UserConfig,
wxtConfig: Omit<InternalConfig, 'builder'>,
+3
View File
@@ -20,6 +20,7 @@ import {
getInternalConfig,
detectDevChanges,
rebuild,
findEntrypoints,
} from '~/core/utils/building';
import { createExtensionRunner } from '~/core/runners';
import { consola } from 'consola';
@@ -159,8 +160,10 @@ function createFileReloader(options: {
.join(pc.dim(', '));
// Rebuild entrypoints on change
const allEntrypoints = await findEntrypoints(config);
const { output: newOutput } = await rebuild(
config,
allEntrypoints,
// TODO: this excludes new entrypoints, so they're not built until the dev command is restarted
changes.rebuildGroups,
changes.cachedOutput,
@@ -47,6 +47,7 @@ describe('Entrypoint Utils', () => {
name,
outputDir,
options: {},
skipped: false,
};
const actual = getEntrypointOutputFile(entrypoint, ext);
+184 -37
View File
@@ -6,6 +6,7 @@ import {
fakeBuildOutput,
fakeEntrypoint,
fakeInternalConfig,
fakeManifestCommand,
fakeOptionsEntrypoint,
fakePopupEntrypoint,
} from '../testing/fake-objects';
@@ -52,7 +53,11 @@ describe('Manifest Utils', () => {
},
};
const actual = await generateManifest([popup], buildOutput, config);
const { manifest: actual } = await generateManifest(
[popup],
buildOutput,
config,
);
expect(actual).toMatchObject(expected);
});
@@ -79,7 +84,11 @@ describe('Manifest Utils', () => {
default_popup: 'popup.html',
};
const actual = await generateManifest([popup], buildOutput, config);
const { manifest: actual } = await generateManifest(
[popup],
buildOutput,
config,
);
expect(actual[expectedType]).toEqual(expected);
},
@@ -102,7 +111,11 @@ describe('Manifest Utils', () => {
action: config.manifest.action,
};
const actual = await generateManifest([], buildOutput, config);
const { manifest: actual } = await generateManifest(
[],
buildOutput,
config,
);
expect(actual).toMatchObject(expected);
});
@@ -131,7 +144,11 @@ describe('Manifest Utils', () => {
page: 'options.html',
};
const actual = await generateManifest([options], buildOutput, config);
const { manifest: actual } = await generateManifest(
[options],
buildOutput,
config,
);
expect(actual.options_ui).toEqual(expected);
});
@@ -149,7 +166,11 @@ describe('Manifest Utils', () => {
page: 'options.html',
};
const actual = await generateManifest([options], buildOutput, config);
const { manifest: actual } = await generateManifest(
[options],
buildOutput,
config,
);
expect(actual.options_ui).toEqual(expected);
});
@@ -179,7 +200,7 @@ describe('Manifest Utils', () => {
service_worker: 'background.js',
};
const actual = await generateManifest(
const { manifest: actual } = await generateManifest(
[background],
buildOutput,
config,
@@ -201,7 +222,7 @@ describe('Manifest Utils', () => {
scripts: ['background.js'],
};
const actual = await generateManifest(
const { manifest: actual } = await generateManifest(
[background],
buildOutput,
config,
@@ -226,7 +247,7 @@ describe('Manifest Utils', () => {
scripts: ['background.js'],
};
const actual = await generateManifest(
const { manifest: actual } = await generateManifest(
[background],
buildOutput,
config,
@@ -248,7 +269,7 @@ describe('Manifest Utils', () => {
scripts: ['background.js'],
};
const actual = await generateManifest(
const { manifest: actual } = await generateManifest(
[background],
buildOutput,
config,
@@ -274,7 +295,11 @@ describe('Manifest Utils', () => {
});
const config = fakeInternalConfig();
const actual = await generateManifest(entrypoints, buildOutput, config);
const { manifest: actual } = await generateManifest(
entrypoints,
buildOutput,
config,
);
expect(actual.icons).toEqual({
16: 'icon-16.png',
@@ -296,7 +321,11 @@ describe('Manifest Utils', () => {
});
const config = fakeInternalConfig();
const actual = await generateManifest(entrypoints, buildOutput, config);
const { manifest: actual } = await generateManifest(
entrypoints,
buildOutput,
config,
);
expect(actual.icons).toBeUndefined();
});
@@ -323,7 +352,11 @@ describe('Manifest Utils', () => {
},
});
const actual = await generateManifest(entrypoints, buildOutput, config);
const { manifest: actual } = await generateManifest(
entrypoints,
buildOutput,
config,
);
expect(actual.icons).toEqual(expected);
});
@@ -339,6 +372,7 @@ describe('Manifest Utils', () => {
options: {
matches: ['*://google.com/*'],
},
skipped: false,
};
const cs1Styles: OutputAsset = {
type: 'asset',
@@ -353,6 +387,7 @@ describe('Manifest Utils', () => {
matches: ['*://google.com/*'],
runAt: 'document_end',
},
skipped: false,
};
const cs2Styles: OutputAsset = {
type: 'asset',
@@ -367,6 +402,7 @@ describe('Manifest Utils', () => {
matches: ['*://google.com/*'],
runAt: 'document_end',
},
skipped: false,
};
const cs3Styles: OutputAsset = {
type: 'asset',
@@ -381,6 +417,7 @@ describe('Manifest Utils', () => {
matches: ['*://duckduckgo.com/*'],
runAt: 'document_end',
},
skipped: false,
};
const cs4Styles: OutputAsset = {
type: 'asset',
@@ -395,6 +432,7 @@ describe('Manifest Utils', () => {
matches: ['*://google.com/*'],
world: 'MAIN',
},
skipped: false,
};
const cs5Styles: OutputAsset = {
type: 'asset',
@@ -418,7 +456,11 @@ describe('Manifest Utils', () => {
],
};
const actual = await generateManifest(entrypoints, buildOutput, config);
const { manifest: actual } = await generateManifest(
entrypoints,
buildOutput,
config,
);
expect(actual.content_scripts).toContainEqual({
matches: ['*://google.com/*'],
@@ -454,6 +496,7 @@ describe('Manifest Utils', () => {
options: {
matches: ['*://google.com/*'],
},
skipped: false,
};
const generatedContentScript = {
matches: ['*://google.com/*'],
@@ -474,7 +517,11 @@ describe('Manifest Utils', () => {
},
});
const actual = await generateManifest(entrypoints, buildOutput, config);
const { manifest: actual } = await generateManifest(
entrypoints,
buildOutput,
config,
);
expect(actual.content_scripts).toContainEqual(userContentScript);
expect(actual.content_scripts).toContainEqual(generatedContentScript);
@@ -493,6 +540,7 @@ describe('Manifest Utils', () => {
matches: ['*://google.com/*'],
cssInjectionMode,
},
skipped: false,
};
const styles: OutputAsset = {
type: 'asset',
@@ -509,7 +557,7 @@ describe('Manifest Utils', () => {
command: 'build',
});
const actual = await generateManifest(
const { manifest: actual } = await generateManifest(
entrypoints,
buildOutput,
config,
@@ -537,6 +585,7 @@ describe('Manifest Utils', () => {
matches: ['*://google.com/*'],
cssInjectionMode,
},
skipped: false,
};
const styles: OutputAsset = {
type: 'asset',
@@ -553,7 +602,7 @@ describe('Manifest Utils', () => {
command: 'build',
});
const actual = await generateManifest(
const { manifest: actual } = await generateManifest(
entrypoints,
buildOutput,
config,
@@ -578,6 +627,7 @@ describe('Manifest Utils', () => {
matches: ['*://google.com/*'],
cssInjectionMode: 'ui',
},
skipped: false,
};
const styles: OutputAsset = {
type: 'asset',
@@ -595,7 +645,7 @@ describe('Manifest Utils', () => {
manifestVersion: 3,
});
const actual = await generateManifest(
const { manifest: actual } = await generateManifest(
entrypoints,
buildOutput,
config,
@@ -619,6 +669,7 @@ describe('Manifest Utils', () => {
matches: ['*://google.com/*'],
cssInjectionMode: 'ui',
},
skipped: false,
};
const styles: OutputAsset = {
type: 'asset',
@@ -636,7 +687,7 @@ describe('Manifest Utils', () => {
manifestVersion: 2,
});
const actual = await generateManifest(
const { manifest: actual } = await generateManifest(
entrypoints,
buildOutput,
config,
@@ -657,6 +708,7 @@ describe('Manifest Utils', () => {
matches: ['*://play.google.com/books/*'],
cssInjectionMode: 'ui',
},
skipped: false,
};
const styles: OutputAsset = {
type: 'asset',
@@ -674,7 +726,7 @@ describe('Manifest Utils', () => {
manifestVersion: 3,
});
const actual = await generateManifest(
const { manifest: actual } = await generateManifest(
entrypoints,
buildOutput,
config,
@@ -701,6 +753,7 @@ describe('Manifest Utils', () => {
matches: ['*://google.com/*'],
cssInjectionMode: 'ui',
},
skipped: false,
};
const styles: OutputAsset = {
type: 'asset',
@@ -723,7 +776,11 @@ describe('Manifest Utils', () => {
},
});
const actual = await generateManifest(entrypoints, buildOutput, config);
const { manifest: actual } = await generateManifest(
entrypoints,
buildOutput,
config,
);
expect(actual.web_accessible_resources).toEqual([
{ resources: ['one.png'], matches: ['*://one.com/*'] },
@@ -744,6 +801,7 @@ describe('Manifest Utils', () => {
matches: ['*://google.com/*'],
cssInjectionMode: 'ui',
},
skipped: false,
};
const styles: OutputAsset = {
type: 'asset',
@@ -764,7 +822,11 @@ describe('Manifest Utils', () => {
},
});
const actual = await generateManifest(entrypoints, buildOutput, config);
const { manifest: actual } = await generateManifest(
entrypoints,
buildOutput,
config,
);
expect(actual.web_accessible_resources).toEqual([
'one.png',
@@ -787,7 +849,11 @@ describe('Manifest Utils', () => {
author: newAuthor,
};
const actual = await generateManifest(entrypoints, buildOutput, config);
const { manifest: actual } = await generateManifest(
entrypoints,
buildOutput,
config,
);
expect(actual).toMatchObject(expected);
});
@@ -809,7 +875,7 @@ describe('Manifest Utils', () => {
},
});
const actual = await generateManifest(
const { manifest: actual } = await generateManifest(
entrypoints,
buildOutput,
config,
@@ -835,7 +901,7 @@ describe('Manifest Utils', () => {
},
});
const actual = await generateManifest(
const { manifest: actual } = await generateManifest(
entrypoints,
buildOutput,
config,
@@ -860,7 +926,7 @@ describe('Manifest Utils', () => {
},
});
const actual = await generateManifest(
const { manifest: actual } = await generateManifest(
entrypoints,
buildOutput,
config,
@@ -881,7 +947,11 @@ describe('Manifest Utils', () => {
},
});
const actual = await generateManifest(entrypoints, buildOutput, config);
const { manifest: actual } = await generateManifest(
entrypoints,
buildOutput,
config,
);
expect(actual.version).toBe('0.0.0');
expect(actual.version_name).toBeUndefined();
@@ -895,6 +965,7 @@ describe('Manifest Utils', () => {
describe('commands', () => {
const reloadCommandName = 'wxt:reload-extension';
const reloadCommand = {
description: expect.any(String),
suggested_key: {
default: 'Alt+R',
},
@@ -905,21 +976,65 @@ describe('Manifest Utils', () => {
const output = fakeBuildOutput();
const entrypoints = fakeArray(fakeEntrypoint);
const actual = await generateManifest(entrypoints, output, config);
const { manifest: actual } = await generateManifest(
entrypoints,
output,
config,
);
expect(actual.commands).toMatchObject({
expect(actual.commands).toEqual({
[reloadCommandName]: reloadCommand,
});
});
it('should customize the reload commands key binding if passing a custom command', async () => {
const config = fakeInternalConfig({
command: 'serve',
dev: {
reloadCommand: 'Ctrl+E',
},
});
const output = fakeBuildOutput();
const entrypoints = fakeArray(fakeEntrypoint);
const { manifest: actual } = await generateManifest(
entrypoints,
output,
config,
);
expect(actual.commands).toEqual({
[reloadCommandName]: {
...reloadCommand,
suggested_key: {
default: 'Ctrl+E',
},
},
});
});
it("should not include the reload command when it's been disabled", async () => {
const config = fakeInternalConfig({
command: 'serve',
dev: {
reloadCommand: false,
},
});
const output = fakeBuildOutput();
const entrypoints = fakeArray(fakeEntrypoint);
const { manifest: actual } = await generateManifest(
entrypoints,
output,
config,
);
expect(actual.commands).toBeUndefined();
});
it('should not override any existing commands when adding the one to reload the extension', async () => {
const customCommandName = 'custom-command';
const customCommand = {
description: 'Some other command',
suggested_key: {
default: 'Ctrl+H',
},
};
const customCommand = fakeManifestCommand();
const config = fakeInternalConfig({
command: 'serve',
manifest: {
@@ -931,20 +1046,52 @@ describe('Manifest Utils', () => {
const output = fakeBuildOutput();
const entrypoints = fakeArray(fakeEntrypoint);
const actual = await generateManifest(entrypoints, output, config);
const { manifest: actual } = await generateManifest(
entrypoints,
output,
config,
);
expect(actual.commands).toMatchObject({
expect(actual.commands).toEqual({
[reloadCommandName]: reloadCommand,
[customCommandName]: customCommand,
});
});
it('should not include the command if there are already 4 others (the max)', async () => {
const commands = {
command1: fakeManifestCommand(),
command2: fakeManifestCommand(),
command3: fakeManifestCommand(),
command4: fakeManifestCommand(),
};
const config = fakeInternalConfig({
command: 'serve',
manifest: { commands },
});
const output = fakeBuildOutput();
const entrypoints = fakeArray(fakeEntrypoint);
const { manifest: actual, warnings } = await generateManifest(
entrypoints,
output,
config,
);
expect(actual.commands).toEqual(commands);
expect(warnings).toHaveLength(1);
});
it('should not include the command when building an extension', async () => {
const config = fakeInternalConfig({ command: 'build' });
const output = fakeBuildOutput();
const entrypoints = fakeArray(fakeEntrypoint);
const actual = await generateManifest(entrypoints, output, config);
const { manifest: actual } = await generateManifest(
entrypoints,
output,
config,
);
expect(actual.commands).toBeUndefined();
});
@@ -0,0 +1,7 @@
import { defineContentScript } from '~/sandbox';
import { faker } from '@faker-js/faker';
export default defineContentScript({
matches: [faker.string.nanoid()],
main() {},
});
@@ -53,6 +53,7 @@ describe('findEntrypoints', () => {
defaultIcon: { '16': '/icon/16.png' },
defaultTitle: 'Default Title',
},
skipped: false,
},
],
[
@@ -72,6 +73,7 @@ describe('findEntrypoints', () => {
options: {
defaultTitle: 'Title',
},
skipped: false,
},
],
])(
@@ -103,6 +105,7 @@ describe('findEntrypoints', () => {
inputPath: resolve(config.entrypointsDir, 'options.html'),
outputDir: config.outDir,
options: {},
skipped: false,
},
],
[
@@ -123,6 +126,7 @@ describe('findEntrypoints', () => {
options: {
openInTab: true,
},
skipped: false,
},
],
])(
@@ -146,6 +150,7 @@ describe('findEntrypoints', () => {
name: 'content',
inputPath: resolve(config.entrypointsDir, 'content.ts'),
outputDir: resolve(config.outDir, 'content-scripts'),
skipped: false,
},
],
[
@@ -155,6 +160,7 @@ describe('findEntrypoints', () => {
name: 'overlay',
inputPath: resolve(config.entrypointsDir, 'overlay.content.ts'),
outputDir: resolve(config.outDir, 'content-scripts'),
skipped: false,
},
],
[
@@ -164,6 +170,7 @@ describe('findEntrypoints', () => {
name: 'content',
inputPath: resolve(config.entrypointsDir, 'content/index.ts'),
outputDir: resolve(config.outDir, 'content-scripts'),
skipped: false,
},
],
[
@@ -173,6 +180,7 @@ describe('findEntrypoints', () => {
name: 'overlay',
inputPath: resolve(config.entrypointsDir, 'overlay.content/index.ts'),
outputDir: resolve(config.outDir, 'content-scripts'),
skipped: false,
},
],
[
@@ -182,6 +190,7 @@ describe('findEntrypoints', () => {
name: 'overlay',
inputPath: resolve(config.entrypointsDir, 'overlay.content.tsx'),
outputDir: resolve(config.outDir, 'content-scripts'),
skipped: false,
},
],
])(
@@ -212,6 +221,7 @@ describe('findEntrypoints', () => {
name: 'background',
inputPath: resolve(config.entrypointsDir, 'background.ts'),
outputDir: config.outDir,
skipped: false,
},
],
[
@@ -221,6 +231,7 @@ describe('findEntrypoints', () => {
name: 'background',
inputPath: resolve(config.entrypointsDir, 'background/index.ts'),
outputDir: config.outDir,
skipped: false,
},
],
])(
@@ -258,6 +269,7 @@ describe('findEntrypoints', () => {
name: 'background',
options: {},
outputDir: config.outDir,
skipped: false,
});
});
@@ -278,6 +290,7 @@ describe('findEntrypoints', () => {
name: 'injected',
inputPath: resolve(config.entrypointsDir, path),
outputDir: config.outDir,
skipped: false,
};
const options: GenericEntrypoint['options'] = {};
globMock.mockResolvedValueOnce([path]);
@@ -304,6 +317,7 @@ describe('findEntrypoints', () => {
inputPath: resolve(config.entrypointsDir, 'sandbox.html'),
outputDir: config.outDir,
options: {},
skipped: false,
},
],
[
@@ -314,6 +328,7 @@ describe('findEntrypoints', () => {
inputPath: resolve(config.entrypointsDir, 'sandbox/index.html'),
outputDir: config.outDir,
options: {},
skipped: false,
},
],
[
@@ -324,6 +339,7 @@ describe('findEntrypoints', () => {
inputPath: resolve(config.entrypointsDir, 'named.sandbox.html'),
outputDir: config.outDir,
options: {},
skipped: false,
},
],
[
@@ -334,6 +350,7 @@ describe('findEntrypoints', () => {
inputPath: resolve(config.entrypointsDir, 'named.sandbox/index.html'),
outputDir: config.outDir,
options: {},
skipped: false,
},
],
@@ -346,6 +363,7 @@ describe('findEntrypoints', () => {
inputPath: resolve(config.entrypointsDir, 'bookmarks.html'),
outputDir: config.outDir,
options: {},
skipped: false,
},
],
[
@@ -356,6 +374,7 @@ describe('findEntrypoints', () => {
inputPath: resolve(config.entrypointsDir, 'bookmarks/index.html'),
outputDir: config.outDir,
options: {},
skipped: false,
},
],
@@ -368,6 +387,7 @@ describe('findEntrypoints', () => {
inputPath: resolve(config.entrypointsDir, 'history.html'),
outputDir: config.outDir,
options: {},
skipped: false,
},
],
[
@@ -378,6 +398,7 @@ describe('findEntrypoints', () => {
inputPath: resolve(config.entrypointsDir, 'history/index.html'),
outputDir: config.outDir,
options: {},
skipped: false,
},
],
@@ -390,6 +411,7 @@ describe('findEntrypoints', () => {
inputPath: resolve(config.entrypointsDir, 'newtab.html'),
outputDir: config.outDir,
options: {},
skipped: false,
},
],
[
@@ -400,6 +422,7 @@ describe('findEntrypoints', () => {
inputPath: resolve(config.entrypointsDir, 'newtab/index.html'),
outputDir: config.outDir,
options: {},
skipped: false,
},
],
@@ -412,6 +435,7 @@ describe('findEntrypoints', () => {
inputPath: resolve(config.entrypointsDir, 'sidepanel.html'),
outputDir: config.outDir,
options: {},
skipped: false,
},
],
[
@@ -422,6 +446,7 @@ describe('findEntrypoints', () => {
inputPath: resolve(config.entrypointsDir, 'sidepanel/index.html'),
outputDir: config.outDir,
options: {},
skipped: false,
},
],
[
@@ -432,6 +457,7 @@ describe('findEntrypoints', () => {
inputPath: resolve(config.entrypointsDir, 'named.sidepanel.html'),
outputDir: config.outDir,
options: {},
skipped: false,
},
],
[
@@ -442,6 +468,7 @@ describe('findEntrypoints', () => {
inputPath: resolve(config.entrypointsDir, 'named.sidepanel/index.html'),
outputDir: config.outDir,
options: {},
skipped: false,
},
],
@@ -454,6 +481,7 @@ describe('findEntrypoints', () => {
inputPath: resolve(config.entrypointsDir, 'devtools.html'),
outputDir: config.outDir,
options: {},
skipped: false,
},
],
[
@@ -464,6 +492,7 @@ describe('findEntrypoints', () => {
inputPath: resolve(config.entrypointsDir, 'devtools/index.html'),
outputDir: config.outDir,
options: {},
skipped: false,
},
],
@@ -476,6 +505,7 @@ describe('findEntrypoints', () => {
inputPath: resolve(config.entrypointsDir, 'onboarding.html'),
outputDir: config.outDir,
options: {},
skipped: false,
},
],
[
@@ -486,6 +516,7 @@ describe('findEntrypoints', () => {
inputPath: resolve(config.entrypointsDir, 'onboarding/index.html'),
outputDir: config.outDir,
options: {},
skipped: false,
},
],
@@ -498,6 +529,7 @@ describe('findEntrypoints', () => {
inputPath: resolve(config.entrypointsDir, 'iframe.scss'),
outputDir: config.outDir,
options: {},
skipped: false,
},
],
[
@@ -508,6 +540,7 @@ describe('findEntrypoints', () => {
inputPath: resolve(config.entrypointsDir, 'iframe.css'),
outputDir: config.outDir,
options: {},
skipped: false,
},
],
@@ -520,6 +553,7 @@ describe('findEntrypoints', () => {
inputPath: resolve(config.entrypointsDir, 'content.css'),
outputDir: resolve(config.outDir, 'content-scripts'),
options: {},
skipped: false,
},
],
[
@@ -530,6 +564,7 @@ describe('findEntrypoints', () => {
inputPath: resolve(config.entrypointsDir, 'overlay.content.css'),
outputDir: resolve(config.outDir, 'content-scripts'),
options: {},
skipped: false,
},
],
[
@@ -540,6 +575,7 @@ describe('findEntrypoints', () => {
inputPath: resolve(config.entrypointsDir, 'content/index.css'),
outputDir: resolve(config.outDir, 'content-scripts'),
options: {},
skipped: false,
},
],
[
@@ -550,6 +586,7 @@ describe('findEntrypoints', () => {
inputPath: resolve(config.entrypointsDir, 'overlay.content/index.css'),
outputDir: resolve(config.outDir, 'content-scripts'),
options: {},
skipped: false,
},
],
])('should find entrypoint for %s', async (path, expected) => {
@@ -737,4 +774,29 @@ describe('findEntrypoints', () => {
expect(entrypoints).toEqual([]);
});
});
describe('filterEntrypoints option', () => {
it('should control entrypoints accessible', async () => {
globMock.mockResolvedValue([
'options/index.html',
'popup/index.html',
'ui.content/index.ts',
'injected.content/index.ts',
]);
importEntrypointFileMock.mockResolvedValue({});
const filterEntrypoints = ['popup', 'ui'];
const config = fakeInternalConfig({
root: '/',
entrypointsDir: resolve('/src/entrypoints'),
outDir: resolve('.output'),
command: 'build',
filterEntrypoints: new Set(filterEntrypoints),
});
const entrypoints = await findEntrypoints(config);
const names = entrypoints.map((item) => item.name);
expect(names).toHaveLength(2);
expect(names).toEqual(filterEntrypoints);
});
});
});
@@ -8,6 +8,7 @@ const background: Entrypoint = {
inputPath: '/background.ts',
outputDir: '/.output/background',
options: {},
skipped: false,
};
const contentScript: Entrypoint = {
type: 'content-script',
@@ -17,6 +18,7 @@ const contentScript: Entrypoint = {
options: {
matches: ['<all_urls>'],
},
skipped: false,
};
const unlistedScript: Entrypoint = {
type: 'unlisted-script',
@@ -24,6 +26,7 @@ const unlistedScript: Entrypoint = {
inputPath: '/injected.ts',
outputDir: '/.output/injected',
options: {},
skipped: false,
};
const popup: Entrypoint = {
type: 'popup',
@@ -31,6 +34,7 @@ const popup: Entrypoint = {
inputPath: '/popup.html',
outputDir: '/.output/popup',
options: {},
skipped: false,
};
const unlistedPage: Entrypoint = {
type: 'unlisted-page',
@@ -38,6 +42,7 @@ const unlistedPage: Entrypoint = {
inputPath: '/onboarding.html',
outputDir: '/.output/onboarding',
options: {},
skipped: false,
};
const options: Entrypoint = {
type: 'options',
@@ -45,6 +50,7 @@ const options: Entrypoint = {
inputPath: '/options.html',
outputDir: '/.output/options',
options: {},
skipped: false,
};
const sandbox1: Entrypoint = {
type: 'sandbox',
@@ -52,6 +58,7 @@ const sandbox1: Entrypoint = {
inputPath: '/sandbox1.html',
outputDir: '/.output/sandbox1',
options: {},
skipped: false,
};
const sandbox2: Entrypoint = {
type: 'sandbox',
@@ -59,6 +66,7 @@ const sandbox2: Entrypoint = {
inputPath: '/sandbox2.html',
outputDir: '/.output/sandbox2',
options: {},
skipped: false,
};
const unlistedStyle: Entrypoint = {
type: 'unlisted-style',
@@ -66,6 +74,7 @@ const unlistedStyle: Entrypoint = {
inputPath: '/injected.scss',
outputDir: '/.output',
options: {},
skipped: false,
};
const contentScriptStyle: Entrypoint = {
type: 'content-script-style',
@@ -73,6 +82,7 @@ const contentScriptStyle: Entrypoint = {
inputPath: '/overlay.content.scss',
outputDir: '/.output/content-scripts',
options: {},
skipped: false,
};
describe('groupEntrypoints', () => {
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest';
import { importEntrypointFile } from '~/core/utils/building';
import { fakeInternalConfig } from '~/core/utils/testing/fake-objects';
import { resolve } from 'node:path';
import { unnormalizePath } from '../../paths';
const entrypointPath = (filename: string) =>
resolve('src/core/utils/__tests__/test-entrypoints', filename);
@@ -38,4 +39,15 @@ describe('importEntrypointFile', () => {
expect(actual).toBeUndefined();
});
it('should throw a custom error message when an imported variable is used before main', async () => {
const filePath = unnormalizePath(
'../src/core/utils/__tests__/test-entrypoints/imported-option.ts',
);
await expect(() =>
importEntrypointFile(entrypointPath('imported-option.ts'), config),
).rejects.toThrowErrorMatchingInlineSnapshot(
`[Error: ${filePath}: Cannot use imported variable "faker" outside the main function. See https://wxt.dev/guide/entrypoints.html#side-effects]`,
);
});
});
+39 -7
View File
@@ -24,6 +24,7 @@ import {
} from '~/core/utils/entrypoints';
import { VIRTUAL_NOOP_BACKGROUND_MODULE_ID } from '~/core/utils/constants';
import { CSS_EXTENSIONS_PATTERN } from '~/core/utils/paths';
import pc from 'picocolors';
/**
* Return entrypoints and their configuration by looking through the project's files.
@@ -48,7 +49,14 @@ export async function findEntrypoints(
);
if (matchingGlob) {
const type = PATH_GLOB_TO_TYPE_MAP[matchingGlob];
results.push({ name, inputPath, type });
results.push({
name,
inputPath,
type,
skipped:
config.filterEntrypoints != null &&
!config.filterEntrypoints.has(name),
});
}
return results;
}, []);
@@ -106,11 +114,22 @@ export async function findEntrypoints(
inputPath: VIRTUAL_NOOP_BACKGROUND_MODULE_ID,
name: 'background',
type: 'background',
skipped: false,
}),
);
}
config.logger.debug('All entrypoints:', entrypoints);
const skippedEntrypointNames = entrypointInfos
.filter((item) => item.skipped)
.map((item) => item.name);
if (skippedEntrypointNames.length) {
config.logger.warn(
`Filter excluded the following entrypoints:\n${skippedEntrypointNames
.map((item) => `${pc.dim('-')} ${pc.cyan(item)}`)
.join('\n')}`,
);
}
const targetEntrypoints = entrypoints.filter((entry) => {
const { include, exclude } = entry.options;
if (include?.length && exclude?.length) {
@@ -125,6 +144,9 @@ export async function findEntrypoints(
if (include?.length && !exclude?.length) {
return include.includes(config.browser);
}
if (skippedEntrypointNames.includes(entry.name)) {
return false;
}
return true;
});
@@ -136,6 +158,10 @@ interface EntrypointInfo {
name: string;
inputPath: string;
type: Entrypoint['type'];
/**
* @default false
*/
skipped: boolean;
}
function preventDuplicateEntrypointNames(
@@ -202,7 +228,7 @@ function getHtmlBaseOptions(document: Document): BaseEntrypointOptions {
*/
async function getPopupEntrypoint(
config: InternalConfig,
{ inputPath, name }: EntrypointInfo,
{ inputPath, name, skipped }: EntrypointInfo,
): Promise<PopupEntrypoint> {
const content = await fs.readFile(inputPath, 'utf-8');
const { document } = parseHTML(content);
@@ -247,6 +273,7 @@ async function getPopupEntrypoint(
options,
inputPath,
outputDir: config.outDir,
skipped,
};
}
@@ -256,7 +283,7 @@ async function getPopupEntrypoint(
*/
async function getOptionsEntrypoint(
config: InternalConfig,
{ inputPath, name }: EntrypointInfo,
{ inputPath, name, skipped }: EntrypointInfo,
): Promise<OptionsEntrypoint> {
const content = await fs.readFile(inputPath, 'utf-8');
const { document } = parseHTML(content);
@@ -290,6 +317,7 @@ async function getOptionsEntrypoint(
options,
inputPath,
outputDir: config.outDir,
skipped,
};
}
@@ -299,7 +327,7 @@ async function getOptionsEntrypoint(
*/
async function getUnlistedPageEntrypoint(
config: InternalConfig,
{ inputPath, name }: EntrypointInfo,
{ inputPath, name, skipped }: EntrypointInfo,
): Promise<GenericEntrypoint> {
const content = await fs.readFile(inputPath, 'utf-8');
const { document } = parseHTML(content);
@@ -310,6 +338,7 @@ async function getUnlistedPageEntrypoint(
inputPath,
outputDir: config.outDir,
options: getHtmlBaseOptions(document),
skipped,
};
}
@@ -319,7 +348,7 @@ async function getUnlistedPageEntrypoint(
*/
async function getUnlistedScriptEntrypoint(
config: InternalConfig,
{ inputPath, name }: EntrypointInfo,
{ inputPath, name, skipped }: EntrypointInfo,
): Promise<GenericEntrypoint> {
const defaultExport = await importEntrypointFile<UnlistedScriptDefinition>(
inputPath,
@@ -338,6 +367,7 @@ async function getUnlistedScriptEntrypoint(
inputPath,
outputDir: config.outDir,
options,
skipped,
};
}
@@ -346,7 +376,7 @@ async function getUnlistedScriptEntrypoint(
*/
async function getBackgroundEntrypoint(
config: InternalConfig,
{ inputPath, name }: EntrypointInfo,
{ inputPath, name, skipped }: EntrypointInfo,
): Promise<BackgroundEntrypoint> {
let options: Omit<BackgroundDefinition, 'main'> = {};
if (inputPath !== VIRTUAL_NOOP_BACKGROUND_MODULE_ID) {
@@ -372,6 +402,7 @@ async function getBackgroundEntrypoint(
type: resolvePerBrowserOption(options.type, config.browser),
persistent: resolvePerBrowserOption(options.persistent, config.browser),
},
skipped,
};
}
@@ -380,7 +411,7 @@ async function getBackgroundEntrypoint(
*/
async function getContentScriptEntrypoint(
config: InternalConfig,
{ inputPath, name }: EntrypointInfo,
{ inputPath, name, skipped }: EntrypointInfo,
): Promise<ContentScriptEntrypoint> {
const { main: _, ...options } =
await importEntrypointFile<ContentScriptDefinition>(inputPath, config);
@@ -395,6 +426,7 @@ async function getContentScriptEntrypoint(
inputPath,
outputDir: resolve(config.outDir, CONTENT_SCRIPT_OUT_DIR),
options,
skipped,
};
}
@@ -79,8 +79,10 @@ import "wxt/browser";
declare module "wxt/browser" {
export type PublicPath =
{{ union }}
type HtmlPublicPath = Extract<PublicPath, \`\${string}.html\`>
export interface WxtRuntime extends Runtime.Static {
getURL(path: PublicPath): string;
getURL(path: \`\${HtmlPublicPath}\${string}\`): string;
}
}
`;
+16 -2
View File
@@ -12,7 +12,7 @@ import {
import path from 'node:path';
import { createFsCache } from '~/core/utils/cache';
import consola, { LogLevels } from 'consola';
import { craeteViteBuilder } from '~/core/builders/vite';
import { createViteBuilder } from '~/core/builders/vite';
import defu from 'defu';
import { NullablyRequired } from '../types';
@@ -72,10 +72,14 @@ export async function getInternalConfig(
srcDir,
mergedConfig.entrypointsDir ?? 'entrypoints',
);
const filterEntrypoints = !!mergedConfig.filterEntrypoints?.length
? new Set(mergedConfig.filterEntrypoints)
: undefined;
const publicDir = path.resolve(srcDir, mergedConfig.publicDir ?? 'public');
const typesDir = path.resolve(wxtDir, 'types');
const outBaseDir = path.resolve(root, mergedConfig.outDir ?? '.output');
const outDir = path.resolve(outBaseDir, `${browser}-mv${manifestVersion}`);
const reloadCommand = mergedConfig.dev?.reloadCommand ?? 'Alt+R';
const runnerConfig = await loadConfig<ExtensionRunnerConfig>({
name: 'web-ext',
@@ -101,6 +105,7 @@ export async function getInternalConfig(
command,
debug,
entrypointsDir,
filterEntrypoints,
env,
fsCache: createFsCache(wxtDir),
imports: mergedConfig.imports ?? {},
@@ -132,9 +137,12 @@ export async function getInternalConfig(
mergedConfig.experimental?.includeBrowserPolyfill ?? true,
},
server,
dev: {
reloadCommand,
},
};
const builder = await craeteViteBuilder(
const builder = await createViteBuilder(
inlineConfig,
userConfig,
finalConfig,
@@ -191,6 +199,8 @@ function mergeInlineConfig(
configFile: inlineConfig.configFile,
debug: inlineConfig.debug ?? userConfig.debug,
entrypointsDir: inlineConfig.entrypointsDir ?? userConfig.entrypointsDir,
filterEntrypoints:
inlineConfig.filterEntrypoints ?? userConfig.filterEntrypoints,
imports,
logger: inlineConfig.logger ?? userConfig.logger,
manifest,
@@ -215,6 +225,10 @@ function mergeInlineConfig(
},
vite: undefined,
transformManifest: undefined,
dev: {
...userConfig.dev,
...inlineConfig.dev,
},
};
}
+12 -3
View File
@@ -2,7 +2,7 @@ import createJITI, { TransformOptions as JitiTransformOptions } from 'jiti';
import { InternalConfig } from '~/types';
import { createUnimport } from 'unimport';
import fs from 'fs-extra';
import { resolve } from 'node:path';
import { relative, resolve } from 'node:path';
import { getUnimportOptions } from '~/core/utils/unimport';
import { removeProjectImportStatements } from '~/core/utils/strings';
import { normalizePath } from '~/core/utils/paths';
@@ -89,8 +89,17 @@ export async function importEntrypointFile<T>(
const res = await jiti(path);
return res.default;
} catch (err) {
config.logger.error(err);
throw err;
if (err instanceof ReferenceError) {
// "XXX is not defined" - usually due to WXT removing imports
const variableName = err.message.replace(' is not defined', '');
const filePath = relative(config.root, path);
throw Error(
`${filePath}: Cannot use imported variable "${variableName}" outside the main function. See https://wxt.dev/guide/entrypoints.html#side-effects`,
{ cause: err },
);
} else {
throw err;
}
}
}
+10 -1
View File
@@ -38,7 +38,12 @@ export async function internalBuild(
const entrypoints = await findEntrypoints(config);
config.logger.debug('Detected entrypoints:', entrypoints);
const groups = groupEntrypoints(entrypoints);
const { output } = await rebuild(config, groups, undefined);
const { output, warnings } = await rebuild(
config,
entrypoints,
groups,
undefined,
);
// Post-build
await printBuildSummary(
@@ -48,6 +53,10 @@ export async function internalBuild(
config,
);
for (const warning of warnings) {
config.logger.warn(...warning);
}
if (config.analysis.enabled) {
await combineAnalysisStats(config);
config.logger.info(
+21 -9
View File
@@ -1,6 +1,10 @@
import type { Manifest } from '~/browser';
import { BuildOutput, EntrypointGroup, InternalConfig } from '~/types';
import { findEntrypoints } from './find-entrypoints';
import {
BuildOutput,
Entrypoint,
EntrypointGroup,
InternalConfig,
} from '~/types';
import { generateTypesDir } from './generate-wxt-dir';
import { buildEntrypoints } from './build-entrypoints';
import { generateManifest, writeManifest } from '~/core/utils/manifest';
@@ -14,20 +18,30 @@ import { generateManifest, writeManifest } from '~/core/utils/manifest';
* 2. Build the `entrypointGroups` (and copies public files)
* 3. Generate the latest manifest for all entrypoints
* 4. Write the new manifest to the file system
*
* @param config Internal config containing all the project information.
* @param allEntrypoints List of entrypoints used to generate the types inside .wxt directory.
* @param entrypointGroups The list of entrypoint groups to build.
* @param existingOutput The previous output to combine the rebuild results into. An emptry array if
* this is the first build.
*/
export async function rebuild(
config: InternalConfig,
allEntrypoints: Entrypoint[],
entrypointGroups: EntrypointGroup[],
existingOutput: Omit<BuildOutput, 'manifest'> = {
steps: [],
publicAssets: [],
},
): Promise<{ output: BuildOutput; manifest: Manifest.WebExtensionManifest }> {
): Promise<{
output: BuildOutput;
manifest: Manifest.WebExtensionManifest;
warnings: any[][];
}> {
const { default: ora } = await import('ora');
const spinner = ora(`Preparing...`).start();
// Update types directory with new files and types
const allEntrypoints = await findEntrypoints(config);
await generateTypesDir(allEntrypoints, config).catch((err) => {
config.logger.warn('Failed to update .wxt directory:', err);
// Throw the error if doing a regular build, don't for dev mode.
@@ -41,11 +55,8 @@ export async function rebuild(
publicAssets: [...existingOutput.publicAssets, ...newOutput.publicAssets],
};
const newManifest = await generateManifest(
allEntrypoints,
mergedOutput,
config,
);
const { manifest: newManifest, warnings: manifestWarnings } =
await generateManifest(allEntrypoints, mergedOutput, config);
const finalOutput: BuildOutput = {
manifest: newManifest,
...newOutput,
@@ -67,5 +78,6 @@ export async function rebuild(
],
},
manifest: newManifest,
warnings: manifestWarnings,
};
}
+23 -12
View File
@@ -54,7 +54,8 @@ export async function generateManifest(
entrypoints: Entrypoint[],
buildOutput: Omit<BuildOutput, 'manifest'>,
config: InternalConfig,
): Promise<Manifest.WebExtensionManifest> {
): Promise<{ manifest: Manifest.WebExtensionManifest; warnings: any[][] }> {
const warnings: any[][] = [];
const pkg = await getPackageJson(config);
let versionName =
@@ -75,16 +76,6 @@ export async function generateManifest(
short_name: pkg?.shortName,
icons: discoverIcons(buildOutput),
};
if (config.command === 'serve') {
baseManifest.commands = {
'wxt:reload-extension': {
description: 'Reload the extension during development',
suggested_key: {
default: 'Alt+R',
},
},
};
}
const userManifest = config.manifest;
const manifest = defu(
@@ -92,6 +83,23 @@ export async function generateManifest(
baseManifest,
) as Manifest.WebExtensionManifest;
// Add reload command in dev mode
if (config.command === 'serve' && config.dev.reloadCommand) {
if (manifest.commands && Object.keys(manifest.commands).length >= 4) {
warnings.push([
"Extension already has 4 registered commands, WXT's reload command is disabled",
]);
} else {
manifest.commands ??= {};
manifest.commands['wxt:reload-extension'] = {
description: 'Reload the extension during development',
suggested_key: {
default: config.dev.reloadCommand,
},
};
}
}
// Apply the final version fields after merging the user manifest
manifest.version = version;
manifest.version_name =
@@ -117,7 +125,10 @@ export async function generateManifest(
);
}
return finalManifest;
return {
manifest: finalManifest,
warnings,
};
}
/**
+23 -6
View File
@@ -4,7 +4,7 @@
import { resolve } from 'path';
import { faker } from '@faker-js/faker';
import merge from 'lodash.merge';
import type { Manifest } from '~/browser';
import { Commands, type Manifest } from '~/browser';
import {
FsCache,
InternalConfig,
@@ -59,7 +59,7 @@ export const fakeContentScriptEntrypoint =
name: faker.string.alpha(),
options: {
matches: [],
matchAboutBlank: faker.helpers.arrayElement([true, false, undefined]),
matchAboutBlank: faker.datatype.boolean(),
matchOriginAsFallback: faker.helpers.arrayElement([
true,
false,
@@ -73,6 +73,7 @@ export const fakeContentScriptEntrypoint =
]),
},
outputDir: fakeDir('.output'),
skipped: faker.datatype.boolean(),
}));
export const fakeBackgroundEntrypoint = fakeObjectCreator<BackgroundEntrypoint>(
@@ -81,10 +82,11 @@ export const fakeBackgroundEntrypoint = fakeObjectCreator<BackgroundEntrypoint>(
inputPath: 'entrypoints/background.ts',
name: 'background',
options: {
persistent: faker.helpers.arrayElement([true, false, undefined]),
persistent: faker.datatype.boolean(),
type: faker.helpers.maybe(() => 'module'),
},
outputDir: fakeDir('.output'),
skipped: faker.datatype.boolean(),
}),
);
@@ -95,6 +97,7 @@ export const fakeUnlistedScriptEntrypoint =
name: faker.string.alpha(),
outputDir: fakeDir('.output'),
options: {},
skipped: faker.datatype.boolean(),
}));
export const fakeOptionsEntrypoint = fakeObjectCreator<OptionsEntrypoint>(
@@ -104,10 +107,11 @@ export const fakeOptionsEntrypoint = fakeObjectCreator<OptionsEntrypoint>(
name: 'options',
outputDir: fakeDir('.output'),
options: {
browserStyle: faker.helpers.arrayElement([true, false, undefined]),
chromeStyle: faker.helpers.arrayElement([true, false, undefined]),
openInTab: faker.helpers.arrayElement([true, false, undefined]),
browserStyle: faker.datatype.boolean(),
chromeStyle: faker.datatype.boolean(),
openInTab: faker.datatype.boolean(),
},
skipped: faker.datatype.boolean(),
}),
);
@@ -134,6 +138,7 @@ export const fakePopupEntrypoint = fakeObjectCreator<PopupEntrypoint>(() => ({
undefined,
]),
},
skipped: faker.datatype.boolean(),
}));
export const fakeGenericEntrypoint = fakeObjectCreator<GenericEntrypoint>(
@@ -152,6 +157,7 @@ export const fakeGenericEntrypoint = fakeObjectCreator<GenericEntrypoint>(
name: faker.string.alpha(),
outputDir: fakeDir('.output'),
options: {},
skipped: faker.datatype.boolean(),
}),
);
@@ -238,6 +244,9 @@ export const fakeInternalConfig = fakeObjectCreator<InternalConfig>(() => {
includeBrowserPolyfill: true,
},
builder: mock(),
dev: {
reloadCommand: 'Alt+R',
},
};
});
@@ -251,3 +260,11 @@ export const fakeBuildStepOutput = fakeObjectCreator<BuildStepOutput>(() => ({
chunks: fakeArray(fakeOutputChunk),
entrypoints: fakeArray(fakeEntrypoint),
}));
export const fakeManifestCommand = fakeObjectCreator<Commands.Command>(() => ({
description: faker.string.sample(),
shortcut: `${faker.helpers.arrayElement(['ctrl', 'alt'])}+${faker.number.int({
min: 0,
max: 9,
})}`,
}));
+25
View File
@@ -31,6 +31,12 @@ export interface InlineConfig {
* @default "${config.srcDir}/entrypoints"
*/
entrypointsDir?: string;
/**
* A list of entrypoint names (`"popup"`, `"options"`, etc.) to build. Will speed up the build if
* your extension has lots of entrypoints, and you don't need to build all of them to develop a
* feature.
*/
filterEntrypoints?: string[];
/**
* Output directory that stored build folders and ZIPs.
*
@@ -223,6 +229,24 @@ export interface InlineConfig {
*/
includeBrowserPolyfill?: boolean;
};
/**
* Config effecting dev mode only.
*/
dev?: {
/**
* Controls whether a custom keyboard shortcut command, `Alt+R`, is added during dev mode to
* quickly reload the extension.
*
* If false, the shortcut is not added during development.
*
* If set to a custom string, you can override the key combo used. See
* [Chrome's command docs](https://developer.chrome.com/docs/extensions/reference/api/commands)
* for available options.
*
* @default "Alt+R"
*/
reloadCommand?: string | false;
};
}
// TODO: Extract to @wxt/vite-builder and use module augmentation to include the vite field
@@ -377,6 +401,7 @@ export interface BaseEntrypoint {
*/
outputDir: string;
options: BaseEntrypointOptions;
skipped: boolean;
}
export interface GenericEntrypoint extends BaseEntrypoint {
+4
View File
@@ -21,6 +21,7 @@ export interface InternalConfig {
wxtDir: string;
typesDir: string;
entrypointsDir: string;
filterEntrypoints?: Set<string>;
outBaseDir: string;
outDir: string;
debug: boolean;
@@ -56,6 +57,9 @@ export interface InternalConfig {
includeBrowserPolyfill: boolean;
};
builder: WxtBuilder;
dev: {
reloadCommand: string | false;
};
}
export interface FsCache {
+1 -1
View File
@@ -23,6 +23,6 @@
"@types/react-dom": "^18.2.18",
"@vitejs/plugin-react": "^4.2.1",
"typescript": "^5.3.3",
"wxt": "^0.13.0"
"wxt": "^0.14.0"
}
}
+1 -1
View File
@@ -20,6 +20,6 @@
"devDependencies": {
"typescript": "^5.3.3",
"vite-plugin-solid": "^2.8.0",
"wxt": "^0.13.0"
"wxt": "^0.14.0"
}
}
+1 -1
View File
@@ -20,6 +20,6 @@
"svelte-check": "^3.6.2",
"tslib": "^2.6.2",
"typescript": "^5.3.3",
"wxt": "^0.13.0"
"wxt": "^0.14.0"
}
}
+1 -1
View File
@@ -16,6 +16,6 @@
},
"devDependencies": {
"typescript": "^5.3.3",
"wxt": "^0.13.0"
"wxt": "^0.14.0"
}
}
+1 -1
View File
@@ -21,6 +21,6 @@
"@vitejs/plugin-vue": "^5.0.1",
"typescript": "^5.3.3",
"vue-tsc": "^1.8.27",
"wxt": "^0.13.0"
"wxt": "^0.14.0"
}
}