diff --git a/packages/wxt-demo/wxt.config.ts b/packages/wxt-demo/wxt.config.ts index f732c1c3e..6961eea29 100644 --- a/packages/wxt-demo/wxt.config.ts +++ b/packages/wxt-demo/wxt.config.ts @@ -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: { diff --git a/packages/wxt/package.json b/packages/wxt/package.json index 29f1ac720..b8f13fca7 100644 --- a/packages/wxt/package.json +++ b/packages/wxt/package.json @@ -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", diff --git a/packages/wxt/src/core/builders/vite/index.ts b/packages/wxt/src/core/builders/vite/index.ts index 6ab15ab4a..d5a612d92 100644 --- a/packages/wxt/src/core/builders/vite/index.ts +++ b/packages/wxt/src/core/builders/vite/index.ts @@ -28,7 +28,7 @@ import { installSourcemapsSupport } from 'vite-node/source-map'; export async function createViteBuilder( wxtConfig: ResolvedConfig, hooks: Hookable, - server?: WxtDevServer, + getWxtDevServer?: () => WxtDevServer | undefined, ): Promise { 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 { diff --git a/packages/wxt/src/core/create-server.ts b/packages/wxt/src/core/create-server.ts index aa834ae07..0a04eb39c 100644 --- a/packages/wxt/src/core/create-server.ts +++ b/packages/wxt/src/core/create-server.ts @@ -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 { - 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 { + 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; } diff --git a/packages/wxt/src/core/resolve-config.ts b/packages/wxt/src/core/resolve-config.ts index cb6f40cb1..0f91ba516 100644 --- a/packages/wxt/src/core/resolve-config.ts +++ b/packages/wxt/src/core/resolve-config.ts @@ -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, }; } diff --git a/packages/wxt/src/core/utils/building/__tests__/detect-dev-changes.test.ts b/packages/wxt/src/core/utils/building/__tests__/detect-dev-changes.test.ts index a94e6dfcc..838e2061f 100644 --- a/packages/wxt/src/core/utils/building/__tests__/detect-dev-changes.test.ts +++ b/packages/wxt/src/core/utils/building/__tests__/detect-dev-changes.test.ts @@ -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'; diff --git a/packages/wxt/src/core/utils/building/detect-dev-changes.ts b/packages/wxt/src/core/utils/building/detect-dev-changes.ts index 9f7ffbd8a..b58842882 100644 --- a/packages/wxt/src/core/utils/building/detect-dev-changes.ts +++ b/packages/wxt/src/core/utils/building/detect-dev-changes.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, diff --git a/packages/wxt/src/core/wxt.ts b/packages/wxt/src/core/wxt.ts index a95c27ae4..d460542c9 100644 --- a/packages/wxt/src/core/wxt.ts +++ b/packages/wxt/src/core/wxt.ts @@ -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, ): Promise { // 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(); 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 = { @@ -50,27 +40,25 @@ export async function registerWxt( }, pm, builder, - server, + server: undefined, }; - // Initialize modules - const initModule = async (module: WxtModule) => { - 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', @@ -84,6 +72,22 @@ export async function registerWxt( await wxt.hooks.callHook('ready', wxt); } +async function initWxtModule(module: WxtModule): Promise { + 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. * diff --git a/packages/wxt/src/types.ts b/packages/wxt/src/types.ts index 3ad48acfe..e03ca4c3f 100644 --- a/packages/wxt/src/types.ts +++ b/packages/wxt/src/types.ts @@ -1239,7 +1239,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; /** diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c5884d35a..80e4e0619 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: