From 6c7623138e95a138fcf19631cc46b12b2eeab11f Mon Sep 17 00:00:00 2001 From: Aaron Date: Sat, 3 Oct 2026 14:19:07 -0500 Subject: [PATCH 1/8] feat: Overridable `runner` and `entrypointFinder` --- packages/wxt/e2e/tests/runners.test.ts | 239 ------------------ .../wxt/src/core/entrypoint-finders/flat.ts | 108 ++++++++ packages/wxt/src/core/index.ts | 3 + packages/wxt/src/core/resolve-config.ts | 31 +-- .../core/runners/__tests__/web-ext.test.ts | 233 +++++++++++++++++ packages/wxt/src/core/runners/manual.ts | 37 ++- packages/wxt/src/core/runners/safari.ts | 20 -- packages/wxt/src/core/runners/web-ext.ts | 97 +++++-- packages/wxt/src/core/runners/wsl.ts | 20 -- .../__tests__/find-entrypoints.test.ts | 6 +- .../core/utils/building/find-entrypoints.ts | 91 +------ packages/wxt/src/core/wxt.ts | 19 +- packages/wxt/src/types.ts | 50 +++- 13 files changed, 516 insertions(+), 438 deletions(-) delete mode 100644 packages/wxt/e2e/tests/runners.test.ts create mode 100644 packages/wxt/src/core/entrypoint-finders/flat.ts create mode 100644 packages/wxt/src/core/runners/__tests__/web-ext.test.ts delete mode 100644 packages/wxt/src/core/runners/safari.ts delete mode 100644 packages/wxt/src/core/runners/wsl.ts diff --git a/packages/wxt/e2e/tests/runners.test.ts b/packages/wxt/e2e/tests/runners.test.ts deleted file mode 100644 index 211af3b22..000000000 --- a/packages/wxt/e2e/tests/runners.test.ts +++ /dev/null @@ -1,239 +0,0 @@ -import { expect, describe, vi, it, beforeEach } from 'vitest'; -import { ExtensionRunner, WxtCommand } from '../../src'; -import { createSafariRunner } from '../../src/core/runners/safari'; -import { createManualRunner } from '../../src/core/runners/manual'; -import { createWebExtRunner } from '../../src/core/runners/web-ext'; -import { createWslRunner } from '../../src/core/runners/wsl'; -import { TestProject } from '../utils'; -import { wxt } from '../../src/core/wxt'; - -// Globals for modifying mock behaviors - -let isWsl = false; -let importWebExtRunnerError: Error | undefined = undefined; - -// Mock runners to create constants for checking equality - -type TestExtensionRunner = { name: string } & ExtensionRunner; - -function createMockExtensionRunner(name: string): TestExtensionRunner { - return { - name, - closeBrowser: () => Promise.resolve(), - openBrowser: () => Promise.resolve(), - }; -} - -vi.mock('../../src/core/runners/safari', () => { - const runner = createMockExtensionRunner('safari'); - return { createSafariRunner: () => runner }; -}); -const safariRunner = createSafariRunner(); - -vi.mock('../../src/core/runners/manual', () => { - const runner = createMockExtensionRunner('manual'); - return { createManualRunner: () => runner }; -}); -const manualRunner = createManualRunner(); - -vi.mock('../../src/core/runners/web-ext', () => { - const runner = createMockExtensionRunner('web-ext'); - return { - createWebExtRunner: () => { - if (!importWebExtRunnerError) return runner; - else throw importWebExtRunnerError; - }, - }; -}); -const webExtRunner = createWebExtRunner(); - -vi.mock('../../src/core/runners/wsl', () => { - const runner = createMockExtensionRunner('wsl'); - return { createWslRunner: () => runner }; -}); -const wslRunner = createWslRunner(); - -// Other mocks - -vi.mock('is-wsl', () => ({ - get default() { - return isWsl; - }, -})); - -/** Imitate a real module not found error - needs the correct `code` property. */ -class ModuleNotFoundError extends Error { - code = 'ERR_MODULE_NOT_FOUND'; - - constructor(mod: string) { - super(`Cannot find package '${mod}' imported from ...`); - this.name = 'ModuleNotFoundError'; - } -} - -describe('Runners', () => { - beforeEach(() => { - isWsl = false; - importWebExtRunnerError = undefined; - }); - - describe('config', () => { - describe('Merging webExt sources', () => { - const CONFIG_FILE_VALUE = 'config-file'; - const INLINE_VALUE = 'inline'; - const USER_VALUE = 'user'; - - async function runTest( - inline: boolean, - configFile: boolean, - user: boolean, - expected: string, - ) { - const project = TestProject.simple(); - if (configFile) - project.addFile( - 'web-ext.config.ts', - ` - import { defineWebExtConfig } from 'wxt' - - export default defineWebExtConfig({ - src: "${CONFIG_FILE_VALUE}", - }) - `, - ); - if (user) - project.addFile( - 'wxt.config.ts', - ` - import { defineConfig } from 'wxt' - - export default defineConfig({ - webExt: { - src: "${USER_VALUE}", - } - }) - `, - ); - - await project.prepare({ - // @ts-expect-error: Random fields should be allowed through - webExt: inline ? { src: INLINE_VALUE } : {}, - }); - - expect(wxt.config.webExt.config).toEqual({ - src: expected, - }); - } - - it('should prioritize the inline config first', async () => { - await runTest(true, true, true, INLINE_VALUE); - }); - - it('should prioritize the config file second', async () => { - await runTest(false, true, true, CONFIG_FILE_VALUE); - }); - - it('should prioritize the user config third', async () => { - await runTest(false, false, true, USER_VALUE); - }); - }); - - it.each(['build', 'serve'])( - 'should use the manual runner when disabled from web-ext.config.ts file for %s', - async (cmd) => { - const project = TestProject.simple(); - project.addFile( - 'web-ext.config.ts', - ` - import { defineWebExtConfig } from 'wxt' - - export default defineWebExtConfig({ - disabled: true, - }) - `, - ); - await project.registerWxt(cmd); - - expect(wxt.config.runner).toBe(manualRunner); - }, - ); - }); - - describe('build', () => { - const command = 'build'; - - it('should use the manual runner as a placeholder since the runner is not used during builds', async () => { - await TestProject.simple().registerWxt(command); - - expect(wxt.config.runner).toBe(manualRunner); - }); - }); - - describe('dev', () => { - const command = 'serve'; - - describe('inside WSL', () => { - beforeEach(() => { - isWsl = true; - }); - - it('should use the WSL runner', async () => { - await TestProject.simple().registerWxt(command); - - expect(wxt.config.runner).toBe(wslRunner); - }); - }); - - describe('web-ext is installed', () => { - it('should use the web-ext runner', async () => { - await TestProject.simple().registerWxt(command); - - expect(wxt.config.runner).toBe(webExtRunner); - }); - - describe('disabled', () => { - it('should use the manual runner', async () => { - await TestProject.simple().registerWxt(command, { - webExt: { disabled: true }, - }); - - expect(wxt.config.runner).toBe(manualRunner); - }); - }); - }); - - describe('web-ext is not installed', () => { - beforeEach(() => { - importWebExtRunnerError = new ModuleNotFoundError('web-ext'); - }); - - it('should use the manual runner', async () => { - await TestProject.simple().registerWxt(command); - - expect(wxt.config.runner).toBe(manualRunner); - }); - }); - - describe('some other error when importing the web-ext runner', () => { - beforeEach(() => { - importWebExtRunnerError = Error('test'); - }); - - it('should throw the error', async () => { - await expect(TestProject.simple().registerWxt(command)).rejects.toThrow( - importWebExtRunnerError, - ); - }); - }); - - describe('targeting safari', () => { - it('should use the safari runner', async () => { - await TestProject.simple().registerWxt(command, { - browser: 'safari', - }); - - expect(wxt.config.runner).toBe(safariRunner); - }); - }); - }); -}); diff --git a/packages/wxt/src/core/entrypoint-finders/flat.ts b/packages/wxt/src/core/entrypoint-finders/flat.ts new file mode 100644 index 000000000..56a839517 --- /dev/null +++ b/packages/wxt/src/core/entrypoint-finders/flat.ts @@ -0,0 +1,108 @@ +import type { + Entrypoint, + EntrypointFinder, + EntrypointInfo, + ResolvedConfig, +} from '../../types'; +import { VIRTUAL_NOOP_BACKGROUND_MODULE_ID } from '../utils/constants'; +import { CSS_EXTENSIONS_PATTERN } from '../utils/paths'; +import picomatch from 'picomatch'; +import { glob } from 'tinyglobby'; +import { getEntrypointName } from '../utils/entrypoints'; +import { resolve } from 'node:path'; + +const PATH_GLOB_TO_TYPE_MAP: Record = { + 'sandbox.html': 'sandbox', + 'sandbox/index.html': 'sandbox', + '*.sandbox.html': 'sandbox', + '*.sandbox/index.html': 'sandbox', + + 'bookmarks.html': 'bookmarks', + 'bookmarks/index.html': 'bookmarks', + + 'history.html': 'history', + 'history/index.html': 'history', + + 'newtab.html': 'newtab', + 'newtab/index.html': 'newtab', + + 'sidepanel.html': 'sidepanel', + 'sidepanel/index.html': 'sidepanel', + '*.sidepanel.html': 'sidepanel', + '*.sidepanel/index.html': 'sidepanel', + + 'devtools.html': 'devtools', + 'devtools/index.html': 'devtools', + + 'background.[jt]s': 'background', + 'background/index.[jt]s': 'background', + [VIRTUAL_NOOP_BACKGROUND_MODULE_ID]: 'background', + + 'content.[jt]s?(x)': 'content-script', + 'content/index.[jt]s?(x)': 'content-script', + '*.content.[jt]s?(x)': 'content-script', + '*.content/index.[jt]s?(x)': 'content-script', + [`content.${CSS_EXTENSIONS_PATTERN}`]: 'content-script-style', + [`*.content.${CSS_EXTENSIONS_PATTERN}`]: 'content-script-style', + [`content/index.${CSS_EXTENSIONS_PATTERN}`]: 'content-script-style', + [`*.content/index.${CSS_EXTENSIONS_PATTERN}`]: 'content-script-style', + + 'popup.html': 'popup', + 'popup/index.html': 'popup', + + 'options.html': 'options', + 'options/index.html': 'options', + + '*.html': 'unlisted-page', + '*/index.html': 'unlisted-page', + '*.[jt]s?(x)': 'unlisted-script', + '*/index.[jt]s?(x)': 'unlisted-script', + [`*.${CSS_EXTENSIONS_PATTERN}`]: 'unlisted-style', + [`*/index.${CSS_EXTENSIONS_PATTERN}`]: 'unlisted-style', +}; + +/** + * WXT's default entrypoint finder. It looks up entrypoints using a flat folder + * structure inside the `entrypoints.` directory, as described in the docs: + * https://wxt.dev/guide/essentials/entrypoints.html + */ +export class FlatEntrypointFinder implements EntrypointFinder { + constructor(public config: ResolvedConfig) {} + + async findEntrypoints(): Promise { + const relativePaths = await glob(Object.keys(PATH_GLOB_TO_TYPE_MAP), { + cwd: this.config.entrypointsDir, + expandDirectories: false, + }); + // Ensure consistent output + relativePaths.sort(); + + const pathGlobs = Object.keys(PATH_GLOB_TO_TYPE_MAP); + return relativePaths + .reduce((results, relativePath) => { + const inputPath = resolve(this.config.entrypointsDir, relativePath); + const name = getEntrypointName(this.config.entrypointsDir, inputPath); + const matchingGlob = pathGlobs.find((glob) => + picomatch(glob)(relativePath), + ); + if (matchingGlob) { + const type = PATH_GLOB_TO_TYPE_MAP[matchingGlob]; + results.push({ name, inputPath, type }); + } + return results; + }, []) + .filter(({ name, inputPath }, _, entrypointInfos) => { + // Remove /index.* if /index.html exists + if (inputPath.endsWith('.html')) return true; + const isIndexFile = /index\..+$/.test(inputPath); + if (!isIndexFile) return true; + + const hasIndexHtml = entrypointInfos.some( + (entry) => + entry.name === name && entry.inputPath.endsWith('index.html'), + ); + + return !hasIndexHtml; + }); + } +} diff --git a/packages/wxt/src/core/index.ts b/packages/wxt/src/core/index.ts index b8dfb2618..acf349c62 100644 --- a/packages/wxt/src/core/index.ts +++ b/packages/wxt/src/core/index.ts @@ -7,3 +7,6 @@ export * from './initialize'; export * from './prepare'; export * from './zip'; export * from './utils'; +export * from './entrypoint-finders/flat'; +export * from './runners/manual'; +export * from './runners/web-ext'; diff --git a/packages/wxt/src/core/resolve-config.ts b/packages/wxt/src/core/resolve-config.ts index c74ffbbb8..9207f8c61 100644 --- a/packages/wxt/src/core/resolve-config.ts +++ b/packages/wxt/src/core/resolve-config.ts @@ -30,10 +30,6 @@ import { safeStringToNumber } from './utils/number'; import { loadEnv } from './utils/env'; import { getPort } from 'get-port-please'; import { fileURLToPath, pathToFileURL } from 'node:url'; -import { createSafariRunner } from './runners/safari'; -import isWsl from 'is-wsl'; -import { createWslRunner } from './runners/wsl'; -import { createManualRunner } from './runners/manual'; import { createWxtLogger } from './utils/log/wxtLogger'; /** @@ -215,10 +211,7 @@ export async function resolveConfig( wxtModuleDir, root, webExt, - runner: - command === 'serve' - ? await resolveRunner(browser, logger, webExt.config) - : createManualRunner(), + runner: undefined!, // TODO: Remove this once the deprecated `ResolvedConfig.runner` is removed srcDir, typesDir, wxtDir, @@ -650,25 +643,3 @@ export async function resolveWxtUserModules( ); return [...npmModules, ...localModules]; } - -async function resolveRunner( - browser: string, - logger: Logger, - webExt: WebExtConfig, -): Promise { - if (browser === 'safari') return createSafariRunner(); - - if (isWsl) return createWslRunner(); - - try { - // This module imports `web-ext`, so if it fails, we know `web-ext` isn't installed - const { createWebExtRunner } = await import('./runners/web-ext'); - return webExt.disabled ? createManualRunner() : createWebExtRunner(); - } catch (err: any) { - if (err?.code !== 'ERR_MODULE_NOT_FOUND') throw err; - - logger.debug('Error loading the web-ext runner', err); - } - - return createManualRunner(); -} diff --git a/packages/wxt/src/core/runners/__tests__/web-ext.test.ts b/packages/wxt/src/core/runners/__tests__/web-ext.test.ts new file mode 100644 index 000000000..f2e80734c --- /dev/null +++ b/packages/wxt/src/core/runners/__tests__/web-ext.test.ts @@ -0,0 +1,233 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import webExt, { WebExtRunInstance } from 'web-ext'; +import { WebExtRunner } from '../web-ext'; +import { setFakeWxt } from '../../utils/testing/fake-objects'; +import { mock, MockProxy } from 'vitest-mock-extended'; +import { WebExtConfig } from '../../../types'; + +const DEFAULT_IS_WSL = false; +const DEFAULT_TARGET_BROWSER = 'chrome'; +const DEFAULT_IS_WEB_EXT_INSTALLED = true; +const DEFAULT_WEB_EXT_CONFIG = undefined; + +let isWsl = DEFAULT_IS_WSL; +let targetBrowser = DEFAULT_TARGET_BROWSER; +let isWebExtInstalled = DEFAULT_IS_WEB_EXT_INSTALLED; +let webExtConfig: WebExtConfig | undefined = DEFAULT_WEB_EXT_CONFIG; + +class ModuleNotFoundError extends Error { + code = 'ERR_MODULE_NOT_FOUND'; + constructor() { + super('Module not found: web-ext'); + } +} + +// @ts-expect-error: Overriding the private loadWebExt method +class TestRunner extends WebExtRunner { + private loadWebExt() { + if (isWebExtInstalled) { + return Promise.resolve(webExt); + } + throw new ModuleNotFoundError(); + } +} + +vi.mock('is-wsl', () => ({ + get default() { + return isWsl; + }, +})); + +vi.mock('web-ext', () => ({ + default: { + cmd: { + run: vi.fn(), + }, + }, +})); +const webExtCmdRunMock = vi.mocked(webExt.cmd.run); + +vi.mock('web-ext/util/logger', () => ({ + consoleStream: {}, +})); + +describe('WebExtRunner', () => { + function setupWxt() { + setFakeWxt({ + config: { + browser: targetBrowser, + webExt: { + config: webExtConfig, + }, + }, + }); + } + + beforeEach(() => { + isWsl = DEFAULT_IS_WSL; + targetBrowser = DEFAULT_TARGET_BROWSER; + isWebExtInstalled = DEFAULT_IS_WEB_EXT_INSTALLED; + webExtConfig = DEFAULT_WEB_EXT_CONFIG; + }); + + describe('canOpen', () => { + async function canOpen() { + const runner = new TestRunner(); + setupWxt(); + return await runner.canOpen(); + } + + describe('when in WSL', () => { + beforeEach(() => { + isWsl = true; + }); + + it('should return false', async () => { + expect(await canOpen()).toBe(false); + }); + }); + + describe('when targeting Safari', () => { + beforeEach(() => { + targetBrowser = 'safari'; + }); + + it('should return false', async () => { + expect(await canOpen()).toBe(false); + }); + }); + + describe('when web-ext is not installed', () => { + beforeEach(() => { + isWebExtInstalled = false; + }); + it('should return false', async () => { + expect(await canOpen()).toBe(false); + }); + }); + + it('should return true', async () => { + expect(await canOpen()).toBe(true); + }); + }); + + describe('openBrowser', () => { + async function openBrowser() { + const runner = new TestRunner(); + setupWxt(); + await runner.openBrowser(); + } + function expectNothing() { + expect(webExtCmdRunMock).not.toHaveBeenCalled(); + } + + describe('when in WSL', () => { + beforeEach(() => { + isWsl = true; + }); + + it('should do nothing', async () => { + await openBrowser(); + expectNothing(); + }); + }); + + describe('when targeting Safari', () => { + beforeEach(() => { + targetBrowser = 'safari'; + }); + + it('should do nothing', async () => { + await openBrowser(); + expectNothing(); + }); + }); + + describe('when web-ext is not installed', () => { + beforeEach(() => { + isWebExtInstalled = false; + }); + + it('should do nothing', async () => { + await openBrowser(); + expectNothing(); + }); + }); + + describe('when webExt.disabled=true', () => { + beforeEach(() => { + webExtConfig = { disabled: true }; + }); + + it('should do nothing', async () => { + await openBrowser(); + expectNothing(); + }); + }); + + it('should open the browser', async () => { + await openBrowser(); + expect(webExtCmdRunMock).toHaveBeenCalledTimes(1); + }); + + it('should merge the config sources correctly', async () => { + webExtConfig = { + startUrls: ['http://example.com'], + }; + await openBrowser(); + + expect(webExtCmdRunMock).toHaveBeenCalledTimes(1); + expect(webExtCmdRunMock).toHaveBeenCalledWith( + { + args: ['--unsafely-disable-devtools-self-xss-warnings'], + chromiumPref: { + devtools: { + synced_preferences_sync_disabled: { + 'skip-content-scripts': false, + skipContentScripts: false, + }, + }, + }, + noInput: true, + noReload: true, + noReloadManagerExtension: true, + sourceDir: expect.any(String), + startUrl: ['http://example.com'], + target: 'chromium', + }, + { + shouldExitProgram: false, + }, + ); + }); + }); + + describe('closeBrowser', () => { + async function closeBrowser(runner: TestRunner = new TestRunner()) { + setupWxt(); + await runner.closeBrowser(); + } + let instance: MockProxy; + + beforeEach(() => { + instance = mock(); + webExtCmdRunMock.mockResolvedValueOnce(instance); + }); + + describe("when openBrowser hasn't been called", () => { + it('should do nothing', async () => { + await closeBrowser(); + expect(webExtCmdRunMock).not.toHaveBeenCalled(); + }); + }); + + it('should close the browser', async () => { + const runner = new TestRunner(); + + await runner.openBrowser(); + await closeBrowser(runner); + + expect(instance.exit).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/packages/wxt/src/core/runners/manual.ts b/packages/wxt/src/core/runners/manual.ts index 364165d96..160d163ef 100644 --- a/packages/wxt/src/core/runners/manual.ts +++ b/packages/wxt/src/core/runners/manual.ts @@ -1,17 +1,28 @@ -import { ExtensionRunner } from '../../types'; +import type { ExtensionRunner } from '../../types'; import { relative } from 'node:path'; import { wxt } from '../wxt'; -/** The manual runner tells the user to load the unpacked extension manually. */ -export function createManualRunner(): ExtensionRunner { - return { - async openBrowser() { - wxt.logger.info( - `Load "${relative( - process.cwd(), - wxt.config.outDir, - )}" as an unpacked extension manually`, - ); - }, - }; +/** + * Base runner that just tells developers to load the extension into their + * browser manually. + * + * You can extend this class and call any `super` method when your runner can't + * open the browser. + */ +export class ManualRunner implements ExtensionRunner { + constructor() {} + + canOpen(): Promise { + return Promise.resolve(false); + } + + async openBrowser(): Promise { + wxt.logger.info( + `Load "${this.relativeOutDir()}" as an unpacked extension manually`, + ); + } + + protected relativeOutDir(): string { + return relative(process.cwd(), wxt.config.outDir); + } } diff --git a/packages/wxt/src/core/runners/safari.ts b/packages/wxt/src/core/runners/safari.ts deleted file mode 100644 index 5533e9c2c..000000000 --- a/packages/wxt/src/core/runners/safari.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { ExtensionRunner } from '../../types'; -import { relative } from 'node:path'; -import { wxt } from '../wxt'; - -/** - * The Safari runner just logs a warning message because `web-ext` doesn't work - * with Safari. - */ -export function createSafariRunner(): ExtensionRunner { - return { - async openBrowser() { - wxt.logger.warn( - `Cannot Safari using web-ext. Load "${relative( - process.cwd(), - wxt.config.outDir, - )}" as an unpacked extension manually`, - ); - }, - }; -} diff --git a/packages/wxt/src/core/runners/web-ext.ts b/packages/wxt/src/core/runners/web-ext.ts index 90d64e4c7..57c6a21da 100644 --- a/packages/wxt/src/core/runners/web-ext.ts +++ b/packages/wxt/src/core/runners/web-ext.ts @@ -1,24 +1,68 @@ -import type { WebExtRunInstance } from 'web-ext'; +import isWsl from 'is-wsl'; import { ExtensionRunner } from '../../types'; +import { wxt } from '../wxt'; +import { ManualRunner } from './manual'; import { formatDuration } from '../utils/time'; import defu from 'defu'; -import { wxt } from '../wxt'; -import webExt from 'web-ext'; -import { consoleStream } from 'web-ext/util/logger'; -/** Create an `ExtensionRunner` backed by `web-ext`. */ -export function createWebExtRunner(): ExtensionRunner { - let runner: WebExtRunInstance | undefined; +type WebExtModule = (typeof import('web-ext'))['default']; +type LoggerModule = typeof import('web-ext/util/logger'); +const MODULE_NOT_FOUND_CODE = 'ERR_MODULE_NOT_FOUND'; + +/** + * WXT's default `ExtensionRunner` that uses web-ext to open the browser. It + * cannot open the browser automatically in some environments, like WSL. + * + * Because `web-ext` is a peer dependency, this runner falls back on the + * `ManualRunner` when the module is not installed. + */ +export class WebExtRunner extends ManualRunner implements ExtensionRunner { + private webExt: import('web-ext').WebExtRunInstance | undefined; + private webExtModulePromise: Promise | undefined; + private loggerModulePromise: Promise | undefined; + + async canOpen(): Promise { + if (wxt.config.browser === 'safari' || isWsl) return false; - return { - canOpen() { + try { + await this.loadWebExt(); return true; - }, - async openBrowser() { + } catch (err: any) { + if (err?.code === MODULE_NOT_FOUND_CODE) return false; + + throw err; + } + } + + async openBrowser(): Promise { + console.log(1); + if (wxt.config.browser === 'safari') { + console.log(2); + wxt.logger.warn( + `Cannot Safari using web-ext. Load "${this.relativeOutDir()}" as an unpacked extension manually`, + ); + return; + } + if (isWsl) { + console.log(3); + wxt.logger.warn( + `Cannot open browser when using WSL. Load "${this.relativeOutDir()}" as an unpacked extension manually`, + ); + return; + } + if (wxt.config.webExt.config.disabled) { + console.log(4); + return super.openBrowser(); + } + + try { + console.log(5); const startTime = Date.now(); + const webExt = await this.loadWebExt(); + const logger = await this.loadLogger(); // Use WXT's logger instead of web-ext's built-in one. - consoleStream.write = ({ level, msg, name }) => { + logger.consoleStream.write = ({ level, msg, name }) => { if (level >= ERROR_LOG_LEVEL) wxt.logger.error(name, msg); if (level >= WARN_LOG_LEVEL) wxt.logger.warn(msg); }; @@ -67,19 +111,36 @@ export function createWebExtRunner(): ExtensionRunner { // Don't call `process.exit(0)` after starting web-ext shouldExitProgram: false, }; + wxt.logger.debug('web-ext config:', finalConfig); wxt.logger.debug('web-ext options:', options); - runner = await webExt.cmd.run(finalConfig, options); + console.log('called'); + this.webExt = await webExt.cmd.run(finalConfig, options); const duration = Date.now() - startTime; wxt.logger.success(`Opened browser in ${formatDuration(duration)}`); - }, + } catch (err: any) { + console.log(6, err); + if (err?.code === MODULE_NOT_FOUND_CODE) return super.openBrowser(); - async closeBrowser() { - await runner?.exit(); - }, - }; + wxt.logger.warn('Error loading the web-ext runner', err); + } + } + + async closeBrowser(): Promise { + await this.webExt?.exit(); + } + + private loadWebExt(): Promise { + this.webExtModulePromise ??= import('web-ext').then((mod) => mod.default); + return this.webExtModulePromise; + } + + private loadLogger(): Promise { + this.loggerModulePromise ??= import('web-ext/util/logger'); + return this.loggerModulePromise; + } } // https://github.com/mozilla/web-ext/blob/e37e60a2738478f512f1255c537133321f301771/src/util/logger.js#L12 diff --git a/packages/wxt/src/core/runners/wsl.ts b/packages/wxt/src/core/runners/wsl.ts deleted file mode 100644 index 4caa698a7..000000000 --- a/packages/wxt/src/core/runners/wsl.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { ExtensionRunner } from '../../types'; -import { relative } from 'node:path'; -import { wxt } from '../wxt'; - -/** - * The WSL runner just logs a warning message because `web-ext` doesn't work in - * WSL. - */ -export function createWslRunner(): ExtensionRunner { - return { - async openBrowser() { - wxt.logger.warn( - `Cannot open browser when using WSL. Load "${relative( - process.cwd(), - wxt.config.outDir, - )}" as an unpacked extension manually`, - ); - }, - }; -} diff --git a/packages/wxt/src/core/utils/building/__tests__/find-entrypoints.test.ts b/packages/wxt/src/core/utils/building/__tests__/find-entrypoints.test.ts index 1343b5d1c..7461d51bd 100644 --- a/packages/wxt/src/core/utils/building/__tests__/find-entrypoints.test.ts +++ b/packages/wxt/src/core/utils/building/__tests__/find-entrypoints.test.ts @@ -16,6 +16,7 @@ import { wxt } from '../../../wxt'; import { unnormalizePath } from '../../paths'; import { fakeResolvedConfig, setFakeWxt } from '../../testing/fake-objects'; import { findEntrypoints } from '../find-entrypoints'; +import { FlatEntrypointFinder } from '../../../entrypoint-finders/flat'; vi.mock('tinyglobby'); const globMock = vi.mocked(glob); @@ -34,7 +35,10 @@ describe('findEntrypoints', () => { let importEntrypointsMock: Mock; beforeEach(() => { - setFakeWxt({ config }); + setFakeWxt({ + config, + entrypointFinder: new FlatEntrypointFinder(config), + }); importEntrypointsMock = vi.mocked(wxt.builder.importEntrypoints); importEntrypointsMock.mockResolvedValue([]); }); diff --git a/packages/wxt/src/core/utils/building/find-entrypoints.ts b/packages/wxt/src/core/utils/building/find-entrypoints.ts index 0d1c27d3c..bac6aa8a8 100644 --- a/packages/wxt/src/core/utils/building/find-entrypoints.ts +++ b/packages/wxt/src/core/utils/building/find-entrypoints.ts @@ -13,18 +13,14 @@ import { UnlistedScriptEntrypoint, } from '../../../types'; import { mkdir, readFile, writeFile } from 'node:fs/promises'; -import picomatch from 'picomatch'; import { parseHTML } from 'linkedom'; import JSON5 from 'json5'; -import { glob } from 'tinyglobby'; import { - getEntrypointName, isHtmlEntrypoint, isJsEntrypoint, resolvePerBrowserOptions, } from '../entrypoints'; import { VIRTUAL_NOOP_BACKGROUND_MODULE_ID } from '../constants'; -import { CSS_EXTENSIONS_PATTERN } from '../paths'; import { wxt } from '../../wxt'; import { camelCase } from 'scule'; import { styleText } from 'node:util'; @@ -48,42 +44,7 @@ export async function findEntrypoints(): Promise { } } - const relativePaths = await glob(Object.keys(PATH_GLOB_TO_TYPE_MAP), { - cwd: wxt.config.entrypointsDir, - expandDirectories: false, - }); - // Ensure consistent output - relativePaths.sort(); - - const pathGlobs = Object.keys(PATH_GLOB_TO_TYPE_MAP); - const entrypointInfos: EntrypointInfo[] = relativePaths - .reduce((results, relativePath) => { - const inputPath = resolve(wxt.config.entrypointsDir, relativePath); - const name = getEntrypointName(wxt.config.entrypointsDir, inputPath); - const matchingGlob = pathGlobs.find((glob) => - picomatch(glob)(relativePath), - ); - if (matchingGlob) { - const type = PATH_GLOB_TO_TYPE_MAP[matchingGlob]; - results.push({ name, inputPath, type }); - } - return results; - }, []) - .filter(({ name, inputPath }, _, entrypointInfos) => { - // Remove /index.* if /index.html exists - - if (inputPath.endsWith('.html')) return true; - const isIndexFile = /index\..+$/.test(inputPath); - if (!isIndexFile) return true; - - const hasIndexHtml = entrypointInfos.some( - (entry) => - entry.name === name && entry.inputPath.endsWith('index.html'), - ); - - return !hasIndexHtml; - }); - + const entrypointInfos = await wxt.entrypointFinder.findEntrypoints(); await wxt.hooks.callHook('entrypoints:found', wxt, entrypointInfos); // Validation @@ -435,54 +396,4 @@ function isEntrypointSkipped(entry: Omit): boolean { return false; } -const PATH_GLOB_TO_TYPE_MAP: Record = { - 'sandbox.html': 'sandbox', - 'sandbox/index.html': 'sandbox', - '*.sandbox.html': 'sandbox', - '*.sandbox/index.html': 'sandbox', - - 'bookmarks.html': 'bookmarks', - 'bookmarks/index.html': 'bookmarks', - - 'history.html': 'history', - 'history/index.html': 'history', - - 'newtab.html': 'newtab', - 'newtab/index.html': 'newtab', - - 'sidepanel.html': 'sidepanel', - 'sidepanel/index.html': 'sidepanel', - '*.sidepanel.html': 'sidepanel', - '*.sidepanel/index.html': 'sidepanel', - - 'devtools.html': 'devtools', - 'devtools/index.html': 'devtools', - - 'background.[jt]s': 'background', - 'background/index.[jt]s': 'background', - [VIRTUAL_NOOP_BACKGROUND_MODULE_ID]: 'background', - - 'content.[jt]s?(x)': 'content-script', - 'content/index.[jt]s?(x)': 'content-script', - '*.content.[jt]s?(x)': 'content-script', - '*.content/index.[jt]s?(x)': 'content-script', - [`content.${CSS_EXTENSIONS_PATTERN}`]: 'content-script-style', - [`*.content.${CSS_EXTENSIONS_PATTERN}`]: 'content-script-style', - [`content/index.${CSS_EXTENSIONS_PATTERN}`]: 'content-script-style', - [`*.content/index.${CSS_EXTENSIONS_PATTERN}`]: 'content-script-style', - - 'popup.html': 'popup', - 'popup/index.html': 'popup', - - 'options.html': 'options', - 'options/index.html': 'options', - - '*.html': 'unlisted-page', - '*/index.html': 'unlisted-page', - '*.[jt]s?(x)': 'unlisted-script', - '*/index.[jt]s?(x)': 'unlisted-script', - [`*.${CSS_EXTENSIONS_PATTERN}`]: 'unlisted-style', - [`*/index.${CSS_EXTENSIONS_PATTERN}`]: 'unlisted-style', -}; - const CONTENT_SCRIPT_OUT_DIR = 'content-scripts'; diff --git a/packages/wxt/src/core/wxt.ts b/packages/wxt/src/core/wxt.ts index 6b5ad003e..ae2eceed2 100644 --- a/packages/wxt/src/core/wxt.ts +++ b/packages/wxt/src/core/wxt.ts @@ -5,6 +5,8 @@ import { InlineConfig, Wxt, WxtCommand, WxtHooks, WxtModule } from '../types'; import { createViteBuilder } from './builders/vite'; import { createWxtPackageManager } from './package-managers'; import { resolveConfig } from './resolve-config'; +import { FlatEntrypointFinder } from './entrypoint-finders/flat'; +import { WebExtRunner } from './runners/web-ext'; /** * Global variable set once `createWxt` is called once. Since this variable is @@ -14,8 +16,8 @@ import { resolveConfig } from './resolve-config'; export let wxt: Wxt; /** - * Create and register a global instance of the Wxt interface for use throughout - * the project. + * Create and register a global instance of the `Wxt` interface for use + * throughout the project. */ export async function registerWxt( command: WxtCommand, @@ -28,10 +30,21 @@ export async function registerWxt( const hooks = createHooks(); const config = await resolveConfig(inlineConfig, command); + const runner = new WebExtRunner(); + // TODO: Remove once config.runner is deprecated + Object.defineProperty(config, 'runner', { + get() { + return runner; + }, + set(value) { + wxt.runner = value; + }, + }); const [builder, pm] = await Promise.all([ createViteBuilder(config, hooks, () => wxt.server), createWxtPackageManager(config.root), ]); + const entrypointFinder = new FlatEntrypointFinder(config); wxt = { config, @@ -55,6 +68,8 @@ export async function registerWxt( }, pm, builder, + runner, + entrypointFinder, server: undefined, }; diff --git a/packages/wxt/src/types.ts b/packages/wxt/src/types.ts index 6331bd028..013c64dbb 100644 --- a/packages/wxt/src/types.ts +++ b/packages/wxt/src/types.ts @@ -644,8 +644,8 @@ export interface Logger { } /** - * The logger available at `wxt.logger`. Extends {@link Logger} with a `warnOnce` - * which only logs a message once per process. + * The logger available at `wxt.logger` and `wxt.config.logger`. Extends + * {@link Logger} with a `warnOnce` which only logs a message once per process. */ export interface WxtLogger extends Logger { /** @@ -1517,7 +1517,7 @@ export interface Wxt { hooks: Hookable; /** Alias for `wxt.hooks.hook(...)`. */ hook: Hookable['hook']; - /** Wraps `config.logger`, adding `warnOnce`. */ + /** Alias for `config.logger`. */ logger: WxtLogger; /** Reload config file and update `wxt.config` with the result. */ reloadConfig: () => Promise; @@ -1525,8 +1525,39 @@ export interface Wxt { pm: WxtPackageManager; /** If the dev server was started, it will be available. */ server?: WxtDevServer; - /** The module in charge of executing all the build steps. */ + /** + * The module in charge of executing all the build steps. + * + * To override the entire builder process, set this value in a module's + * `setup` function or during the `config:resolved` hook. + * + * ```ts + * wxt.build = new MyBuilder(wxt.config); + * ``` + */ builder: WxtBuilder; + /** + * Service responsible for discovering entrypoints in a project. + * + * To override how entrypoints are discovered, set this value in a module's + * `setup` function or during the `config:resolved` hook. + * + * ```ts + * wxt.entrypointFinder = new MyEntrypointFinder(wxt.config); + * ``` + */ + entrypointFinder: EntrypointFinder; + /** + * Service responsible for opening the browser during dev mode. + * + * To override how the browser is opened, set this value in a module's `setup` + * function or during the `config:resolved` hook. + * + * ```ts + * wxt.runner = new MyRunner(wxt.config); + * ``` + */ + runner: ExtensionRunner; } export interface ResolvedConfig { @@ -1575,6 +1606,7 @@ export interface ResolvedConfig { manifest: UserManifest; fsCache: FsCache; webExt: C12ResolvedConfig; + /** @deprecated Use `wxt.runner` instead of `wxt.config.runner` */ runner: ExtensionRunner; zip: { name?: string; @@ -1669,7 +1701,7 @@ export interface ExtensionRunner { closeBrowser?(): Promise; /** Whether or not this runner actually opens the browser. */ - canOpen?(): boolean; + canOpen?(): Promise; } export type EslintGlobalsPropValue = @@ -1880,3 +1912,11 @@ export interface WxtDirFileEntry { /** Set to `true` to add a reference to this file in `.wxt/wxt.d.ts`. */ tsReference?: boolean; } + +export interface EntrypointFinder { + /** + * @returns A list of file paths (absolute or relative to + * `wxt.config.rootDir`) to load as entrypoints for. + */ + findEntrypoints(): Promise | EntrypointInfo[]; +} From 9a93c169afd6a6d9e342a2a63b643ea19ff8f97f Mon Sep 17 00:00:00 2001 From: Aaron Date: Sat, 3 Oct 2026 14:30:50 -0500 Subject: [PATCH 2/8] Remove comments --- packages/wxt/src/core/runners/web-ext.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/wxt/src/core/runners/web-ext.ts b/packages/wxt/src/core/runners/web-ext.ts index 57c6a21da..6b6a0e958 100644 --- a/packages/wxt/src/core/runners/web-ext.ts +++ b/packages/wxt/src/core/runners/web-ext.ts @@ -35,28 +35,23 @@ export class WebExtRunner extends ManualRunner implements ExtensionRunner { } async openBrowser(): Promise { - console.log(1); if (wxt.config.browser === 'safari') { - console.log(2); wxt.logger.warn( `Cannot Safari using web-ext. Load "${this.relativeOutDir()}" as an unpacked extension manually`, ); return; } if (isWsl) { - console.log(3); wxt.logger.warn( `Cannot open browser when using WSL. Load "${this.relativeOutDir()}" as an unpacked extension manually`, ); return; } if (wxt.config.webExt.config.disabled) { - console.log(4); return super.openBrowser(); } try { - console.log(5); const startTime = Date.now(); const webExt = await this.loadWebExt(); const logger = await this.loadLogger(); @@ -115,13 +110,11 @@ export class WebExtRunner extends ManualRunner implements ExtensionRunner { wxt.logger.debug('web-ext config:', finalConfig); wxt.logger.debug('web-ext options:', options); - console.log('called'); this.webExt = await webExt.cmd.run(finalConfig, options); const duration = Date.now() - startTime; wxt.logger.success(`Opened browser in ${formatDuration(duration)}`); } catch (err: any) { - console.log(6, err); if (err?.code === MODULE_NOT_FOUND_CODE) return super.openBrowser(); wxt.logger.warn('Error loading the web-ext runner', err); From e7e0066fbfa1dd970ba72bdd0722456a33775f23 Mon Sep 17 00:00:00 2001 From: Aaron Date: Sat, 3 Oct 2026 14:34:14 -0500 Subject: [PATCH 3/8] Replace internal deprecated API usage --- packages/wxt/src/core/create-server.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/wxt/src/core/create-server.ts b/packages/wxt/src/core/create-server.ts index aa246e36d..432408e73 100644 --- a/packages/wxt/src/core/create-server.ts +++ b/packages/wxt/src/core/create-server.ts @@ -94,7 +94,7 @@ async function createServerInternal(): Promise { async stop() { wasStopped = true; keyboardShortcuts.stop(); - await wxt.config.runner.closeBrowser?.(); + await wxt.runner.closeBrowser?.(); await builderServer.close(); await wxt.hooks.callHook('server:closed', wxt, server); @@ -118,14 +118,14 @@ async function createServerInternal(): Promise { server.ws.send('wxt:reload-extension'); }, async restartBrowser() { - await wxt.config.runner.closeBrowser?.(); + await wxt.runner.closeBrowser?.(); keyboardShortcuts.stop(); await wxt.reloadConfig(); - await wxt.config.runner.openBrowser(); + await wxt.runner.openBrowser(); keyboardShortcuts.start(); keyboardShortcuts.printHelp({ canReopenBrowser: - !wxt.config.webExt.config.disabled && !!wxt.config.runner.canOpen?.(), + !wxt.config.webExt.config.disabled && !!wxt.runner.canOpen?.(), }); }, }; @@ -164,11 +164,11 @@ async function createServerInternal(): Promise { } // Open browser after everything is ready to go. - await wxt.config.runner.openBrowser(); + await wxt.runner.openBrowser(); keyboardShortcuts.start(); keyboardShortcuts.printHelp({ canReopenBrowser: - !wxt.config.webExt.config.disabled && !!wxt.config.runner.canOpen?.(), + !wxt.config.webExt.config.disabled && !!wxt.runner.canOpen?.(), }); }; From f1e9592d9251ac9b263a987fda44e69a96570e5c Mon Sep 17 00:00:00 2001 From: Aaron Date: Sat, 3 Oct 2026 14:34:40 -0500 Subject: [PATCH 4/8] Fix checks --- packages/wxt/src/core/resolve-config.ts | 1 - packages/wxt/src/core/utils/testing/fake-objects.ts | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/wxt/src/core/resolve-config.ts b/packages/wxt/src/core/resolve-config.ts index 9207f8c61..6c2b2c06c 100644 --- a/packages/wxt/src/core/resolve-config.ts +++ b/packages/wxt/src/core/resolve-config.ts @@ -13,7 +13,6 @@ import { WxtModule, WxtModuleWithMetadata, WxtResolvedUnimportOptions, - ExtensionRunner, WxtLogger, } from '../types'; import path from 'node:path'; diff --git a/packages/wxt/src/core/utils/testing/fake-objects.ts b/packages/wxt/src/core/utils/testing/fake-objects.ts index 59dd38d8a..8b3eae685 100644 --- a/packages/wxt/src/core/utils/testing/fake-objects.ts +++ b/packages/wxt/src/core/utils/testing/fake-objects.ts @@ -320,6 +320,8 @@ export const fakeWxt = fakeObjectCreator(() => ({ pm: mock(), server: faker.helpers.arrayElement([undefined, fakeWxtDevServer()]), builder: mock(), + entrypointFinder: mock(), + runner: mock(), })); export const fakeWxtDevServer = fakeObjectCreator(() => ({ From ba09dea7bdc66cd17ed3725c3221d34c29b0a47f Mon Sep 17 00:00:00 2001 From: Aaron Date: Sat, 3 Oct 2026 14:37:19 -0500 Subject: [PATCH 5/8] Update failed tests --- packages/wxt/src/core/utils/testing/fake-objects.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/wxt/src/core/utils/testing/fake-objects.ts b/packages/wxt/src/core/utils/testing/fake-objects.ts index 8b3eae685..6ed7f265e 100644 --- a/packages/wxt/src/core/utils/testing/fake-objects.ts +++ b/packages/wxt/src/core/utils/testing/fake-objects.ts @@ -320,7 +320,9 @@ export const fakeWxt = fakeObjectCreator(() => ({ pm: mock(), server: faker.helpers.arrayElement([undefined, fakeWxtDevServer()]), builder: mock(), - entrypointFinder: mock(), + entrypointFinder: mock({ + findEntrypoints: () => [], + }), runner: mock(), })); From f78912f31e7893b30e3ad8c97d3e9c55c583d0e8 Mon Sep 17 00:00:00 2001 From: Aaron Date: Sat, 3 Oct 2026 14:45:07 -0500 Subject: [PATCH 6/8] Cleanup --- packages/wxt/src/core/builders/vite/index.ts | 1 + .../core/runners/__tests__/web-ext.test.ts | 5 +++ packages/wxt/src/core/runners/manual.ts | 9 +++-- packages/wxt/src/core/runners/web-ext.ts | 35 ++++++++++--------- packages/wxt/src/core/wxt.ts | 5 +-- 5 files changed, 31 insertions(+), 24 deletions(-) diff --git a/packages/wxt/src/core/builders/vite/index.ts b/packages/wxt/src/core/builders/vite/index.ts index df00fcd78..1e2db63e4 100644 --- a/packages/wxt/src/core/builders/vite/index.ts +++ b/packages/wxt/src/core/builders/vite/index.ts @@ -36,6 +36,7 @@ export async function createViteBuilder( hooks: Hookable, getWxtDevServer?: () => WxtDevServer | undefined, ): Promise { + // TODO: This doesn't need to be async. Convert to class like other overridable services as well. const vite = await import('vite'); /** diff --git a/packages/wxt/src/core/runners/__tests__/web-ext.test.ts b/packages/wxt/src/core/runners/__tests__/web-ext.test.ts index f2e80734c..524f34c77 100644 --- a/packages/wxt/src/core/runners/__tests__/web-ext.test.ts +++ b/packages/wxt/src/core/runners/__tests__/web-ext.test.ts @@ -4,6 +4,7 @@ import { WebExtRunner } from '../web-ext'; import { setFakeWxt } from '../../utils/testing/fake-objects'; import { mock, MockProxy } from 'vitest-mock-extended'; import { WebExtConfig } from '../../../types'; +import { wxt } from '../../wxt'; const DEFAULT_IS_WSL = false; const DEFAULT_TARGET_BROWSER = 'chrome'; @@ -24,6 +25,10 @@ class ModuleNotFoundError extends Error { // @ts-expect-error: Overriding the private loadWebExt method class TestRunner extends WebExtRunner { + constructor() { + super(wxt.config); + } + private loadWebExt() { if (isWebExtInstalled) { return Promise.resolve(webExt); diff --git a/packages/wxt/src/core/runners/manual.ts b/packages/wxt/src/core/runners/manual.ts index 160d163ef..90b87d408 100644 --- a/packages/wxt/src/core/runners/manual.ts +++ b/packages/wxt/src/core/runners/manual.ts @@ -1,6 +1,5 @@ -import type { ExtensionRunner } from '../../types'; +import type { ExtensionRunner, ResolvedConfig } from '../../types'; import { relative } from 'node:path'; -import { wxt } from '../wxt'; /** * Base runner that just tells developers to load the extension into their @@ -10,19 +9,19 @@ import { wxt } from '../wxt'; * open the browser. */ export class ManualRunner implements ExtensionRunner { - constructor() {} + constructor(protected config: ResolvedConfig) {} canOpen(): Promise { return Promise.resolve(false); } async openBrowser(): Promise { - wxt.logger.info( + this.config.logger.info( `Load "${this.relativeOutDir()}" as an unpacked extension manually`, ); } protected relativeOutDir(): string { - return relative(process.cwd(), wxt.config.outDir); + return relative(process.cwd(), this.config.outDir); } } diff --git a/packages/wxt/src/core/runners/web-ext.ts b/packages/wxt/src/core/runners/web-ext.ts index 6b6a0e958..15729e3fe 100644 --- a/packages/wxt/src/core/runners/web-ext.ts +++ b/packages/wxt/src/core/runners/web-ext.ts @@ -1,6 +1,5 @@ import isWsl from 'is-wsl'; import { ExtensionRunner } from '../../types'; -import { wxt } from '../wxt'; import { ManualRunner } from './manual'; import { formatDuration } from '../utils/time'; import defu from 'defu'; @@ -22,7 +21,7 @@ export class WebExtRunner extends ManualRunner implements ExtensionRunner { private loggerModulePromise: Promise | undefined; async canOpen(): Promise { - if (wxt.config.browser === 'safari' || isWsl) return false; + if (this.config.browser === 'safari' || isWsl) return false; try { await this.loadWebExt(); @@ -35,19 +34,19 @@ export class WebExtRunner extends ManualRunner implements ExtensionRunner { } async openBrowser(): Promise { - if (wxt.config.browser === 'safari') { - wxt.logger.warn( + if (this.config.browser === 'safari') { + this.config.logger.warn( `Cannot Safari using web-ext. Load "${this.relativeOutDir()}" as an unpacked extension manually`, ); return; } if (isWsl) { - wxt.logger.warn( + this.config.logger.warn( `Cannot open browser when using WSL. Load "${this.relativeOutDir()}" as an unpacked extension manually`, ); return; } - if (wxt.config.webExt.config.disabled) { + if (this.config.webExt.config.disabled) { return super.openBrowser(); } @@ -58,18 +57,18 @@ export class WebExtRunner extends ManualRunner implements ExtensionRunner { // Use WXT's logger instead of web-ext's built-in one. logger.consoleStream.write = ({ level, msg, name }) => { - if (level >= ERROR_LOG_LEVEL) wxt.logger.error(name, msg); - if (level >= WARN_LOG_LEVEL) wxt.logger.warn(msg); + if (level >= ERROR_LOG_LEVEL) this.config.logger.error(name, msg); + if (level >= WARN_LOG_LEVEL) this.config.logger.warn(msg); }; - const wxtUserConfig = wxt.config.webExt.config; + const wxtUserConfig = this.config.webExt.config; const userConfig = { browserConsole: wxtUserConfig?.openConsole, devtools: wxtUserConfig?.openDevtools, startUrl: wxtUserConfig?.startUrls, keepProfileChanges: wxtUserConfig?.keepProfileChanges, chromiumPort: wxtUserConfig?.chromiumPort, - ...(wxt.config.browser === 'firefox' + ...(this.config.browser === 'firefox' ? { firefox: wxtUserConfig?.binaries?.firefox, firefoxProfile: wxtUserConfig?.firefoxProfile, @@ -77,7 +76,7 @@ export class WebExtRunner extends ManualRunner implements ExtensionRunner { args: wxtUserConfig?.firefoxArgs, } : { - chromiumBinary: wxtUserConfig?.binaries?.[wxt.config.browser], + chromiumBinary: wxtUserConfig?.binaries?.[this.config.browser], chromiumProfile: wxtUserConfig?.chromiumProfile, chromiumPref: defu( wxtUserConfig?.chromiumPref, @@ -93,8 +92,8 @@ export class WebExtRunner extends ManualRunner implements ExtensionRunner { const finalConfig = { ...userConfig, target: - wxt.config.browser === 'firefox' ? 'firefox-desktop' : 'chromium', - sourceDir: wxt.config.outDir, + this.config.browser === 'firefox' ? 'firefox-desktop' : 'chromium', + sourceDir: this.config.outDir, // Don't add a "Reload Manager" extension alongside dev extension, WXT // already handles reloads internally. noReloadManagerExtension: true, @@ -107,17 +106,19 @@ export class WebExtRunner extends ManualRunner implements ExtensionRunner { shouldExitProgram: false, }; - wxt.logger.debug('web-ext config:', finalConfig); - wxt.logger.debug('web-ext options:', options); + this.config.logger.debug('web-ext config:', finalConfig); + this.config.logger.debug('web-ext options:', options); this.webExt = await webExt.cmd.run(finalConfig, options); const duration = Date.now() - startTime; - wxt.logger.success(`Opened browser in ${formatDuration(duration)}`); + this.config.logger.success( + `Opened browser in ${formatDuration(duration)}`, + ); } catch (err: any) { if (err?.code === MODULE_NOT_FOUND_CODE) return super.openBrowser(); - wxt.logger.warn('Error loading the web-ext runner', err); + this.config.logger.warn('Error loading the web-ext runner', err); } } diff --git a/packages/wxt/src/core/wxt.ts b/packages/wxt/src/core/wxt.ts index ae2eceed2..ffaac4397 100644 --- a/packages/wxt/src/core/wxt.ts +++ b/packages/wxt/src/core/wxt.ts @@ -6,7 +6,6 @@ import { createViteBuilder } from './builders/vite'; import { createWxtPackageManager } from './package-managers'; import { resolveConfig } from './resolve-config'; import { FlatEntrypointFinder } from './entrypoint-finders/flat'; -import { WebExtRunner } from './runners/web-ext'; /** * Global variable set once `createWxt` is called once. Since this variable is @@ -30,7 +29,9 @@ export async function registerWxt( const hooks = createHooks(); const config = await resolveConfig(inlineConfig, command); - const runner = new WebExtRunner(); + // Imported lazily to avoid a circular import (runners import `wxt`) + const { WebExtRunner } = await import('./runners/web-ext'); + const runner = new WebExtRunner(config); // TODO: Remove once config.runner is deprecated Object.defineProperty(config, 'runner', { get() { From d4285a7c71cd4449d59b5dce761590855e7f174c Mon Sep 17 00:00:00 2001 From: Aaron Date: Sat, 3 Oct 2026 14:46:44 -0500 Subject: [PATCH 7/8] Cleanup --- packages/wxt/src/core/wxt.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/wxt/src/core/wxt.ts b/packages/wxt/src/core/wxt.ts index ffaac4397..26c23f426 100644 --- a/packages/wxt/src/core/wxt.ts +++ b/packages/wxt/src/core/wxt.ts @@ -6,6 +6,7 @@ import { createViteBuilder } from './builders/vite'; import { createWxtPackageManager } from './package-managers'; import { resolveConfig } from './resolve-config'; import { FlatEntrypointFinder } from './entrypoint-finders/flat'; +import { WebExtRunner } from './runners/web-ext'; /** * Global variable set once `createWxt` is called once. Since this variable is @@ -29,8 +30,6 @@ export async function registerWxt( const hooks = createHooks(); const config = await resolveConfig(inlineConfig, command); - // Imported lazily to avoid a circular import (runners import `wxt`) - const { WebExtRunner } = await import('./runners/web-ext'); const runner = new WebExtRunner(config); // TODO: Remove once config.runner is deprecated Object.defineProperty(config, 'runner', { From f782927209e8c9b0d1085acdbc7355c5656e4e55 Mon Sep 17 00:00:00 2001 From: Aaron Date: Sat, 3 Oct 2026 14:48:23 -0500 Subject: [PATCH 8/8] Fix failing tests --- .../core/runners/__tests__/web-ext.test.ts | 23 ++++++------ .../__tests__/find-entrypoints.test.ts | 37 +++++++++---------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/packages/wxt/src/core/runners/__tests__/web-ext.test.ts b/packages/wxt/src/core/runners/__tests__/web-ext.test.ts index 524f34c77..7d21ba5b5 100644 --- a/packages/wxt/src/core/runners/__tests__/web-ext.test.ts +++ b/packages/wxt/src/core/runners/__tests__/web-ext.test.ts @@ -4,7 +4,7 @@ import { WebExtRunner } from '../web-ext'; import { setFakeWxt } from '../../utils/testing/fake-objects'; import { mock, MockProxy } from 'vitest-mock-extended'; import { WebExtConfig } from '../../../types'; -import { wxt } from '../../wxt'; +import { ResolvedConfig } from '../../../types'; const DEFAULT_IS_WSL = false; const DEFAULT_TARGET_BROWSER = 'chrome'; @@ -25,8 +25,8 @@ class ModuleNotFoundError extends Error { // @ts-expect-error: Overriding the private loadWebExt method class TestRunner extends WebExtRunner { - constructor() { - super(wxt.config); + constructor(config: ResolvedConfig) { + super(config); } private loadWebExt() { @@ -57,8 +57,12 @@ vi.mock('web-ext/util/logger', () => ({ })); describe('WebExtRunner', () => { + function setupRunner() { + return new TestRunner(setupWxt().config); + } + function setupWxt() { - setFakeWxt({ + return setFakeWxt({ config: { browser: targetBrowser, webExt: { @@ -77,8 +81,7 @@ describe('WebExtRunner', () => { describe('canOpen', () => { async function canOpen() { - const runner = new TestRunner(); - setupWxt(); + const runner = setupRunner(); return await runner.canOpen(); } @@ -118,8 +121,7 @@ describe('WebExtRunner', () => { describe('openBrowser', () => { async function openBrowser() { - const runner = new TestRunner(); - setupWxt(); + const runner = setupRunner(); await runner.openBrowser(); } function expectNothing() { @@ -208,8 +210,7 @@ describe('WebExtRunner', () => { }); describe('closeBrowser', () => { - async function closeBrowser(runner: TestRunner = new TestRunner()) { - setupWxt(); + async function closeBrowser(runner: TestRunner = setupRunner()) { await runner.closeBrowser(); } let instance: MockProxy; @@ -227,7 +228,7 @@ describe('WebExtRunner', () => { }); it('should close the browser', async () => { - const runner = new TestRunner(); + const runner = setupRunner(); await runner.openBrowser(); await closeBrowser(runner); diff --git a/packages/wxt/src/core/utils/building/__tests__/find-entrypoints.test.ts b/packages/wxt/src/core/utils/building/__tests__/find-entrypoints.test.ts index 7461d51bd..8118e29c1 100644 --- a/packages/wxt/src/core/utils/building/__tests__/find-entrypoints.test.ts +++ b/packages/wxt/src/core/utils/building/__tests__/find-entrypoints.test.ts @@ -25,13 +25,14 @@ vi.mock('node:fs/promises'); const readFileMock = vi.mocked(readFile); describe('findEntrypoints', () => { - const config = fakeResolvedConfig({ + const baseConfig = { manifestVersion: 3, root: '/', entrypointsDir: resolve('/src/entrypoints'), outDir: resolve('.output'), command: 'build', - }); + } as const; + const config = fakeResolvedConfig(baseConfig); let importEntrypointsMock: Mock; beforeEach(() => { @@ -363,11 +364,11 @@ describe('findEntrypoints', () => { ); it('should remove type=module from MV2 background scripts', async () => { + const config = fakeResolvedConfig({ ...baseConfig, manifestVersion: 2 }); setFakeWxt({ - config: { - manifestVersion: 2, - }, + config, builder: wxt.builder, + entrypointFinder: new FlatEntrypointFinder(config), }); const options = { type: 'module', @@ -381,11 +382,11 @@ describe('findEntrypoints', () => { }); it('should allow type=module for MV3 background service workers', async () => { + const config = fakeResolvedConfig({ ...baseConfig, manifestVersion: 3 }); setFakeWxt({ - config: { - manifestVersion: 3, - }, + config, builder: wxt.builder, + entrypointFinder: new FlatEntrypointFinder(config), }); const options = { type: 'module', @@ -399,12 +400,11 @@ describe('findEntrypoints', () => { }); it("should include a virtual background script so dev reloading works when there isn't a background entrypoint defined by the user", async () => { + const serveConfig = fakeResolvedConfig({ ...baseConfig, command: 'serve' }); setFakeWxt({ - config: { - ...config, - command: 'serve', - }, + config: serveConfig, builder: wxt.builder, + entrypointFinder: new FlatEntrypointFinder(serveConfig), }); globMock.mockResolvedValueOnce(['popup.html']); @@ -948,15 +948,14 @@ describe('findEntrypoints', () => { 'injected.content/index.ts', ]); const filterEntrypoints = ['popup', 'ui']; + const filterConfig = fakeResolvedConfig({ + ...baseConfig, + filterEntrypoints: new Set(filterEntrypoints), + }); setFakeWxt({ - config: { - root: '/', - entrypointsDir: resolve('/src/entrypoints'), - outDir: resolve('.output'), - command: 'build', - filterEntrypoints: new Set(filterEntrypoints), - }, + config: filterConfig, builder: wxt.builder, + entrypointFinder: new FlatEntrypointFinder(filterConfig), }); importEntrypointsMock.mockResolvedValue([{}]);