From d982f97bd8a600dfad1ba6747b61fad4a393afc0 Mon Sep 17 00:00:00 2001 From: Aaron Date: Sat, 21 Feb 2026 15:57:49 -0600 Subject: [PATCH] feat(modules): Add support for augumenting entrypoint options (#2149) --- docs/guide/essentials/wxt-modules.md | 52 ++++++++++++ .../__tests__/find-entrypoints.test.ts | 32 ++++++- .../core/utils/building/find-entrypoints.ts | 84 ++++++------------- packages/wxt/src/types.ts | 1 + 4 files changed, 111 insertions(+), 58 deletions(-) diff --git a/docs/guide/essentials/wxt-modules.md b/docs/guide/essentials/wxt-modules.md index 6296e261..22ac9763 100644 --- a/docs/guide/essentials/wxt-modules.md +++ b/docs/guide/essentials/wxt-modules.md @@ -141,6 +141,58 @@ console.log(config.myModule); This is very useful when [generating runtime code](#generate-runtime-module). +#### Add custom entrypoint options + +Modules can add custom options to entrypoints by augmenting the entrypoint options types. This allows you to add custom configuration that can be accessed during the build process. + +```ts +import { defineWxtModule } from 'wxt/modules'; +import 'wxt'; + +declare module 'wxt' { + export interface BackgroundEntrypointOptions { + // Add custom options to the background entrypoint + myCustomOption?: string; + } +} + +export default defineWxtModule({ + setup(wxt) { + wxt.hook('entrypoints:resolved', (_, entrypoints) => { + const background = entrypoints.find((e) => e.type === 'background'); + if (background) { + console.log('Custom option:', background.options.myCustomOption); + } + }); + }, +}); +``` + +Now users can set the custom option in their entrypoint: + +```ts [entrypoints/background.ts] +export default defineBackground({ + myCustomOption: 'custom value', + main() { + // ... + }, +}); +``` + +This works for all other JS and HTML entrypoints, here's an example of how to pass a custom option from an HTML file. + +```html [entrypoints/popup.html] + + + + Popup + + + + + +``` + #### Generate output file ```ts 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 3355bd51..a8edb215 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 @@ -112,7 +112,9 @@ describe('findEntrypoints', () => { name: 'options', inputPath: resolve(config.entrypointsDir, 'options.html'), outputDir: config.outDir, - options: {}, + options: { + title: 'Default Title', + }, skipped: false, }, ], @@ -133,6 +135,7 @@ describe('findEntrypoints', () => { outputDir: config.outDir, options: { openInTab: true, + title: 'Title', }, skipped: false, }, @@ -150,6 +153,33 @@ describe('findEntrypoints', () => { }, ); + it('should extract wxt.* meta tags from HTML entrypoints', async () => { + const path = 'popup.html'; + const content = ` + + + + + + Test Title + + + `; + + globMock.mockResolvedValueOnce([path]); + readFileMock.mockResolvedValueOnce(content); + + const entrypoints = await findEntrypoints(); + + expect(entrypoints).toHaveLength(1); + expect(entrypoints[0].options).toMatchObject({ + defaultIcon: { '16': '/icon/16.png' }, + customOption: 'custom_value', + anotherOption: true, + defaultTitle: 'Test Title', + }); + }); + it.each<[string, Omit]>([ [ 'content.ts', diff --git a/packages/wxt/src/core/utils/building/find-entrypoints.ts b/packages/wxt/src/core/utils/building/find-entrypoints.ts index 81abc071..f6992fe0 100644 --- a/packages/wxt/src/core/utils/building/find-entrypoints.ts +++ b/packages/wxt/src/core/utils/building/find-entrypoints.ts @@ -115,20 +115,14 @@ export async function findEntrypoints(): Promise { ...info, type, outputDir: resolve(wxt.config.outDir, CONTENT_SCRIPT_OUT_DIR), - options: { - include: options.include, - exclude: options.exclude, - }, + options, }; default: return { ...info, type, outputDir: wxt.config.outDir, - options: { - include: options.include, - exclude: options.exclude, - }, + options, }; } }), @@ -200,7 +194,7 @@ async function importEntrypoints(infos: EntrypointInfo[]) { return resMap; } -/** Extract `manifest.` options from meta tags, converting snake_case keys to camelCase */ +/** Extract `manifest.` and `wxt.` options from meta tags, converting snake_case keys to camelCase */ async function importHtmlEntrypoint( info: EntrypointInfo, ): Promise> { @@ -214,9 +208,16 @@ async function importHtmlEntrypoint( metaTags.forEach((tag) => { const name = tag.name; - if (!name.startsWith('manifest.')) return; + let key: string; + + if (name.startsWith('manifest.')) { + key = camelCase(name.slice(9)); + } else if (name.startsWith('wxt.')) { + key = camelCase(name.slice(4)); + } else { + return; + } - const key = camelCase(name.slice(9)); try { res[key] = JSON5.parse(tag.content); } catch { @@ -266,24 +267,20 @@ async function getPopupEntrypoint( info: EntrypointInfo, options: Record, ): Promise { + // Extract non-per-browser options + const { themeIcons, title, type, ...perBrowserOptions } = options; + const strictOptions: PopupEntrypoint['options'] = resolvePerBrowserOptions( { - browserStyle: options.browserStyle, - exclude: options.exclude, - include: options.include, - defaultIcon: options.defaultIcon, - defaultTitle: options.title, - mv2Key: options.type, - defaultArea: options.defaultArea, + ...perBrowserOptions, + defaultTitle: title, + mv2Key: type, }, wxt.config.browser, ); if (strictOptions.mv2Key && strictOptions.mv2Key !== 'page_action') strictOptions.mv2Key = 'browser_action'; - // themeIcons is an array of objects, not a per-browser option - const themeIcons = options.themeIcons; - return { type: 'popup', name: 'popup', @@ -300,16 +297,7 @@ async function getOptionsEntrypoint( return { type: 'options', name: 'options', - options: resolvePerBrowserOptions( - { - browserStyle: options.browserStyle, - chromeStyle: options.chromeStyle, - exclude: options.exclude, - include: options.include, - openInTab: options.openInTab, - }, - wxt.config.browser, - ), + options: resolvePerBrowserOptions(options, wxt.config.browser), inputPath: info.inputPath, outputDir: wxt.config.outDir, }; @@ -324,10 +312,7 @@ async function getUnlistedPageEntrypoint( name: info.name, inputPath: info.inputPath, outputDir: wxt.config.outDir, - options: { - include: options.include, - exclude: options.exclude, - }, + options, }; } @@ -340,13 +325,7 @@ async function getUnlistedScriptEntrypoint( name, inputPath, outputDir: wxt.config.outDir, - options: resolvePerBrowserOptions( - { - include: options.include, - exclude: options.exclude, - }, - wxt.config.browser, - ), + options: resolvePerBrowserOptions(options, wxt.config.browser), }; } @@ -355,15 +334,7 @@ async function getBackgroundEntrypoint( options: Record, ): Promise { const strictOptions: BackgroundEntrypoint['options'] = - resolvePerBrowserOptions( - { - include: options.include, - exclude: options.exclude, - persistent: options.persistent, - type: options.type, - }, - wxt.config.browser, - ); + resolvePerBrowserOptions(options, wxt.config.browser); if (wxt.config.manifestVersion !== 3) { delete strictOptions.type; @@ -400,17 +371,16 @@ async function getSidepanelEntrypoint( info: EntrypointInfo, options: Record, ): Promise { + // Extract non-per-browser options and rename title to defaultTitle + const { title, ...perBrowserOptions } = options; + return { type: 'sidepanel', name: info.name, options: resolvePerBrowserOptions( { - browserStyle: options.browserStyle, - exclude: options.exclude, - include: options.include, - defaultIcon: options.defaultIcon, - defaultTitle: options.title, - openAtInstall: options.openAtInstall, + ...perBrowserOptions, + defaultTitle: title, }, wxt.config.browser, ), diff --git a/packages/wxt/src/types.ts b/packages/wxt/src/types.ts index 43ba58ea..04b5306e 100644 --- a/packages/wxt/src/types.ts +++ b/packages/wxt/src/types.ts @@ -684,6 +684,7 @@ export interface PopupEntrypointOptions extends BaseEntrypointOptions { } export interface OptionsEntrypointOptions extends BaseEntrypointOptions { + title?: string; openInTab?: PerBrowserOption; browserStyle?: PerBrowserOption; chromeStyle?: PerBrowserOption;