fix: Re-initialize WXT modules correctly during development (#1176)
This commit is contained in:
@@ -25,7 +25,7 @@ export default defineConfig({
|
||||
},
|
||||
example: {
|
||||
a: 'a',
|
||||
// @ts-expect-error: c is not defined, this should error out
|
||||
// @ts-expect-error: c is not defined, this should be a type error, but it should show up in the module
|
||||
c: 'c',
|
||||
},
|
||||
unocss: {
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
"fast-glob": "^3.3.2",
|
||||
"filesize": "^10.1.6",
|
||||
"fs-extra": "^11.2.0",
|
||||
"get-port": "^7.1.0",
|
||||
"get-port-please": "^3.1.2",
|
||||
"giget": "^1.2.3",
|
||||
"hookable": "^5.5.3",
|
||||
"is-wsl": "^3.1.0",
|
||||
|
||||
@@ -28,7 +28,7 @@ import { installSourcemapsSupport } from 'vite-node/source-map';
|
||||
export async function createViteBuilder(
|
||||
wxtConfig: ResolvedConfig,
|
||||
hooks: Hookable<WxtHooks>,
|
||||
server?: WxtDevServer,
|
||||
getWxtDevServer?: () => WxtDevServer | undefined,
|
||||
): Promise<WxtBuilder> {
|
||||
const vite = await import('vite');
|
||||
|
||||
@@ -65,6 +65,8 @@ export async function createViteBuilder(
|
||||
ignored: [`${wxtConfig.outBaseDir}/**`, `${wxtConfig.wxtDir}/**`],
|
||||
};
|
||||
|
||||
const server = getWxtDevServer?.();
|
||||
|
||||
config.plugins ??= [];
|
||||
config.plugins.push(
|
||||
wxtPlugins.download(wxtConfig),
|
||||
@@ -193,7 +195,7 @@ export async function createViteBuilder(
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the basic config for building a sinlge CSS entrypoint in [multi-page mode](https://vitejs.dev/guide/build.html#multi-page-app).
|
||||
* Return the basic config for building a single CSS entrypoint in [multi-page mode](https://vitejs.dev/guide/build.html#multi-page-app).
|
||||
*/
|
||||
const getCssConfig = (entrypoint: Entrypoint): vite.InlineConfig => {
|
||||
return {
|
||||
|
||||
@@ -21,7 +21,7 @@ import { createExtensionRunner } from './runners';
|
||||
import { Mutex } from 'async-mutex';
|
||||
import pc from 'picocolors';
|
||||
import { relative } from 'node:path';
|
||||
import { registerWxt, wxt } from './wxt';
|
||||
import { deinitWxtModules, initWxtModules, registerWxt, wxt } from './wxt';
|
||||
import { unnormalizePath } from './utils/paths';
|
||||
import {
|
||||
getContentScriptJs,
|
||||
@@ -40,64 +40,102 @@ import {
|
||||
export async function createServer(
|
||||
inlineConfig?: InlineConfig,
|
||||
): Promise<WxtDevServer> {
|
||||
await registerWxt('serve', inlineConfig, async (config) => {
|
||||
const { port, hostname } = config.dev.server!;
|
||||
const serverInfo: ServerInfo = {
|
||||
await registerWxt('serve', inlineConfig);
|
||||
|
||||
return (wxt.server = await createServerInternal());
|
||||
}
|
||||
|
||||
async function createServerInternal(): Promise<WxtDevServer> {
|
||||
const getServerInfo = (): ServerInfo => {
|
||||
const { port, hostname } = wxt.config.dev.server!;
|
||||
return {
|
||||
port,
|
||||
hostname,
|
||||
origin: `http://${hostname}:${port}`,
|
||||
};
|
||||
};
|
||||
|
||||
// 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,
|
||||
get watcher() {
|
||||
return builderServer.watcher;
|
||||
},
|
||||
get ws() {
|
||||
return builderServer.ws;
|
||||
},
|
||||
currentOutput: undefined,
|
||||
async start() {
|
||||
await builderServer.listen();
|
||||
wxt.logger.success(`Started dev server @ ${serverInfo.origin}`);
|
||||
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);
|
||||
},
|
||||
reloadContentScript(payload) {
|
||||
server.ws.send('wxt:reload-content-script', payload);
|
||||
},
|
||||
reloadPage(path) {
|
||||
server.ws.send('wxt:reload-page', path);
|
||||
},
|
||||
reloadExtension() {
|
||||
server.ws.send('wxt:reload-extension');
|
||||
},
|
||||
async restartBrowser() {
|
||||
await closeAndRecreateRunner();
|
||||
await runner.openBrowser();
|
||||
},
|
||||
};
|
||||
return server;
|
||||
});
|
||||
|
||||
const server = wxt.server!;
|
||||
let [runner, builderServer] = await Promise.all([
|
||||
createExtensionRunner(),
|
||||
wxt.builder.createServer(server),
|
||||
wxt.builder.createServer(getServerInfo()),
|
||||
]);
|
||||
|
||||
// Used to track if modules need to be re-initialized
|
||||
let wasStopped = false;
|
||||
|
||||
// Server instance must be created first so its reference can be added to the internal config used
|
||||
// to pre-render entrypoints
|
||||
const server: WxtDevServer = {
|
||||
get hostname() {
|
||||
return getServerInfo().hostname;
|
||||
},
|
||||
get port() {
|
||||
return getServerInfo().port;
|
||||
},
|
||||
get origin() {
|
||||
return getServerInfo().origin;
|
||||
},
|
||||
get watcher() {
|
||||
return builderServer.watcher;
|
||||
},
|
||||
get ws() {
|
||||
return builderServer.ws;
|
||||
},
|
||||
currentOutput: undefined,
|
||||
async start() {
|
||||
if (wasStopped) {
|
||||
await wxt.reloadConfig();
|
||||
runner = await createExtensionRunner();
|
||||
builderServer = await wxt.builder.createServer(getServerInfo());
|
||||
await initWxtModules();
|
||||
}
|
||||
|
||||
await builderServer.listen();
|
||||
wxt.logger.success(`Started dev server @ ${server.origin}`);
|
||||
await buildAndOpenBrowser();
|
||||
|
||||
// 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, server);
|
||||
});
|
||||
|
||||
// Listen for file changes and reload different parts of the extension accordingly
|
||||
const reloadOnChange = createFileReloader(server);
|
||||
server.watcher.on('all', reloadOnChange);
|
||||
},
|
||||
async stop() {
|
||||
wasStopped = true;
|
||||
await runner.closeBrowser();
|
||||
await builderServer.close();
|
||||
deinitWxtModules();
|
||||
server.currentOutput = undefined;
|
||||
},
|
||||
async restart() {
|
||||
await server.stop();
|
||||
await server.start();
|
||||
},
|
||||
transformHtml(url, html, originalUrl) {
|
||||
return builderServer.transformHtml(url, html, originalUrl);
|
||||
},
|
||||
reloadContentScript(payload) {
|
||||
server.ws.send('wxt:reload-content-script', payload);
|
||||
},
|
||||
reloadPage(path) {
|
||||
server.ws.send('wxt:reload-page', path);
|
||||
},
|
||||
reloadExtension() {
|
||||
server.ws.send('wxt:reload-extension');
|
||||
},
|
||||
async restartBrowser() {
|
||||
await runner.closeBrowser();
|
||||
await wxt.reloadConfig();
|
||||
runner = await createExtensionRunner();
|
||||
await runner.openBrowser();
|
||||
},
|
||||
};
|
||||
|
||||
const buildAndOpenBrowser = async () => {
|
||||
// Build after starting the dev server so it can be used to transform HTML files
|
||||
server.currentOutput = await internalBuild();
|
||||
@@ -114,26 +152,6 @@ export async function createServer(
|
||||
await runner.openBrowser();
|
||||
};
|
||||
|
||||
/**
|
||||
* Stops the previous runner, grabs the latest config, and recreates the runner.
|
||||
*/
|
||||
const closeAndRecreateRunner = async () => {
|
||||
await runner.closeBrowser();
|
||||
await wxt.reloadConfig();
|
||||
runner = await createExtensionRunner();
|
||||
};
|
||||
|
||||
// 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, server);
|
||||
});
|
||||
|
||||
// Listen for file changes and reload different parts of the extension accordingly
|
||||
const reloadOnChange = createFileReloader(server);
|
||||
server.watcher.on('all', reloadOnChange);
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import { builtinModules } from '../builtin-modules';
|
||||
import { getEslintVersion } from './utils/eslint';
|
||||
import { safeStringToNumber } from './utils/number';
|
||||
import { loadEnv } from './utils/env';
|
||||
import { getPort } from 'get-port-please';
|
||||
|
||||
/**
|
||||
* Given an inline config, discover the config file if necessary, merge the results, resolve any
|
||||
@@ -137,14 +138,19 @@ export async function resolveConfig(
|
||||
|
||||
let devServerConfig: ResolvedConfig['dev']['server'];
|
||||
if (command === 'serve') {
|
||||
const hostname = mergedConfig.dev?.server?.hostname ?? 'localhost';
|
||||
let port = mergedConfig.dev?.server?.port;
|
||||
if (port == null || !isFinite(port)) {
|
||||
const { default: getPort, portNumbers } = await import('get-port');
|
||||
port = await getPort({ port: portNumbers(3000, 3010) });
|
||||
port = await getPort({
|
||||
port: 3000,
|
||||
portRange: [3001, 3010],
|
||||
// Passing host required for Mac, unsure of Windows/Linux
|
||||
host: hostname,
|
||||
});
|
||||
}
|
||||
devServerConfig = {
|
||||
port,
|
||||
hostname: mergedConfig.dev?.server?.hostname ?? 'localhost',
|
||||
hostname,
|
||||
watchDebounce: safeStringToNumber(process.env.WXT_WATCH_DEBOUNCE) ?? 800,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -71,6 +71,33 @@ describe('Detect Dev Changes', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('modules/*', () => {
|
||||
it("should return 'full-restart' when one of the changed files is in the WXT modules folder", () => {
|
||||
const modulesDir = '/root/modules';
|
||||
setFakeWxt({
|
||||
config: {
|
||||
modulesDir,
|
||||
},
|
||||
});
|
||||
const changes = [
|
||||
'/root/src/public/image.svg',
|
||||
`${modulesDir}/example.ts`,
|
||||
];
|
||||
const currentOutput: BuildOutput = {
|
||||
manifest: fakeManifest(),
|
||||
publicAssets: [],
|
||||
steps: [],
|
||||
};
|
||||
const expected: DevModeChange = {
|
||||
type: 'full-restart',
|
||||
};
|
||||
|
||||
const actual = detectDevChanges(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';
|
||||
|
||||
@@ -26,7 +26,11 @@ import { wxt } from '../../wxt';
|
||||
* - Background script is changed
|
||||
* - Manifest is different
|
||||
* - Restart browser
|
||||
* - Config file changed (wxt.config.ts, .env, web-ext.config.ts, etc)
|
||||
* - web-ext.config.ts (runner config changes)
|
||||
* - Full dev server restart
|
||||
* - wxt.config.ts (main config file)
|
||||
* - modules/* (any file related to WXT modules)
|
||||
* - .env (environment variable changed could effect build)
|
||||
*/
|
||||
export function detectDevChanges(
|
||||
changedFiles: string[],
|
||||
@@ -38,6 +42,11 @@ export function detectDevChanges(
|
||||
);
|
||||
if (isConfigChange) return { type: 'full-restart' };
|
||||
|
||||
const isWxtModuleChange = some(changedFiles, (file) =>
|
||||
file.startsWith(wxt.config.modulesDir),
|
||||
);
|
||||
if (isWxtModuleChange) return { type: 'full-restart' };
|
||||
|
||||
const isRunnerChange = some(
|
||||
changedFiles,
|
||||
(file) => file === wxt.config.runnerConfig.configFile,
|
||||
|
||||
@@ -1,12 +1,4 @@
|
||||
import {
|
||||
InlineConfig,
|
||||
ResolvedConfig,
|
||||
Wxt,
|
||||
WxtCommand,
|
||||
WxtDevServer,
|
||||
WxtHooks,
|
||||
WxtModule,
|
||||
} from '../types';
|
||||
import { InlineConfig, Wxt, WxtCommand, WxtHooks, WxtModule } from '../types';
|
||||
import { resolveConfig } from './resolve-config';
|
||||
import { createHooks } from 'hookable';
|
||||
import { createWxtPackageManager } from './package-managers';
|
||||
@@ -26,7 +18,6 @@ export let wxt: Wxt;
|
||||
export async function registerWxt(
|
||||
command: WxtCommand,
|
||||
inlineConfig: InlineConfig = {},
|
||||
getServer?: (config: ResolvedConfig) => Promise<WxtDevServer>,
|
||||
): Promise<void> {
|
||||
// Default NODE_ENV environment variable before other packages, like vite, do it
|
||||
// See https://github.com/wxt-dev/wxt/issues/873#issuecomment-2254555523
|
||||
@@ -34,8 +25,7 @@ export async function registerWxt(
|
||||
|
||||
const hooks = createHooks<WxtHooks>();
|
||||
const config = await resolveConfig(inlineConfig, command);
|
||||
const server = await getServer?.(config);
|
||||
const builder = await createViteBuilder(config, hooks, server);
|
||||
const builder = await createViteBuilder(config, hooks, () => wxt.server);
|
||||
const pm = await createWxtPackageManager(config.root);
|
||||
|
||||
wxt = {
|
||||
@@ -51,27 +41,25 @@ export async function registerWxt(
|
||||
},
|
||||
pm,
|
||||
builder,
|
||||
server,
|
||||
server: undefined,
|
||||
};
|
||||
|
||||
// Initialize modules
|
||||
const initModule = async (module: WxtModule<any>) => {
|
||||
if (module.hooks) wxt.hooks.addHooks(module.hooks);
|
||||
await module.setup?.(
|
||||
wxt,
|
||||
// @ts-expect-error: Untyped configKey field
|
||||
module.configKey ? config[module.configKey] : undefined,
|
||||
);
|
||||
};
|
||||
for (const builtinModule of builtinModules) await initModule(builtinModule);
|
||||
for (const userModule of config.userModules) await initModule(userModule);
|
||||
await initWxtModules();
|
||||
}
|
||||
|
||||
export async function initWxtModules() {
|
||||
// Call setup function and add hooks
|
||||
for (const mod of builtinModules) await initWxtModule(mod);
|
||||
for (const mod of wxt.config.userModules) await initWxtModule(mod);
|
||||
|
||||
// Initialize hooks
|
||||
wxt.hooks.addHooks(config.hooks);
|
||||
wxt.hooks.addHooks(wxt.config.hooks);
|
||||
|
||||
// Print order for debugging
|
||||
if (wxt.config.debug) {
|
||||
const order = [
|
||||
...builtinModules.map((module) => module.name),
|
||||
...config.userModules.map((module) =>
|
||||
...wxt.config.userModules.map((module) =>
|
||||
relative(wxt.config.root, module.id),
|
||||
),
|
||||
'wxt.config.ts > hooks',
|
||||
@@ -86,6 +74,22 @@ export async function registerWxt(
|
||||
await wxt.hooks.callHook('config:resolved', wxt);
|
||||
}
|
||||
|
||||
async function initWxtModule(module: WxtModule<any>): Promise<void> {
|
||||
if (module.hooks) wxt.hooks.addHooks(module.hooks);
|
||||
await module.setup?.(
|
||||
wxt,
|
||||
// @ts-expect-error: Untyped configKey field
|
||||
module.configKey ? wxt.config[module.configKey] : undefined,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unloads WXT modules.
|
||||
*/
|
||||
export function deinitWxtModules(): void {
|
||||
wxt.hooks.removeAllHooks();
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal ONLY USE FOR TESTING.
|
||||
*
|
||||
|
||||
@@ -1243,7 +1243,7 @@ export interface Wxt {
|
||||
*/
|
||||
logger: Logger;
|
||||
/**
|
||||
* Reload config file and update the `config` field with the result.
|
||||
* Reload config file and update `wxt.config` with the result.
|
||||
*/
|
||||
reloadConfig: () => Promise<void>;
|
||||
/**
|
||||
|
||||
Generated
+6
-11
@@ -384,9 +384,9 @@ importers:
|
||||
fs-extra:
|
||||
specifier: ^11.2.0
|
||||
version: 11.2.0
|
||||
get-port:
|
||||
specifier: ^7.1.0
|
||||
version: 7.1.0
|
||||
get-port-please:
|
||||
specifier: ^3.1.2
|
||||
version: 3.1.2
|
||||
giget:
|
||||
specifier: ^1.2.3
|
||||
version: 1.2.3
|
||||
@@ -3006,9 +3006,8 @@ packages:
|
||||
get-func-name@2.0.2:
|
||||
resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==}
|
||||
|
||||
get-port@7.1.0:
|
||||
resolution: {integrity: sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw==}
|
||||
engines: {node: '>=16'}
|
||||
get-port-please@3.1.2:
|
||||
resolution: {integrity: sha512-Gxc29eLs1fbn6LQ4jSU4vXjlwyZhF5HsGuMAa7gqBP4Rw4yxxltyDUuF5MBclFzDTXO+ACchGQoeela4DSfzdQ==}
|
||||
|
||||
get-stream@5.2.0:
|
||||
resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==}
|
||||
@@ -3042,12 +3041,10 @@ packages:
|
||||
|
||||
glob@6.0.4:
|
||||
resolution: {integrity: sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A==}
|
||||
deprecated: Glob versions prior to v9 are no longer supported
|
||||
|
||||
glob@8.1.0:
|
||||
resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==}
|
||||
engines: {node: '>=12'}
|
||||
deprecated: Glob versions prior to v9 are no longer supported
|
||||
|
||||
global-dirs@3.0.1:
|
||||
resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==}
|
||||
@@ -3177,7 +3174,6 @@ packages:
|
||||
|
||||
inflight@1.0.6:
|
||||
resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
|
||||
deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
|
||||
|
||||
inherits@2.0.4:
|
||||
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||
@@ -4287,7 +4283,6 @@ packages:
|
||||
|
||||
rimraf@2.4.5:
|
||||
resolution: {integrity: sha512-J5xnxTyqaiw06JjMftq7L9ouA448dw/E7dKghkP9WpKNuwmARNNg+Gk8/u5ryb9N/Yo2+z3MCwuqFK/+qPOPfQ==}
|
||||
deprecated: Rimraf versions prior to v4 are no longer supported
|
||||
hasBin: true
|
||||
|
||||
rollup-plugin-dts@6.1.1:
|
||||
@@ -7586,7 +7581,7 @@ snapshots:
|
||||
|
||||
get-func-name@2.0.2: {}
|
||||
|
||||
get-port@7.1.0: {}
|
||||
get-port-please@3.1.2: {}
|
||||
|
||||
get-stream@5.2.0:
|
||||
dependencies:
|
||||
|
||||
Reference in New Issue
Block a user