Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c298d8552b | |||
| 87267347f3 | |||
| 326e84b667 | |||
| 4b005b450e | |||
| 3bf40fc4ed | |||
| 9ace395e11 | |||
| 64f42064ae | |||
| a525d786f1 | |||
| 948ee810da | |||
| 8687b2980e | |||
| 6f0814bc97 | |||
| 148c0da51f |
@@ -30,10 +30,10 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup
|
||||
- run: pnpm build:all
|
||||
working-directory: demo
|
||||
- run: pnpm tsc --noEmit
|
||||
working-directory: demo
|
||||
- run: pnpm build:all
|
||||
working-directory: demo
|
||||
- run: pnpm wxt zip
|
||||
working-directory: demo
|
||||
- run: pnpm vitest run
|
||||
|
||||
@@ -1,5 +1,47 @@
|
||||
# Changelog
|
||||
|
||||
## v0.14.6
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.14.5...v0.14.6)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Restart dev mode when saving config ([#365](https://github.com/wxt-dev/wxt/pull/365))
|
||||
- Add basic validation for entrypoint options ([#368](https://github.com/wxt-dev/wxt/pull/368))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Add subdependency bin directory so `wxt build --analyze` works with PNPM ([#363](https://github.com/wxt-dev/wxt/pull/363))
|
||||
- Sort build output files naturally ([#364](https://github.com/wxt-dev/wxt/pull/364))
|
||||
|
||||
### 🤖 CI
|
||||
|
||||
- Check for type errors in demo before building ([4b005b4](https://github.com/wxt-dev/wxt/commit/4b005b4))
|
||||
|
||||
## 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)
|
||||
|
||||
+4
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "wxt",
|
||||
"type": "module",
|
||||
"version": "0.14.3",
|
||||
"version": "0.14.6",
|
||||
"description": "Next gen framework for developing web extensions",
|
||||
"engines": {
|
||||
"node": ">=18",
|
||||
@@ -125,7 +125,9 @@
|
||||
"jiti": "^1.21.0",
|
||||
"json5": "^2.2.3",
|
||||
"linkedom": "^0.16.1",
|
||||
"manage-path": "^2.0.0",
|
||||
"minimatch": "^9.0.3",
|
||||
"natural-compare": "^1.4.0",
|
||||
"normalize-path": "^3.0.0",
|
||||
"ora": "^7.0.1",
|
||||
"picocolors": "^1.0.0",
|
||||
@@ -141,6 +143,7 @@
|
||||
"@faker-js/faker": "^8.3.1",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/lodash.merge": "^4.6.9",
|
||||
"@types/natural-compare": "^1.4.3",
|
||||
"@types/node": "^20.10.3",
|
||||
"@types/normalize-path": "^3.0.2",
|
||||
"@types/prompts": "^2.4.9",
|
||||
|
||||
Generated
+21
@@ -74,9 +74,15 @@ importers:
|
||||
linkedom:
|
||||
specifier: ^0.16.1
|
||||
version: 0.16.1
|
||||
manage-path:
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0
|
||||
minimatch:
|
||||
specifier: ^9.0.3
|
||||
version: 9.0.3
|
||||
natural-compare:
|
||||
specifier: ^1.4.0
|
||||
version: 1.4.0
|
||||
normalize-path:
|
||||
specifier: ^3.0.0
|
||||
version: 3.0.0
|
||||
@@ -117,6 +123,9 @@ importers:
|
||||
'@types/lodash.merge':
|
||||
specifier: ^4.6.9
|
||||
version: 4.6.9
|
||||
'@types/natural-compare':
|
||||
specifier: ^1.4.3
|
||||
version: 1.4.3
|
||||
'@types/node':
|
||||
specifier: ^20.10.3
|
||||
version: 20.10.3
|
||||
@@ -1106,6 +1115,10 @@ packages:
|
||||
resolution: {integrity: sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==}
|
||||
dev: false
|
||||
|
||||
/@types/natural-compare@1.4.3:
|
||||
resolution: {integrity: sha512-XCAxy+Gg6+S6VagwzcknnvCKujj/bVv1q+GFuCrFEelqaZPqJoC+FeXLwc2dp+oLP7qDZQ4ZfQiTJQ9sIUmlLw==}
|
||||
dev: true
|
||||
|
||||
/@types/node@20.10.3:
|
||||
resolution: {integrity: sha512-XJavIpZqiXID5Yxnxv3RUDKTN5b81ddNC3ecsA0SoFXz/QU8OGBwZGMomiq0zw+uuqbL/krztv/DINAQ/EV4gg==}
|
||||
dependencies:
|
||||
@@ -3352,6 +3365,10 @@ packages:
|
||||
resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
|
||||
dev: false
|
||||
|
||||
/manage-path@2.0.0:
|
||||
resolution: {integrity: sha512-NJhyB+PJYTpxhxZJ3lecIGgh4kwIY2RAh44XvAz9UlqthlQwtPBf62uBVR8XaD8CRuSjQ6TnZH2lNJkbLPZM2A==}
|
||||
dev: false
|
||||
|
||||
/mark.js@8.11.1:
|
||||
resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==}
|
||||
dev: true
|
||||
@@ -3533,6 +3550,10 @@ packages:
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
|
||||
/natural-compare@1.4.0:
|
||||
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||
dev: false
|
||||
|
||||
/ncp@2.0.0:
|
||||
resolution: {integrity: sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==}
|
||||
hasBin: true
|
||||
|
||||
@@ -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);",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
+8
-5
@@ -4,6 +4,7 @@ import { build, clean, createServer, initialize, prepare, zip } from '~/core';
|
||||
import consola, { LogLevels } from 'consola';
|
||||
import { printHeader } from '~/core/utils/log';
|
||||
import { formatDuration } from '~/core/utils/time';
|
||||
import { ValidationError } from './core/utils/validation';
|
||||
|
||||
// TODO: Remove. See https://github.com/wxt-dev/wxt/issues/277
|
||||
process.env.VITE_CJS_IGNORE_WARNING = 'true';
|
||||
@@ -70,9 +71,7 @@ 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'),
|
||||
});
|
||||
}),
|
||||
@@ -141,7 +140,7 @@ cli
|
||||
),
|
||||
);
|
||||
|
||||
cli.parse();
|
||||
cli.parse(process.argv);
|
||||
|
||||
/**
|
||||
* Wrap an action handler to add a timer, error handling, and maybe enable debug mode.
|
||||
@@ -176,7 +175,11 @@ function wrapAction(
|
||||
consola.fail(
|
||||
`Command failed after ${formatDuration(Date.now() - startTime)}`,
|
||||
);
|
||||
consola.error(err);
|
||||
if (err instanceof ValidationError) {
|
||||
// Don't log these errors, they've already been logged
|
||||
} else {
|
||||
consola.error(err);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -230,6 +230,9 @@ export async function createViteBuilder(
|
||||
async listen() {
|
||||
await viteServer.listen(info.port);
|
||||
},
|
||||
async close() {
|
||||
await viteServer.close();
|
||||
},
|
||||
transformHtml(...args) {
|
||||
return viteServer.transformIndexHtml(...args);
|
||||
},
|
||||
|
||||
+65
-16
@@ -49,22 +49,46 @@ export async function createServer(
|
||||
origin,
|
||||
};
|
||||
|
||||
const buildAndOpenBrowser = async () => {
|
||||
// Build after starting the dev server so it can be used to transform HTML files
|
||||
server.currentOutput = await internalBuild(config);
|
||||
|
||||
// Open browser after everything is ready to go.
|
||||
await runner.openBrowser(config);
|
||||
};
|
||||
|
||||
/**
|
||||
* Stops the previous runner, grabs the latest config, and recreates the runner.
|
||||
*/
|
||||
const closeAndRecreateRunner = async () => {
|
||||
await runner.closeBrowser();
|
||||
config = await getLatestConfig();
|
||||
runner = await createExtensionRunner(config);
|
||||
};
|
||||
|
||||
// Server instance must be created first so its reference can be added to the internal config used
|
||||
// to pre-render entrypoints
|
||||
const server: WxtDevServer = {
|
||||
...serverInfo,
|
||||
watcher: undefined as any, // Filled out later down below
|
||||
ws: undefined as any, // Filled out later down below
|
||||
currentOutput: undefined as any, // Filled out later down below
|
||||
get watcher() {
|
||||
return builderServer.watcher;
|
||||
},
|
||||
get ws() {
|
||||
return builderServer.ws;
|
||||
},
|
||||
currentOutput: undefined,
|
||||
async start() {
|
||||
await builderServer.listen();
|
||||
config.logger.success(`Started dev server @ ${serverInfo.origin}`);
|
||||
|
||||
// Build after starting the dev server so it can be used to transform HTML files
|
||||
server.currentOutput = await internalBuild(config);
|
||||
|
||||
// Open browser after everything is ready to go.
|
||||
await runner.openBrowser(config);
|
||||
await buildAndOpenBrowser();
|
||||
},
|
||||
async stop() {
|
||||
await runner.closeBrowser();
|
||||
await builderServer.close();
|
||||
},
|
||||
async restart() {
|
||||
await closeAndRecreateRunner();
|
||||
await buildAndOpenBrowser();
|
||||
},
|
||||
transformHtml(url, html, originalUrl) {
|
||||
return builderServer.transformHtml(url, html, originalUrl);
|
||||
@@ -78,23 +102,25 @@ export async function createServer(
|
||||
reloadExtension() {
|
||||
server.ws.send('wxt:reload-extension');
|
||||
},
|
||||
async restartBrowser() {
|
||||
await closeAndRecreateRunner();
|
||||
await runner.openBrowser(config);
|
||||
},
|
||||
};
|
||||
|
||||
const getLatestConfig = () =>
|
||||
getInternalConfig(inlineConfig ?? {}, 'serve', server);
|
||||
let config = await getLatestConfig();
|
||||
|
||||
const [runner, builderServer] = await Promise.all([
|
||||
let [runner, builderServer] = await Promise.all([
|
||||
createExtensionRunner(config),
|
||||
config.builder.createServer(server),
|
||||
]);
|
||||
|
||||
server.watcher = builderServer.watcher;
|
||||
server.ws = builderServer.ws;
|
||||
|
||||
// Register content scripts for the first time after the background starts up since they're not
|
||||
// listed in the manifest
|
||||
server.ws.on('wxt:background-initialized', () => {
|
||||
if (server.currentOutput == null) return;
|
||||
reloadContentScripts(server.currentOutput.steps, config, server);
|
||||
});
|
||||
|
||||
@@ -138,18 +164,39 @@ function createFileReloader(options: {
|
||||
changeQueue.push([event, path]);
|
||||
|
||||
await fileChangedMutex.runExclusive(async () => {
|
||||
const fileChanges = changeQueue.splice(0, changeQueue.length);
|
||||
if (server.currentOutput == null) return;
|
||||
|
||||
const fileChanges = changeQueue
|
||||
.splice(0, changeQueue.length)
|
||||
.map(([_, file]) => file);
|
||||
if (fileChanges.length === 0) return;
|
||||
|
||||
const changes = detectDevChanges(fileChanges, server.currentOutput);
|
||||
const changes = detectDevChanges(
|
||||
config,
|
||||
fileChanges,
|
||||
server.currentOutput,
|
||||
);
|
||||
if (changes.type === 'no-change') return;
|
||||
|
||||
if (changes.type === 'full-restart') {
|
||||
config.logger.info('Config changed, restarting server...');
|
||||
server.restart();
|
||||
return;
|
||||
}
|
||||
|
||||
if (changes.type === 'browser-restart') {
|
||||
config.logger.info('Runner config changed, restarting browser...');
|
||||
server.restartBrowser();
|
||||
return;
|
||||
}
|
||||
|
||||
// Log the entrypoints that were effected
|
||||
config.logger.info(
|
||||
`Changed: ${Array.from(new Set(fileChanges.map((change) => change[1])))
|
||||
`Changed: ${Array.from(new Set(fileChanges))
|
||||
.map((file) => pc.dim(relative(config.root, file)))
|
||||
.join(', ')}`,
|
||||
);
|
||||
|
||||
const rebuiltNames = changes.rebuildGroups
|
||||
.flat()
|
||||
.map((entry) => {
|
||||
@@ -197,6 +244,8 @@ function reloadContentScripts(
|
||||
) {
|
||||
if (config.manifestVersion === 3) {
|
||||
steps.forEach((step) => {
|
||||
if (server.currentOutput == null) return;
|
||||
|
||||
const entry = step.entrypoints;
|
||||
if (Array.isArray(entry) || entry.type !== 'content-script') return;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { WebExtRunInstance } from 'web-ext-run';
|
||||
import { ExtensionRunner } from '~/types';
|
||||
import { formatDuration } from '../utils/time';
|
||||
|
||||
/**
|
||||
* Create an `ExtensionRunner` backed by `web-ext`.
|
||||
@@ -9,7 +10,7 @@ export function createWebExtRunner(): ExtensionRunner {
|
||||
|
||||
return {
|
||||
async openBrowser(config) {
|
||||
config.logger.info('Opening browser...');
|
||||
const startTime = Date.now();
|
||||
|
||||
if (config.browser === 'firefox' && config.manifestVersion === 3) {
|
||||
throw Error(
|
||||
@@ -17,7 +18,7 @@ export function createWebExtRunner(): ExtensionRunner {
|
||||
);
|
||||
}
|
||||
|
||||
// Use the plugin's logger instead of web-ext's built-in one.
|
||||
// Use WXT's logger instead of web-ext's built-in one.
|
||||
const webExtLogger = await import('web-ext-run/util/logger');
|
||||
webExtLogger.consoleStream.write = ({ level, msg, name }) => {
|
||||
if (level >= ERROR_LOG_LEVEL) config.logger.error(name, msg);
|
||||
@@ -61,7 +62,8 @@ export function createWebExtRunner(): ExtensionRunner {
|
||||
const webExt = await import('web-ext-run');
|
||||
runner = await webExt.default.cmd.run(finalConfig, options);
|
||||
|
||||
config.logger.success('Opened!');
|
||||
const duration = Date.now() - startTime;
|
||||
config.logger.success(`Opened browser in ${formatDuration(duration)}`);
|
||||
},
|
||||
|
||||
async closeBrowser() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { every } from '~/core/utils/arrays';
|
||||
import { every, some } from '~/core/utils/arrays';
|
||||
|
||||
describe('Array Utils', () => {
|
||||
describe('every', () => {
|
||||
@@ -15,4 +15,20 @@ describe('Array Utils', () => {
|
||||
expect(every([1, 2, 1], (item) => item === 1)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('some', () => {
|
||||
it('should return true if one value returns true', () => {
|
||||
const array = [1, 2, 3];
|
||||
const predicate = (item: number) => item === 2;
|
||||
|
||||
expect(some(array, predicate)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if no values match', () => {
|
||||
const array = [1, 2, 3];
|
||||
const predicate = (item: number) => item === 4;
|
||||
|
||||
expect(some(array, predicate)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -423,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/*'],
|
||||
@@ -480,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);
|
||||
@@ -516,7 +557,7 @@ describe('Manifest Utils', () => {
|
||||
command: 'build',
|
||||
});
|
||||
|
||||
const actual = await generateManifest(
|
||||
const { manifest: actual } = await generateManifest(
|
||||
entrypoints,
|
||||
buildOutput,
|
||||
config,
|
||||
@@ -561,7 +602,7 @@ describe('Manifest Utils', () => {
|
||||
command: 'build',
|
||||
});
|
||||
|
||||
const actual = await generateManifest(
|
||||
const { manifest: actual } = await generateManifest(
|
||||
entrypoints,
|
||||
buildOutput,
|
||||
config,
|
||||
@@ -604,7 +645,7 @@ describe('Manifest Utils', () => {
|
||||
manifestVersion: 3,
|
||||
});
|
||||
|
||||
const actual = await generateManifest(
|
||||
const { manifest: actual } = await generateManifest(
|
||||
entrypoints,
|
||||
buildOutput,
|
||||
config,
|
||||
@@ -646,7 +687,7 @@ describe('Manifest Utils', () => {
|
||||
manifestVersion: 2,
|
||||
});
|
||||
|
||||
const actual = await generateManifest(
|
||||
const { manifest: actual } = await generateManifest(
|
||||
entrypoints,
|
||||
buildOutput,
|
||||
config,
|
||||
@@ -685,7 +726,7 @@ describe('Manifest Utils', () => {
|
||||
manifestVersion: 3,
|
||||
});
|
||||
|
||||
const actual = await generateManifest(
|
||||
const { manifest: actual } = await generateManifest(
|
||||
entrypoints,
|
||||
buildOutput,
|
||||
config,
|
||||
@@ -735,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/*'] },
|
||||
@@ -777,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',
|
||||
@@ -800,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);
|
||||
});
|
||||
@@ -822,7 +875,7 @@ describe('Manifest Utils', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const actual = await generateManifest(
|
||||
const { manifest: actual } = await generateManifest(
|
||||
entrypoints,
|
||||
buildOutput,
|
||||
config,
|
||||
@@ -848,7 +901,7 @@ describe('Manifest Utils', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const actual = await generateManifest(
|
||||
const { manifest: actual } = await generateManifest(
|
||||
entrypoints,
|
||||
buildOutput,
|
||||
config,
|
||||
@@ -873,7 +926,7 @@ describe('Manifest Utils', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const actual = await generateManifest(
|
||||
const { manifest: actual } = await generateManifest(
|
||||
entrypoints,
|
||||
buildOutput,
|
||||
config,
|
||||
@@ -894,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();
|
||||
@@ -908,6 +965,7 @@ describe('Manifest Utils', () => {
|
||||
describe('commands', () => {
|
||||
const reloadCommandName = 'wxt:reload-extension';
|
||||
const reloadCommand = {
|
||||
description: expect.any(String),
|
||||
suggested_key: {
|
||||
default: 'Alt+R',
|
||||
},
|
||||
@@ -918,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: {
|
||||
@@ -944,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,100 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
fakeArray,
|
||||
fakeContentScriptEntrypoint,
|
||||
fakeEntrypoint,
|
||||
fakeGenericEntrypoint,
|
||||
} from '../testing/fake-objects';
|
||||
import { validateEntrypoints } from '../validation';
|
||||
|
||||
describe('Validation Utils', () => {
|
||||
describe('validateEntrypoints', () => {
|
||||
it('should return no errors when there are no errors', () => {
|
||||
const entrypoints = fakeArray(fakeEntrypoint);
|
||||
const expected = {
|
||||
errors: [],
|
||||
errorCount: 0,
|
||||
warningCount: 0,
|
||||
};
|
||||
|
||||
const actual = validateEntrypoints(entrypoints);
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should return an error when exclude is not an array', () => {
|
||||
const entrypoint = fakeGenericEntrypoint({
|
||||
options: {
|
||||
// @ts-expect-error
|
||||
exclude: 0,
|
||||
},
|
||||
});
|
||||
const expected = {
|
||||
errors: [
|
||||
{
|
||||
type: 'error',
|
||||
message: '`exclude` must be an array of browser names',
|
||||
value: 0,
|
||||
entrypoint,
|
||||
},
|
||||
],
|
||||
errorCount: 1,
|
||||
warningCount: 0,
|
||||
};
|
||||
|
||||
const actual = validateEntrypoints([entrypoint]);
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should return an error when include is not an array', () => {
|
||||
const entrypoint = fakeGenericEntrypoint({
|
||||
options: {
|
||||
// @ts-expect-error
|
||||
include: 0,
|
||||
},
|
||||
});
|
||||
const expected = {
|
||||
errors: [
|
||||
{
|
||||
type: 'error',
|
||||
message: '`include` must be an array of browser names',
|
||||
value: 0,
|
||||
entrypoint,
|
||||
},
|
||||
],
|
||||
errorCount: 1,
|
||||
warningCount: 0,
|
||||
};
|
||||
|
||||
const actual = validateEntrypoints([entrypoint]);
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
it("should return an error when content scripts don't have a matches", () => {
|
||||
const entrypoint = fakeContentScriptEntrypoint({
|
||||
options: {
|
||||
// @ts-expect-error
|
||||
matches: null,
|
||||
},
|
||||
});
|
||||
const expected = {
|
||||
errors: [
|
||||
{
|
||||
type: 'error',
|
||||
message: '`matches` is required',
|
||||
value: null,
|
||||
entrypoint,
|
||||
},
|
||||
],
|
||||
errorCount: 1,
|
||||
warningCount: 0,
|
||||
};
|
||||
|
||||
const actual = validateEntrypoints([entrypoint]);
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -9,3 +9,15 @@ export function every<T>(
|
||||
if (!predicate(array[i], i)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true when any of the predicates return true;
|
||||
*/
|
||||
export function some<T>(
|
||||
array: T[],
|
||||
predicate: (item: T, index: number) => boolean,
|
||||
): boolean {
|
||||
for (let i = 0; i < array.length; i++)
|
||||
if (predicate(array[i], i)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -10,22 +10,15 @@ import {
|
||||
fakePopupEntrypoint,
|
||||
fakeOutputAsset,
|
||||
fakeOutputChunk,
|
||||
fakeInternalConfig,
|
||||
} from '~/core/utils/testing/fake-objects';
|
||||
import { BuildOutput, BuildStepOutput } from '~/types';
|
||||
|
||||
describe('Detect Dev Changes', () => {
|
||||
describe('No changes', () => {
|
||||
it("should return 'no-change' when a build hasn't finished", () => {
|
||||
const actual = detectDevChanges(
|
||||
[['unknown', '/path/to/file.ts']],
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(actual).toEqual({ type: 'no-change' });
|
||||
});
|
||||
|
||||
it("should return 'no-change' when the changed file isn't used by any of the entrypoints", () => {
|
||||
const change: [string, string] = ['unknown', '/some/path.ts'];
|
||||
const config = fakeInternalConfig();
|
||||
const changes = ['/some/path.ts'];
|
||||
const currentOutput: BuildOutput = {
|
||||
manifest: fakeManifest(),
|
||||
publicAssets: [],
|
||||
@@ -41,18 +34,64 @@ describe('Detect Dev Changes', () => {
|
||||
],
|
||||
};
|
||||
|
||||
const actual = detectDevChanges([change], currentOutput);
|
||||
const actual = detectDevChanges(config, changes, currentOutput);
|
||||
|
||||
expect(actual).toEqual({ type: 'no-change' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('wxt.config.ts', () => {
|
||||
it("should return 'full-restart' when one of the changed files is the config file", () => {
|
||||
const configFile = '/root/wxt.config.ts';
|
||||
const config = fakeInternalConfig({
|
||||
userConfigMetadata: {
|
||||
configFile,
|
||||
},
|
||||
});
|
||||
const changes = ['/root/src/public/image.svg', configFile];
|
||||
const currentOutput: BuildOutput = {
|
||||
manifest: fakeManifest(),
|
||||
publicAssets: [],
|
||||
steps: [],
|
||||
};
|
||||
const expected: DevModeChange = {
|
||||
type: 'full-restart',
|
||||
};
|
||||
|
||||
const actual = detectDevChanges(config, changes, currentOutput);
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('web-ext.config.ts', () => {
|
||||
it("should return 'browser-restart' when one of the changed files is the config file", () => {
|
||||
const runnerFile = '/root/web-ext.config.ts';
|
||||
const config = fakeInternalConfig({
|
||||
runnerConfig: {
|
||||
configFile: runnerFile,
|
||||
},
|
||||
});
|
||||
const changes = ['/root/src/public/image.svg', runnerFile];
|
||||
const currentOutput: BuildOutput = {
|
||||
manifest: fakeManifest(),
|
||||
publicAssets: [],
|
||||
steps: [],
|
||||
};
|
||||
const expected: DevModeChange = {
|
||||
type: 'browser-restart',
|
||||
};
|
||||
|
||||
const actual = detectDevChanges(config, changes, currentOutput);
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Public Assets', () => {
|
||||
it("should return 'extension-reload' without any groups to rebuild when the changed file is a public asset", () => {
|
||||
const change: [string, string] = [
|
||||
'unknown',
|
||||
'/root/src/public/image.svg',
|
||||
];
|
||||
const config = fakeInternalConfig();
|
||||
const changes = ['/root/src/public/image.svg'];
|
||||
const asset1 = fakeOutputAsset({
|
||||
fileName: 'image.svg',
|
||||
});
|
||||
@@ -73,7 +112,7 @@ describe('Detect Dev Changes', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const actual = detectDevChanges([change], currentOutput);
|
||||
const actual = detectDevChanges(config, changes, currentOutput);
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
@@ -81,6 +120,7 @@ describe('Detect Dev Changes', () => {
|
||||
|
||||
describe('Background', () => {
|
||||
it("should rebuild the background and reload the extension when the changed file in it's chunks' `moduleIds` field", () => {
|
||||
const config = fakeInternalConfig();
|
||||
const changedPath = '/root/utils/shared.ts';
|
||||
const contentScript = fakeContentScriptEntrypoint({
|
||||
inputPath: '/root/overlay.content.ts',
|
||||
@@ -120,10 +160,7 @@ describe('Detect Dev Changes', () => {
|
||||
rebuildGroups: [background],
|
||||
};
|
||||
|
||||
const actual = detectDevChanges(
|
||||
[['unknown', changedPath]],
|
||||
currentOutput,
|
||||
);
|
||||
const actual = detectDevChanges(config, [changedPath], currentOutput);
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
@@ -131,6 +168,7 @@ describe('Detect Dev Changes', () => {
|
||||
|
||||
describe('HTML Pages', () => {
|
||||
it('should rebuild then reload only the effected pages', async () => {
|
||||
const config = fakeInternalConfig();
|
||||
const changedPath = '/root/page1/index.html';
|
||||
const htmlPage1 = fakePopupEntrypoint({
|
||||
inputPath: changedPath,
|
||||
@@ -174,10 +212,7 @@ describe('Detect Dev Changes', () => {
|
||||
rebuildGroups: [[htmlPage1, htmlPage2]],
|
||||
};
|
||||
|
||||
const actual = detectDevChanges(
|
||||
[['unknown', changedPath]],
|
||||
currentOutput,
|
||||
);
|
||||
const actual = detectDevChanges(config, [changedPath], currentOutput);
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
@@ -185,6 +220,7 @@ describe('Detect Dev Changes', () => {
|
||||
|
||||
describe('Content Scripts', () => {
|
||||
it('should rebuild then reload only the effected content scripts', async () => {
|
||||
const config = fakeInternalConfig();
|
||||
const changedPath = '/root/utils/shared.ts';
|
||||
const script1 = fakeContentScriptEntrypoint({
|
||||
inputPath: '/root/overlay1.content/index.ts',
|
||||
@@ -236,10 +272,7 @@ describe('Detect Dev Changes', () => {
|
||||
rebuildGroups: [script1, script3],
|
||||
};
|
||||
|
||||
const actual = detectDevChanges(
|
||||
[['unknown', changedPath]],
|
||||
currentOutput,
|
||||
);
|
||||
const actual = detectDevChanges(config, [changedPath], currentOutput);
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
@@ -2,10 +2,11 @@ import {
|
||||
BuildOutput,
|
||||
BuildStepOutput,
|
||||
EntrypointGroup,
|
||||
InternalConfig,
|
||||
OutputAsset,
|
||||
OutputFile,
|
||||
} from '~/types';
|
||||
import { every } from '~/core/utils/arrays';
|
||||
import { every, some } from '~/core/utils/arrays';
|
||||
import { normalizePath } from '~/core/utils/paths';
|
||||
|
||||
/**
|
||||
@@ -29,10 +30,21 @@ import { normalizePath } from '~/core/utils/paths';
|
||||
* - Config file changed (wxt.config.ts, .env, web-ext.config.ts, etc)
|
||||
*/
|
||||
export function detectDevChanges(
|
||||
changedFiles: [event: string, path: string][],
|
||||
currentOutput: BuildOutput | undefined,
|
||||
config: InternalConfig,
|
||||
changedFiles: string[],
|
||||
currentOutput: BuildOutput,
|
||||
): DevModeChange {
|
||||
if (currentOutput == null) return { type: 'no-change' };
|
||||
const isConfigChange = some(
|
||||
changedFiles,
|
||||
(file) => file === config.userConfigMetadata.configFile,
|
||||
);
|
||||
if (isConfigChange) return { type: 'full-restart' };
|
||||
|
||||
const isRunnerChange = some(
|
||||
changedFiles,
|
||||
(file) => file === config.runnerConfig.configFile,
|
||||
);
|
||||
if (isRunnerChange) return { type: 'browser-restart' };
|
||||
|
||||
const changedSteps = new Set(
|
||||
changedFiles.flatMap((changedFile) =>
|
||||
@@ -69,7 +81,7 @@ export function detectDevChanges(
|
||||
|
||||
const isOnlyHtmlChanges =
|
||||
changedFiles.length > 0 &&
|
||||
every(changedFiles, ([_, file]) => file.endsWith('.html'));
|
||||
every(changedFiles, (file) => file.endsWith('.html'));
|
||||
if (isOnlyHtmlChanges) {
|
||||
return {
|
||||
type: 'html-reload',
|
||||
@@ -104,11 +116,11 @@ export function detectDevChanges(
|
||||
* For a single change, return all the step of the build output that were effected by it.
|
||||
*/
|
||||
function findEffectedSteps(
|
||||
changedFile: [event: string, path: string],
|
||||
changedFile: string,
|
||||
currentOutput: BuildOutput,
|
||||
): DetectedChange[] {
|
||||
const changes: DetectedChange[] = [];
|
||||
const changedPath = normalizePath(changedFile[1]);
|
||||
const changedPath = normalizePath(changedFile);
|
||||
|
||||
const isChunkEffected = (chunk: OutputFile): boolean =>
|
||||
// If it's an HTML file with the same path, is is effected because HTML files need to be pre-rendered
|
||||
@@ -139,8 +151,9 @@ export type DevModeChange =
|
||||
| NoChange
|
||||
| HtmlReload
|
||||
| ExtensionReload
|
||||
| ContentScriptReload;
|
||||
// | BrowserRestart
|
||||
| ContentScriptReload
|
||||
| FullRestart
|
||||
| BrowserRestart;
|
||||
|
||||
interface NoChange {
|
||||
type: 'no-change';
|
||||
@@ -157,6 +170,14 @@ interface RebuildChange {
|
||||
cachedOutput: BuildOutput;
|
||||
}
|
||||
|
||||
interface FullRestart {
|
||||
type: 'full-restart';
|
||||
}
|
||||
|
||||
interface BrowserRestart {
|
||||
type: 'browser-restart';
|
||||
}
|
||||
|
||||
interface HtmlReload extends RebuildChange {
|
||||
type: 'html-reload';
|
||||
}
|
||||
|
||||
@@ -79,6 +79,7 @@ export async function getInternalConfig(
|
||||
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',
|
||||
@@ -136,6 +137,9 @@ export async function getInternalConfig(
|
||||
mergedConfig.experimental?.includeBrowserPolyfill ?? true,
|
||||
},
|
||||
server,
|
||||
dev: {
|
||||
reloadCommand,
|
||||
},
|
||||
};
|
||||
|
||||
const builder = await createViteBuilder(
|
||||
@@ -221,6 +225,10 @@ function mergeInlineConfig(
|
||||
},
|
||||
vite: undefined,
|
||||
transformManifest: undefined,
|
||||
dev: {
|
||||
...userConfig.dev,
|
||||
...inlineConfig.dev,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { findEntrypoints } from './find-entrypoints';
|
||||
import { InternalConfig, BuildOutput } from '~/types';
|
||||
import { InternalConfig, BuildOutput, Entrypoint } from '~/types';
|
||||
import pc from 'picocolors';
|
||||
import fs from 'fs-extra';
|
||||
import { groupEntrypoints } from './group-entrypoints';
|
||||
@@ -8,6 +8,15 @@ import { printBuildSummary } from '~/core/utils/log';
|
||||
import glob from 'fast-glob';
|
||||
import { unnormalizePath } from '~/core/utils/paths';
|
||||
import { rebuild } from './rebuild';
|
||||
import managePath from 'manage-path';
|
||||
import { resolve, relative } from 'node:path';
|
||||
import {
|
||||
ValidationError,
|
||||
ValidationResult,
|
||||
ValidationResults,
|
||||
validateEntrypoints,
|
||||
} from '../validation';
|
||||
import consola from 'consola';
|
||||
|
||||
/**
|
||||
* Builds the extension based on an internal config. No more config discovery is performed, the
|
||||
@@ -37,8 +46,24 @@ export async function internalBuild(
|
||||
|
||||
const entrypoints = await findEntrypoints(config);
|
||||
config.logger.debug('Detected entrypoints:', entrypoints);
|
||||
|
||||
const validationResults = validateEntrypoints(entrypoints);
|
||||
if (validationResults.errorCount + validationResults.warningCount > 0) {
|
||||
printValidationResults(config, validationResults);
|
||||
}
|
||||
if (validationResults.errorCount > 0) {
|
||||
throw new ValidationError(`Entrypoint validation failed`, {
|
||||
cause: validationResults,
|
||||
});
|
||||
}
|
||||
|
||||
const groups = groupEntrypoints(entrypoints);
|
||||
const { output } = await rebuild(config, entrypoints, groups, undefined);
|
||||
const { output, warnings } = await rebuild(
|
||||
config,
|
||||
entrypoints,
|
||||
groups,
|
||||
undefined,
|
||||
);
|
||||
|
||||
// Post-build
|
||||
await printBuildSummary(
|
||||
@@ -48,6 +73,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(
|
||||
@@ -66,6 +95,10 @@ async function combineAnalysisStats(config: InternalConfig): Promise<void> {
|
||||
});
|
||||
const absolutePaths = unixFiles.map(unnormalizePath);
|
||||
|
||||
const alterPath = managePath(process.env);
|
||||
// Add subdependency path for PNPM shamefully-hoist=false
|
||||
alterPath.push(resolve(config.root, 'node_modules/wxt/node_modules/.bin'));
|
||||
|
||||
await execaCommand(
|
||||
`rollup-plugin-visualizer ${absolutePaths.join(' ')} --template ${
|
||||
config.analysis.template
|
||||
@@ -73,3 +106,33 @@ async function combineAnalysisStats(config: InternalConfig): Promise<void> {
|
||||
{ cwd: config.root, stdio: 'inherit' },
|
||||
);
|
||||
}
|
||||
|
||||
function printValidationResults(
|
||||
config: InternalConfig,
|
||||
{ errorCount, errors, warningCount }: ValidationResults,
|
||||
) {
|
||||
(errorCount > 0 ? config.logger.error : config.logger.warn)(
|
||||
`Entrypoint validation failed: ${errorCount} error${
|
||||
errorCount === 1 ? '' : 's'
|
||||
}, ${warningCount} warning${warningCount === 1 ? '' : 's'}`,
|
||||
);
|
||||
|
||||
const cwd = process.cwd();
|
||||
const entrypointErrors = errors.reduce((map, error) => {
|
||||
const entryErrors = map.get(error.entrypoint) ?? [];
|
||||
entryErrors.push(error);
|
||||
map.set(error.entrypoint, entryErrors);
|
||||
return map;
|
||||
}, new Map<Entrypoint, ValidationResult[]>());
|
||||
|
||||
Array.from(entrypointErrors.entries()).forEach(([entrypoint, errors]) => {
|
||||
consola.log(relative(cwd, entrypoint.inputPath));
|
||||
console.log();
|
||||
errors.forEach((err) => {
|
||||
const type = err.type === 'error' ? pc.red('ERROR') : pc.yellow('WARN');
|
||||
const recieved = pc.dim(`(recieved: ${JSON.stringify(err.value)})`);
|
||||
consola.log(` - ${type} ${err.message} ${recieved}`);
|
||||
});
|
||||
console.log();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -33,7 +33,11 @@ export async function rebuild(
|
||||
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();
|
||||
|
||||
@@ -51,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,
|
||||
@@ -77,5 +78,6 @@ export async function rebuild(
|
||||
],
|
||||
},
|
||||
manifest: newManifest,
|
||||
warnings: manifestWarnings,
|
||||
};
|
||||
}
|
||||
|
||||
+23
-12
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
@@ -244,6 +244,9 @@ export const fakeInternalConfig = fakeObjectCreator<InternalConfig>(() => {
|
||||
includeBrowserPolyfill: true,
|
||||
},
|
||||
builder: mock(),
|
||||
dev: {
|
||||
reloadCommand: 'Alt+R',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -257,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,
|
||||
})}`,
|
||||
}));
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { ContentScriptEntrypoint, Entrypoint } from '~/types';
|
||||
|
||||
export function validateEntrypoints(
|
||||
entrypoints: Entrypoint[],
|
||||
): ValidationResults {
|
||||
const errors = entrypoints.flatMap((entrypoint) => {
|
||||
switch (entrypoint.type) {
|
||||
case 'content-script':
|
||||
return validateContentScriptEntrypoint(entrypoint);
|
||||
default:
|
||||
return validateBaseEntrypoint(entrypoint);
|
||||
}
|
||||
});
|
||||
|
||||
let errorCount = 0;
|
||||
let warningCount = 0;
|
||||
for (const err of errors) {
|
||||
if (err.type === 'warning') warningCount++;
|
||||
else errorCount++;
|
||||
}
|
||||
|
||||
return {
|
||||
errors,
|
||||
errorCount,
|
||||
warningCount,
|
||||
};
|
||||
}
|
||||
|
||||
function validateContentScriptEntrypoint(
|
||||
definition: ContentScriptEntrypoint,
|
||||
): ValidationResult[] {
|
||||
const errors = validateBaseEntrypoint(definition);
|
||||
if (definition.options.matches == null) {
|
||||
errors.push({
|
||||
type: 'error',
|
||||
message: '`matches` is required',
|
||||
value: definition.options.matches,
|
||||
entrypoint: definition,
|
||||
});
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
function validateBaseEntrypoint(definition: Entrypoint): ValidationResult[] {
|
||||
const errors: ValidationResult[] = [];
|
||||
|
||||
if (
|
||||
definition.options.exclude != null &&
|
||||
!Array.isArray(definition.options.exclude)
|
||||
) {
|
||||
errors.push({
|
||||
type: 'error',
|
||||
message: '`exclude` must be an array of browser names',
|
||||
value: definition.options.exclude,
|
||||
entrypoint: definition,
|
||||
});
|
||||
}
|
||||
if (
|
||||
definition.options.include != null &&
|
||||
!Array.isArray(definition.options.include)
|
||||
) {
|
||||
errors.push({
|
||||
type: 'error',
|
||||
message: '`include` must be an array of browser names',
|
||||
value: definition.options.include,
|
||||
entrypoint: definition,
|
||||
});
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
export interface ValidationResult {
|
||||
type: 'warning' | 'error';
|
||||
message: string;
|
||||
entrypoint: Entrypoint;
|
||||
value: any;
|
||||
}
|
||||
|
||||
export interface ValidationResults {
|
||||
errors: ValidationResult[];
|
||||
errorCount: number;
|
||||
warningCount: number;
|
||||
}
|
||||
|
||||
export class ValidationError extends Error {}
|
||||
+36
-2
@@ -229,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
|
||||
@@ -290,16 +308,24 @@ export interface BuildStepOutput {
|
||||
}
|
||||
|
||||
export interface WxtDevServer
|
||||
extends Omit<WxtBuilderServer, 'listen'>,
|
||||
extends Omit<WxtBuilderServer, 'listen' | 'close'>,
|
||||
ServerInfo {
|
||||
/**
|
||||
* Stores the current build output of the server.
|
||||
*/
|
||||
currentOutput: BuildOutput;
|
||||
currentOutput: BuildOutput | undefined;
|
||||
/**
|
||||
* Start the server.
|
||||
*/
|
||||
start(): Promise<void>;
|
||||
/**
|
||||
* Stop the server.
|
||||
*/
|
||||
stop(): Promise<void>;
|
||||
/**
|
||||
* Close the browser, stop the server, rebuild the entire extension, and start the server again.
|
||||
*/
|
||||
restart(): Promise<void>;
|
||||
/**
|
||||
* Transform the HTML for dev mode.
|
||||
*/
|
||||
@@ -332,6 +358,10 @@ export interface WxtDevServer
|
||||
reloadContentScript: (
|
||||
contentScript: Omit<Scripting.RegisteredContentScript, 'id'>,
|
||||
) => void;
|
||||
/**
|
||||
* Grab the latest runner config and restart the browser.
|
||||
*/
|
||||
restartBrowser: () => void;
|
||||
}
|
||||
|
||||
export type TargetBrowser = string;
|
||||
@@ -674,6 +704,10 @@ export interface WxtBuilderServer {
|
||||
* Start the server.
|
||||
*/
|
||||
listen(): Promise<void>;
|
||||
/**
|
||||
* Stop the server.
|
||||
*/
|
||||
close(): Promise<void>;
|
||||
/**
|
||||
* Transform the HTML for dev mode.
|
||||
*/
|
||||
|
||||
@@ -57,6 +57,9 @@ export interface InternalConfig {
|
||||
includeBrowserPolyfill: boolean;
|
||||
};
|
||||
builder: WxtBuilder;
|
||||
dev: {
|
||||
reloadCommand: string | false;
|
||||
};
|
||||
}
|
||||
|
||||
export interface FsCache {
|
||||
|
||||
Vendored
+4
@@ -50,6 +50,10 @@ declare module 'web-ext-run/util/logger' {
|
||||
export const consoleStream: IConsoleStream;
|
||||
}
|
||||
|
||||
declare module 'manage-path' {
|
||||
export default function managePath(env: object): string[];
|
||||
}
|
||||
|
||||
declare module 'wxt/browser' {
|
||||
// Overridden when types are generated per project
|
||||
export type PublicPath = string;
|
||||
|
||||
Reference in New Issue
Block a user