fix: Re-initialize WXT modules correctly during development (#1176)

This commit is contained in:
Aaron
2024-11-16 08:56:08 -06:00
committed by GitHub
parent 57229c69c1
commit db025cd3a2
10 changed files with 177 additions and 116 deletions
+1 -1
View File
@@ -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: {
+1 -1
View File
@@ -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",
+4 -2
View File
@@ -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 {
+88 -70
View File
@@ -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;
}
+9 -3
View File
@@ -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,
+30 -26
View File
@@ -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.
*
+1 -1
View File
@@ -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>;
/**
+6 -11
View File
@@ -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: