Compare commits

...

25 Commits

Author SHA1 Message Date
github-actions[bot] 155626fbed chore(release): wxt v0.20.25
📼 VHS / Create VHS (push) Waiting to run
2026-04-18 14:24:37 +00:00
Aaron e802aa211f docs: Update permissions section with per-browser warning (#2284) 2026-04-18 09:08:09 -05:00
Aaron 54fe8c6687 fix: Port change in dev server on reload (#2283) 2026-04-18 08:42:04 -05:00
aklinker1 5da6e4ff75 fix: Upgrade @wxt-dev/browser to latest @types/chrome version 2026-04-18 00:46:17 +00:00
Aaron 4090bb100e chore: Simplify tests (#2279) 2026-04-17 12:01:15 -05:00
Aaron 54ed99d91d chore: Update browser fallbacks (#2280) 2026-04-17 12:00:19 -05:00
Aaron 3db3eac62a chore: Increase PNPM test timeout 2026-04-17 11:55:51 -05:00
Patryk Kuniczak 0315882fa4 chore: Add new types for i18n (#2121) 2026-04-17 14:38:22 +00:00
github-actions[bot] 9757e2634f chore(release): wxt v0.20.24
📼 VHS / Create VHS (push) Waiting to run
2026-04-17 14:32:45 +00:00
John Wong 42b55f66fb fix: Correct actionKey for Firefox in MV3 (#2274) 2026-04-17 14:12:37 +00:00
aklinker1 e64058bc94 fix: Upgrade @wxt-dev/browser to latest @types/chrome version 2026-04-17 00:50:56 +00:00
github-actions[bot] 386503cce0 chore(release): wxt v0.20.23
📼 VHS / Create VHS (push) Waiting to run
2026-04-16 18:00:00 +00:00
Aaron 6224ff49f6 fix: Use correct color name
Addresses
https://github.com/wxt-dev/wxt/issues/2180#issuecomment-4259430737
2026-04-16 12:55:55 -05:00
aklinker1 6578be00ce fix: Upgrade @wxt-dev/browser to latest @types/chrome version 2026-04-15 00:51:40 +00:00
Aaron 2f325d898f docs: Re-structure the WXT Modules documentation (#2260) 2026-04-14 12:28:45 -05:00
github-actions[bot] b039c599c8 chore(release): wxt v0.20.22
📼 VHS / Create VHS (push) Waiting to run
2026-04-14 14:38:40 +00:00
Aaron bec4b73839 fix: Apply expanded env to process.env (#2267) 2026-04-14 09:33:36 -05:00
aklinker1 e67bac0868 fix: Upgrade @wxt-dev/browser to latest @types/chrome version 2026-04-14 00:51:06 +00:00
Aaron 24ff92068d ci: Update lockfile with new version during release
Without this, the lockfile becomes out-of-date and CI fails after a
release.
2026-04-13 18:56:52 -05:00
Aaron 6e20036d6c fix: Skip applying esbuild.charset=ascii vite 8 (#2264) 2026-04-13 18:54:14 -05:00
offish b6059cfe4a docs: Add TF2 Trader to the list of extensions (#2213) 2026-04-13 11:31:42 -05:00
Aaron 4a305a2726 feat: Add dev.server.strictPort option (#2261) 2026-04-13 11:31:09 -05:00
Patryk Kuniczak 4ae6d8135f fix: Scripts injection for MV2 (#2232) 2026-04-13 13:47:24 +00:00
Suvesh Moza 225a94199c feat: add support for Firefox data collection permissions (#1976)
Co-authored-by: Patryk Kuniczak <p.kuniczak@gmail.com>
Co-authored-by: Aaron <aaronklinker1@gmail.com>
Co-authored-by: Nick Doan <nickbar01234@gmail.com>
Co-authored-by: Tam Dang <139360620+dahomita@users.noreply.github.com>
2026-04-13 13:34:46 +00:00
GuaGua 35ffa00bae docs: fix Read Frog Firefox showcase link (#2256)
Co-authored-by: frogGuaGuaGuaGua <268840081+frogGuaGuaGuaGua@users.noreply.github.com>
2026-04-13 12:28:53 +00:00
37 changed files with 494 additions and 109 deletions
+1
View File
@@ -1,6 +1,7 @@
.DS_Store
.env
.env.*
!packages/wxt/src/core/utils/__tests__/fixtures/.env
.idea
.output
.webextrc
+1
View File
@@ -1 +1,2 @@
bun 1.3.12
nodejs 24.14.1
Generated
+1 -1
View File
@@ -283,7 +283,7 @@
},
"packages/wxt": {
"name": "wxt",
"version": "0.20.20",
"version": "0.20.25",
"bin": {
"wxt": "./bin/wxt.mjs",
"wxt-publish-extension": "./bin/wxt-publish-extension.mjs",
+5 -2
View File
@@ -260,9 +260,9 @@
- # CanCopy - A web extension that allow you to copy any content from website
chromeId: ggcfemmoabhhelfkhknhbnkmeahloiod
- # Language Learning with AI
- # Read Frog
chromeId: modkelfkcfjpgbfmnbnllalkiogfofhb
firefoxSlug: intersub
firefoxSlug: read-frog-open-ai-translator
- # Bilibili Feed History Helper
chromeId: npfopljnjbamegincfjelhjhnonnjloo
@@ -433,3 +433,6 @@
- # AlarmBot: ULTIMATE Web Monitoring & Smart Price Alerts
chromeId: mpckalcodookackleecihhnngdibelif
- # TF2 Trader - TF2 & Steam Trading Extension - https://github.com/offish/tf2-trader
chromeId: gmicpekfpbikhibodgokfpghadkclhoe
+17
View File
@@ -200,6 +200,23 @@ export default defineConfig({
});
```
:::warning
Different browsers support different permissions. You are responsible for passing only the permissions required for each browser:
```ts
export default defineConfig({
manifest: ({ browser }) => ({
permissions:
browser === 'chrome'
? ['storage', 'favicon', 'declarativeNetRequest']
: ['storage', 'webRequest'],
}),
});
```
:::
## Host Permissions
> [Chrome docs](https://developer.chrome.com/docs/extensions/develop/concepts/declare-permissions#host-permissions)
+18 -22
View File
@@ -4,33 +4,21 @@ outline: deep
# WXT Modules
WXT provides a "module system" that let's you run code at different steps in the build process to modify it.
WXT provides a "module system" that lets you run code at different steps in the build process to modify it.
[[toc]]
## Adding a Module
## Installing a Module
There are two ways to add a module to your project:
To use a published module from NPM, install the package and add it to your config:
1. **NPM**: install an NPM package, like [`@wxt-dev/auto-icons`](https://www.npmjs.com/package/@wxt-dev/auto-icons) and add it to your config:
```ts [wxt.config.ts]
export default defineConfig({
modules: ['@wxt-dev/auto-icons'],
});
```
```ts [wxt.config.ts]
export default defineConfig({
modules: ['@wxt-dev/auto-icons'],
});
```
> Searching for ["wxt module"](https://www.npmjs.com/search?q=wxt%20module) on NPM is a good way to find published WXT modules.
2. **Local**: add a file to your project's `modules/` directory:
```plaintext
<rootDir>/
modules/
my-module.ts
```
> To learn more about writing your own modules, read the [Writing Modules](/guide/essentials/wxt-modules) docs.
> Searching for ["wxt module"](https://www.npmjs.com/search?q=wxt%20module) on NPM is a good way to find published WXT modules.
## Module Options
@@ -49,7 +37,7 @@ Modules are loaded in the same order as hooks are executed. Refer to the [Hooks
## Writing Modules
Here's what a basic WXT module looks like:
If you need custom build logic for your project, you can write your own local module. Here's what a basic WXT module looks like:
```ts
import { defineWxtModule } from 'wxt/modules';
@@ -61,6 +49,14 @@ export default defineWxtModule({
});
```
To add it to your project, place the file in the `modules/` directory at the root of your project. Any module file in this directory is **automatically discovered and loaded** — no additional configuration is needed:
```plaintext
<rootDir>/
modules/
my-module.ts ← loaded automatically
```
Each module's setup function is executed after the `wxt.config.ts` file is loaded. The `wxt` object provides everything you need to write a module:
- Use `wxt.hook(...)` to hook into the build's lifecycle and make changes
+4 -2
View File
@@ -35,14 +35,16 @@ describe('I18n Types', () => {
});
describe('With type-safety', () => {
const i18n = createI18n<{
type MyStructure = {
simple: { plural: false; substitutions: 0 };
simpleSub1: { plural: false; substitutions: 1 };
simpleSub2: { plural: false; substitutions: 2 };
plural: { plural: true; substitutions: 0 };
pluralSub1: { plural: true; substitutions: 1 };
pluralSub2: { plural: true; substitutions: 2 };
}>();
};
const i18n = createI18n<MyStructure>();
describe('t', () => {
it('should only allow passing valid combinations of arguments', () => {
+6 -11
View File
@@ -1,16 +1,11 @@
/** @module @wxt-dev/i18n */
import {
I18nStructure,
DefaultI18nStructure,
I18n,
Substitution,
} from './types';
import { I18nStructure, I18n, Substitution, UntypedI18n } from './types';
import { browser } from '@wxt-dev/browser';
export function createI18n<
T extends I18nStructure = DefaultI18nStructure,
>(): I18n<T> {
const t = (key: string, ...args: any[]) => {
export function createI18n(): UntypedI18n;
export function createI18n<T extends I18nStructure>(): I18n<T>;
export function createI18n(): UntypedI18n {
const t: UntypedI18n['t'] = (key: string, ...args: unknown[]) => {
// Resolve args
let sub: Substitution[] | undefined;
let count: number | undefined;
@@ -64,5 +59,5 @@ export function createI18n<
}
};
return { t } as I18n<T>;
return { t };
}
+14 -9
View File
@@ -3,13 +3,22 @@ export interface I18nFeatures {
substitutions: SubstitutionCount;
}
export type I18nStructure = {
[K: string]: I18nFeatures;
export interface UntypedI18n {
t: UntypedTFunction;
}
export type UntypedTFunction = {
(key: string): string;
(key: string, substitutions: Substitution[]): string;
(key: string, n: number): string;
(key: string, n: number, substitutions: Substitution[]): string;
};
export type DefaultI18nStructure = {
[K: string]: any;
};
export type I18nStructure = Record<string, I18nFeatures>;
export interface I18n<T extends I18nStructure> {
t: TFunction<T>;
}
// prettier-ignore
export type SubstitutionTuple<T extends SubstitutionCount> =
@@ -66,10 +75,6 @@ export type TFunction<T extends I18nStructure> = {
): string;
};
export interface I18n<T extends DefaultI18nStructure> {
t: TFunction<T>;
}
export type Substitution = string | number;
type SubstitutionCount = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
+26 -6
View File
@@ -25,6 +25,7 @@ describe('Options', () => {
describe('extensionDir', () => {
it('should default to the current working directory', async () => {
const actual = await resolveRunOptions({});
expect(actual).toMatchObject<Partial<ResolvedRunOptions>>({
extensionDir: process.cwd(),
});
@@ -34,17 +35,27 @@ describe('Options', () => {
const actual = await resolveRunOptions({
extensionDir: './path/to/extension',
});
expect(actual).toMatchObject<Partial<ResolvedRunOptions>>({
extensionDir: resolve(process.cwd(), './path/to/extension'),
});
});
it('should use absolute paths as-is', async () => {
const actual = await resolveRunOptions({
extensionDir: '/abs/path/to/extension2',
});
expect(actual).toMatchObject<Partial<ResolvedRunOptions>>({
extensionDir: resolve('/abs/path/to/extension2'),
});
});
});
describe('target', () => {
it('should be "chrome" by default', async () => {
const actual = await resolveRunOptions({
extensionDir: 'path/to/extension',
});
const actual = await resolveRunOptions({});
expect(actual).toMatchObject<Partial<ResolvedRunOptions>>({
target: 'chrome',
});
@@ -52,12 +63,12 @@ describe('Options', () => {
it('should be what is passed in', async () => {
const actual = await resolveRunOptions({
extensionDir: 'path/to/extension',
target: 'custom',
browserBinaries: {
custom: '/path/to/custom/browser',
},
});
expect(actual).toMatchObject<Partial<ResolvedRunOptions>>({
target: 'custom',
});
@@ -65,9 +76,9 @@ describe('Options', () => {
it('should throw an error if the target binary could not be found', async () => {
const actual = resolveRunOptions({
extensionDir: 'path/to/extension',
target: 'custom',
});
await expect(actual).rejects.toThrow('Could not find "custom" binary.');
});
});
@@ -83,7 +94,6 @@ describe('Options', () => {
? 'C:\\path\\to\\custom\\browser.exe'
: path;
const actual = await resolveRunOptions({
extensionDir: 'path/to/extension',
target: 'custom',
browserBinaries: {
custom: path,
@@ -98,9 +108,11 @@ describe('Options', () => {
describe('chromiumArgs', () => {
it('should log a warning when --user-data-dir is passed in', async () => {
const warnSpy = vi.spyOn(console, 'warn');
await resolveRunOptions({
chromiumArgs: ['--user-data-dir=some/custom/path'],
});
expect(warnSpy).toBeCalledTimes(1);
expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining(
@@ -111,9 +123,11 @@ describe('Options', () => {
it('should log a warning when --remote-debugging-port is passed in', async () => {
const warnSpy = vi.spyOn(console, 'warn');
await resolveRunOptions({
chromiumArgs: ['--remote-debugging-port=9222'],
});
expect(warnSpy).toBeCalledTimes(1);
expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining(
@@ -126,6 +140,7 @@ describe('Options', () => {
const actual = await resolveRunOptions({
chromiumArgs: ['--window-size=1920,1080'],
});
expect(actual.chromiumArgs).toEqual([
// Defaults
'--disable-features=Translate,OptimizationHints,MediaRouter,DialMediaRouteProvider,CalculateNativeWinOcclusion,InterestFeedContentSuggestions,CertificateTransparencyComponentUpdater,AutofillServerCommunication,PrivacySandboxSettings4',
@@ -161,9 +176,11 @@ describe('Options', () => {
describe('firefoxArgs', () => {
it('should log a warning when --profile is passed in', async () => {
const warnSpy = vi.spyOn(console, 'warn');
await resolveRunOptions({
firefoxArgs: ['--profile=some/custom/path'],
});
expect(warnSpy).toBeCalledTimes(1);
expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining('Custom Firefox --profile argument ignored'),
@@ -172,9 +189,11 @@ describe('Options', () => {
it('should log a warning when --remote-debugging-port is passed in', async () => {
const warnSpy = vi.spyOn(console, 'warn');
await resolveRunOptions({
firefoxArgs: ['--remote-debugging-port=9222'],
});
expect(warnSpy).toBeCalledTimes(1);
expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining(
@@ -187,6 +206,7 @@ describe('Options', () => {
const actual = await resolveRunOptions({
firefoxArgs: ['--window-size=1920,1080'],
});
expect(actual.firefoxArgs).toEqual([
// Defaults
'--new-instance',
+2 -1
View File
@@ -135,11 +135,12 @@ export const KNOWN_BROWSER_PATHS: Record<
*/
export const FALLBACK_TARGETS: Partial<Record<KnownTarget, KnownTarget[]>> = {
chrome: [
'arc',
'chromium',
'chrome-canary',
'chrome-beta',
'chrome-dev',
'brave',
'arc',
'dia',
'edge',
'edge-canary',
+1 -1
View File
@@ -10,7 +10,7 @@ export default defineConfig({
web_accessible_resources: [
{
resources: ['iframe-src.html', 'unlisted.js'],
matches: ['*://*.google.com/*'],
matches: ['*://*.google.com/*', '*://*.example.com/*'],
},
],
},
+76
View File
@@ -1,5 +1,81 @@
# Changelog
## v0.20.25
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.24...wxt-v0.20.25)
### 🩹 Fixes
- Port change in dev server on reload ([#2283](https://github.com/wxt-dev/wxt/pull/2283))
### 📖 Documentation
- Update `permissions` section with per-browser warning ([#2284](https://github.com/wxt-dev/wxt/pull/2284))
### 🏡 Chore
- Increase PNPM test timeout ([3db3eac6](https://github.com/wxt-dev/wxt/commit/3db3eac6))
### ❤️ Contributors
- Aaron ([@aklinker1](https://github.com/aklinker1))
## v0.20.24
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.23...wxt-v0.20.24)
### 🩹 Fixes
- Correct `actionKey` for Firefox in MV3 ([#2274](https://github.com/wxt-dev/wxt/pull/2274))
### ❤️ Contributors
- John Wong <john_wong@live.com>
## v0.20.23
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.22...wxt-v0.20.23)
### 🩹 Fixes
- Use correct color name ([6224ff49](https://github.com/wxt-dev/wxt/commit/6224ff49))
### 📖 Documentation
- Re-structure the WXT Modules documentation ([#2260](https://github.com/wxt-dev/wxt/pull/2260))
### ❤️ Contributors
- Aaron ([@aklinker1](https://github.com/aklinker1))
## v0.20.22
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.21...wxt-v0.20.22)
### 🚀 Enhancements
- Add support for Firefox data collection permissions ([#1976](https://github.com/wxt-dev/wxt/pull/1976))
- Add `dev.server.strictPort` option ([#2261](https://github.com/wxt-dev/wxt/pull/2261))
### 🩹 Fixes
- Scripts injection for MV2 ([#2232](https://github.com/wxt-dev/wxt/pull/2232))
- Skip applying `esbuild.charset=ascii` vite 8 ([#2264](https://github.com/wxt-dev/wxt/pull/2264))
- Apply expanded env to `process.env` ([#2267](https://github.com/wxt-dev/wxt/pull/2267))
### 📖 Documentation
- Fix Read Frog Firefox showcase link ([#2256](https://github.com/wxt-dev/wxt/pull/2256))
- Add TF2 Trader to the list of extensions ([#2213](https://github.com/wxt-dev/wxt/pull/2213))
### ❤️ Contributors
- Aaron ([@aklinker1](https://github.com/aklinker1))
- Offish ([@offish](https://github.com/offish))
- Patryk Kuniczak ([@PatrykKuniczak](https://github.com/PatrykKuniczak))
- Suvesh Moza <anmolmoza2@gmail.com>
- GuaGua <readfrogguagua@gmail.com>
## v0.20.21
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.20...wxt-v0.20.21)
+65 -2
View File
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import { TestProject } from '../utils';
import { describe, expect, it } from 'vitest';
import { TestProject, occupyPort } from '../utils';
describe('Dev Mode', () => {
it('should not change ports when restarting the server', async () => {
@@ -21,4 +21,67 @@ describe('Dev Mode', () => {
expect(finalPort).toBe(initialPort);
});
it('should use the specified port when it is available', async () => {
const project = new TestProject();
project.addFile(
'entrypoints/background.ts',
'export default defineBackground(() => {})',
);
const server = await project.startServer({
runner: { disabled: true },
dev: { server: { port: 4400 } },
});
try {
expect(server.port).toBe(4400);
} finally {
await server.stop();
}
});
it('should fall back to the next available port by default when the port is occupied', async () => {
const port = 4500;
const freePort = await occupyPort(port);
const project = new TestProject();
project.addFile(
'entrypoints/background.ts',
'export default defineBackground(() => {})',
);
const server = await project.startServer({
runner: { disabled: true },
dev: { server: { port } },
});
try {
expect(server.port).not.toBe(port);
expect(server.port).toBeGreaterThan(port);
} finally {
await server.stop();
await freePort();
}
});
it('should throw an error when strictPort is true and the port is occupied', async () => {
const port = 4600;
const freePort = await occupyPort(port);
const project = new TestProject();
project.addFile(
'entrypoints/background.ts',
'export default defineBackground(() => {})',
);
try {
await expect(
project.startServer({
runner: { disabled: true },
dev: { server: { port, strictPort: true } },
}),
).rejects.toThrow();
} finally {
await freePort();
}
});
});
+25
View File
@@ -0,0 +1,25 @@
import { describe, expect, it } from 'vitest';
import { registerWxt, wxt } from '../../src/core/wxt';
import { TestProject, occupyPort } from '../utils';
describe('WXT Global', () => {
describe('reloadConfig', () => {
it('should not change dev server ports when reloading config, even if the port is in-use', async () => {
const project = new TestProject();
await registerWxt('serve', { root: project.root });
const firstPort = wxt.config.dev.server!.port;
expect(firstPort).toBeDefined();
const cleanup = await occupyPort(firstPort);
try {
await wxt.reloadConfig();
const secondPort = wxt.config.dev.server?.port;
expect(secondPort).toBe(firstPort);
} finally {
cleanup();
}
});
});
});
+12
View File
@@ -1,6 +1,7 @@
import merge from 'lodash.merge';
import spawn, { Subprocess } from 'nano-spawn';
import { mkdir, readFile, writeFile } from 'node:fs/promises';
import { createServer as createNetServer } from 'node:net';
import { dirname, relative, resolve } from 'path';
import { glob } from 'tinyglobby';
import {
@@ -204,3 +205,14 @@ export class TestProject {
});
}
}
/** Starts a TCP server on the given port and returns a cleanup function. */
export function occupyPort(port: number): Promise<() => Promise<void>> {
return new Promise((resolve, reject) => {
const srv = createNetServer();
srv.listen(port, 'localhost', () => {
resolve(() => new Promise<void>((res) => srv.close(() => res())));
});
srv.on('error', reject);
});
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "wxt",
"type": "module",
"version": "0.20.21",
"version": "0.20.25",
"description": "⚡ Next-gen Web Extension Framework",
"license": "MIT",
"scripts": {
+12 -2
View File
@@ -76,8 +76,12 @@ export async function createViteBuilder(
config.legacy.skipWebSocketTokenCheck = true;
// Solves https://github.com/wxt-dev/wxt/issues/353
config.esbuild ??= {};
if (config.esbuild) config.esbuild.charset = 'ascii';
if (isRolldownVersion(vite.version)) {
// TODO: Add charset ascii when supported by oxc
} else {
config.esbuild ??= {};
if (config.esbuild) config.esbuild.charset = 'ascii';
}
const server = getWxtDevServer?.();
@@ -348,6 +352,8 @@ export async function createViteBuilder(
server: {
host: info.host,
port: info.port,
// The port is already resolved to an available one during config
// resolution, and vite needs to use the port the rest of WXT uses.
strictPort: true,
origin: info.origin,
},
@@ -492,3 +498,7 @@ export async function removeEmptyDirs(dir: string): Promise<void> {
// noop on failure - this means the directory was not empty.
}
}
function isRolldownVersion(version: string): boolean {
return Number(version.split('.')[0]) >= 8;
}
@@ -6,13 +6,13 @@ import { pnpm } from '../pnpm';
process.env.WXT_PNPM_IGNORE_WORKSPACE = 'true';
describe('PNPM Package Management Utils', () => {
describe('listDependencies', { timeout: 30e3 }, () => {
describe('listDependencies', () => {
const cwd = path.resolve(__dirname, 'fixtures/simple-pnpm-project');
beforeAll(async () => {
// PNPM needs the modules installed, or 'pnpm ls' will return a blank list.
await spawn('pnpm', ['install'], { cwd });
});
}, 30e3);
it('should list direct dependencies', async () => {
const actual = await pnpm.listDependencies({ cwd });
+8
View File
@@ -160,6 +160,7 @@ export async function resolveConfig(
mergedConfig.dev?.server?.origin ??
mergedConfig.dev?.server?.hostname ??
'localhost';
const strictPort = mergedConfig.dev?.server?.strictPort ?? false;
if (port == null || !isFinite(port)) {
port = await getPort({
// Passing host required for Mac, unsure of Windows/Linux
@@ -167,6 +168,11 @@ export async function resolveConfig(
port: 3000,
portRange: [3001, 3010],
});
} else if (!strictPort) {
port = await getPort({
host,
port,
});
}
const originWithProtocolAndPort = [
origin.match(/^https?:\/\//) ? '' : 'http://',
@@ -177,6 +183,7 @@ export async function resolveConfig(
host,
port,
origin: originWithProtocolAndPort,
strictPort,
watchDebounce: safeStringToNumber(process.env.WXT_WATCH_DEBOUNCE) ?? 800,
};
}
@@ -227,6 +234,7 @@ export async function resolveConfig(
userConfigMetadata: userConfigMetadata ?? {},
alias,
experimental: defu(mergedConfig.experimental, {}),
suppressWarnings: mergedConfig.suppressWarnings ?? {},
dev: {
server: devServerConfig,
reloadCommand,
@@ -0,0 +1,42 @@
import { beforeEach, describe, expect, it } from 'vitest';
import { loadEnv } from '../env';
const cwd = process.cwd();
describe('Env Utils', () => {
beforeEach(() => {
if (process.cwd() !== cwd) process.chdir(cwd);
delete process.env.TEST_VAR;
delete process.env.EXPANDED;
});
describe('loadEnv', () => {
beforeEach(() => {
process.chdir(`${import.meta.dirname}/fixtures`);
});
it('should load env vars into the real `process.env`', () => {
loadEnv('testing', 'chrome');
expect(process.env.TEST_VAR).toEqual('expected');
});
it('should override blank strings in process.env', () => {
process.env.TEST_VAR = '';
loadEnv('testing', 'chrome');
expect(process.env.TEST_VAR).toEqual('expected');
});
it('should not override non-blank strings in process.env', () => {
process.env.TEST_VAR = 'non-blank';
loadEnv('testing', 'chrome');
expect(process.env.TEST_VAR).toEqual('non-blank');
});
// Node doesn't return vars in the same order as they're defined:
// https://github.com/nodejs/node/issues/62736
it.skip('should expand env vars into the real `process.env`', () => {
loadEnv('testing', 'chrome');
expect(process.env.EXPANDED).toEqual('expected expanded');
});
});
});
@@ -0,0 +1,2 @@
TEST_VAR="expected"
EXPANDED="$TEST_VAR expanded"
@@ -34,6 +34,7 @@ describe('Manifest Utils', () => {
describe('generateManifest', () => {
describe('popup', () => {
type ActionType = 'browser_action' | 'page_action';
type Mv3ActionType = 'action' | 'page_action';
const popupEntrypoint = (type?: ActionType) =>
fakePopupEntrypoint({
options: {
@@ -107,29 +108,39 @@ describe('Manifest Utils', () => {
},
);
it('should allow converting action to page_action for Firefox MV3', async () => {
const popup = popupEntrypoint('page_action');
const buildOutput = fakeBuildOutput();
setFakeWxt({
config: {
manifestVersion: 3,
outDir,
browser: 'firefox',
},
});
const expected = {
default_icon: popup.options.defaultIcon,
default_title: popup.options.defaultTitle,
default_popup: 'popup.html',
};
it.each<{
inputType: ActionType | undefined;
expectedType: Mv3ActionType;
}>([
{ inputType: undefined, expectedType: 'action' },
{ inputType: 'browser_action', expectedType: 'action' },
{ inputType: 'page_action', expectedType: 'page_action' },
])(
'should use the correct action for Firefox in mv3: %j',
async ({ inputType, expectedType }) => {
const popup = popupEntrypoint(inputType);
const buildOutput = fakeBuildOutput();
setFakeWxt({
config: {
manifestVersion: 3,
outDir,
browser: 'firefox',
},
});
const expected = {
default_icon: popup.options.defaultIcon,
default_title: popup.options.defaultTitle,
default_popup: 'popup.html',
};
const { manifest: actual } = await generateManifest(
[popup],
buildOutput,
);
const { manifest: actual } = await generateManifest(
[popup],
buildOutput,
);
expect(actual.page_action).toEqual(expected);
});
expect(actual[expectedType]).toEqual(expected);
},
);
it('should include default_area for Firefox in mv3', async () => {
const popup = fakePopupEntrypoint({
@@ -1578,6 +1589,9 @@ describe('Manifest Utils', () => {
// @ts-ignore: Purposefully removing version from fake object
version: null,
},
suppressWarnings: {
firefoxDataCollection: true,
},
},
});
@@ -2011,6 +2025,9 @@ describe('Manifest Utils', () => {
manifest: {
manifest_version: 3,
},
suppressWarnings: {
firefoxDataCollection: true,
},
},
});
+2 -7
View File
@@ -1,6 +1,6 @@
import { readFileSync, existsSync } from 'node:fs';
import { parseEnv } from 'node:util';
import { expand } from 'dotenv-expand';
import { existsSync, readFileSync } from 'node:fs';
import { parseEnv } from 'node:util';
import type { TargetBrowser } from '../../types';
/** Load environment files based on the current mode and browser. */
@@ -32,13 +32,8 @@ export function loadEnv(mode: string, browser: TargetBrowser) {
}),
);
// Make a copy of `process.env` so that `dotenv-expand` doesn't re-assign the
// expanded values to the global `process.env`.
const processEnv = { ...process.env } as Record<string, string>;
expand({
parsed,
processEnv,
});
return parsed;
@@ -4,6 +4,6 @@ import { styleText } from 'node:util';
export function printHeader() {
consola.log(
`\n${styleText('gray', 'WXT')} ${styleText(['bold', 'grey'], version)}`,
`\n${styleText('gray', 'WXT')} ${styleText(['bold', 'gray'], version)}`,
);
}
+17 -2
View File
@@ -115,6 +115,20 @@ export async function generateManifest(
? undefined
: versionName;
// Warn if building for Firefox without data_collection_permissions
if (
wxt.config.browser === 'firefox' &&
!userManifest.browser_specific_settings?.gecko
?.data_collection_permissions &&
!wxt.config.suppressWarnings?.firefoxDataCollection
) {
wxt.logger.warn(
'Firefox requires `data_collection_permissions` for new extensions from November 3, 2025. Existing extensions are exempt for now.\n' +
'For more details, see: https://extensionworkshop.com/documentation/develop/firefox-builtin-data-consent/\n' +
'To suppress this warning, set `suppressWarnings.firefoxDataCollection` to `true` in your wxt config.\n',
);
}
addEntrypoints(manifest, entrypoints, buildOutput);
if (wxt.config.browser === 'firefox') {
@@ -293,8 +307,9 @@ function addEntrypoints(
const actionKey =
manifest.manifest_version === 2
? (popup.options.actionType ?? 'browser_action')
: wxt.config.browser === 'firefox'
? (popup.options.actionType ?? 'action')
: wxt.config.browser === 'firefox' &&
popup.options.actionType === 'page_action'
? 'page_action'
: 'action';
manifest[actionKey] = {
@@ -26,7 +26,7 @@ import { vi } from 'vitest';
import { setWxtForTesting } from '../../wxt';
import type { Browser } from '@wxt-dev/browser';
faker.seed(import.meta.test.SEED);
faker.seed(import.meta.env.TEST_SEED);
type DeepPartial<T> = T extends object
? {
@@ -302,6 +302,7 @@ export const fakeResolvedConfig = fakeObjectCreator<ResolvedConfig>(() => {
hooks: {},
vite: () => ({}),
plugins: [],
suppressWarnings: {},
};
});
+6 -5
View File
@@ -1,10 +1,10 @@
import { InlineConfig, Wxt, WxtCommand, WxtHooks, WxtModule } from '../types';
import { resolveConfig } from './resolve-config';
import { createHooks } from 'hookable';
import { createWxtPackageManager } from './package-managers';
import { createViteBuilder } from './builders/vite';
import { builtinModules } from '../builtin-modules';
import { relative } from 'path';
import { builtinModules } from '../builtin-modules';
import { InlineConfig, Wxt, WxtCommand, WxtHooks, WxtModule } from '../types';
import { createViteBuilder } from './builders/vite';
import { createWxtPackageManager } from './package-managers';
import { resolveConfig } from './resolve-config';
/**
* Global variable set once `createWxt` is called once. Since this variable is
@@ -47,6 +47,7 @@ export async function registerWxt(
inlineConfig.dev ??= {};
inlineConfig.dev.server ??= {};
inlineConfig.dev.server.port = wxt.config.dev.server.port;
inlineConfig.dev.server.strictPort = true;
}
wxt.config = await resolveConfig(inlineConfig, command);
+70
View File
@@ -143,6 +143,25 @@ export interface InlineConfig {
* function that returns an object or promise.
*/
manifest?: UserManifest | Promise<UserManifest> | UserManifestFn;
/**
* Suppress specific warnings during the build process.
*
* @example
* ```ts
* export default defineConfig({
* suppressWarnings: {
* firefoxDataCollection: true,
* },
* })
* ```;
*/
suppressWarnings?: {
/**
* Suppress warnings for:
* https://extensionworkshop.com/documentation/develop/firefox-builtin-data-consent
*/
firefoxDataCollection?: boolean;
};
/**
* Configure browser startup. Options set here can be overridden in a
* `web-ext.config.ts` file.
@@ -368,6 +387,14 @@ export interface InlineConfig {
* @default 'http://localhost:3000'
*/
origin?: string;
/**
* Whether the dev server should fail if the specified port is already in
* use. When `false` and a `port` is specified, the next available port
* will be used instead of throwing an error.
*
* @default false
*/
strictPort?: boolean;
/**
* Hostname to run the dev server on.
*
@@ -944,6 +971,41 @@ export type ResolvedPerBrowserOptions<T, TOmitted extends keyof T = never> = {
: T[key];
} & { [key in TOmitted]: T[key] };
/**
* Firefox data collection permission types for personal data. See:
* https://extensionworkshop.com/documentation/develop/firefox-builtin-data-consent/#specifying-data-types
*/
export type FirefoxDataCollectionType =
| 'locationInfo'
| 'browsingActivity'
| 'websiteContent'
| 'websiteActivity'
| 'searchTerms'
| 'bookmarksInfo'
| 'healthInfo'
| 'contactInfo'
| 'socialInfo'
| (string & {});
/**
* Firefox data collection permissions configuration. See:
* https://extensionworkshop.com/documentation/develop/firefox-builtin-data-consent/#specifying-data-types
*/
export interface FirefoxDataCollectionPermissions {
/**
* Required data collection permissions. Users must opt in to use the
* extension. Can include personal data types or "none" to explicitly indicate
* no data collection.
*/
required?: Array<FirefoxDataCollectionType | 'none'>;
/**
* Optional data collection permissions. Users can opt in after installation.
* Can include personal data types or "technicalAndInteraction" (which can
* only be optional).
*/
optional?: Array<FirefoxDataCollectionType | 'technicalAndInteraction'>;
}
/**
* Manifest customization available in the `wxt.config.ts` file. You cannot
* configure entrypoints here, they are configured inline.
@@ -982,6 +1044,11 @@ export type UserManifest = {
strict_min_version?: string;
strict_max_version?: string;
update_url?: string;
/**
* Firefox data collection permissions configuration. See:
* https://extensionworkshop.com/documentation/develop/firefox-builtin-data-consent/#specifying-data-types
*/
data_collection_permissions?: FirefoxDataCollectionPermissions;
};
gecko_android?: {
strict_min_version?: string;
@@ -1442,12 +1509,15 @@ export interface ResolvedConfig {
/** Import aliases to absolute paths. */
alias: Record<string, string>;
experimental: {};
/** List of warning identifiers to suppress during the build process. */
suppressWarnings: { firefoxDataCollection?: boolean };
dev: {
/** Only defined during dev command */
server?: {
host: string;
port: number;
origin: string;
strictPort: boolean;
/**
* The milliseconds to debounce when a file is saved before reloading. The
* only way to set this option is to set the `WXT_WATCH_DEBOUNCE`
+6 -2
View File
@@ -26,7 +26,9 @@ export async function injectScript(
const url = browser.runtime.getURL(path);
const script = document.createElement('script');
if (browser.runtime.getManifest().manifest_version === 2) {
const isManifestV2 = browser.runtime.getManifest().manifest_version === 2;
if (isManifestV2) {
// MV2 requires using an inline script
script.text = await fetch(url).then((res) => res.text());
} else {
@@ -34,7 +36,9 @@ export async function injectScript(
script.src = url;
}
const loadedPromise = makeLoadedPromise(script);
// For MV2: Inline scripts execute synchronously when appended
// For MV3: We need to wait for the load event
const loadedPromise = isManifestV2 ? undefined : makeLoadedPromise(script);
await options?.modifyScript?.(script);
+1 -1
View File
@@ -19,7 +19,7 @@ export default defineConfig({
ignored: '**/dist/**',
},
},
plugins: [RandomSeed()],
plugins: [RandomSeed({ define: 'import.meta.env.TEST_SEED' })],
resolve: {
alias: {
'wxt/testing': path.resolve('src/testing'),
+5 -2
View File
@@ -6,7 +6,7 @@ import {
parseCommits,
} from 'changelogen';
import { consola } from 'consola';
import { readdir, readFile, writeFile } from 'node:fs/promises';
import { readdir } from 'node:fs/promises';
import { join } from 'node:path';
import { getPkgTag, grabPackageDetails, listCommitsInDir } from './git';
@@ -106,8 +106,11 @@ if (pkg === 'wxt') {
}
}
// Run a bun install to update the lockfile after the version change
await Bun.$`bun install --ignore-scripts`;
// Commit changes
await Bun.$`git add "${pkgJsonPath}" "${changelogPath}"`;
await Bun.$`git add "${pkgJsonPath}" "${changelogPath}" bun.lock`;
for (const packageJsonPath of templatePkgJsonPaths) {
await Bun.$`git add "${packageJsonPath}"`;
}
+1 -1
View File
@@ -23,6 +23,6 @@
"@types/react-dom": "^19.2.3",
"@wxt-dev/module-react": "^1.1.5",
"typescript": "^5.9.3",
"wxt": "^0.20.21"
"wxt": "^0.20.25"
}
}
+1 -1
View File
@@ -20,6 +20,6 @@
"devDependencies": {
"@wxt-dev/module-solid": "^1.1.4",
"typescript": "^5.9.3",
"wxt": "^0.20.21"
"wxt": "^0.20.25"
}
}
+1 -1
View File
@@ -21,6 +21,6 @@
"svelte-check": "^4.4.4",
"tslib": "^2.8.1",
"typescript": "^5.9.3",
"wxt": "^0.20.21"
"wxt": "^0.20.25"
}
}
+1 -1
View File
@@ -16,6 +16,6 @@
},
"devDependencies": {
"typescript": "^5.9.3",
"wxt": "^0.20.21"
"wxt": "^0.20.25"
}
}
+1 -1
View File
@@ -21,6 +21,6 @@
"@wxt-dev/module-vue": "^1.0.3",
"typescript": "^5.9.3",
"vue-tsc": "^3.2.5",
"wxt": "^0.20.21"
"wxt": "^0.20.25"
}
}