feat!: Remove webextension-polyfill (#1084)

This commit is contained in:
Aaron
2024-10-19 04:51:07 -05:00
parent 4fe04c6f8a
commit 0cf34d3170
36 changed files with 157 additions and 397 deletions
+16 -36
View File
@@ -4,61 +4,41 @@
Different browsers provide different global variables for accessing the extension APIs (chrome provides `chrome`, firefox provides `browser`, etc).
WXT simplifies this - always use `browser`:
WXT merges these two into a unified API accessed through the `browser` variable.
```ts
import { browser } from 'wxt/browser';
browser.action.onClicked.addListener(() => {
// ...
});
```
Other than that, refer to Chrome and Mozilla's documentation for how to use specific APIs. Everything a normal extension can do, WXT can do as well, just via `browser` instead of `chrome`.
:::tip
With auto-imports enabled, you don't even need to import this variable from `wxt/browser`!
:::
## Webextension Polyfill
> Since `v0.1.0`
By default, WXT uses the [`webextension-polyfill` by Mozilla](https://www.npmjs.com/package/webextension-polyfill) to make the extension API consistent between browsers.
To access types, you should import the relevant namespace from `wxt/browser`:
```ts
import { Runtime } from 'wxt/browser';
function handleMessage(message: any, sender: Runtime.Sender) {
// ...
}
```
### Disabling the polyfill
> Since `v0.19.0`
After the release of MV3 and Chrome's official deprecation of MV2 in June 2024, the polyfill isn't really doing anything useful anymore.
You can disable it with a single line:
```ts [wxt.config.ts]
export default defineConfig({
extensionApi: 'chrome',
});
```
This will change `wxt/browser` to simply export the `browser` or `chrome` globals based on browser at runtime:
The `browser` variable WXT provides is a simple export of the `browser` or `chrome` globals provided by the browser at runtime:
<<< @/../packages/browser/src/index.mjs#snippet
Accessing types is a little different with the polyfill disabled. They do not need to be imported; they're available on the `browser` object itself:
This means you can use the promise-style API for both MV2 and MV3, and it will work across all browsers (Chromium, Firefox, Safari, etc).
## Accessing Types
All types can be accessed via WXT's `Browser` namespace:
```ts
function handleMessage(message: any, sender: browser.runtime.Sender) {
import { Browser } from 'wxt/browser';
function handleMessage(message: any, sender: Browser.runtime.MessageSender) {
// ...
}
```
## Feature Detection
Depending on the manifest version and browser, some APIs are not available at runtime. If an API is not available, it will be `undefined`.
Depending on the manifest version, browser, and permissions, some APIs are not available at runtime. If an API is not available, it will be `undefined`.
:::warning
Types will not help you here. The types WXT provides for `browser` assume all APIs exist. You are responsible for knowing whether an API is available or not.
@@ -1,7 +1,7 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import background from '../background';
chrome.i18n.getMessage = () => 'fake-message';
browser.i18n.getMessage = () => 'fake-message';
const logMock = vi.fn();
console.log = logMock;
@@ -11,7 +11,7 @@ describe('Background Entrypoint', () => {
fakeBrowser.reset();
});
it("should log the extenion's runtime ID", () => {
it("should log the extension's runtime ID", () => {
const id = 'some-id';
fakeBrowser.runtime.id = id;
-1
View File
@@ -3,7 +3,6 @@ import { presetUno } from 'unocss';
export default defineConfig({
srcDir: 'src',
extensionApi: 'chrome',
manifest: {
permissions: ['storage'],
default_locale: 'en',
+4 -16
View File
@@ -191,10 +191,7 @@ describe('Module Helpers', () => {
);
const expectedText = addPluginModule(project);
await project.build({
// reduce build output when comparing test failures
extensionApi: 'chrome',
});
await project.build();
await expect(project.serializeOutput()).resolves.toContain(expectedText);
});
@@ -211,10 +208,7 @@ describe('Module Helpers', () => {
);
const expectedText = addPluginModule(project);
await project.build({
// reduce build output when comparing test failures
extensionApi: 'chrome',
});
await project.build();
await expect(project.serializeOutput()).resolves.toContain(expectedText);
});
@@ -232,10 +226,7 @@ describe('Module Helpers', () => {
);
const expectedText = addPluginModule(project);
await project.build({
// reduce build output when comparing test failures
extensionApi: 'chrome',
});
await project.build();
await expect(project.serializeOutput()).resolves.toContain(expectedText);
});
@@ -248,10 +239,7 @@ describe('Module Helpers', () => {
);
const expectedText = addPluginModule(project);
await project.build({
// reduce build output when comparing test failures
extensionApi: 'chrome',
});
await project.build();
await expect(project.serializeOutput()).resolves.toContain(expectedText);
});
@@ -262,9 +262,6 @@ describe('Output Directory Structure', () => {
project.addFile('entrypoints/popup/main.ts', `logHello('popup')`);
await project.build({
// Simplify the build output for comparison
extensionApi: 'chrome',
vite: () => ({
build: {
// Make output for snapshot readible
@@ -289,11 +286,7 @@ describe('Output Directory Structure', () => {
logHello("background");
}
});
// @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
);
((_b = (_a = globalThis.browser) == null ? void 0 : _a.runtime) == null ? void 0 : _b.id) ? globalThis.browser : globalThis.chrome;
function print(method, ...args) {
return;
}
@@ -347,9 +340,6 @@ describe('Output Directory Structure', () => {
project.addFile('entrypoints/popup/main.ts', `logHello('popup')`);
await project.build({
// Simplify the build output for comparison
extensionApi: 'chrome',
vite: () => ({
build: {
// Make output for snapshot readible
@@ -381,11 +371,7 @@ describe('Output Directory Structure', () => {
background;
function initPlugins() {
}
// @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
);
((_b = (_a = globalThis.browser) == null ? void 0 : _a.runtime) == null ? void 0 : _b.id) ? globalThis.browser : globalThis.chrome;
function print(method, ...args) {
return;
}
@@ -1,6 +1,5 @@
import { describe, it, expect } from 'vitest';
import { TestProject } from '../utils';
import { InlineConfig } from '../../src/types';
describe('User Config', () => {
// Root directory is tested with all tests.
@@ -88,24 +87,6 @@ describe('User Config', () => {
`);
});
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();
projectWithPolyfill.addFile('entrypoints/background.ts', background);
await projectWithPolyfill.build(config);
return await projectWithPolyfill.serializeFile(
'.output/chrome-mv3/background.js',
);
};
const withPolyfill = await buildBackground();
const withoutPolyfill = await buildBackground({
extensionApi: 'chrome',
});
expect(withoutPolyfill).not.toBe(withPolyfill);
});
it('should respect changing config files', async () => {
const project = new TestProject();
project.addFile(
+5 -15
View File
@@ -47,12 +47,8 @@
"default": "./dist/sandbox/index.mjs"
},
"./browser": {
"types": "./dist/browser/index.d.ts",
"default": "./dist/browser/index.mjs"
},
"./browser/chrome": {
"types": "./dist/browser/chrome.d.ts",
"import": "./dist/browser/chrome.mjs"
"types": "./dist/browser.d.ts",
"default": "./dist/browser.mjs"
},
"./testing": {
"types": "./dist/testing/index.d.ts",
@@ -84,11 +80,10 @@
"dependencies": {
"@1natsu/wait-element": "catalog:",
"@aklinker1/rollup-plugin-visualizer": "catalog:",
"@types/chrome": "catalog:",
"@types/webextension-polyfill": "catalog:",
"@webext-core/fake-browser": "catalog:",
"@webext-core/isolated-element": "catalog:",
"@webext-core/match-patterns": "catalog:",
"@wxt-dev/browser": "workspace:*",
"@wxt-dev/storage": "workspace:^1.0.0",
"async-mutex": "catalog:",
"c12": "catalog:",
@@ -128,8 +123,7 @@
"unimport": "catalog:",
"vite": "catalog:",
"vite-node": "catalog:",
"web-ext-run": "catalog:",
"webextension-polyfill": "catalog:"
"web-ext-run": "catalog:"
},
"devDependencies": {
"@aklinker1/check": "catalog:",
@@ -149,9 +143,5 @@
"vitest": "catalog:",
"vitest-plugin-random-seed": "catalog:"
},
"peerDependenciesMeta": {
"@types/chrome": {
"optional": true
}
}
"peerDependenciesMeta": {}
}
+25
View File
@@ -0,0 +1,25 @@
/**
* @module wxt/browser
*/
import { browser as _browser, type Browser } from '@wxt-dev/browser';
/**
* This interface is empty because it is generated per-project when running `wxt prepare`. See:
* - `.wxt/types/paths.d.ts`
*/
export interface WxtRuntime {}
/**
* This interface is empty because it is generated per-project when running `wxt prepare`. See:
* - `.wxt/types/i18n.d.ts`
*/
export interface WxtI18n {}
export type WxtBrowser = Omit<typeof _browser, 'runtime' | 'i18n'> & {
runtime: WxtRuntime & Omit<(typeof _browser)['runtime'], 'getURL'>;
i18n: WxtI18n & Omit<(typeof _browser)['i18n'], 'getMessage'>;
};
export const browser: WxtBrowser = _browser;
export { Browser };
-23
View File
@@ -1,23 +0,0 @@
/// <reference types="chrome" />
/**
* EXPERIMENTAL
*
* Includes the `chrome` API and types when using `extensionApi: 'chrome'`.
*
* @module wxt/browser/chrome
*/
import type { WxtRuntime, WxtI18n } from './index';
export type WxtBrowser = Omit<typeof chrome, 'runtime' | 'i18n'> & {
runtime: WxtRuntime & Omit<(typeof chrome)['runtime'], 'getURL'>;
i18n: WxtI18n & Omit<(typeof chrome)['i18n'], 'getMessage'>;
};
// #region snippet
export const browser: WxtBrowser =
// @ts-expect-error
globalThis.browser?.runtime?.id == null
? globalThis.chrome
: // @ts-expect-error
globalThis.browser;
// #endregion snippet
-87
View File
@@ -1,87 +0,0 @@
/**
* Includes the `browser` API and types when using `extensionApi: 'webextension-polyfill'` (the default).
*
* @module wxt/browser
*/
import originalBrowser, { Browser } from 'webextension-polyfill';
export type AugmentedBrowser = Omit<Browser, 'runtime' | 'i18n'> & {
runtime: WxtRuntime & Omit<Browser['runtime'], 'getURL'>;
i18n: WxtI18n & Omit<Browser['i18n'], 'getMessage'>;
};
/**
* This interface is empty because it is generated per-project when running `wxt prepare`. See:
* - `.wxt/types/paths.d.ts`
*/
export interface WxtRuntime {}
/**
* This interface is empty because it is generated per-project when running `wxt prepare`. See:
* - `.wxt/types/i18n.d.ts`
*/
export interface WxtI18n {}
export const browser: AugmentedBrowser = originalBrowser;
// re-export all the types from webextension-polyfill
// Because webextension-polyfill uses a weird namespace with "import export", there isn't a good way
// to get these types without re-listing them.
/** @ignore */
export type {
ActivityLog,
Alarms,
Bookmarks,
Action,
BrowserAction,
BrowserSettings,
BrowsingData,
CaptivePortal,
Clipboard,
Commands,
ContentScripts,
ContextualIdentities,
Cookies,
DeclarativeNetRequest,
Devtools,
Dns,
Downloads,
Events,
Experiments,
Extension,
ExtensionTypes,
Find,
GeckoProfiler,
History,
I18n,
Identity,
Idle,
Management,
Manifest, // TODO: Export custom manifest types that are valid for both Chrome and Firefox.
ContextMenus,
Menus,
NetworkStatus,
NormandyAddonStudy,
Notifications,
Omnibox,
PageAction,
Permissions,
Pkcs11,
Privacy,
Proxy,
Runtime,
Scripting,
Search,
Sessions,
SidebarAction,
Storage,
Tabs,
Theme,
TopSites,
Types,
UserScripts,
WebNavigation,
WebRequest,
Windows,
} from 'webextension-polyfill';
@@ -24,7 +24,7 @@ describe('Content Script Context', () => {
const onInvalidated = vi.fn();
ctx.onInvalidated(onInvalidated);
// @ts-expect-error
// @ts-ignore
delete fakeBrowser.runtime.id;
const isValid = ctx.isValid;
@@ -83,7 +83,6 @@ export async function createViteBuilder(
wxtPlugins.tsconfigPaths(wxtConfig),
wxtPlugins.noopBackground(),
wxtPlugins.globals(wxtConfig),
wxtPlugins.resolveExtensionApi(wxtConfig),
wxtPlugins.defineImportMeta(),
wxtPlugins.wxtPluginLoader(wxtConfig),
wxtPlugins.resolveAppConfig(wxtConfig),
@@ -3,12 +3,7 @@ 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.
* Mock `wxt/browser` and stub the global `browser`/`chrome` types with a fake version of the extension APIs
*/
export function extensionApiMock(config: ResolvedConfig): vite.PluginOption {
const virtualSetupModule = 'virtual:wxt-setup';
@@ -27,14 +22,12 @@ export function extensionApiMock(config: ResolvedConfig): vite.PluginOption {
},
resolve: {
alias: [
{ find: 'webextension-polyfill', replacement },
// wxt/browser, wxt/browser/...
{ find: /^wxt\/browser.*/, replacement },
{ find: 'wxt/browser', replacement },
],
},
ssr: {
// Inline all WXT modules so vite processes them so the aliases can
// be resolved
// Inline all WXT modules subdependencies can be mocked
noExternal: ['wxt'],
},
};
@@ -9,7 +9,6 @@ export * from './cssEntrypoints';
export * from './bundleAnalysis';
export * from './globals';
export * from './extensionApiMock';
export * from './resolveExtensionApi';
export * from './entrypointGroupGlobals';
export * from './defineImportMeta';
export * from './removeEntrypointMainFunction';
@@ -1,27 +0,0 @@
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' },
],
},
};
},
};
}
@@ -37,11 +37,6 @@ export async function generateWxtDir(entrypoints: Entrypoint[]): Promise<void> {
// 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());
+2 -15
View File
@@ -171,8 +171,6 @@ export async function resolveConfig(
{},
);
const extensionApi = mergedConfig.extensionApi ?? 'webextension-polyfill';
return {
browser,
command,
@@ -182,13 +180,7 @@ export async function resolveConfig(
filterEntrypoints,
env,
fsCache: createFsCache(wxtDir),
imports: await getUnimportOptions(
wxtDir,
srcDir,
logger,
extensionApi,
mergedConfig,
),
imports: await getUnimportOptions(wxtDir, srcDir, logger, mergedConfig),
logger,
manifest: await resolveManifestConfig(env, mergedConfig.manifest),
manifestVersion,
@@ -207,7 +199,6 @@ export async function resolveConfig(
analysis: resolveAnalysisConfig(root, mergedConfig),
userConfigMetadata: userConfigMetadata ?? {},
alias,
extensionApi,
entrypointLoader: mergedConfig.entrypointLoader ?? 'vite-node',
experimental: defu(mergedConfig.experimental, {}),
dev: {
@@ -342,7 +333,6 @@ async function getUnimportOptions(
wxtDir: string,
srcDir: string,
logger: Logger,
extensionApi: ResolvedConfig['extensionApi'],
config: InlineConfig,
): Promise<WxtResolvedUnimportOptions | false> {
if (config.imports === false) return false;
@@ -361,10 +351,7 @@ async function getUnimportOptions(
// ignored.
ignore: ['options'],
},
{
package:
extensionApi === 'chrome' ? 'wxt/browser/chrome' : 'wxt/browser',
},
{ package: 'wxt/browser' },
{ package: 'wxt/sandbox' },
{ package: 'wxt/storage' },
],
@@ -13,7 +13,6 @@ import {
fakeWxtDevServer,
setFakeWxt,
} from '../testing/fake-objects';
import { Manifest } from 'webextension-polyfill';
import {
BuildOutput,
ContentScriptEntrypoint,
@@ -22,6 +21,7 @@ import {
} from '../../../types';
import { wxt } from '../../wxt';
import { mock } from 'vitest-mock-extended';
import type { Browser } from '@wxt-dev/browser';
const outDir = '/output';
const contentScriptOutDir = '/output/content-scripts';
@@ -58,7 +58,7 @@ describe('Manifest Utils', () => {
outDir,
},
});
const expected: Partial<Manifest.WebExtensionManifest> = {
const expected: Partial<Browser.runtime.Manifest> = {
action: {
default_icon: popup.options.defaultIcon,
default_title: popup.options.defaultTitle,
@@ -1,9 +1,9 @@
import type { Manifest } from 'wxt/browser';
import { BuildOutput, Entrypoint, EntrypointGroup } from '../../../types';
import { generateWxtDir } from '../../generate-wxt-dir';
import { buildEntrypoints } from './build-entrypoints';
import { generateManifest, writeManifest } from '../../utils/manifest';
import { wxt } from '../../wxt';
import type { Browser } from '@wxt-dev/browser';
/**
* Given a configuration, list of entrypoints, and an existing, partial output, build the
@@ -30,7 +30,7 @@ export async function rebuild(
},
): Promise<{
output: BuildOutput;
manifest: Manifest.WebExtensionManifest;
manifest: Browser.runtime.Manifest;
warnings: any[][];
}> {
const { default: ora } = await import('ora');
@@ -1,6 +1,7 @@
import type { Manifest, Scripting } from 'wxt/browser';
import type { Browser } from '@wxt-dev/browser';
import { ContentScriptEntrypoint, ResolvedConfig } from '../../types';
import { getEntrypointBundlePath } from './entrypoints';
import { ManifestContentScript } from './types';
/**
* Returns a unique and consistent string hash based on a content scripts options.
@@ -22,13 +23,14 @@ export function hashContentScriptOptions(
if (simplifiedOptions[key] == null) delete simplifiedOptions[key];
});
const withDefaults: Manifest.ContentScript = {
const withDefaults: ManifestContentScript = {
exclude_globs: [],
exclude_matches: [],
include_globs: [],
match_about_blank: false,
run_at: 'document_idle',
all_frames: false,
// @ts-expect-error: Untyped
match_origin_as_fallback: false,
world: 'ISOLATED',
...simplifiedOptions,
@@ -49,7 +51,7 @@ export function mapWxtOptionsToContentScript(
options: ContentScriptEntrypoint['options'],
js: string[] | undefined,
css: string[] | undefined,
): Manifest.ContentScript {
): ManifestContentScript {
return {
matches: options.matches ?? [],
all_frames: options.allFrames,
@@ -61,6 +63,7 @@ export function mapWxtOptionsToContentScript(
css,
js,
// @ts-expect-error: Untyped
match_origin_as_fallback: options.matchOriginAsFallback,
world: options.world,
};
@@ -70,7 +73,7 @@ export function mapWxtOptionsToRegisteredContentScript(
options: ContentScriptEntrypoint['options'],
js: string[] | undefined,
css: string[] | undefined,
): Omit<Scripting.RegisteredContentScript, 'id'> {
): Omit<Browser.scripting.RegisteredContentScript, 'id'> {
return {
allFrames: options.allFrames,
excludeMatches: options.excludeMatches,
+31 -33
View File
@@ -1,4 +1,3 @@
import type { Manifest } from 'wxt/browser';
import {
Entrypoint,
BackgroundEntrypoint,
@@ -21,12 +20,14 @@ import { normalizePath } from './paths';
import { writeFileIfDifferent } from './fs';
import defu from 'defu';
import { wxt } from '../wxt';
import { ManifestV3WebAccessibleResource } from './types';
import type { Browser } from '@wxt-dev/browser';
/**
* Writes the manifest to the output directory and the build output.
*/
export async function writeManifest(
manifest: Manifest.WebExtensionManifest,
manifest: Browser.runtime.Manifest,
output: BuildOutput,
): Promise<void> {
const str =
@@ -49,7 +50,7 @@ export async function writeManifest(
export async function generateManifest(
allEntrypoints: Entrypoint[],
buildOutput: Omit<BuildOutput, 'manifest'>,
): Promise<{ manifest: Manifest.WebExtensionManifest; warnings: any[][] }> {
): Promise<{ manifest: Browser.runtime.Manifest; warnings: any[][] }> {
const entrypoints = allEntrypoints.filter((entry) => !entry.skipped);
const warnings: any[][] = [];
@@ -67,7 +68,7 @@ export async function generateManifest(
}
const version = wxt.config.manifest.version ?? simplifyVersion(versionName);
const baseManifest: Manifest.WebExtensionManifest = {
const baseManifest: Browser.runtime.Manifest = {
manifest_version: wxt.config.manifestVersion,
name: pkg?.name,
description: pkg?.description,
@@ -83,10 +84,7 @@ export async function generateManifest(
);
}
let manifest = defu(
userManifest,
baseManifest,
) as Manifest.WebExtensionManifest;
let manifest = defu(userManifest, baseManifest) as Browser.runtime.Manifest;
// Add reload command in dev mode
if (wxt.config.command === 'serve' && wxt.config.dev.reloadCommand) {
@@ -177,7 +175,7 @@ function simplifyVersion(versionName: string): string {
}
function addEntrypoints(
manifest: Manifest.WebExtensionManifest,
manifest: Browser.runtime.Manifest,
entrypoints: Entrypoint[],
buildOutput: Omit<BuildOutput, 'manifest'>,
): void {
@@ -239,7 +237,6 @@ function addEntrypoints(
);
} else {
manifest.chrome_url_overrides ??= {};
// @ts-expect-error: bookmarks is untyped in webextension-polyfill, but supported by chrome
manifest.chrome_url_overrides.bookmarks = getEntrypointBundlePath(
bookmarks,
wxt.config.outDir,
@@ -255,7 +252,6 @@ function addEntrypoints(
);
} else {
manifest.chrome_url_overrides ??= {};
// @ts-expect-error: history is untyped in webextension-polyfill, but supported by chrome
manifest.chrome_url_overrides.history = getEntrypointBundlePath(
history,
wxt.config.outDir,
@@ -279,12 +275,13 @@ function addEntrypoints(
wxt.config.outDir,
'.html',
);
const options: Manifest.ActionManifest = {};
const options: Browser.runtime.ManifestAction = {};
if (popup.options.defaultIcon)
options.default_icon = popup.options.defaultIcon;
if (popup.options.defaultTitle)
options.default_title = popup.options.defaultTitle;
if (popup.options.browserStyle)
// @ts-expect-error: Not typed by @wxt-dev/browser, but supported by Firefox
options.browser_style = popup.options.browserStyle;
if (manifest.manifest_version === 3) {
manifest.action = {
@@ -314,6 +311,7 @@ function addEntrypoints(
const page = getEntrypointBundlePath(options, wxt.config.outDir, '.html');
manifest.options_ui = {
open_in_tab: options.options.openInTab,
// @ts-expect-error: Not typed by @wxt-dev/browser, but supported by Firefox
browser_style:
wxt.config.browser === 'firefox'
? options.options.browserStyle
@@ -332,7 +330,6 @@ function addEntrypoints(
'Sandboxed pages not supported by Firefox. sandbox.pages was not added to the manifest',
);
} else {
// @ts-expect-error: sandbox not typed
manifest.sandbox = {
pages: sandboxes.map((entry) =>
getEntrypointBundlePath(entry, wxt.config.outDir, '.html'),
@@ -359,7 +356,6 @@ function addEntrypoints(
open_at_install: defaultSidepanel.options.openAtInstall,
};
} else if (wxt.config.manifestVersion === 3) {
// @ts-expect-error: Untyped
manifest.side_panel = {
default_path: page,
};
@@ -432,7 +428,7 @@ function addEntrypoints(
function discoverIcons(
buildOutput: Omit<BuildOutput, 'manifest'>,
): Manifest.WebExtensionManifest['icons'] {
): Browser.runtime.Manifest['icons'] {
const icons: [string, string][] = [];
// prettier-ignore
// #region snippet
@@ -464,7 +460,7 @@ function discoverIcons(
return icons.length > 0 ? Object.fromEntries(icons) : undefined;
}
function addDevModeCsp(manifest: Manifest.WebExtensionManifest): void {
function addDevModeCsp(manifest: Browser.runtime.Manifest): void {
const permission = `http://${wxt.server?.hostname ?? ''}/*`;
const allowedCsp = wxt.server?.origin ?? 'http://localhost:*';
@@ -499,7 +495,7 @@ function addDevModeCsp(manifest: Manifest.WebExtensionManifest): void {
manifest.content_security_policy.sandbox = sandboxCsp.toString();
}
function addDevModePermissions(manifest: Manifest.WebExtensionManifest) {
function addDevModePermissions(manifest: Browser.runtime.Manifest) {
// For reloading the page
addPermission(manifest, 'tabs');
@@ -543,8 +539,7 @@ export function getContentScriptCssWebAccessibleResources(
contentScripts: ContentScriptEntrypoint[],
contentScriptCssMap: Record<string, string | undefined>,
): any[] {
const resources: Manifest.WebExtensionManifestWebAccessibleResourcesC2ItemType[] =
[];
const resources: ManifestV3WebAccessibleResource[] = [];
contentScripts.forEach((script) => {
if (script.options.cssInjectionMode !== 'ui') return;
@@ -554,9 +549,10 @@ export function getContentScriptCssWebAccessibleResources(
resources.push({
resources: [cssFile],
matches: script.options.matches?.map((matchPattern) =>
stripPathFromMatchPattern(matchPattern),
),
matches:
script.options.matches?.map((matchPattern) =>
stripPathFromMatchPattern(matchPattern),
) ?? [],
});
});
@@ -583,16 +579,18 @@ export function getContentScriptsCssMap(
}
function addPermission(
manifest: Manifest.WebExtensionManifest,
manifest: Browser.runtime.Manifest,
permission: string,
): void {
manifest.permissions ??= [];
// @ts-expect-error: Allow using strings for permissions for MV2 support
if (manifest.permissions.includes(permission)) return;
// @ts-expect-error: Allow using strings for permissions for MV2 support
manifest.permissions.push(permission);
}
function addHostPermission(
manifest: Manifest.WebExtensionManifest,
manifest: Browser.runtime.Manifest,
hostPermission: string,
): void {
manifest.host_permissions ??= [];
@@ -618,7 +616,7 @@ export function stripPathFromMatchPattern(pattern: string) {
* targeting MV2, automatically convert their definitions down to the basic MV2 array.
*/
export function convertWebAccessibleResourcesToMv2(
manifest: Manifest.WebExtensionManifest,
manifest: Browser.runtime.Manifest,
): void {
if (manifest.web_accessible_resources == null) return;
@@ -633,17 +631,17 @@ export function convertWebAccessibleResourcesToMv2(
}
function moveHostPermissionsToPermissions(
manifest: Manifest.WebExtensionManifest,
manifest: Browser.runtime.Manifest,
): void {
if (!manifest.host_permissions?.length) return;
manifest.host_permissions.forEach((permission) =>
manifest.host_permissions.forEach((permission: string) =>
addPermission(manifest, permission),
);
delete manifest.host_permissions;
}
function convertActionToMv2(manifest: Manifest.WebExtensionManifest): void {
function convertActionToMv2(manifest: Browser.runtime.Manifest): void {
if (
manifest.action == null ||
manifest.browser_action != null ||
@@ -654,7 +652,7 @@ function convertActionToMv2(manifest: Manifest.WebExtensionManifest): void {
manifest.browser_action = manifest.action;
}
function convertCspToMv2(manifest: Manifest.WebExtensionManifest): void {
function convertCspToMv2(manifest: Browser.runtime.Manifest): void {
if (
typeof manifest.content_security_policy === 'string' ||
manifest.content_security_policy?.extension_pages == null
@@ -668,8 +666,8 @@ function convertCspToMv2(manifest: Manifest.WebExtensionManifest): void {
/**
* Make sure all resources are in MV3 format. If not, add a wanring
*/
export function validateMv3WebAccessibleResources(
manifest: Manifest.WebExtensionManifest,
function validateMv3WebAccessibleResources(
manifest: Browser.runtime.Manifest,
): void {
if (manifest.web_accessible_resources == null) return;
@@ -688,7 +686,7 @@ export function validateMv3WebAccessibleResources(
/**
* Remove keys from the manifest based on the build target.
*/
function stripKeys(manifest: Manifest.WebExtensionManifest): void {
function stripKeys(manifest: Browser.runtime.Manifest): void {
let keysToRemove: string[] = [];
if (wxt.config.manifestVersion === 2) {
keysToRemove.push(...mv3OnlyKeys);
@@ -699,7 +697,7 @@ function stripKeys(manifest: Manifest.WebExtensionManifest): void {
}
keysToRemove.forEach((key) => {
delete manifest[key as keyof Manifest.WebExtensionManifest];
delete manifest[key as keyof Browser.runtime.Manifest];
});
}
@@ -4,7 +4,6 @@
import { resolve } from 'path';
import { faker } from '@faker-js/faker';
import merge from 'lodash.merge';
import type { Manifest } from 'wxt/browser';
import {
FsCache,
ResolvedConfig,
@@ -27,6 +26,7 @@ import {
import { mock } from 'vitest-mock-extended';
import { vi } from 'vitest';
import { setWxtForTesting } from '../../../core/wxt';
import type { Browser } from '@wxt-dev/browser';
faker.seed(import.meta.test.SEED);
@@ -208,13 +208,11 @@ export function fakeOutputFile(): OutputFile {
return faker.helpers.arrayElement([fakeOutputAsset(), fakeOutputChunk()]);
}
export const fakeManifest = fakeObjectCreator<Manifest.WebExtensionManifest>(
() => ({
manifest_version: faker.helpers.arrayElement([2, 3]),
name: faker.string.alphanumeric(),
version: `${faker.number.int()}.${faker.number.int()}.${faker.number.int()}`,
}),
);
export const fakeManifest = fakeObjectCreator<Browser.runtime.Manifest>(() => ({
manifest_version: faker.helpers.arrayElement([2, 3]),
name: faker.string.alphanumeric(),
version: `${faker.number.int()}.${faker.number.int()}.${faker.number.int()}`,
}));
export const fakeUserManifest = fakeObjectCreator<UserManifest>(() => ({
name: faker.string.alphanumeric(),
@@ -299,7 +297,6 @@ export const fakeResolvedConfig = fakeObjectCreator<ResolvedConfig>(() => {
transformManifest: () => {},
userConfigMetadata: {},
alias: {},
extensionApi: 'webextension-polyfill',
entrypointLoader: 'vite-node',
experimental: {},
dev: {
@@ -356,8 +353,8 @@ export const fakeBuildStepOutput = fakeObjectCreator<BuildStepOutput>(() => ({
entrypoints: fakeArray(fakeEntrypoint),
}));
export const fakeManifestCommand =
fakeObjectCreator<Manifest.WebExtensionManifestCommandsType>(() => ({
export const fakeManifestCommand = fakeObjectCreator<Browser.commands.Command>(
() => ({
description: faker.string.sample(),
suggested_key: {
default: `${faker.helpers.arrayElement(['ctrl', 'alt'])}+${faker.number.int(
@@ -367,7 +364,8 @@ export const fakeManifestCommand =
},
)}`,
},
}));
}),
);
export const fakeDevServer = fakeObjectCreator<WxtDevServer>(() => ({
hostname: 'localhost',
+1 -1
View File
@@ -5,7 +5,7 @@ import { ProxifiedModule, parseModule } from 'magicast';
* 1. Removes or clears out `main` function from returned object
* 2. Removes any unused functions/variables outside the definition that aren't being called/used
* 3. Removes unused imports
* 3. Removes value-less, side-effect only imports (like `import "./styles.css"` or `import "webextension-polyfill"`)
* 3. Removes value-less, side-effect only imports (like `import "./styles.css"` or `import "polyfill"`)
*/
export function removeMainFunctionCode(code: string): {
code: string;
+10
View File
@@ -1,3 +1,5 @@
import type { Browser } from '@wxt-dev/browser';
/**
* Remove optional from key, but keep undefined if present
*
@@ -6,3 +8,11 @@
* // type Test = {a: string | undefined, b: number}
*/
export type NullablyRequired<T> = { [K in keyof Required<T>]: T[K] };
export type ManifestContentScript = NonNullable<
Browser.runtime.Manifest['content_scripts']
>[number];
export type ManifestV3WebAccessibleResource = NonNullable<
Browser.runtime.ManifestV3['web_accessible_resources']
>[number];
+21 -31
View File
@@ -1,5 +1,4 @@
import type * as vite from 'vite';
import type { Manifest, Scripting } from 'wxt/browser';
import { UnimportOptions, Import } from 'unimport';
import { LogLevel } from 'consola';
import type { ContentScriptContext } from './client/content-scripts/content-script-context';
@@ -7,6 +6,8 @@ import type { PluginVisualizerOptions } from '@aklinker1/rollup-plugin-visualize
import { ResolvedConfig as C12ResolvedConfig } from 'c12';
import { Hookable, NestedHooks } from 'hookable';
import type * as Nypm from 'nypm';
import { ManifestContentScript } from './core/utils/types';
import type { Browser } from '@wxt-dev/browser';
export interface InlineConfig {
/**
@@ -267,7 +268,7 @@ export interface InlineConfig {
* }
* })
*/
transformManifest?: (manifest: Manifest.WebExtensionManifest) => void;
transformManifest?: (manifest: Browser.runtime.Manifest) => void;
analysis?: {
/**
* Explicitly include bundle analysis when running `wxt build`. This can be overridden by the
@@ -325,16 +326,6 @@ export interface InlineConfig {
* }
*/
alias?: Record<string, string>;
/**
* Which extension API to use.
*
* - `"webextension-polyfill"`: Use `browser` and types from [`webextension-polyfill`](https://www.npmjs.com/package/webextension-polyfill).
* - `"chrome"`: 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).
*
* @default "webextension-polyfill"
* @since 0.19.0
*/
extensionApi?: 'webextension-polyfill' | 'chrome';
/**
* @deprecated Will be removed in v0.20.0, please migrate to using `vite-node`, the new default.
*
@@ -444,7 +435,7 @@ export interface WxtHooks {
}
export interface BuildOutput {
manifest: Manifest.WebExtensionManifest;
manifest: Browser.runtime.Manifest;
publicAssets: OutputAsset[];
steps: BuildStepOutput[];
}
@@ -537,7 +528,7 @@ export interface WxtDevServer
export interface ReloadContentScriptPayload {
registration?: BaseContentScriptEntrypointOptions['registration'];
contentScript: Omit<Scripting.RegisteredContentScript, 'id'>;
contentScript: Omit<Browser.scripting.RegisteredContentScript, 'id'>;
}
export type TargetBrowser = string;
@@ -588,39 +579,39 @@ export interface BackgroundEntrypointOptions extends BaseEntrypointOptions {
export interface BaseContentScriptEntrypointOptions
extends BaseEntrypointOptions {
matches?: PerBrowserOption<Manifest.ContentScript['matches']>;
matches?: PerBrowserOption<NonNullable<ManifestContentScript['matches']>>;
/**
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
* @default "documentIdle"
*/
runAt?: PerBrowserOption<Manifest.ContentScript['run_at']>;
runAt?: PerBrowserOption<Browser.scripting.RegisteredContentScript['runAt']>;
/**
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
* @default false
*/
matchAboutBlank?: PerBrowserOption<
Manifest.ContentScript['match_about_blank']
ManifestContentScript['match_about_blank']
>;
/**
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
* @default []
*/
excludeMatches?: PerBrowserOption<Manifest.ContentScript['exclude_matches']>;
excludeMatches?: PerBrowserOption<ManifestContentScript['exclude_matches']>;
/**
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
* @default []
*/
includeGlobs?: PerBrowserOption<Manifest.ContentScript['include_globs']>;
includeGlobs?: PerBrowserOption<ManifestContentScript['include_globs']>;
/**
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
* @default []
*/
excludeGlobs?: PerBrowserOption<Manifest.ContentScript['exclude_globs']>;
excludeGlobs?: PerBrowserOption<ManifestContentScript['exclude_globs']>;
/**
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
* @default false
*/
allFrames?: PerBrowserOption<Manifest.ContentScript['all_frames']>;
allFrames?: PerBrowserOption<ManifestContentScript['all_frames']>;
/**
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
* @default false
@@ -868,7 +859,7 @@ export type ResolvedPerBrowserOptions<T, TOmitted extends keyof T = never> = {
* here, they are configured inline.
*/
export type UserManifest = {
[key in keyof chrome.runtime.ManifestV3 as key extends
[key in keyof Browser.runtime.ManifestV3 as key extends
| 'action'
| 'background'
| 'chrome_url_overrides'
@@ -880,16 +871,16 @@ export type UserManifest = {
| 'sandbox'
| 'web_accessible_resources'
? never
: key]?: chrome.runtime.ManifestV3[key];
: key]?: Browser.runtime.ManifestV3[key];
} & {
// Add any Browser-specific or MV2 properties that WXT supports here
action?: chrome.runtime.ManifestV3['action'] & {
action?: Browser.runtime.ManifestV3['action'] & {
browser_style?: boolean;
};
browser_action?: chrome.runtime.ManifestV2['browser_action'] & {
browser_action?: Browser.runtime.ManifestV2['browser_action'] & {
browser_style?: boolean;
};
page_action?: chrome.runtime.ManifestV2['page_action'] & {
page_action?: Browser.runtime.ManifestV2['page_action'] & {
browser_style?: boolean;
};
browser_specific_settings?: {
@@ -909,12 +900,12 @@ export type UserManifest = {
};
};
permissions?: (
| chrome.runtime.ManifestPermissions
| Browser.runtime.ManifestPermissions
| (string & Record<never, never>)
)[];
web_accessible_resources?:
| string[]
| chrome.runtime.ManifestV3['web_accessible_resources'];
| Browser.runtime.ManifestV3['web_accessible_resources'];
};
export type UserManifestFn = (
@@ -1187,7 +1178,7 @@ export interface WxtHooks {
*/
'build:manifestGenerated': (
wxt: Wxt,
manifest: Manifest.WebExtensionManifest,
manifest: Browser.runtime.Manifest,
) => HookResult;
/**
* Called once the names and paths of all entrypoints have been resolved.
@@ -1359,7 +1350,7 @@ export interface ResolvedConfig {
/**
* @deprecated Use `build:manifestGenerated` hook instead.
*/
transformManifest?: (manifest: Manifest.WebExtensionManifest) => void;
transformManifest?: (manifest: Browser.runtime.Manifest) => void;
analysis: {
enabled: boolean;
open: boolean;
@@ -1377,7 +1368,6 @@ export interface ResolvedConfig {
* Import aliases to absolute paths.
*/
alias: Record<string, string>;
extensionApi: 'webextension-polyfill' | 'chrome';
entrypointLoader: 'vite-node' | 'jiti';
experimental: {};
dev: {
@@ -84,7 +84,7 @@ async function reloadTabsForContentScript(contentScript: ContentScript) {
await Promise.all(
matchingTabs.map(async (tab) => {
try {
await browser.tabs.reload(tab.id);
await browser.tabs.reload(tab.id!);
} catch (err) {
logger.warn('Failed to reload tab:', err);
}
+1 -1
View File
@@ -21,7 +21,7 @@ declare module 'virtual:user-unlisted-script-entrypoint' {
}
declare module 'wxt/browser' {
export const browser: import('webextension-polyfill').Browser;
export { browser, Browser } from '@wxt-dev/browser';
}
declare module 'virtual:wxt-plugins' {
+1 -2
View File
@@ -3,8 +3,7 @@
"src/client/index.ts",
"src/testing/index.ts",
"src/sandbox/index.ts",
"src/browser/index.ts",
"src/browser/chrome.ts",
"src/browser.ts",
"src/index.ts",
"src/modules.ts",
"src/storage.ts"
+1 -1
View File
@@ -11,6 +11,7 @@ export default defineConfig({
include: ['src/**'],
exclude: ['**/dist', '**/__tests__', 'src/utils/testing'],
},
setupFiles: ['./vitest.setup.ts'],
globalSetup: ['./vitest.globalSetup.ts'],
},
server: {
@@ -22,7 +23,6 @@ export default defineConfig({
resolve: {
alias: {
'wxt/testing': path.resolve('src/testing'),
'webextension-polyfill': path.resolve('src/virtual/mock-browser'),
},
},
});
+5
View File
@@ -0,0 +1,5 @@
import { fakeBrowser } from '@webext-core/fake-browser';
import { vi } from 'vitest';
vi.stubGlobal('chrome', fakeBrowser);
vi.stubGlobal('browser', fakeBrowser);
+3 -25
View File
@@ -57,9 +57,6 @@ catalogs:
'@types/ua-parser-js':
specifier: ^0.7.39
version: 0.7.39
'@types/webextension-polyfill':
specifier: ^0.12.1
version: 0.12.1
'@vitejs/plugin-react':
specifier: ^4.3.4
version: 4.3.4
@@ -297,9 +294,6 @@ catalogs:
web-ext-run:
specifier: ^0.2.1
version: 0.2.1
webextension-polyfill:
specifier: ^0.12.0
version: 0.12.0
patchedDependencies:
markdown-it-footnote:
@@ -698,12 +692,6 @@ importers:
'@aklinker1/rollup-plugin-visualizer':
specifier: 'catalog:'
version: 5.12.0(rollup@4.34.9)
'@types/chrome':
specifier: 'catalog:'
version: 0.0.280
'@types/webextension-polyfill':
specifier: 'catalog:'
version: 0.12.1
'@webext-core/fake-browser':
specifier: 'catalog:'
version: 1.3.1
@@ -713,6 +701,9 @@ importers:
'@webext-core/match-patterns':
specifier: 'catalog:'
version: 1.0.3
'@wxt-dev/browser':
specifier: workspace:*
version: link:../browser
'@wxt-dev/storage':
specifier: workspace:^1.0.0
version: link:../storage
@@ -833,9 +824,6 @@ importers:
web-ext-run:
specifier: 'catalog:'
version: 0.2.1
webextension-polyfill:
specifier: 'catalog:'
version: 0.12.0
devDependencies:
'@aklinker1/check':
specifier: 'catalog:'
@@ -2324,9 +2312,6 @@ packages:
'@types/web-bluetooth@0.0.20':
resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==}
'@types/webextension-polyfill@0.12.1':
resolution: {integrity: sha512-xPTFWwQ8BxPevPF2IKsf4hpZNss4LxaOLZXypQH4E63BDLmcwX/RMGdI4tB4VO4Nb6xDBH3F/p4gz4wvof1o9w==}
'@types/yauzl@2.10.3':
resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==}
@@ -5365,9 +5350,6 @@ packages:
resolution: {integrity: sha512-5D11VcjdGkA1/xax5UWL0YeAbDySKHzWFe6EpsoPNUMw5Uk9tKk9p6GUOfcaI5N7sINKfBMZYNsTBiu5dzJB9A==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
webextension-polyfill@0.12.0:
resolution: {integrity: sha512-97TBmpoWJEE+3nFBQ4VocyCdLKfw54rFaJ6EVQYLBCXqCIpLSZkwGgASpv4oPt9gdKCJ80RJlcmNzNn008Ag6Q==}
webidl-conversions@7.0.0:
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
engines: {node: '>=12'}
@@ -6664,8 +6646,6 @@ snapshots:
'@types/web-bluetooth@0.0.20': {}
'@types/webextension-polyfill@0.12.1': {}
'@types/yauzl@2.10.3':
dependencies:
'@types/node': 20.17.6
@@ -10092,8 +10072,6 @@ snapshots:
- supports-color
- utf-8-validate
webextension-polyfill@0.12.0: {}
webidl-conversions@7.0.0: {}
webpack-sources@3.2.3:
-1
View File
@@ -2,6 +2,5 @@ import { defineConfig } from 'wxt';
// See https://wxt.dev/api/config.html
export default defineConfig({
extensionApi: 'chrome',
modules: ['@wxt-dev/module-react'],
});
-1
View File
@@ -2,6 +2,5 @@ import { defineConfig } from 'wxt';
// See https://wxt.dev/api/config.html
export default defineConfig({
extensionApi: 'chrome',
modules: ['@wxt-dev/module-solid'],
});
-1
View File
@@ -3,6 +3,5 @@ import { defineConfig } from 'wxt';
// See https://wxt.dev/api/config.html
export default defineConfig({
srcDir: 'src',
extensionApi: 'chrome',
modules: ['@wxt-dev/module-svelte'],
});
+1 -3
View File
@@ -1,6 +1,4 @@
import { defineConfig } from 'wxt';
// See https://wxt.dev/api/config.html
export default defineConfig({
extensionApi: 'chrome',
});
export default defineConfig({});
-1
View File
@@ -2,6 +2,5 @@ import { defineConfig } from 'wxt';
// See https://wxt.dev/api/config.html
export default defineConfig({
extensionApi: 'chrome',
modules: ['@wxt-dev/module-vue'],
});