Compare commits

...

14 Commits

Author SHA1 Message Date
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
27 changed files with 323 additions and 48 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.23",
"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
+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
+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/*'],
},
],
},
+44
View File
@@ -1,5 +1,49 @@
# Changelog
## 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)
+75
View File
@@ -1,5 +1,17 @@
import { describe, it, expect } from 'vitest';
import { TestProject } from '../utils';
import { createServer as createNetServer } from 'node:net';
/** Starts a TCP server on the given port and returns a cleanup function. */
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);
});
}
describe('Dev Mode', () => {
it('should not change ports when restarting the server', async () => {
@@ -21,4 +33,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();
}
});
});
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "wxt",
"type": "module",
"version": "0.20.21",
"version": "0.20.23",
"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;
}
+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"
@@ -1578,6 +1578,9 @@ describe('Manifest Utils', () => {
// @ts-ignore: Purposefully removing version from fake object
version: null,
},
suppressWarnings: {
firefoxDataCollection: true,
},
},
});
@@ -2011,6 +2014,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)}`,
);
}
+14
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') {
@@ -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: {},
};
});
+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.23"
}
}
+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.23"
}
}
+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.23"
}
}
+1 -1
View File
@@ -16,6 +16,6 @@
},
"devDependencies": {
"typescript": "^5.9.3",
"wxt": "^0.20.21"
"wxt": "^0.20.23"
}
}
+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.23"
}
}