From f1bb7cb0c22dc36259c13292b8f11d35e2f3f4cb Mon Sep 17 00:00:00 2001 From: Aaron Date: Wed, 24 Jul 2024 17:09:56 -0500 Subject: [PATCH] feat(experimental): First-class support for excluding `webextension-polyfill` (#847) --- packages/wxt-demo/package.json | 1 + packages/wxt-demo/wxt.config.ts | 1 + packages/wxt/build.config.ts | 2 +- packages/wxt/e2e/tests/modules.test.ts | 24 ++++------- .../wxt/e2e/tests/output-structure.test.ts | 28 ++++++++----- .../wxt/e2e/tests/typescript-project.test.ts | 2 +- packages/wxt/e2e/tests/user-config.test.ts | 6 +-- packages/wxt/package.json | 17 +++++++- .../wxt/src/{types => @types}/globals.d.ts | 0 .../wxt/src/{types => @types}/modules.d.ts | 5 --- .../src/{types => @types}/project-types.d.ts | 0 packages/wxt/src/__tests__/storage.test.ts | 2 +- packages/wxt/src/browser/chrome.ts | 28 +++++++++++++ .../wxt/src/{browser.ts => browser/index.ts} | 3 ++ .../content-scripts/content-script-context.ts | 2 +- .../client/content-scripts/custom-events.ts | 4 +- .../src/client/content-scripts/ui/index.ts | 2 +- packages/wxt/src/core/builders/vite/index.ts | 6 +-- .../vite/plugins/excludeBrowserPolyfill.ts | 33 --------------- .../builders/vite/plugins/extensionApiMock.ts | 37 ++++++++++++++++ .../src/core/builders/vite/plugins/index.ts | 4 +- .../vite/plugins/resolveExtensionApi.ts | 27 ++++++++++++ .../vite/plugins/webextensionPolyfillMock.ts | 41 ------------------ .../core/utils/building/generate-wxt-dir.ts | 15 ++++--- .../wxt/src/core/utils/building/rebuild.ts | 2 +- .../src/core/utils/building/resolve-config.ts | 20 +++++++-- .../wxt/src/core/utils/content-scripts.ts | 2 +- packages/wxt/src/core/utils/manifest.ts | 2 +- .../src/core/utils/testing/fake-objects.ts | 5 ++- packages/wxt/src/storage.ts | 2 +- packages/wxt/src/testing/wxt-vitest-plugin.ts | 37 ++++++++-------- packages/wxt/src/{types/index.ts => types.ts} | 42 +++++++------------ packages/wxt/src/virtual/mock-browser.ts | 5 ++- packages/wxt/src/virtual/tsconfig.json | 2 +- .../src/virtual/virtual-module-globals.d.ts | 22 ++-------- packages/wxt/typedoc.json | 3 +- packages/wxt/vitest.config.ts | 5 ++- pnpm-lock.yaml | 29 ++++++++++++- 38 files changed, 264 insertions(+), 204 deletions(-) rename packages/wxt/src/{types => @types}/globals.d.ts (100%) rename packages/wxt/src/{types => @types}/modules.d.ts (91%) rename packages/wxt/src/{types => @types}/project-types.d.ts (100%) create mode 100644 packages/wxt/src/browser/chrome.ts rename packages/wxt/src/{browser.ts => browser/index.ts} (93%) delete mode 100644 packages/wxt/src/core/builders/vite/plugins/excludeBrowserPolyfill.ts create mode 100644 packages/wxt/src/core/builders/vite/plugins/extensionApiMock.ts create mode 100644 packages/wxt/src/core/builders/vite/plugins/resolveExtensionApi.ts delete mode 100644 packages/wxt/src/core/builders/vite/plugins/webextensionPolyfillMock.ts rename packages/wxt/src/{types/index.ts => types.ts} (97%) diff --git a/packages/wxt-demo/package.json b/packages/wxt-demo/package.json index fa700c29d..142e8dfe8 100644 --- a/packages/wxt-demo/package.json +++ b/packages/wxt-demo/package.json @@ -21,6 +21,7 @@ "react-dom": "^18.3.1" }, "devDependencies": { + "@types/chrome": "^0.0.268", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "sass": "^1.77.8", diff --git a/packages/wxt-demo/wxt.config.ts b/packages/wxt-demo/wxt.config.ts index e1571334c..2300f6762 100644 --- a/packages/wxt-demo/wxt.config.ts +++ b/packages/wxt-demo/wxt.config.ts @@ -2,6 +2,7 @@ import { defineConfig } from 'wxt'; export default defineConfig({ srcDir: 'src', + extensionApi: 'chrome', manifest: { permissions: ['storage'], default_locale: 'en', diff --git a/packages/wxt/build.config.ts b/packages/wxt/build.config.ts index 52a76e307..48fa5ccc6 100644 --- a/packages/wxt/build.config.ts +++ b/packages/wxt/build.config.ts @@ -13,7 +13,7 @@ export default defineBuildConfig([ { builder: 'mkdist', input: 'src', - pattern: ['**/*', '!**/__tests__', '!**/*.md', '!virtual'], + pattern: ['**/*', '!**/__tests__', '!**/*.md', '!virtual', '!@types'], declaration: true, }, ], diff --git a/packages/wxt/e2e/tests/modules.test.ts b/packages/wxt/e2e/tests/modules.test.ts index 768e9099a..367099b52 100644 --- a/packages/wxt/e2e/tests/modules.test.ts +++ b/packages/wxt/e2e/tests/modules.test.ts @@ -192,10 +192,8 @@ describe('Module Helpers', () => { const expectedText = addPluginModule(project); await project.build({ - experimental: { - // reduce build output when comparing test failures - includeBrowserPolyfill: false, - }, + // reduce build output when comparing test failures + extensionApi: 'chrome', }); await expect(project.serializeOutput()).resolves.toContain(expectedText); @@ -214,10 +212,8 @@ describe('Module Helpers', () => { const expectedText = addPluginModule(project); await project.build({ - experimental: { - // reduce build output when comparing test failures - includeBrowserPolyfill: false, - }, + // reduce build output when comparing test failures + extensionApi: 'chrome', }); await expect(project.serializeOutput()).resolves.toContain(expectedText); @@ -237,10 +233,8 @@ describe('Module Helpers', () => { const expectedText = addPluginModule(project); await project.build({ - experimental: { - // reduce build output when comparing test failures - includeBrowserPolyfill: false, - }, + // reduce build output when comparing test failures + extensionApi: 'chrome', }); await expect(project.serializeOutput()).resolves.toContain(expectedText); @@ -255,10 +249,8 @@ describe('Module Helpers', () => { const expectedText = addPluginModule(project); await project.build({ - experimental: { - // reduce build output when comparing test failures - includeBrowserPolyfill: false, - }, + // reduce build output when comparing test failures + extensionApi: 'chrome', }); await expect(project.serializeOutput()).resolves.toContain(expectedText); diff --git a/packages/wxt/e2e/tests/output-structure.test.ts b/packages/wxt/e2e/tests/output-structure.test.ts index 07a47be07..7349ae10a 100644 --- a/packages/wxt/e2e/tests/output-structure.test.ts +++ b/packages/wxt/e2e/tests/output-structure.test.ts @@ -262,10 +262,9 @@ describe('Output Directory Structure', () => { project.addFile('entrypoints/popup/main.ts', `logHello('popup')`); await project.build({ - experimental: { - // Simplify the build output for comparison - includeBrowserPolyfill: false, - }, + // Simplify the build output for comparison + extensionApi: 'chrome', + vite: () => ({ build: { // Make output for snapshot readible @@ -278,6 +277,7 @@ describe('Output Directory Structure', () => { .toMatchInlineSnapshot(` ".output/chrome-mv3/background.js ---------------------------------------- + var _a, _b; import { l as logHello, i as initPlugins } from "./chunks/_virtual_wxt-plugins-OjKtWpmY.js"; function defineBackground(arg) { if (typeof arg === "function") return { main: arg }; @@ -289,7 +289,11 @@ describe('Output Directory Structure', () => { logHello("background"); } }); - chrome; + // @ts-expect-error + ((_b = (_a = globalThis.browser) == null ? void 0 : _a.runtime) == null ? void 0 : _b.id) == null ? globalThis.chrome : ( + // @ts-expect-error + globalThis.browser + ); function print(method, ...args) { return; } @@ -343,10 +347,9 @@ describe('Output Directory Structure', () => { project.addFile('entrypoints/popup/main.ts', `logHello('popup')`); await project.build({ - experimental: { - // Simplify the build output for comparison - includeBrowserPolyfill: false, - }, + // Simplify the build output for comparison + extensionApi: 'chrome', + vite: () => ({ build: { // Make output for snapshot readible @@ -361,6 +364,7 @@ describe('Output Directory Structure', () => { ---------------------------------------- var _background = function() { "use strict"; + var _a, _b; function defineBackground(arg) { if (typeof arg === "function") return { main: arg }; return arg; @@ -377,7 +381,11 @@ describe('Output Directory Structure', () => { _background; function initPlugins() { } - chrome; + // @ts-expect-error + ((_b = (_a = globalThis.browser) == null ? void 0 : _a.runtime) == null ? void 0 : _b.id) == null ? globalThis.chrome : ( + // @ts-expect-error + globalThis.browser + ); function print(method, ...args) { return; } diff --git a/packages/wxt/e2e/tests/typescript-project.test.ts b/packages/wxt/e2e/tests/typescript-project.test.ts index cdf800908..5878e21f3 100644 --- a/packages/wxt/e2e/tests/typescript-project.test.ts +++ b/packages/wxt/e2e/tests/typescript-project.test.ts @@ -53,7 +53,7 @@ describe('TypeScript Project', () => { | "/popup.html" | "/sandbox.html" type HtmlPublicPath = Extract - export interface WxtRuntime extends Runtime.Static { + export interface WxtRuntime { getURL(path: PublicPath): string; getURL(path: \`\${HtmlPublicPath}\${string}\`): string; } diff --git a/packages/wxt/e2e/tests/user-config.test.ts b/packages/wxt/e2e/tests/user-config.test.ts index cc9f2da3d..221200351 100644 --- a/packages/wxt/e2e/tests/user-config.test.ts +++ b/packages/wxt/e2e/tests/user-config.test.ts @@ -88,7 +88,7 @@ describe('User Config', () => { `); }); - it('should exclude the polyfill when the experimental setting is set to false', async () => { + it('should exclude the polyfill when extensionApi="chrome"', async () => { const buildBackground = async (config?: InlineConfig) => { const background = `export default defineBackground(() => console.log(browser.runtime.id));`; const projectWithPolyfill = new TestProject(); @@ -101,9 +101,7 @@ describe('User Config', () => { const withPolyfill = await buildBackground(); const withoutPolyfill = await buildBackground({ - experimental: { - includeBrowserPolyfill: false, - }, + extensionApi: 'chrome', }); expect(withoutPolyfill).not.toBe(withPolyfill); }); diff --git a/packages/wxt/package.json b/packages/wxt/package.json index 7557174f0..0b20fd9d7 100644 --- a/packages/wxt/package.json +++ b/packages/wxt/package.json @@ -46,8 +46,12 @@ "default": "./dist/sandbox/index.mjs" }, "./browser": { - "types": "./dist/browser.d.ts", - "default": "./dist/browser.mjs" + "types": "./dist/browser/index.d.ts", + "default": "./dist/browser/index.mjs" + }, + "./browser/chrome": { + "types": "./dist/browser/chrome.d.ts", + "import": "./dist/browser/chrome.mjs" }, "./testing": { "types": "./dist/testing/index.d.ts", @@ -121,6 +125,7 @@ "devDependencies": { "@aklinker1/check": "^1.3.1", "@faker-js/faker": "^8.4.1", + "@types/chrome": "^0.0.268", "@types/fs-extra": "^11.0.4", "@types/lodash.merge": "^4.6.9", "@types/natural-compare": "^1.4.3", @@ -138,5 +143,13 @@ "unbuild": "^2.0.0", "vitest": "^2.0.3", "vitest-plugin-random-seed": "^1.1.0" + }, + "peerDependencies": { + "@types/chrome": "*" + }, + "peerDependenciesMeta": { + "@types/chrome": { + "optional": true + } } } diff --git a/packages/wxt/src/types/globals.d.ts b/packages/wxt/src/@types/globals.d.ts similarity index 100% rename from packages/wxt/src/types/globals.d.ts rename to packages/wxt/src/@types/globals.d.ts diff --git a/packages/wxt/src/types/modules.d.ts b/packages/wxt/src/@types/modules.d.ts similarity index 91% rename from packages/wxt/src/types/modules.d.ts rename to packages/wxt/src/@types/modules.d.ts index 49fb40ff4..99fbc79af 100644 --- a/packages/wxt/src/types/modules.d.ts +++ b/packages/wxt/src/@types/modules.d.ts @@ -49,8 +49,3 @@ declare module 'web-ext-run/util/logger' { } export const consoleStream: IConsoleStream; } - -declare module 'wxt/browser' { - // Overridden when types are generated per project - export type PublicPath = string; -} diff --git a/packages/wxt/src/types/project-types.d.ts b/packages/wxt/src/@types/project-types.d.ts similarity index 100% rename from packages/wxt/src/types/project-types.d.ts rename to packages/wxt/src/@types/project-types.d.ts diff --git a/packages/wxt/src/__tests__/storage.test.ts b/packages/wxt/src/__tests__/storage.test.ts index bd465f05f..6373991d1 100644 --- a/packages/wxt/src/__tests__/storage.test.ts +++ b/packages/wxt/src/__tests__/storage.test.ts @@ -1,6 +1,6 @@ import { fakeBrowser } from '@webext-core/fake-browser'; import { describe, it, expect, beforeEach, vi, expectTypeOf } from 'vitest'; -import { browser } from '../browser'; +import { browser } from 'wxt/browser'; import { WxtStorageItem, storage } from '../storage'; /** diff --git a/packages/wxt/src/browser/chrome.ts b/packages/wxt/src/browser/chrome.ts new file mode 100644 index 000000000..6111ac7d7 --- /dev/null +++ b/packages/wxt/src/browser/chrome.ts @@ -0,0 +1,28 @@ +/// +/** + * EXPERIMENTAL + * + * Includes the `chrome` API and types when using `extensionApi: 'chrome'`. + * + * @module wxt/browser/chrome + */ + +export interface WxtRuntime { + // Overriden per-project +} +export interface WxtI18n { + // Overriden per-project +} + +export type Chrome = typeof chrome; +export type WxtBrowser = Omit & { + runtime: WxtRuntime & Omit; + i18n: WxtI18n & Omit; +}; + +export const browser: WxtBrowser = + // @ts-expect-error + globalThis.browser?.runtime?.id == null + ? globalThis.chrome + : // @ts-expect-error + globalThis.browser; diff --git a/packages/wxt/src/browser.ts b/packages/wxt/src/browser/index.ts similarity index 93% rename from packages/wxt/src/browser.ts rename to packages/wxt/src/browser/index.ts index 93f0ddbee..fee152da6 100644 --- a/packages/wxt/src/browser.ts +++ b/packages/wxt/src/browser/index.ts @@ -1,6 +1,9 @@ /** + * Includes the `browser` API and types when using `extensionApi: 'webextension-polyfill'` (the default). + * * @module wxt/browser */ + import originalBrowser, { Browser, Runtime, I18n } from 'webextension-polyfill'; export interface AugmentedBrowser extends Browser { diff --git a/packages/wxt/src/client/content-scripts/content-script-context.ts b/packages/wxt/src/client/content-scripts/content-script-context.ts index f3ac0def3..47d414e04 100644 --- a/packages/wxt/src/client/content-scripts/content-script-context.ts +++ b/packages/wxt/src/client/content-scripts/content-script-context.ts @@ -1,5 +1,5 @@ import { ContentScriptDefinition } from '../../types'; -import { browser } from '../../browser'; +import { browser } from 'wxt/browser'; import { logger } from '../../sandbox/utils/logger'; import { WxtLocationChangeEvent, getUniqueEventName } from './custom-events'; import { createLocationWatcher } from './location-watcher'; diff --git a/packages/wxt/src/client/content-scripts/custom-events.ts b/packages/wxt/src/client/content-scripts/custom-events.ts index 29747c734..8b14a1b83 100644 --- a/packages/wxt/src/client/content-scripts/custom-events.ts +++ b/packages/wxt/src/client/content-scripts/custom-events.ts @@ -1,4 +1,4 @@ -import { browser } from '../../browser'; +import { browser } from 'wxt/browser'; export class WxtLocationChangeEvent extends Event { static EVENT_NAME = getUniqueEventName('wxt:locationchange'); @@ -22,5 +22,5 @@ export function getUniqueEventName(eventName: string): string { ? 'build' : import.meta.env.ENTRYPOINT; - return `${browser.runtime.id}:${entrypointName}:${eventName}`; + return `${browser?.runtime?.id}:${entrypointName}:${eventName}`; } diff --git a/packages/wxt/src/client/content-scripts/ui/index.ts b/packages/wxt/src/client/content-scripts/ui/index.ts index dd648cf3d..9abd5cf6c 100644 --- a/packages/wxt/src/client/content-scripts/ui/index.ts +++ b/packages/wxt/src/client/content-scripts/ui/index.ts @@ -1,4 +1,4 @@ -import { browser } from '../../../browser'; +import { browser } from 'wxt/browser'; import { ContentScriptContext } from '..'; import { ContentScriptAnchoredOptions, diff --git a/packages/wxt/src/core/builders/vite/index.ts b/packages/wxt/src/core/builders/vite/index.ts index 8f2bd1631..520511c62 100644 --- a/packages/wxt/src/core/builders/vite/index.ts +++ b/packages/wxt/src/core/builders/vite/index.ts @@ -66,7 +66,7 @@ export async function createViteBuilder( wxtPlugins.tsconfigPaths(wxtConfig), wxtPlugins.noopBackground(), wxtPlugins.globals(wxtConfig), - wxtPlugins.excludeBrowserPolyfill(wxtConfig), + wxtPlugins.resolveExtensionApi(wxtConfig), wxtPlugins.defineImportMeta(), wxtPlugins.wxtPluginLoader(wxtConfig), wxtPlugins.resolveAppConfig(wxtConfig), @@ -218,7 +218,7 @@ export async function createViteBuilder( const baseConfig = await getBaseConfig(); const envConfig: vite.InlineConfig = { plugins: [ - wxtPlugins.webextensionPolyfillMock(wxtConfig), + wxtPlugins.extensionApiMock(wxtConfig), wxtPlugins.removeEntrypointMainFunction(wxtConfig, path), ], }; @@ -238,7 +238,7 @@ export async function createViteBuilder( baseConfig.optimizeDeps.include = []; const envConfig: vite.InlineConfig = { plugins: [ - wxtPlugins.webextensionPolyfillMock(wxtConfig), + wxtPlugins.extensionApiMock(wxtConfig), wxtPlugins.removeEntrypointMainFunction(wxtConfig, path), ], }; diff --git a/packages/wxt/src/core/builders/vite/plugins/excludeBrowserPolyfill.ts b/packages/wxt/src/core/builders/vite/plugins/excludeBrowserPolyfill.ts deleted file mode 100644 index 51486933a..000000000 --- a/packages/wxt/src/core/builders/vite/plugins/excludeBrowserPolyfill.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { ResolvedConfig } from '../../../../types'; -import type * as vite from 'vite'; - -/** - * Apply the experimental config for disabling the polyfill. It works by aliasing the - * `webextension-polyfill` module to a virtual module and exporting the `chrome` global from the - * virtual module. - */ -export function excludeBrowserPolyfill(config: ResolvedConfig): vite.Plugin { - const virtualId = 'virtual:wxt-webextension-polyfill-disabled'; - - return { - name: 'wxt:exclude-browser-polyfill', - config() { - // Only apply the config if we're disabling the polyfill - if (config.experimental.includeBrowserPolyfill) return; - - return { - resolve: { - alias: { - 'webextension-polyfill': virtualId, - }, - }, - }; - }, - load(id) { - if (id === virtualId) { - // Use chrome instead of the polyfill when disabled. - return 'export default chrome'; - } - }, - }; -} diff --git a/packages/wxt/src/core/builders/vite/plugins/extensionApiMock.ts b/packages/wxt/src/core/builders/vite/plugins/extensionApiMock.ts new file mode 100644 index 000000000..f7d6d8636 --- /dev/null +++ b/packages/wxt/src/core/builders/vite/plugins/extensionApiMock.ts @@ -0,0 +1,37 @@ +import path from 'node:path'; +import type * as vite from 'vite'; +import { ResolvedConfig } from '../../../../types'; + +/** + * Mock `webextension-polyfill`, `wxt/browser`, and `wxt/browser/*` by inlining + * all dependencies that import them and adding a custom alias so that Vite + * resolves to a mocked version of the module. + * + * TODO: Detect non-wxt dependencies (like `@webext-core/*`) that import `webextension-polyfill` via + * `npm list` and inline them automatically. + */ +export function extensionApiMock(config: ResolvedConfig): vite.PluginOption { + return { + name: 'wxt:extension-api-mock', + config() { + const replacement = path.resolve( + config.wxtModuleDir, + 'dist/virtual/mock-browser', + ); + return { + resolve: { + alias: [ + { find: 'webextension-polyfill', replacement }, + // wxt/browser, wxt/browser/... + { find: /^wxt\/browser.*/, replacement }, + ], + }, + ssr: { + // Inline all WXT modules so vite processes them so the aliases can + // be resolved + noExternal: ['wxt'], + }, + }; + }, + }; +} diff --git a/packages/wxt/src/core/builders/vite/plugins/index.ts b/packages/wxt/src/core/builders/vite/plugins/index.ts index 3bf3b9303..f70dde240 100644 --- a/packages/wxt/src/core/builders/vite/plugins/index.ts +++ b/packages/wxt/src/core/builders/vite/plugins/index.ts @@ -8,8 +8,8 @@ export * from './noopBackground'; export * from './cssEntrypoints'; export * from './bundleAnalysis'; export * from './globals'; -export * from './webextensionPolyfillMock'; -export * from './excludeBrowserPolyfill'; +export * from './extensionApiMock'; +export * from './resolveExtensionApi'; export * from './entrypointGroupGlobals'; export * from './defineImportMeta'; export * from './removeEntrypointMainFunction'; diff --git a/packages/wxt/src/core/builders/vite/plugins/resolveExtensionApi.ts b/packages/wxt/src/core/builders/vite/plugins/resolveExtensionApi.ts new file mode 100644 index 000000000..17ec02064 --- /dev/null +++ b/packages/wxt/src/core/builders/vite/plugins/resolveExtensionApi.ts @@ -0,0 +1,27 @@ +import { ResolvedConfig } from '../../../../types'; +import type * as vite from 'vite'; + +/** + * Apply the experimental config for which extension API is used. This only + * effects the extension API included at RUNTIME - during development, types + * depend on the import. + * + * NOTE: this only works if we import `wxt/browser` instead of using the relative path. + */ +export function resolveExtensionApi(config: ResolvedConfig): vite.Plugin { + return { + name: 'wxt:resolve-extension-api', + config() { + // Only apply the config if we're disabling the polyfill + if (config.extensionApi === 'webextension-polyfill') return; + + return { + resolve: { + alias: [ + { find: /^wxt\/browser$/, replacement: 'wxt/browser/chrome' }, + ], + }, + }; + }, + }; +} diff --git a/packages/wxt/src/core/builders/vite/plugins/webextensionPolyfillMock.ts b/packages/wxt/src/core/builders/vite/plugins/webextensionPolyfillMock.ts deleted file mode 100644 index 50eb8cc90..000000000 --- a/packages/wxt/src/core/builders/vite/plugins/webextensionPolyfillMock.ts +++ /dev/null @@ -1,41 +0,0 @@ -import path from 'node:path'; -import type * as vite from 'vite'; -import { ResolvedConfig } from '../../../../types'; - -/** - * Mock `webextension-polyfill` by inlining all dependencies that import it and adding a custom - * alias so that Vite resolves to a mocked version of the module. - * - * There are two ways to mark a module as inline: - * 1. Use partial file paths ("wxt/dist/browser.js") in the `test.server.deps.inline` option. - * 2. Use module names ("wxt" or "@webext-core/messaging") in the `ssr.noExternalize` option. - * - * This plugin uses the second approach since it's a little more intuative to understand. - * - * TODO: Detect non-wxt dependencies (like `@webext-core/*`) that import `webextension-polyfill` via - * `npm list` and inline them automatically. - */ -export function webextensionPolyfillMock( - config: ResolvedConfig, -): vite.PluginOption { - return { - name: 'wxt:testing-inline-deps', - config() { - return { - resolve: { - alias: { - // Alias to use a mocked version of the polyfill - 'webextension-polyfill': path.resolve( - config.wxtModuleDir, - 'dist/virtual/mock-browser', - ), - }, - }, - ssr: { - // Inline all WXT modules - noExternal: ['wxt'], - }, - }; - }, - }; -} diff --git a/packages/wxt/src/core/utils/building/generate-wxt-dir.ts b/packages/wxt/src/core/utils/building/generate-wxt-dir.ts index aab6c8b1b..a91fa169a 100644 --- a/packages/wxt/src/core/utils/building/generate-wxt-dir.ts +++ b/packages/wxt/src/core/utils/building/generate-wxt-dir.ts @@ -42,6 +42,11 @@ export async function generateTypesDir( // import.meta.env.* entries.push(await getGlobalsDeclarationEntry()); + // @types/chrome + if (wxt.config.extensionApi === 'chrome') { + entries.push({ module: '@types/chrome' }); + } + // tsconfig.json entries.push(await getTsConfigEntry()); @@ -85,13 +90,13 @@ async function getPathsDeclarationEntry( .join('\n'); const template = `// Generated by wxt -import "wxt/browser"; +import "${wxt.config.browserModule}"; -declare module "wxt/browser" { +declare module "${wxt.config.browserModule}" { export type PublicPath = {{ union }} type HtmlPublicPath = Extract - export interface WxtRuntime extends Runtime.Static { + export interface WxtRuntime { getURL(path: PublicPath): string; getURL(path: \`\${HtmlPublicPath}\${string}\`): string; } @@ -108,9 +113,9 @@ declare module "wxt/browser" { async function getI18nDeclarationEntry(): Promise { const defaultLocale = wxt.config.manifest.default_locale; const template = `// Generated by wxt -import "wxt/browser"; +import "${wxt.config.browserModule}"; -declare module "wxt/browser" { +declare module "${wxt.config.browserModule}" { /** * See https://developer.chrome.com/docs/extensions/reference/i18n/#method-getMessage */ diff --git a/packages/wxt/src/core/utils/building/rebuild.ts b/packages/wxt/src/core/utils/building/rebuild.ts index 90c47f49d..fa6cfbd6b 100644 --- a/packages/wxt/src/core/utils/building/rebuild.ts +++ b/packages/wxt/src/core/utils/building/rebuild.ts @@ -1,4 +1,4 @@ -import type { Manifest } from '../../../browser'; +import type { Manifest } from 'wxt/browser'; import { BuildOutput, Entrypoint, EntrypointGroup } from '../../../types'; import { generateTypesDir } from './generate-wxt-dir'; import { buildEntrypoints } from './build-entrypoints'; diff --git a/packages/wxt/src/core/utils/building/resolve-config.ts b/packages/wxt/src/core/utils/building/resolve-config.ts index 6d0e5196e..2781bbb56 100644 --- a/packages/wxt/src/core/utils/building/resolve-config.ts +++ b/packages/wxt/src/core/utils/building/resolve-config.ts @@ -146,6 +146,8 @@ export async function resolveConfig( {}, ); + const extensionApi = mergedConfig.extensionApi ?? 'webextension-polyfill'; + return { browser, command, @@ -155,7 +157,13 @@ export async function resolveConfig( filterEntrypoints, env, fsCache: createFsCache(wxtDir), - imports: await getUnimportOptions(wxtDir, srcDir, logger, mergedConfig), + imports: await getUnimportOptions( + wxtDir, + srcDir, + logger, + extensionApi, + mergedConfig, + ), logger, manifest: await resolveManifestConfig(env, mergedConfig.manifest), manifestVersion, @@ -174,8 +182,10 @@ export async function resolveConfig( analysis: resolveAnalysisConfig(root, mergedConfig), userConfigMetadata: userConfigMetadata ?? {}, alias, + extensionApi, + browserModule: + extensionApi === 'chrome' ? 'wxt/browser/chrome' : 'wxt/browser', experimental: defu(mergedConfig.experimental, { - includeBrowserPolyfill: true, entrypointImporter: 'jiti' as const, }), dev: { @@ -306,6 +316,7 @@ async function getUnimportOptions( wxtDir: string, srcDir: string, logger: Logger, + extensionApi: ResolvedConfig['extensionApi'], config: InlineConfig, ): Promise { if (config.imports === false) return false; @@ -324,7 +335,10 @@ async function getUnimportOptions( // ignored. ignore: ['options'], }, - { package: 'wxt/browser' }, + { + package: + extensionApi === 'chrome' ? 'wxt/browser/chrome' : 'wxt/browser', + }, { package: 'wxt/sandbox' }, { package: 'wxt/storage' }, ], diff --git a/packages/wxt/src/core/utils/content-scripts.ts b/packages/wxt/src/core/utils/content-scripts.ts index 158761e92..d9b1425dd 100644 --- a/packages/wxt/src/core/utils/content-scripts.ts +++ b/packages/wxt/src/core/utils/content-scripts.ts @@ -1,4 +1,4 @@ -import type { Manifest, Scripting } from '../../browser'; +import type { Manifest, Scripting } from 'wxt/browser'; import { ContentScriptEntrypoint, ResolvedConfig } from '../../types'; import { getEntrypointBundlePath } from './entrypoints'; diff --git a/packages/wxt/src/core/utils/manifest.ts b/packages/wxt/src/core/utils/manifest.ts index 1fc68a6c9..d2554b4bf 100644 --- a/packages/wxt/src/core/utils/manifest.ts +++ b/packages/wxt/src/core/utils/manifest.ts @@ -1,4 +1,4 @@ -import type { Manifest } from '../../browser'; +import type { Manifest } from 'wxt/browser'; import { Entrypoint, BackgroundEntrypoint, diff --git a/packages/wxt/src/core/utils/testing/fake-objects.ts b/packages/wxt/src/core/utils/testing/fake-objects.ts index 3990836f1..d28e03af3 100644 --- a/packages/wxt/src/core/utils/testing/fake-objects.ts +++ b/packages/wxt/src/core/utils/testing/fake-objects.ts @@ -4,7 +4,7 @@ import { resolve } from 'path'; import { faker } from '@faker-js/faker'; import merge from 'lodash.merge'; -import { Commands, type Manifest } from '../../../browser'; +import { Commands, type Manifest } from 'wxt/browser'; import { FsCache, ResolvedConfig, @@ -296,8 +296,9 @@ export const fakeResolvedConfig = fakeObjectCreator(() => { transformManifest: () => {}, userConfigMetadata: {}, alias: {}, + extensionApi: 'webextension-polyfill', + browserModule: 'wxt/browser', experimental: { - includeBrowserPolyfill: true, entrypointImporter: 'jiti', }, dev: { diff --git a/packages/wxt/src/storage.ts b/packages/wxt/src/storage.ts index 1de315820..425bfcb3a 100644 --- a/packages/wxt/src/storage.ts +++ b/packages/wxt/src/storage.ts @@ -5,7 +5,7 @@ * * @module wxt/storage */ -import { Storage, browser } from './browser'; +import { Storage, browser } from 'wxt/browser'; import { dequal } from 'dequal/lite'; import { logger } from './sandbox/utils/logger'; import { toArray } from './core/utils/arrays'; diff --git a/packages/wxt/src/testing/wxt-vitest-plugin.ts b/packages/wxt/src/testing/wxt-vitest-plugin.ts index 2f8dd744f..5495dba2a 100644 --- a/packages/wxt/src/testing/wxt-vitest-plugin.ts +++ b/packages/wxt/src/testing/wxt-vitest-plugin.ts @@ -3,7 +3,7 @@ import { download, tsconfigPaths, globals, - webextensionPolyfillMock, + extensionApiMock, resolveAppConfig, } from '../core/builders/vite/plugins'; import { resolveConfig } from '../core/utils/building'; @@ -26,20 +26,23 @@ import { createUnimport } from 'unimport'; * * @param inlineConfig Customize WXT's config for testing. Any config specified here overrides the config from your `wxt.config.ts` file. */ -export function WxtVitest(inlineConfig?: InlineConfig): vite.PluginOption { - return resolveConfig(inlineConfig ?? {}, 'serve').then(async (config) => { - const plugins = [ - webextensionPolyfillMock(config), - globals(config), - download(config), - tsconfigPaths(config), - resolveAppConfig(config), - ]; - if (config.imports !== false) { - const unimport = createUnimport(config.imports); - await unimport.init(); - plugins.push(unimportPlugin(unimport)); - } - return plugins; - }); +export async function WxtVitest( + inlineConfig?: InlineConfig, +): Promise { + const config = await resolveConfig(inlineConfig ?? {}, 'serve'); + + const plugins: vite.PluginOption[] = [ + globals(config), + download(config), + tsconfigPaths(config), + resolveAppConfig(config), + extensionApiMock(config), + ]; + if (config.imports !== false) { + const unimport = createUnimport(config.imports); + await unimport.init(); + plugins.push(unimportPlugin(unimport)); + } + + return plugins; } diff --git a/packages/wxt/src/types/index.ts b/packages/wxt/src/types.ts similarity index 97% rename from packages/wxt/src/types/index.ts rename to packages/wxt/src/types.ts index aa6e09d65..47bf7064a 100644 --- a/packages/wxt/src/types/index.ts +++ b/packages/wxt/src/types.ts @@ -1,8 +1,8 @@ import type * as vite from 'vite'; -import type { Manifest, Scripting } from '../browser'; +import type { Manifest, Scripting } from 'wxt/browser'; import { UnimportOptions, Import } from 'unimport'; import { LogLevel } from 'consola'; -import { ContentScriptContext } from '../client/content-scripts/content-script-context'; +import type { ContentScriptContext } from './client/content-scripts/content-script-context'; import type { PluginVisualizerOptions } from '@aklinker1/rollup-plugin-visualizer'; import type { FSWatcher } from 'chokidar'; import { ResolvedConfig as C12ResolvedConfig } from 'c12'; @@ -292,36 +292,25 @@ export interface InlineConfig { * } */ alias?: Record; + /** + * Which extension API to use. + * + * - `"webextension-polyfill"`: Use `browser` and types from [`webextension-polyfill`](https://www.npmjs.com/package/webextension-polyfill). + * - `"chrome"` (unstable): Use the regular `chrome` (or `browser` for Firefox/Safari) globals provided by the browser. Types provided by [`@types/chrome`](https://www.npmjs.com/package/@types/chrome), make sure to install the package or types won't work. + * + * @default "webextension-polyfill" + */ + extensionApi?: 'webextension-polyfill' | 'chrome'; /** * Experimental settings - use with caution. */ experimental?: { - /** - * Whether to use [`webextension-polyfill`](https://www.npmjs.com/package/webextension-polyfill) - * when importing `browser` from `wxt/browser`. - * - * When set to `false`, WXT will export the chrome global instead of the polyfill from - * `wxt/browser`. - * - * You should use `browser` to access the web extension APIs. - * - * @experimental This option will remain experimental until Manifest V2 is dead. - * - * @default true - * @example - * export default defineConfig({ - * experimental: { - * includeBrowserPolyfill: false - * } - * }) - */ - includeBrowserPolyfill?: boolean; /** * Method used to import entrypoint files during the build process to extract their options. * - * - "jiti": Simplest and fastest, but doesn't allow using any imported variables outside the entrypoint's main function - * - "vite-runtime" (unstable): Uses Vite 5.3's new runtime API to import the entrypoints. Automatically includes vite config based on your wxt.config.ts file - * - "vite-node" (unstable): Uses `vite-node` to import the entrypoints. Automatically includes vite config based on your wxt.config.ts file + * - `"jiti"`: Simplest and fastest, but doesn't allow using any imported variables outside the entrypoint's main function + * - `"vite-runtime"` (unstable): Uses Vite 5.3's new runtime API to import the entrypoints. Automatically includes vite config based on your wxt.config.ts file + * - `"vite-node"` (unstable): Uses `vite-node` to import the entrypoints. Automatically includes vite config based on your wxt.config.ts file * * @see {@link https://wxt.dev/guide/go-further/entrypoint-side-effects.html|Entrypoint Side-effect Docs} * @@ -1202,8 +1191,9 @@ export interface ResolvedConfig { * Import aliases to absolute paths. */ alias: Record; + extensionApi: 'webextension-polyfill' | 'chrome'; + browserModule: 'wxt/browser' | 'wxt/browser/chrome'; experimental: { - includeBrowserPolyfill: boolean; entrypointImporter: 'jiti' | 'vite-runtime' | 'vite-node'; }; dev: { diff --git a/packages/wxt/src/virtual/mock-browser.ts b/packages/wxt/src/virtual/mock-browser.ts index cf37d2f77..380247101 100644 --- a/packages/wxt/src/virtual/mock-browser.ts +++ b/packages/wxt/src/virtual/mock-browser.ts @@ -1,3 +1,4 @@ -import { fakeBrowser as mockBrowser } from 'wxt/testing'; +import { fakeBrowser } from 'wxt/testing'; -export default mockBrowser; +export const browser = fakeBrowser; +export default fakeBrowser; diff --git a/packages/wxt/src/virtual/tsconfig.json b/packages/wxt/src/virtual/tsconfig.json index 19d511266..6f74c3f88 100644 --- a/packages/wxt/src/virtual/tsconfig.json +++ b/packages/wxt/src/virtual/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../../../../tsconfig.base.json", "compilerOptions": { - "types": ["vite/client", "../types/globals.d.ts"] + "types": ["vite/client", "../@types/globals.d.ts"] }, "include": ["./*"] } diff --git a/packages/wxt/src/virtual/virtual-module-globals.d.ts b/packages/wxt/src/virtual/virtual-module-globals.d.ts index ba96b54fe..5199a83a9 100644 --- a/packages/wxt/src/virtual/virtual-module-globals.d.ts +++ b/packages/wxt/src/virtual/virtual-module-globals.d.ts @@ -1,26 +1,22 @@ // Types required to make the virtual modules happy. declare module 'virtual:user-background-entrypoint' { - const definition: { main: () => void }; + const definition: import('wxt').BackgroundDefinition; export default definition; } declare module 'virtual:user-content-script-isolated-world-entrypoint' { - const definition: { - main: ( - ctx: import('wxt/client').ContentScriptContext, - ) => void | Promise; - }; + const definition: import('wxt').IsolatedWorldContentScriptDefinition; export default definition; } declare module 'virtual:user-content-script-main-world-entrypoint' { - const definition: { main: () => void | Promise }; + const definition: import('wxt').MainWorldContentScriptDefinition; export default definition; } declare module 'virtual:user-unlisted-script-entrypoint' { - const definition: { main: () => void | Promise }; + const definition: import('wxt').UnlistedScriptDefinition; export default definition; } @@ -28,16 +24,6 @@ declare module 'wxt/browser' { export const browser: import('webextension-polyfill').Browser; } -declare module 'wxt/client' { - export class ContentScriptContext { - constructor(name: string, options: any); - } -} - -declare module 'wxt/testing' { - export const fakeBrowser: import('webextension-polyfill').Browser; -} - declare module 'virtual:wxt-plugins' { export function initPlugins(): void; } diff --git a/packages/wxt/typedoc.json b/packages/wxt/typedoc.json index b5472db17..312472814 100644 --- a/packages/wxt/typedoc.json +++ b/packages/wxt/typedoc.json @@ -3,7 +3,8 @@ "src/client/index.ts", "src/testing/index.ts", "src/sandbox/index.ts", - "src/browser.ts", + "src/browser/index.ts", + "src/browser/chrome.ts", "src/index.ts", "src/storage.ts", "src/modules.ts" diff --git a/packages/wxt/vitest.config.ts b/packages/wxt/vitest.config.ts index e22c7f0c5..8e941f0ca 100644 --- a/packages/wxt/vitest.config.ts +++ b/packages/wxt/vitest.config.ts @@ -18,7 +18,10 @@ export default defineConfig({ ignored: '**/dist/**', }, }, - plugins: [RandomSeed()], + plugins: [ + // @ts-expect-error: Vite version mismatch + RandomSeed(), + ], resolve: { alias: { 'wxt/testing': path.resolve('src/testing'), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3107b07ff..b8642d886 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -316,6 +316,9 @@ importers: '@faker-js/faker': specifier: ^8.4.1 version: 8.4.1 + '@types/chrome': + specifier: ^0.0.268 + version: 0.0.268 '@types/fs-extra': specifier: ^11.0.4 version: 11.0.4 @@ -377,6 +380,9 @@ importers: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) devDependencies: + '@types/chrome': + specifier: ^0.0.268 + version: 0.0.268 '@types/react': specifier: ^18.3.3 version: 18.3.3 @@ -1915,9 +1921,26 @@ packages: '@babel/types': 7.24.7 dev: false + /@types/chrome@0.0.268: + resolution: {integrity: sha512-7N1QH9buudSJ7sI8Pe4mBHJr5oZ48s0hcanI9w3wgijAlv1OZNUZve9JR4x42dn5lJ5Sm87V1JNfnoh10EnQlA==} + dependencies: + '@types/filesystem': 0.0.36 + '@types/har-format': 1.2.15 + dev: true + /@types/estree@1.0.5: resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + /@types/filesystem@0.0.36: + resolution: {integrity: sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA==} + dependencies: + '@types/filewriter': 0.0.33 + dev: true + + /@types/filewriter@0.0.33: + resolution: {integrity: sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g==} + dev: true + /@types/fs-extra@11.0.4: resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==} dependencies: @@ -1925,6 +1948,10 @@ packages: '@types/node': 20.10.3 dev: true + /@types/har-format@1.2.15: + resolution: {integrity: sha512-RpQH4rXLuvTXKR0zqHq3go0RVXYv/YVqv4TnPH95VbwUxZdQlK1EtcMvQvMpDngHbt13Csh9Z4qT9AbkiQH5BA==} + dev: true + /@types/hast@3.0.4: resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} dependencies: @@ -7239,7 +7266,7 @@ packages: peerDependencies: vite: ^4.0.0 || ^5.0.0 dependencies: - vite: 5.3.4(@types/node@20.14.11) + vite: 5.3.4(sass@1.77.8) dev: true /vitest@2.0.3: