diff --git a/cspell.yml b/cspell.yml index 95dc1a1cb..ba16fce52 100644 --- a/cspell.yml +++ b/cspell.yml @@ -134,6 +134,7 @@ words: - unwatch - USSS - vitepress + - waterfox - webext - webextension - webextrc diff --git a/packages/wxt/src/core/__tests__/resolve-config.test.ts b/packages/wxt/src/core/__tests__/resolve-config.test.ts index 983049f93..e2c656961 100644 --- a/packages/wxt/src/core/__tests__/resolve-config.test.ts +++ b/packages/wxt/src/core/__tests__/resolve-config.test.ts @@ -37,4 +37,61 @@ 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 resolve unknown browsers to themselves', async () => { + const config = await resolveConfig({ browser: 'waterfox' }, 'build'); + + expect(config.actualBrowser).toBe('waterfox'); + }); + + 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 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); + }); + }); }); diff --git a/packages/wxt/src/core/resolve-config.ts b/packages/wxt/src/core/resolve-config.ts index c74ffbbb8..bbe641305 100644 --- a/packages/wxt/src/core/resolve-config.ts +++ b/packages/wxt/src/core/resolve-config.ts @@ -24,6 +24,10 @@ import { NullishRequired } from './utils/types'; import { pathExists } from './utils/fs'; import { normalizePath } from './utils'; import { glob } from 'tinyglobby'; +import { + defaultTargetBrowserMap, + resolveActualBrowser, +} from './utils/target-browser'; import { builtinModules } from '../builtin-modules'; import { getEslintVersion } from './utils/eslint'; import { safeStringToNumber } from './utils/number'; @@ -79,9 +83,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 }; @@ -197,6 +206,7 @@ export async function resolveConfig( return { browser, targetBrowsers, + actualBrowser, command, debug, entrypointsDir, diff --git a/packages/wxt/src/core/utils/__tests__/manifest.test.ts b/packages/wxt/src/core/utils/__tests__/manifest.test.ts index bba468822..1f2f04912 100644 --- a/packages/wxt/src/core/utils/__tests__/manifest.test.ts +++ b/packages/wxt/src/core/utils/__tests__/manifest.test.ts @@ -1380,6 +1380,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', () => { diff --git a/packages/wxt/src/core/utils/manifest.ts b/packages/wxt/src/core/utils/manifest.ts index b39ad4fc7..7ade80ca5 100644 --- a/packages/wxt/src/core/utils/manifest.ts +++ b/packages/wxt/src/core/utils/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/core/utils/target-browser.ts b/packages/wxt/src/core/utils/target-browser.ts new file mode 100644 index 000000000..683c1807c --- /dev/null +++ b/packages/wxt/src/core/utils/target-browser.ts @@ -0,0 +1,33 @@ +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. + * Unknown values resolve to themselves so their behavior is unchanged until the + * project maps them to an actual browser. + */ +export function resolveActualBrowser( + browser: TargetBrowser, + targetBrowserMap: TargetBrowserMap = defaultTargetBrowserMap, +): ActualBrowser { + return targetBrowserMap[browser] ?? (browser as ActualBrowser); +} diff --git a/packages/wxt/src/core/utils/testing/fake-objects.ts b/packages/wxt/src/core/utils/testing/fake-objects.ts index 59dd38d8a..42a895a38 100644 --- a/packages/wxt/src/core/utils/testing/fake-objects.ts +++ b/packages/wxt/src/core/utils/testing/fake-objects.ts @@ -24,6 +24,7 @@ import { import { mock } from 'vitest-mock-extended'; import { vi } from 'vitest'; import { setWxtForTesting } from '../../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(), @@ -341,6 +343,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/types.ts b/packages/wxt/src/types.ts index 6331bd028..345479528 100644 --- a/packages/wxt/src/types.ts +++ b/packages/wxt/src/types.ts @@ -125,6 +125,18 @@ 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. Values not + * in the map are not mapped to any browser, so add an entry here for any + * custom label. + * + * @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 +641,13 @@ 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`. + */ +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; @@ -1568,6 +1587,11 @@ export interface ResolvedConfig { command: WxtCommand; browser: TargetBrowser; targetBrowsers: TargetBrowser[]; + /** + * The actual browser WXT generates code for, resolved from `browser` via + * `targetBrowserMap`. + */ + actualBrowser: ActualBrowser; manifestVersion: TargetManifestVersion; env: ConfigEnv; logger: WxtLogger;