From cb179df0a000aea0596a1b39c2177d35bbd16927 Mon Sep 17 00:00:00 2001 From: fkatsuhiro Date: Sun, 6 Sep 2026 23:01:42 +0900 Subject: [PATCH 1/4] feat: Add apply option to addWxtPlugin to filter which endpoint pluguin load --- packages/analytics/modules/analytics/index.ts | 5 +- .../internal-utils/testing/fake-objects.ts | 1 + .../vite/plugins/resolve-virtual-modules.ts | 12 ++- .../vite/plugins/wxt-plugin-loader.ts | 85 ++++++++++++++----- packages/wxt/src/internal/find-entrypoints.ts | 3 + packages/wxt/src/internal/wxt.ts | 1 + packages/wxt/src/modules/index.ts | 16 +++- packages/wxt/src/types.ts | 28 +++++- 8 files changed, 119 insertions(+), 32 deletions(-) diff --git a/packages/analytics/modules/analytics/index.ts b/packages/analytics/modules/analytics/index.ts index 07af9a307..c0eddc258 100644 --- a/packages/analytics/modules/analytics/index.ts +++ b/packages/analytics/modules/analytics/index.ts @@ -73,9 +73,8 @@ export const analytics = createAnalytics(useAppConfig().analytics); } }); - // Ensure analytics is initialized in every context, mainly the background. - // TODO: Once there's a way to filter which entrypoints a plugin is applied to, only apply this to the background - addWxtPlugin(wxt, pluginModuleId); + // Ensure analytics is initialized in the background only. + addWxtPlugin(wxt, pluginModuleId, (entry) => entry.type === 'background'); // Fix issues with dependencies addViteConfig(wxt, () => ({ diff --git a/packages/wxt/src/internal-utils/testing/fake-objects.ts b/packages/wxt/src/internal-utils/testing/fake-objects.ts index b9836092b..fec4a9ff6 100644 --- a/packages/wxt/src/internal-utils/testing/fake-objects.ts +++ b/packages/wxt/src/internal-utils/testing/fake-objects.ts @@ -324,6 +324,7 @@ export const fakeWxt = fakeObjectCreator(() => ({ findEntrypoints: () => [], }), runner: mock(), + entrypoints: [], })); export const fakeWxtDevServer = fakeObjectCreator(() => ({ diff --git a/packages/wxt/src/internal/builders/vite/plugins/resolve-virtual-modules.ts b/packages/wxt/src/internal/builders/vite/plugins/resolve-virtual-modules.ts index ae041257f..1c006f383 100644 --- a/packages/wxt/src/internal/builders/vite/plugins/resolve-virtual-modules.ts +++ b/packages/wxt/src/internal/builders/vite/plugins/resolve-virtual-modules.ts @@ -3,6 +3,7 @@ import { resolve } from 'path'; import type { Plugin } from 'vite'; import { ResolvedConfig } from '../../../../types'; import { normalizePath } from '../../../../internal-utils/path-utils'; +import { getEntrypointName } from '../../../../internal-utils/entrypoint-utils'; import { VirtualModuleId, virtualModuleNames } from '../../../virtual-modules'; /** @@ -37,7 +38,16 @@ export function resolveVirtualModules(config: ResolvedConfig): Plugin[] { resolve(config.wxtModuleDir, `dist/virtual/${name}.mjs`), 'utf-8', ); - return template.replace(`virtual:user-${name}`, inputPath); + const entrypointName = getEntrypointName( + config.entrypointsDir, + inputPath, + ); + return template + .replace(`virtual:user-${name}`, inputPath) + .replace( + 'virtual:wxt-plugins', + `virtual:wxt-plugins?entrypoint=${encodeURIComponent(entrypointName)}`, + ); }, }, }; diff --git a/packages/wxt/src/internal/builders/vite/plugins/wxt-plugin-loader.ts b/packages/wxt/src/internal/builders/vite/plugins/wxt-plugin-loader.ts index 3943458a1..f5d0bb688 100644 --- a/packages/wxt/src/internal/builders/vite/plugins/wxt-plugin-loader.ts +++ b/packages/wxt/src/internal/builders/vite/plugins/wxt-plugin-loader.ts @@ -1,12 +1,15 @@ import { parseHTML } from 'linkedom'; import type * as vite from 'vite'; import { normalizePath } from '../../../../internal-utils/path-utils'; -import { ResolvedConfig } from '../../../../types'; +import { getEntrypointName } from '../../../../internal-utils/entrypoint-utils'; +import { Entrypoint, ResolvedConfig } from '../../../../types'; +import { wxt } from '../../../wxt'; /** * Resolve and load plugins for each entrypoint. This handles both JS * entrypoints via the `virtual:wxt-plugins` import, and HTML files by adding - * `virtual:wxt-html-plugins` to the document's `` + * `virtual:wxt-html-plugins` to the document's ``. Accepts an optional + * `?entrypoint=` query to evaluate plugin `apply` hooks per request. */ export function wxtPluginLoader(config: ResolvedConfig): vite.Plugin { const virtualModuleId = 'virtual:wxt-plugins'; @@ -14,45 +17,72 @@ export function wxtPluginLoader(config: ResolvedConfig): vite.Plugin { const virtualHtmlModuleId = 'virtual:wxt-html-plugins'; const resolvedVirtualHtmlModuleId = '\0' + virtualHtmlModuleId; + function getEntrypointFromId(id: string): Entrypoint | undefined { + const queryIndex = id.indexOf('?'); + if (queryIndex === -1) return undefined; + const entrypointNameFromQueryParam = new URLSearchParams( + id.slice(queryIndex), + ).get('entrypoint'); + if (!entrypointNameFromQueryParam) return undefined; + return wxt.entrypoints.find( + (entry) => entry.name === entrypointNameFromQueryParam, + ); + } + + function getPluginModules(entrypoint: Entrypoint | undefined): string[] { + return config.plugins + .filter( + (plugin) => entrypoint == null || (plugin.apply?.(entrypoint) ?? true), + ) + .map((plugin) => plugin.module); + } + return { name: 'wxt:plugin-loader', resolveId: { filter: { id: [ - new RegExp(`^${virtualModuleId}$`), - new RegExp(`^${virtualHtmlModuleId}$`), + new RegExp(`^${virtualModuleId}`), + new RegExp(`^${virtualHtmlModuleId}`), ], }, handler(id) { - if (id === virtualModuleId) { - return resolvedVirtualModuleId; - } - - return resolvedVirtualHtmlModuleId; + return '\0' + id; }, }, load: { filter: { id: [ - new RegExp(`^${resolvedVirtualModuleId}$`), - new RegExp(`^${resolvedVirtualHtmlModuleId}$`), + new RegExp(`^${resolvedVirtualModuleId}`), + new RegExp(`^${resolvedVirtualHtmlModuleId}`), ], }, handler(id) { - if (id === resolvedVirtualModuleId) { - // Import and init all plugins - const imports = config.plugins + const bareId = id.slice(1); + const entrypoint = getEntrypointFromId(bareId); + + if ( + bareId === virtualModuleId || + bareId.startsWith(`${virtualModuleId}?`) + ) { + // Import and init only the plugins that apply to this entrypoint + const modules = getPluginModules(entrypoint); + const imports = modules .map( - (plugin, i) => - `import initPlugin${i} from '${normalizePath(plugin)}';`, + (module, i) => + `import initPlugin${i} from '${normalizePath(module)}';`, ) .join('\n'); - const initCalls = config.plugins + const initCalls = modules .map((_, i) => ` initPlugin${i}();`) .join('\n'); return `${imports}\n\nexport function initPlugins() {\n${initCalls}\n}`; } else { - return `import { initPlugins } from '${virtualModuleId}'; + // Forward resolved entrypoint to match the JS module. + const jsModuleId = entrypoint + ? `${virtualModuleId}?entrypoint=${encodeURIComponent(entrypoint.name)}` + : virtualModuleId; + return `import { initPlugins } from '${jsModuleId}'; try { initPlugins(); } catch (err) { @@ -64,11 +94,24 @@ export function wxtPluginLoader(config: ResolvedConfig): vite.Plugin { transformIndexHtml: { // Use "pre" so the new script is added before vite bundles all the scripts order: 'pre', - handler(html, _ctx) { + // `_ctx` -> `ctx` To access entrypoint info for per-page plugin filtering. + handler(html, ctx) { + const entrypointName = getEntrypointName( + config.entrypointsDir, + ctx.filename, + ); + const entrypoint = wxt.entrypoints.find( + (entry) => entry.name === entrypointName, + ); + const query = entrypoint + ? `?entrypoint=${encodeURIComponent(entrypoint.name)}` + : ''; + const htmlModuleId = `${virtualHtmlModuleId}${query}`; + const src = config.command === 'serve' - ? `${config.dev.server?.origin}/@id/${virtualHtmlModuleId}` - : virtualHtmlModuleId; + ? `${config.dev.server?.origin}/@id/${htmlModuleId}` + : htmlModuleId; const { document } = parseHTML(html); const existing = document.querySelector(`script[src='${src}']`); diff --git a/packages/wxt/src/internal/find-entrypoints.ts b/packages/wxt/src/internal/find-entrypoints.ts index d94dc664a..ea846d5ea 100644 --- a/packages/wxt/src/internal/find-entrypoints.ts +++ b/packages/wxt/src/internal/find-entrypoints.ts @@ -111,6 +111,9 @@ export async function findEntrypoints(): Promise { skipped: isEntrypointSkipped(entry), })); + // Expose the latest entrypoints + wxt.entrypoints = entrypoints; + await wxt.hooks.callHook('entrypoints:resolved', wxt, entrypoints); wxt.logger.debug('All entrypoints:', entrypoints); diff --git a/packages/wxt/src/internal/wxt.ts b/packages/wxt/src/internal/wxt.ts index f14c2ab8f..705c3f171 100644 --- a/packages/wxt/src/internal/wxt.ts +++ b/packages/wxt/src/internal/wxt.ts @@ -72,6 +72,7 @@ export async function registerWxt( runner, entrypointFinder, server: undefined, + entrypoints: [], }; await initWxtModules(); diff --git a/packages/wxt/src/modules/index.ts b/packages/wxt/src/modules/index.ts index 14b4e7116..6a739a121 100644 --- a/packages/wxt/src/modules/index.ts +++ b/packages/wxt/src/modules/index.ts @@ -129,16 +129,26 @@ export function addViteConfig( * * @example * export default defineWxtModule((wxt) => { - * addWxtPlugin(wxt, 'wxt-module-analytics/client-plugin'); + * addWxtPlugin( + * wxt, + * 'wxt-module-analytics/background-plugin', + * (entrypoint) => entrypoint.type === 'background', + * ); * }); * * @param wxt The wxt instance provided by the module's setup function. * @param plugin An import from an NPM module, or an absolute file path to the * file to load at runtime. + * @param apply Optional. Return `true` to load the plugin in the given + * entrypoint. Defaults to loading the plugin in every entrypoint. */ -export function addWxtPlugin(wxt: Wxt, plugin: string): void { +export function addWxtPlugin( + wxt: Wxt, + plugin: string, + apply?: (entrypoint: Entrypoint) => boolean, +): void { wxt.hooks.hook('config:resolved', (wxt) => { - wxt.config.plugins.push(plugin); + wxt.config.plugins.push({ module: plugin, apply }); }); } diff --git a/packages/wxt/src/types.ts b/packages/wxt/src/types.ts index 1e2b9b477..de64b8159 100644 --- a/packages/wxt/src/types.ts +++ b/packages/wxt/src/types.ts @@ -1558,6 +1558,11 @@ export interface Wxt { * ``` */ runner: ExtensionRunner; + /** + * Refreshed each time entrypoints are resolved. Empty until initial + * resolution. + */ + entrypoints: Entrypoint[]; } export interface ResolvedConfig { @@ -1681,13 +1686,28 @@ export interface ResolvedConfig { builtinModules: WxtModule[]; userModules: WxtModuleWithMetadata[]; /** - * An array of string to import plugins from. These paths should be resolvable - * by vite, and they should `export default defineWxtPlugin(...)`. + * Plugins registered via `addWxtPlugin`. Each entry's `module` should be + * resolvable by vite, and should `export default defineWxtPlugin(...)`. An + * optional `apply` function restricts which entrypoints the plugin loads in. * * @example - * ['@wxt-dev/module-vue/plugin', 'wxt-module-google-analytics/plugin']; + * [{ module: '@wxt-dev/module-vue/plugin' }]; + */ + plugins: WxtPluginEntry[]; +} + +/** + * A plugin registered via `addWxtPlugin`, along with an optional `apply` + * function controlling which entrypoints it's loaded in. + */ +export interface WxtPluginEntry { + /** Import path or absolute file path to the plugin module. */ + module: string; + /** + * Return `true` to include the plugin in this entrypoint. When omitted, the + * plugin is included in every entrypoint. */ - plugins: string[]; + apply?: (entrypoint: Entrypoint) => boolean; } export interface FsCache { From 9ded819ace5e043456dd4b34b3b1848985ae89a6 Mon Sep 17 00:00:00 2001 From: fkatsuhiro Date: Thu, 10 Sep 2026 07:34:00 +0900 Subject: [PATCH 2/4] feat: test check modules --- packages/wxt/e2e/tests/modules.test.ts | 71 +++++++++++++++++++ .../wxt/e2e/tests/output-structure.test.ts | 2 +- .../__tests__/wxt-plugin-loader.test.ts | 36 ++++++++++ .../vite/plugins/wxt-plugin-loader.ts | 23 +++--- 4 files changed, 121 insertions(+), 11 deletions(-) create mode 100644 packages/wxt/src/internal/builders/vite/plugins/__tests__/wxt-plugin-loader.test.ts diff --git a/packages/wxt/e2e/tests/modules.test.ts b/packages/wxt/e2e/tests/modules.test.ts index 56ef03849..4bc710f24 100644 --- a/packages/wxt/e2e/tests/modules.test.ts +++ b/packages/wxt/e2e/tests/modules.test.ts @@ -243,6 +243,77 @@ describe('Module Helpers', () => { await expect(project.serializeOutput()).resolves.toContain(expectedText); }); + + it('should apply plugins only to entrypoints matching their filter', async () => { + const project = new TestProject(); + project.addFile( + 'entrypoints/background.ts', + 'export default defineBackground(() => {})', + ); + project.addFile( + 'entrypoints/content.ts', + ` + export default defineContentScript({ + matches: ["*://*/*"], + main: () => {}, + }) + `, + ); + project.addFile('entrypoints/popup/index.html', ''); + project.addFile('entrypoints/options/index.html', ''); + + const backgroundPluginPath = project.addFile( + 'modules/test/background-plugin.ts', + `export default defineWxtPlugin(() => console.log("BACKGROUND_PLUGIN"))`, + ); + const popupPluginPath = project.addFile( + 'modules/test/popup-plugin.ts', + `export default defineWxtPlugin(() => console.log("POPUP_PLUGIN"))`, + ); + project.addFile( + 'modules/test.ts', + ` + import { defineWxtModule, addWxtPlugin } from 'wxt/modules'; + + export default defineWxtModule((wxt) => { + addWxtPlugin(wxt, "${normalizePath(backgroundPluginPath)}", (entrypoint) => entrypoint.type === 'background'); + addWxtPlugin(wxt, "${normalizePath(popupPluginPath)}", (entrypoint) => entrypoint.name === 'popup'); + }); + `, + ); + + await project.build(); + + await expect( + project.serializeFile('.output/chrome-mv3/background.js'), + ).resolves.toContain('BACKGROUND_PLUGIN'); + await expect( + project.serializeFile('.output/chrome-mv3/content-scripts/content.js'), + ).resolves.not.toContain('BACKGROUND_PLUGIN'); + + const getInjectedScript = async (htmlFile: string) => { + const html = await readFile( + project.resolvePath('.output/chrome-mv3', htmlFile), + 'utf-8', + ); + const match = html.match(/]*\ssrc="([^"]+)"/); + if (!match) throw Error(`No script tag in ${htmlFile}`); + return readFile( + project.resolvePath( + '.output/chrome-mv3', + match[1].replace(/^\//, ''), + ), + 'utf-8', + ); + }; + + await expect(getInjectedScript('popup.html')).resolves.toContain( + 'POPUP_PLUGIN', + ); + await expect(getInjectedScript('options.html')).resolves.not.toContain( + 'POPUP_PLUGIN', + ); + }); }); describe('imports', () => { diff --git a/packages/wxt/e2e/tests/output-structure.test.ts b/packages/wxt/e2e/tests/output-structure.test.ts index 8dd06e61d..7d9b3a34b 100644 --- a/packages/wxt/e2e/tests/output-structure.test.ts +++ b/packages/wxt/e2e/tests/output-structure.test.ts @@ -381,7 +381,7 @@ describe('Output Directory Structure', () => { .toMatchInlineSnapshot(` ".output/chrome-mv3/background.js ---------------------------------------- - import { n as logHello } from "./chunks/_virtual_wxt-plugins-BdnAIYoG.js"; + import { t as logHello } from "./chunks/log-DNePel8J.js"; function defineBackground(arg) { if (arg == null || typeof arg === "function") return { main: arg }; return arg; diff --git a/packages/wxt/src/internal/builders/vite/plugins/__tests__/wxt-plugin-loader.test.ts b/packages/wxt/src/internal/builders/vite/plugins/__tests__/wxt-plugin-loader.test.ts new file mode 100644 index 000000000..7ae99088b --- /dev/null +++ b/packages/wxt/src/internal/builders/vite/plugins/__tests__/wxt-plugin-loader.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it } from 'vitest'; +import { getPluginModules } from '../wxt-plugin-loader'; +import { + fakeBackgroundEntrypoint, + fakeContentScriptEntrypoint, +} from '../../../../../internal-utils/testing/fake-objects'; +import type { WxtPluginEntry } from '../../../../../types'; + +describe('Plugin Loader', () => { + describe('getPluginModules', () => { + const background = fakeBackgroundEntrypoint(); + const contentScript = fakeContentScriptEntrypoint(); + + it('includes plugins without an apply function', () => { + const plugins: WxtPluginEntry[] = [{ module: 'a' }]; + expect(getPluginModules(plugins, background)).toEqual(['a']); + }); + + it('includes only plugins whose apply function returns true', () => { + const plugins: WxtPluginEntry[] = [ + { module: 'a', apply: (entry) => entry.type === 'background' }, + { module: 'b', apply: (entry) => entry.type === 'content-script' }, + ]; + expect(getPluginModules(plugins, background)).toEqual(['a']); + expect(getPluginModules(plugins, contentScript)).toEqual(['b']); + }); + + it('includes every plugin when the entrypoint is unresolved', () => { + const plugins: WxtPluginEntry[] = [ + { module: 'a', apply: () => false }, + { module: 'b' }, + ]; + expect(getPluginModules(plugins, undefined)).toEqual(['a', 'b']); + }); + }); +}); diff --git a/packages/wxt/src/internal/builders/vite/plugins/wxt-plugin-loader.ts b/packages/wxt/src/internal/builders/vite/plugins/wxt-plugin-loader.ts index f5d0bb688..a953d2a26 100644 --- a/packages/wxt/src/internal/builders/vite/plugins/wxt-plugin-loader.ts +++ b/packages/wxt/src/internal/builders/vite/plugins/wxt-plugin-loader.ts @@ -2,9 +2,20 @@ import { parseHTML } from 'linkedom'; import type * as vite from 'vite'; import { normalizePath } from '../../../../internal-utils/path-utils'; import { getEntrypointName } from '../../../../internal-utils/entrypoint-utils'; -import { Entrypoint, ResolvedConfig } from '../../../../types'; +import { Entrypoint, ResolvedConfig, WxtPluginEntry } from '../../../../types'; import { wxt } from '../../../wxt'; +export function getPluginModules( + plugins: WxtPluginEntry[], + entrypoint: Entrypoint | undefined, +): string[] { + return plugins + .filter( + (plugin) => entrypoint == null || (plugin.apply?.(entrypoint) ?? true), + ) + .map((plugin) => plugin.module); +} + /** * Resolve and load plugins for each entrypoint. This handles both JS * entrypoints via the `virtual:wxt-plugins` import, and HTML files by adding @@ -29,14 +40,6 @@ export function wxtPluginLoader(config: ResolvedConfig): vite.Plugin { ); } - function getPluginModules(entrypoint: Entrypoint | undefined): string[] { - return config.plugins - .filter( - (plugin) => entrypoint == null || (plugin.apply?.(entrypoint) ?? true), - ) - .map((plugin) => plugin.module); - } - return { name: 'wxt:plugin-loader', resolveId: { @@ -66,7 +69,7 @@ export function wxtPluginLoader(config: ResolvedConfig): vite.Plugin { bareId.startsWith(`${virtualModuleId}?`) ) { // Import and init only the plugins that apply to this entrypoint - const modules = getPluginModules(entrypoint); + const modules = getPluginModules(config.plugins, entrypoint); const imports = modules .map( (module, i) => From ce968035d722e8a8a30262113a92c680dd02664a Mon Sep 17 00:00:00 2001 From: fkatsuhiro Date: Thu, 22 Oct 2026 18:42:34 +0900 Subject: [PATCH 3/4] refactor: move entrypoint state out of global wxt object into a built-in module --- .../internal-utils/testing/fake-objects.ts | 1 - .../wxt/src/internal/builders/vite/index.ts | 2 -- .../vite/plugins/wxt-plugin-loader.ts | 20 ++++++++++---- .../wxt/src/internal/builtin-modules/index.ts | 2 ++ .../src/internal/builtin-modules/plugins.ts | 26 +++++++++++++++++++ packages/wxt/src/internal/find-entrypoints.ts | 3 --- packages/wxt/src/internal/wxt.ts | 1 - packages/wxt/src/types.ts | 7 +---- 8 files changed, 44 insertions(+), 18 deletions(-) create mode 100644 packages/wxt/src/internal/builtin-modules/plugins.ts diff --git a/packages/wxt/src/internal-utils/testing/fake-objects.ts b/packages/wxt/src/internal-utils/testing/fake-objects.ts index fec4a9ff6..b9836092b 100644 --- a/packages/wxt/src/internal-utils/testing/fake-objects.ts +++ b/packages/wxt/src/internal-utils/testing/fake-objects.ts @@ -324,7 +324,6 @@ export const fakeWxt = fakeObjectCreator(() => ({ findEntrypoints: () => [], }), runner: mock(), - entrypoints: [], })); export const fakeWxtDevServer = fakeObjectCreator(() => ({ diff --git a/packages/wxt/src/internal/builders/vite/index.ts b/packages/wxt/src/internal/builders/vite/index.ts index 2ad47772a..149884a20 100644 --- a/packages/wxt/src/internal/builders/vite/index.ts +++ b/packages/wxt/src/internal/builders/vite/index.ts @@ -198,13 +198,11 @@ export class ViteBuilder implements WxtBuilder { config.plugins ??= []; config.plugins.push( wxtPlugins.devHtmlPrerender(this.config, server), - wxtPlugins.resolveVirtualModules(this.config), wxtPlugins.devServerGlobals(this.config, server), wxtPlugins.tsconfigPaths(this.config), wxtPlugins.noopBackground(), wxtPlugins.addRuntimeEnvEntries(this.config), wxtPlugins.defineImportMeta(), - wxtPlugins.wxtPluginLoader(this.config), wxtPlugins.resolveAppConfig(this.config), ); if ( diff --git a/packages/wxt/src/internal/builders/vite/plugins/wxt-plugin-loader.ts b/packages/wxt/src/internal/builders/vite/plugins/wxt-plugin-loader.ts index a953d2a26..bb20d2f49 100644 --- a/packages/wxt/src/internal/builders/vite/plugins/wxt-plugin-loader.ts +++ b/packages/wxt/src/internal/builders/vite/plugins/wxt-plugin-loader.ts @@ -3,13 +3,16 @@ import type * as vite from 'vite'; import { normalizePath } from '../../../../internal-utils/path-utils'; import { getEntrypointName } from '../../../../internal-utils/entrypoint-utils'; import { Entrypoint, ResolvedConfig, WxtPluginEntry } from '../../../../types'; -import { wxt } from '../../../wxt'; export function getPluginModules( - plugins: WxtPluginEntry[], + plugins: Array, entrypoint: Entrypoint | undefined, ): string[] { return plugins + .map( + (plugin): WxtPluginEntry => + typeof plugin === 'string' ? { module: plugin } : plugin, + ) .filter( (plugin) => entrypoint == null || (plugin.apply?.(entrypoint) ?? true), ) @@ -21,8 +24,15 @@ export function getPluginModules( * entrypoints via the `virtual:wxt-plugins` import, and HTML files by adding * `virtual:wxt-html-plugins` to the document's ``. Accepts an optional * `?entrypoint=` query to evaluate plugin `apply` hooks per request. + * + * @param getEntrypoints Returns the most recently resolved entrypoints. + * Injected by the caller instead of read from a global, so this plugin + * doesn't depend on global `wxt` state. */ -export function wxtPluginLoader(config: ResolvedConfig): vite.Plugin { +export function wxtPluginLoader( + config: ResolvedConfig, + getEntrypoints: () => Entrypoint[], +): vite.Plugin { const virtualModuleId = 'virtual:wxt-plugins'; const resolvedVirtualModuleId = '\0' + virtualModuleId; const virtualHtmlModuleId = 'virtual:wxt-html-plugins'; @@ -35,7 +45,7 @@ export function wxtPluginLoader(config: ResolvedConfig): vite.Plugin { id.slice(queryIndex), ).get('entrypoint'); if (!entrypointNameFromQueryParam) return undefined; - return wxt.entrypoints.find( + return getEntrypoints().find( (entry) => entry.name === entrypointNameFromQueryParam, ); } @@ -103,7 +113,7 @@ export function wxtPluginLoader(config: ResolvedConfig): vite.Plugin { config.entrypointsDir, ctx.filename, ); - const entrypoint = wxt.entrypoints.find( + const entrypoint = getEntrypoints().find( (entry) => entry.name === entrypointName, ); const query = entrypoint diff --git a/packages/wxt/src/internal/builtin-modules/index.ts b/packages/wxt/src/internal/builtin-modules/index.ts index 4617e8925..3d5a02bb4 100644 --- a/packages/wxt/src/internal/builtin-modules/index.ts +++ b/packages/wxt/src/internal/builtin-modules/index.ts @@ -2,9 +2,11 @@ import { WxtModule } from '../../types'; import faviconPermission from './favicon-permission'; import unimport from './unimport'; import escapeUnicode from './escape-unicode'; +import plugins from './plugins'; export const builtinModules: WxtModule[] = [ unimport, faviconPermission, escapeUnicode, + plugins, ]; diff --git a/packages/wxt/src/internal/builtin-modules/plugins.ts b/packages/wxt/src/internal/builtin-modules/plugins.ts new file mode 100644 index 000000000..15a552463 --- /dev/null +++ b/packages/wxt/src/internal/builtin-modules/plugins.ts @@ -0,0 +1,26 @@ +import { addViteConfig, defineWxtModule } from '../../modules'; +import type { Entrypoint } from '../../types'; +import { resolveVirtualModules } from '../builders/vite/plugins/resolve-virtual-modules'; +import { wxtPluginLoader } from '../builders/vite/plugins/wxt-plugin-loader'; + +/** + * Owns the virtual `wxt-plugins`/`wxt-html-plugins` modules used by + * `addWxtPlugin`. Keeps the latest resolved entrypoints in module-local state + * instead of on the global `wxt` object. + */ +export default defineWxtModule({ + name: 'wxt:built-in:plugins', + setup(wxt) { + let entrypoints: Entrypoint[] = []; + wxt.hooks.hook('entrypoints:resolved', (_, resolved) => { + entrypoints = resolved; + }); + + addViteConfig(wxt, () => ({ + plugins: [ + resolveVirtualModules(wxt.config), + wxtPluginLoader(wxt.config, () => entrypoints), + ], + })); + }, +}); diff --git a/packages/wxt/src/internal/find-entrypoints.ts b/packages/wxt/src/internal/find-entrypoints.ts index ea846d5ea..d94dc664a 100644 --- a/packages/wxt/src/internal/find-entrypoints.ts +++ b/packages/wxt/src/internal/find-entrypoints.ts @@ -111,9 +111,6 @@ export async function findEntrypoints(): Promise { skipped: isEntrypointSkipped(entry), })); - // Expose the latest entrypoints - wxt.entrypoints = entrypoints; - await wxt.hooks.callHook('entrypoints:resolved', wxt, entrypoints); wxt.logger.debug('All entrypoints:', entrypoints); diff --git a/packages/wxt/src/internal/wxt.ts b/packages/wxt/src/internal/wxt.ts index 705c3f171..f14c2ab8f 100644 --- a/packages/wxt/src/internal/wxt.ts +++ b/packages/wxt/src/internal/wxt.ts @@ -72,7 +72,6 @@ export async function registerWxt( runner, entrypointFinder, server: undefined, - entrypoints: [], }; await initWxtModules(); diff --git a/packages/wxt/src/types.ts b/packages/wxt/src/types.ts index de64b8159..b4e389f87 100644 --- a/packages/wxt/src/types.ts +++ b/packages/wxt/src/types.ts @@ -1558,11 +1558,6 @@ export interface Wxt { * ``` */ runner: ExtensionRunner; - /** - * Refreshed each time entrypoints are resolved. Empty until initial - * resolution. - */ - entrypoints: Entrypoint[]; } export interface ResolvedConfig { @@ -1693,7 +1688,7 @@ export interface ResolvedConfig { * @example * [{ module: '@wxt-dev/module-vue/plugin' }]; */ - plugins: WxtPluginEntry[]; + plugins: Array; } /** From b06284d8c38e77e423997abc42d4ae9cd9e971f8 Mon Sep 17 00:00:00 2001 From: Aaron Date: Sun, 25 Oct 2026 18:14:52 -0500 Subject: [PATCH 4/4] Apply suggestion from @aklinker1 --- .../wxt/src/internal/builders/vite/plugins/wxt-plugin-loader.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/wxt/src/internal/builders/vite/plugins/wxt-plugin-loader.ts b/packages/wxt/src/internal/builders/vite/plugins/wxt-plugin-loader.ts index bb20d2f49..a6a01bd44 100644 --- a/packages/wxt/src/internal/builders/vite/plugins/wxt-plugin-loader.ts +++ b/packages/wxt/src/internal/builders/vite/plugins/wxt-plugin-loader.ts @@ -107,7 +107,6 @@ export function wxtPluginLoader( transformIndexHtml: { // Use "pre" so the new script is added before vite bundles all the scripts order: 'pre', - // `_ctx` -> `ctx` To access entrypoint info for per-page plugin filtering. handler(html, ctx) { const entrypointName = getEntrypointName( config.entrypointsDir,