diff --git a/cspell.yml b/cspell.yml index a942f705c..4399d7178 100644 --- a/cspell.yml +++ b/cspell.yml @@ -135,6 +135,7 @@ words: - USSS - verkit - vitepress + - waterfox - webext - webextension - webextrc diff --git a/docs/guide/essentials/target-different-browsers.md b/docs/guide/essentials/target-different-browsers.md index 985fdf78a..ace8d3485 100644 --- a/docs/guide/essentials/target-different-browsers.md +++ b/docs/guide/essentials/target-different-browsers.md @@ -27,12 +27,28 @@ if (import.meta.env.FIREFOX) { Read about [Built-in Environment Variables](/guide/essentials/config/environment-variables.html#built-in-environment-variables) for more details. +## Map Custom Browsers + +WXT knows how a handful of labels behave: `chrome`, `edge`, `brave`, `opera` and `chromium` generate Chromium code, `firefox` generates Firefox code, and `safari` and `ios` generate Safari code. + +When you build with a custom label, WXT falls back to Chromium, since it has no way to know what that label runs in. Use `targetBrowserMap` to tell it: + +```ts +export default defineConfig({ + targetBrowserMap: { + waterfox: 'firefox', + }, +}); +``` + +Now `wxt -b waterfox` generates the same manifest, and the same default manifest version, as `wxt -b firefox`. The build still outputs to `.output/waterfox-mv2`, and `import.meta.env.BROWSER` still reports `"waterfox"`, so output paths and runtime checks keep the label you built with. + ## Target a Manifest Version To target specific manifest versions, use the `--mv2` or `--mv3` CLI flags. :::tip Default Manifest Version -By default, WXT will target MV2 for Safari and Firefox and MV3 for all other browsers. +By default, WXT will target MV2 for Safari and Firefox, including custom labels mapped to them, and MV3 for all other browsers. ::: Similar to the browser, you can get the target manifest version at runtime using the [built-in environment variable](/guide/essentials/config/environment-variables.html#built-in-environment-variables): diff --git a/packages/wxt/e2e/tests/manifest-content.test.ts b/packages/wxt/e2e/tests/manifest-content.test.ts index b9b68f353..d8a6053d4 100644 --- a/packages/wxt/e2e/tests/manifest-content.test.ts +++ b/packages/wxt/e2e/tests/manifest-content.test.ts @@ -30,4 +30,62 @@ describe('Manifest Content', () => { expect(safariManifest.background.persistent).toBe(expected); }, ); + + it('should generate the same manifest for an ios build as for a safari build', async () => { + const project = new TestProject(); + + project.addFile( + 'entrypoints/background.ts', + `export default defineBackground({ main: () => {} })`, + ); + project.addFile('entrypoints/popup.html', ''); + project.addFile( + 'entrypoints/one.content/index.ts', + `import './style.css'; + + export default defineContentScript({ + matches: ['*://google.com/*'], + cssInjectionMode: 'ui', + main: () => {}, + })`, + ); + project.addFile('entrypoints/one.content/style.css', `body { color: red }`); + + await project.build({ browser: 'safari' }); + const safariManifest = await project.getOutputManifest( + '.output/safari-mv2/manifest.json', + ); + + // The output directory and the default manifest version still use the raw + // `--browser` label, only the generated manifest follows the actual browser. + await project.build({ browser: 'ios' }); + const iosManifest = await project.getOutputManifest( + '.output/ios-mv2/manifest.json', + ); + + expect(iosManifest).toEqual(safariManifest); + }); + + it('should apply a custom targetBrowserMap', async () => { + const project = new TestProject(); + project.setConfigFileConfig({ + targetBrowserMap: { + waterfox: 'firefox', + }, + }); + + project.addFile( + 'entrypoints/background.ts', + `export default defineBackground({ main: () => {} })`, + ); + + await project.build({ browser: 'waterfox' }); + const manifest = await project.getOutputManifest( + '.output/waterfox-mv2/manifest.json', + ); + + // Firefox is the only browser that defaults to MV2, so an MV2 build of a + // custom label proves the mapping was applied. + expect(manifest.manifest_version).toBe(2); + }); }); diff --git a/packages/wxt/src/internal-utils/target-browser.ts b/packages/wxt/src/internal-utils/target-browser.ts new file mode 100644 index 000000000..eab13fc3a --- /dev/null +++ b/packages/wxt/src/internal-utils/target-browser.ts @@ -0,0 +1,37 @@ +import type { ActualBrowser, TargetBrowser, TargetBrowserMap } from '../types'; + +/** + * Default mapping from `--browser` values to the actual browser WXT generates + * code for. Custom `--browser` values are only mapped when the project adds an + * entry for them in its `targetBrowserMap` config. + */ +export const defaultTargetBrowserMap: TargetBrowserMap = { + chrome: 'chromium', + edge: 'chromium', + brave: 'chromium', + opera: 'chromium', + chromium: 'chromium', + firefox: 'firefox', + safari: 'safari', + ios: 'safari', +}; + +/** + * Resolve the actual browser to generate code for from a `--browser` value. + * + * Values missing from the map fall back to `chromium`, the code WXT generated + * for unrecognized labels before `targetBrowserMap` existed. Projects can opt a + * custom label into Firefox or Safari behavior by mapping it. + */ +export function resolveActualBrowser( + browser: TargetBrowser, + targetBrowserMap: TargetBrowserMap = defaultTargetBrowserMap, +): ActualBrowser { + // Check own properties, so `--browser constructor` doesn't resolve to + // `Object.prototype.constructor`. + const mapped = Object.hasOwn(targetBrowserMap, browser) + ? targetBrowserMap[browser] + : undefined; + + return mapped ?? 'chromium'; +} diff --git a/packages/wxt/src/internal-utils/testing/fake-objects.ts b/packages/wxt/src/internal-utils/testing/fake-objects.ts index b9836092b..8a819d065 100644 --- a/packages/wxt/src/internal-utils/testing/fake-objects.ts +++ b/packages/wxt/src/internal-utils/testing/fake-objects.ts @@ -24,6 +24,7 @@ import { import { mock } from 'vitest-mock-extended'; import { vi } from 'vitest'; import { setWxtForTesting } from '../../internal/wxt'; +import { resolveActualBrowser } from '../target-browser'; import type { Browser } from '@wxt-dev/browser'; faker.seed(__TEST_SEED__); @@ -232,6 +233,7 @@ export const fakeResolvedConfig = fakeObjectCreator(() => { return { browser, + actualBrowser: resolveActualBrowser(browser), targetBrowsers: [], command, entrypointsDir: fakeDir(), @@ -345,6 +347,11 @@ export const fakeWxtDevServer = fakeObjectCreator(() => ({ export function setFakeWxt(overrides?: DeepPartial) { const wxt = fakeWxt(overrides); + // Mirror resolveConfig: `actualBrowser` is resolved from `browser` unless + // the test overrides it directly. + if (overrides?.config?.actualBrowser == null) { + wxt.config.actualBrowser = resolveActualBrowser(wxt.config.browser); + } setWxtForTesting(wxt); return wxt; } diff --git a/packages/wxt/src/internal/__tests__/manifest.test.ts b/packages/wxt/src/internal/__tests__/manifest.test.ts index ca4f56a52..9c5b48a28 100644 --- a/packages/wxt/src/internal/__tests__/manifest.test.ts +++ b/packages/wxt/src/internal/__tests__/manifest.test.ts @@ -19,10 +19,12 @@ import { Entrypoint, OutputAsset, TargetManifestVersion, + ActualBrowser, } from '../../types'; import { wxt } from '../wxt'; import { mock } from 'vitest-mock-extended'; import type { Browser } from '@wxt-dev/browser'; +import { resolveActualBrowser } from '../../internal-utils/target-browser'; const outDir = '/output'; const contentScriptOutDir = '/output/content-scripts'; @@ -1380,6 +1382,52 @@ describe('Manifest Utils', () => { ]); } }); + + it('should key browser-specific checks off actualBrowser, not the raw browser label', async () => { + const cs: ContentScriptEntrypoint = { + type: 'content-script', + name: 'one', + inputPath: 'entrypoints/one.content.ts', + outputDir: contentScriptOutDir, + options: { + matches: ['*://google.com/*'], + cssInjectionMode: 'ui', + }, + skipped: false, + }; + const styles: OutputAsset = { + type: 'asset', + fileName: 'content-scripts/one.css', + }; + + const entrypoints = [cs]; + const buildOutput: Omit = { + publicAssets: [], + steps: [{ entrypoints: cs, chunks: [styles] }], + }; + + setFakeWxt({ + config: { + outDir, + command: 'build', + manifestVersion: 3, + browser: 'ios', + actualBrowser: 'safari', + }, + }); + + const { manifest: actual } = await generateManifest( + entrypoints, + buildOutput, + ); + + expect(actual.web_accessible_resources).toEqual([ + { + matches: ['*://google.com/*'], + resources: ['content-scripts/one.css'], + }, + ]); + }); }); describe('registration', () => { @@ -2231,6 +2279,155 @@ describe('Manifest Utils', () => { ); }); }); + + describe('actualBrowser', () => { + const background = fakeBackgroundEntrypoint({ + outputDir: outDir, + options: { persistent: true, type: 'module' }, + skipped: false, + }); + const popup = fakePopupEntrypoint({ + outputDir: outDir, + options: { + actionType: 'page_action', + defaultIcon: { '16': '/icon/16.png' }, + defaultTitle: 'Default Title', + }, + skipped: false, + }); + const options = fakeOptionsEntrypoint({ + outputDir: outDir, + options: { + openInTab: true, + chromeStyle: true, + browserStyle: true, + }, + skipped: false, + }); + const sidepanel = fakeSidepanelEntrypoint({ + outputDir: outDir, + options: { + browserStyle: true, + defaultTitle: 'Sidepanel', + }, + skipped: false, + }); + const styles: OutputAsset = { + type: 'asset', + fileName: 'content-scripts/one.css', + }; + const contentScript: ContentScriptEntrypoint = { + type: 'content-script', + name: 'one', + inputPath: 'entrypoints/one.content.ts', + outputDir: contentScriptOutDir, + options: { + matches: ['*://google.com/*'], + cssInjectionMode: 'ui', + }, + skipped: false, + }; + const entrypoints: Entrypoint[] = [ + background, + popup, + options, + sidepanel, + contentScript, + ]; + const buildOutput: Omit = { + publicAssets: [], + steps: [{ entrypoints: contentScript, chunks: [styles] }], + }; + + async function generateFor(params: { + browser: string; + actualBrowser: ActualBrowser; + manifestVersion: TargetManifestVersion; + }) { + setFakeWxt({ + config: { + outDir, + command: 'build', + mode: 'production', + browser: params.browser, + actualBrowser: params.actualBrowser, + manifestVersion: params.manifestVersion, + manifest: { + name: 'wxt-test', + version: '1.2.3', + version_name: '1.2.3-beta', + }, + }, + }); + + return (await generateManifest(entrypoints, buildOutput)).manifest; + } + + const getDynamicUrlFlags = (manifest: Browser.runtime.Manifest) => { + const resources = manifest.web_accessible_resources; + if (!Array.isArray(resources)) return []; + + return resources.map((resource) => + typeof resource === 'object' && 'use_dynamic_url' in resource + ? Boolean(resource.use_dynamic_url) + : undefined, + ); + }; + + it.each<[TargetManifestVersion]>([[2], [3]])( + 'should generate the same manifest for an ios build as a safari build (MV%i)', + async (manifestVersion) => { + const safari = await generateFor({ + browser: 'safari', + actualBrowser: 'safari', + manifestVersion, + }); + const ios = await generateFor({ + browser: 'ios', + actualBrowser: resolveActualBrowser('ios'), + manifestVersion, + }); + + expect(ios).toEqual(safari); + expect(ios.options_ui?.open_in_tab).toBeUndefined(); + expect(getDynamicUrlFlags(ios)).toEqual([undefined]); + }, + ); + + it('should keep Chrome-only manifest fields when the actual browser is chromium', async () => { + const chrome = await generateFor({ + browser: 'chrome', + actualBrowser: 'chromium', + manifestVersion: 3, + }); + + expect(chrome.options_ui?.open_in_tab).toBe(true); + expect(getDynamicUrlFlags(chrome)).toEqual([true]); + }); + + it.each([ + { label: 'my-firefox', actualBrowser: 'firefox' }, + { label: 'my-safari', actualBrowser: 'safari' }, + ] as const)( + 'should generate the $actualBrowser manifest for the custom label "$label"', + async ({ label, actualBrowser }) => { + const mapped = await generateFor({ + browser: label, + actualBrowser: resolveActualBrowser(label, { + [label]: actualBrowser, + }), + manifestVersion: 2, + }); + const native = await generateFor({ + browser: actualBrowser, + actualBrowser, + manifestVersion: 2, + }); + + expect(mapped).toEqual(native); + }, + ); + }); }); describe('stripPathFromMatchPattern', () => { diff --git a/packages/wxt/src/internal/__tests__/resolve-config.test.ts b/packages/wxt/src/internal/__tests__/resolve-config.test.ts index 0b9e90f11..b2a1d5fbc 100644 --- a/packages/wxt/src/internal/__tests__/resolve-config.test.ts +++ b/packages/wxt/src/internal/__tests__/resolve-config.test.ts @@ -38,6 +38,100 @@ describe('resolveConfig', () => { }); }); + describe('actualBrowser', () => { + it.each<[string, string]>([ + ['chrome', 'chromium'], + ['edge', 'chromium'], + ['brave', 'chromium'], + ['opera', 'chromium'], + ['chromium', 'chromium'], + ['firefox', 'firefox'], + ['safari', 'safari'], + ['ios', 'safari'], + ])( + 'should resolve browser "%s" to actualBrowser "%s"', + async (browser, expected) => { + const config = await resolveConfig({ browser }, 'build'); + + expect(config.actualBrowser).toBe(expected); + }, + ); + + it('should fall back to chromium for labels missing from the map', async () => { + const config = await resolveConfig({ browser: 'waterfox' }, 'build'); + + expect(config.actualBrowser).toBe('chromium'); + }); + + it('should let a custom targetBrowserMap entry win over the defaults', async () => { + const config = await resolveConfig( + { + browser: 'waterfox', + targetBrowserMap: { waterfox: 'firefox' }, + }, + 'build', + ); + + expect(config.actualBrowser).toBe('firefox'); + }); + + it('should map several custom labels at once', async () => { + const targetBrowserMap = { + 'my-firefox': 'firefox', + 'my-safari': 'safari', + } as const; + + const firefox = await resolveConfig( + { browser: 'my-firefox', targetBrowserMap }, + 'build', + ); + const safari = await resolveConfig( + { browser: 'my-safari', targetBrowserMap }, + 'build', + ); + + expect(firefox.actualBrowser).toBe('firefox'); + expect(safari.actualBrowser).toBe('safari'); + }); + + it('should let a custom targetBrowserMap entry override a default mapping', async () => { + const config = await resolveConfig( + { + browser: 'chrome', + targetBrowserMap: { chrome: 'firefox' }, + }, + 'build', + ); + + expect(config.actualBrowser).toBe('firefox'); + }); + + it('should not resolve inherited object properties', async () => { + const config = await resolveConfig({ browser: 'constructor' }, 'build'); + + expect(config.actualBrowser).toBe('chromium'); + }); + + it('should not change the raw browser label', async () => { + const config = await resolveConfig({ browser: 'ios' }, 'build'); + + expect(config.browser).toBe('ios'); + }); + + it('should keep the raw browser label in the output directory', async () => { + const config = await resolveConfig({ browser: 'ios' }, 'build'); + + expect(config.outDir).toContain(`ios-mv${config.manifestVersion}`); + }); + + it('should default the manifest version based on the actual browser', async () => { + const config = await resolveConfig({ browser: 'ios' }, 'build'); + + expect(config.actualBrowser).toBe('safari'); + expect(config.manifestVersion).toBe(2); + }); + }); + describe('imports.dirs', () => { const logger = mock(); diff --git a/packages/wxt/src/internal/__tests__/runtime-env-entries.test.ts b/packages/wxt/src/internal/__tests__/runtime-env-entries.test.ts new file mode 100644 index 000000000..ac3714ca0 --- /dev/null +++ b/packages/wxt/src/internal/__tests__/runtime-env-entries.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from 'vitest'; +import { getRuntimeEnvEntries } from '../runtime-env-entries'; +import { fakeResolvedConfig } from '../../internal-utils/testing/fake-objects'; + +describe('getRuntimeEnvEntries', () => { + it('should expose the raw --browser label, not the actual browser', () => { + const config = fakeResolvedConfig({ + browser: 'ios', + actualBrowser: 'safari', + }); + + const values = Object.fromEntries( + getRuntimeEnvEntries(config).map((entry) => [entry.name, entry.value]), + ); + + // `BROWSER` and the per-browser booleans are documented as comparisons + // against the target browser, so they must keep using the label even when + // the actual browser is different. + expect(values.BROWSER).toBe('ios'); + expect(values.SAFARI).toBe(false); + expect(values.CHROME).toBe(false); + expect(values.FIREFOX).toBe(false); + }); + + it('should keep the browser booleans keyed off the label', () => { + const config = fakeResolvedConfig({ + browser: 'edge', + actualBrowser: 'chromium', + }); + + const values = Object.fromEntries( + getRuntimeEnvEntries(config).map((entry) => [entry.name, entry.value]), + ); + + expect(values.BROWSER).toBe('edge'); + expect(values.EDGE).toBe(true); + expect(values.CHROME).toBe(false); + }); +}); diff --git a/packages/wxt/src/internal/manifest.ts b/packages/wxt/src/internal/manifest.ts index 441aedcc9..bf9621000 100644 --- a/packages/wxt/src/internal/manifest.ts +++ b/packages/wxt/src/internal/manifest.ts @@ -111,13 +111,13 @@ export async function generateManifest( manifest.version = version; manifest.version_name = // Firefox doesn't support version_name - wxt.config.browser === 'firefox' || versionName === version + wxt.config.actualBrowser === 'firefox' || versionName === version ? undefined : versionName; // Warn if building for Firefox without data_collection_permissions if ( - wxt.config.browser === 'firefox' && + wxt.config.actualBrowser === 'firefox' && !userManifest.browser_specific_settings?.gecko ?.data_collection_permissions && !wxt.config.suppressWarnings?.firefoxDataCollection @@ -130,7 +130,7 @@ export async function generateManifest( } if ( - wxt.config.browser === 'firefox' && + wxt.config.actualBrowser === 'firefox' && !manifest.browser_specific_settings?.gecko?.id && !wxt.config.suppressWarnings?.firefoxId ) { @@ -142,7 +142,7 @@ export async function generateManifest( addEntrypoints(manifest, entrypoints, buildOutput); - if (wxt.config.browser === 'firefox') { + if (wxt.config.actualBrowser === 'firefox') { addDiscoveredThemeIcons(manifest, buildOutput); } @@ -237,7 +237,10 @@ function addEntrypoints( wxt.config.outDir, '.js', ); - if (wxt.config.browser === 'firefox' && wxt.config.manifestVersion === 3) { + if ( + wxt.config.actualBrowser === 'firefox' && + wxt.config.manifestVersion === 3 + ) { manifest.background = { type: background.options.type, scripts: [script], @@ -256,7 +259,7 @@ function addEntrypoints( } if (bookmarks) { - if (wxt.config.browser === 'firefox') { + if (wxt.config.actualBrowser === 'firefox') { wxt.logger.warn( 'Bookmarks are not supported by Firefox. chrome_url_overrides.bookmarks was not added to the manifest', ); @@ -271,7 +274,7 @@ function addEntrypoints( } if (history) { - if (wxt.config.browser === 'firefox') { + if (wxt.config.actualBrowser === 'firefox') { wxt.logger.warn( 'Bookmarks are not supported by Firefox. chrome_url_overrides.history was not added to the manifest', ); @@ -320,7 +323,7 @@ function addEntrypoints( const actionKey = wxt.config.manifestVersion === 2 ? (popup.options.actionType ?? 'browser_action') - : wxt.config.browser === 'firefox' && + : wxt.config.actualBrowser === 'firefox' && popup.options.actionType === 'page_action' ? 'page_action' : 'action'; @@ -343,16 +346,16 @@ function addEntrypoints( if (options) { const page = getEntrypointBundlePath(options, wxt.config.outDir, '.html'); manifest.options_ui = { - ...(wxt.config.browser !== 'safari' && { + ...(wxt.config.actualBrowser !== 'safari' && { open_in_tab: options.options.openInTab ?? false, }), // @ts-expect-error: Not typed by @wxt-dev/browser, but supported by Firefox browser_style: - wxt.config.browser === 'firefox' + wxt.config.actualBrowser === 'firefox' ? options.options.browserStyle : undefined, chrome_style: - wxt.config.browser !== 'firefox' + wxt.config.actualBrowser !== 'firefox' ? options.options.chromeStyle : undefined, page, @@ -360,7 +363,7 @@ function addEntrypoints( } if (sandboxes?.length) { - if (wxt.config.browser === 'firefox') { + if (wxt.config.actualBrowser === 'firefox') { wxt.logger.warn( 'Sandboxed pages not supported by Firefox. sandbox.pages was not added to the manifest', ); @@ -382,7 +385,7 @@ function addEntrypoints( '.html', ); - if (wxt.config.browser === 'firefox') { + if (wxt.config.actualBrowser === 'firefox') { manifest.sidebar_action = { default_panel: page, browser_style: defaultSidepanel.options.browserStyle, @@ -593,7 +596,8 @@ export function getContentScriptCssWebAccessibleResources( // Chrome-only MV3 field (obscures the resource URL behind a per-session // token); Firefox ignores it, and Safari's web extension converter // rejects it as an unsupported key. - ...(wxt.config.browser !== 'firefox' && wxt.config.browser !== 'safari' + ...(wxt.config.actualBrowser !== 'firefox' && + wxt.config.actualBrowser !== 'safari' ? { use_dynamic_url: true } : {}), matches: @@ -734,11 +738,11 @@ function stripKeys(manifest: Browser.runtime.Manifest): void { let keysToRemove: string[] = []; if (wxt.config.manifestVersion === 2) { keysToRemove.push(...mv3OnlyKeys); - if (wxt.config.browser === 'firefox') + if (wxt.config.actualBrowser === 'firefox') keysToRemove.push(...firefoxMv3OnlyKeys); } else { keysToRemove.push(...mv2OnlyKeys); - if (wxt.config.browser !== 'firefox') + if (wxt.config.actualBrowser !== 'firefox') keysToRemove.push(...chromeMv2OnlyKeys); } diff --git a/packages/wxt/src/internal/resolve-config.ts b/packages/wxt/src/internal/resolve-config.ts index 410686d79..e8f3ded15 100644 --- a/packages/wxt/src/internal/resolve-config.ts +++ b/packages/wxt/src/internal/resolve-config.ts @@ -23,6 +23,10 @@ import { NullishRequired } from '../internal-utils/type-utils'; import { pathExists } from '../internal-utils/fs-utils'; import { normalizePath } from '../internal-utils/path-utils'; import { glob } from 'tinyglobby'; +import { + defaultTargetBrowserMap, + resolveActualBrowser, +} from '../internal-utils/target-browser'; import { builtinModules } from './builtin-modules'; import { getEslintVersion } from '../internal-utils/eslint-utils'; import { safeStringToNumber } from '../internal-utils/number-utils'; @@ -74,9 +78,14 @@ export async function resolveConfig( `Current target browser \`${browser}\` is not in your \`targetBrowsers\` list!`, ); } + const targetBrowserMap = { + ...defaultTargetBrowserMap, + ...mergedConfig.targetBrowserMap, + }; + const actualBrowser = resolveActualBrowser(browser, targetBrowserMap); const manifestVersion = mergedConfig.manifestVersion ?? - (browser === 'firefox' || browser === 'safari' ? 2 : 3); + (actualBrowser === 'firefox' || actualBrowser === 'safari' ? 2 : 3); const mode = mergedConfig.mode ?? COMMAND_MODES[command]; const env: ConfigEnv = { browser, command, manifestVersion, mode }; @@ -192,6 +201,7 @@ export async function resolveConfig( return { browser, targetBrowsers, + actualBrowser, command, debug, entrypointsDir, diff --git a/packages/wxt/src/types.ts b/packages/wxt/src/types.ts index 4fcddb12f..55369a376 100644 --- a/packages/wxt/src/types.ts +++ b/packages/wxt/src/types.ts @@ -125,6 +125,19 @@ export interface InlineConfig { * @default [ ] */ targetBrowsers?: TargetBrowser[]; + /** + * Map `--browser` values to the actual browser WXT generates code for. + * + * WXT makes build-time decisions, like which manifest options to generate, + * based on the actual browser, not the `--browser` label. Use this to map + * custom `--browser` values to the browser they actually run in. + * + * Labels missing from the map fall back to `"chromium"`. Add an entry for + * every custom label that runs somewhere else. + * + * @default `{ chrome: "chromium", edge: "chromium", brave: "chromium", opera: "chromium", chromium: "chromium", firefox: "firefox", safari: "safari", ios: "safari" }` + */ + targetBrowserMap?: TargetBrowserMap; /** * Explicitly set a manifest version to target. This will override the default * manifest version for each command, and can be overridden by the command @@ -629,6 +642,14 @@ export interface ReloadContentScriptPayload { export type TargetBrowser = string; export type TargetManifestVersion = 2 | 3; +/** + * The actual browser WXT generates code for, resolved from the `--browser` + * value via `targetBrowserMap`. Labels missing from the map fall back to + * `"chromium"`. + */ +export type ActualBrowser = 'chromium' | 'firefox' | 'safari'; +/** Maps a `--browser` value to the {@link ActualBrowser} WXT generates code for. */ +export type TargetBrowserMap = Record; export type UserConfig = Omit; @@ -1599,6 +1620,11 @@ export interface ResolvedConfig { command: WxtCommand; browser: TargetBrowser; targetBrowsers: TargetBrowser[]; + /** + * The actual browser WXT generates code for, resolved from `browser` via + * `targetBrowserMap`. Labels missing from the map resolve to `"chromium"`. + */ + actualBrowser: ActualBrowser; manifestVersion: TargetManifestVersion; env: ConfigEnv; logger: WxtLogger;