Compare commits

..

56 Commits

Author SHA1 Message Date
Aaron b99da14799 feat(runner): Create new @wxt-dev/runner package (#1566)
📼 VHS / Create VHS (push) Cancelled after 0s
2025-06-02 15:01:59 -05:00
aklinker1 f02400a1fb fix: Upgrade @wxt-dev/browser to latest @types/chrome version 2025-06-01 02:20:17 +00:00
aklinker1 bbcb7f7cdd fix: Upgrade @wxt-dev/browser to latest @types/chrome version 2025-05-31 01:53:46 +00:00
aklinker1 30cf8e6d54 fix: Upgrade @wxt-dev/browser to latest @types/chrome version 2025-05-30 01:54:08 +00:00
aklinker1 4f772fc0f7 fix: Upgrade @wxt-dev/browser to latest @types/chrome version 2025-05-17 01:52:17 +00:00
Ahmed Rangel 2cf2582138 docs: Added "NetSuite Record Scripts", "VueTracker" to the homepage (#1665)
Co-authored-by: Florian Metz <me@timeraa.dev>
2025-05-14 12:30:19 +02:00
Stepan Rabotkin ad4de4b92c docs: Added CanCopy to homepage (#1667)
Co-authored-by: Florian Metz <me@timeraa.dev>
2025-05-14 12:23:48 +02:00
MengXi 27ed73a6ae docs: Added 'Read Frog' extension to homepage (#1666) 2025-05-14 12:17:18 +02:00
Qiwei Yang a1570c8917 docs: Added "Bilibili Feed History Extension" to the homepage (#1669)
Co-authored-by: Florian Metz <me@timeraa.dev>
2025-05-14 11:59:01 +02:00
ТΞNSΛI 7e419ddd43 docs: Added "NZBDonkey" to the homepage (#1670)
Co-authored-by: Florian Metz <me@timeraa.dev>
2025-05-14 11:54:59 +02:00
Honwhy Wang b045d3616d docs: Added "WeChat Markdown Editor" to the homepage (#1671)
Co-authored-by: Florian Metz <me@timeraa.dev>
2025-05-14 11:53:48 +02:00
𝖆𝖉𝖎𝖙𝖍𝐲𝖆 f6b50e2e77 docs: Added Tab Grab to the homepage (#1672) 2025-05-14 11:45:16 +02:00
aklinker1 9d1381cbb3 fix: Upgrade @wxt-dev/browser to latest @types/chrome version 2025-05-13 01:55:49 +00:00
aklinker1 99b0242d86 fix: Upgrade @wxt-dev/browser to latest @types/chrome version 2025-05-10 01:49:30 +00:00
aklinker1 db27628211 fix: Upgrade @wxt-dev/browser to latest @types/chrome version 2025-05-06 01:53:55 +00:00
aklinker1 9b9d86229a fix: Upgrade @wxt-dev/browser to latest @types/chrome version 2025-05-05 01:57:09 +00:00
Boris Zabolotskikh 46f830dacf docs: Added "Web to PDF", "Online CSV Viewer", "YouTube Video Transcript" to the homepage (#1655)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2025-05-04 19:33:35 -05:00
github-actions[bot] 3dd9bc7772 chore(release): @wxt-dev/i18n v0.2.4
📼 VHS / Create VHS (push) Cancelled after 0s
2025-05-04 12:27:20 +00:00
Aaron c7335add19 chore: Change browser workspace dependency to ^ 2025-05-04 07:21:55 -05:00
Aaron c133498208 chore(deps): Update all dependencies (#1648) 2025-05-02 08:53:38 -05:00
Aaron 6a52bb22a3 fix: Use @wxt-dev/browser instead of @types/chrome (#1645) 2025-05-02 08:41:02 -05:00
Aaron 6f970efda5 chore: Upgrade @aklinker1/check to v2 (#1647) 2025-05-02 08:36:46 -05:00
Aaron c69350a0eb fix: Fix bad links to developer.chrome.com 2025-05-02 07:10:25 -05:00
Aaron 4add8820eb chore: Stop using PNPM catalog (#1644) 2025-05-02 06:26:12 -05:00
Aaron 033dd4811e chore(deps): Upgrade dev dependencies (#1642) 2025-05-02 05:52:06 -05:00
Aaron da5cd32502 fix: Improve CSS reset inside shadow roots 2025-05-02 05:14:42 -05:00
aklinker1 edf33fdec9 fix: Upgrade @wxt-dev/browser to latest @types/chrome version 2025-05-02 01:53:07 +00:00
seaders 448cbf16c9 feat: Add @font-face to be processed by splitShadowRootCss (#1635) 2025-05-01 12:49:54 -05:00
Aaron e45b77b528 chore(deps): Upgrade vite and related dependencies 2025-05-01 12:25:56 -05:00
github-actions[bot] b61fd1716e chore(release): wxt v0.20.6
📼 VHS / Create VHS (push) Cancelled after 0s
2025-04-30 16:22:57 +00:00
Aaron d584b5cc7e chore(deps): Upgrade web-ext-run to v0.2.3 (#1634) 2025-04-30 11:18:30 -05:00
Bang·_· faf9cedfbd docs: Added "[DesignPicker]" to the homepage (#1624) 2025-04-27 07:24:00 -05:00
Aaron 3a38ac9339 Fix changelog contributors 2025-04-26 18:07:17 -05:00
github-actions[bot] 7ea36a3cfc chore(release): wxt v0.20.5
📼 VHS / Create VHS (push) Cancelled after 0s
2025-04-26 13:53:46 +00:00
Aaron 3577c0b47a fix: Don't use crypto.randUUID for shadow root UIs 2025-04-26 08:48:42 -05:00
Aaron d0bef42186 fix: Standardize locale codes and warn about unsupported ones (#1617) 2025-04-26 08:03:22 -05:00
github-actions[bot] d9f10c62ad chore(release): wxt v0.20.4
📼 VHS / Create VHS (push) Cancelled after 0s
2025-04-25 03:03:08 +00:00
aklinker1 35d3c03e4b fix: Upgrade @wxt-dev/browser to latest @types/chrome version 2025-04-25 01:52:10 +00:00
Yunsup Sim fdd38a1580 fix: Fix CORS error in Firefox (#1607) 2025-04-24 19:01:30 -05:00
ТΞNSΛI 3018801f06 feat: add {{packageVersion}} as template variable (#1604) 2025-04-24 12:06:50 -05:00
Anh71me 8e96bfef06 fix: fix typescript error on defineItem fallback (#1601) 2025-04-24 07:31:48 -05:00
ТΞNSΛI 31071bd11e feat: ignore elements with a vite-ignore or wxt-ignore attribute (#1603)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2025-04-24 00:34:49 -05:00
Yuan f5619f6be1 chore: Move the public folder outside of src for the default Svelte template (#1602) 2025-04-24 00:26:34 -05:00
Aaron adad1b5a2c chore: Fix formatting 2025-04-24 00:24:49 -05:00
Nishu a6c4e19a5d fix: adding missing "" to PublicPath and browser.runtime.getUrl (#1597) 2025-04-24 00:18:06 -05:00
Jack af6f74c344 docs: Added "[Always Light Mode]" to the homepage (#1608) 2025-04-24 00:05:37 -05:00
Aaron ce45cb8a89 docs: Add warning about --load-extension deprecation 2025-04-23 23:35:28 -05:00
aklinker1 14d4aa202d fix: Upgrade @wxt-dev/browser to latest @types/chrome version 2025-04-23 01:51:01 +00:00
Nishu 4c35798dba feat: Ignore popup/index.ts instead of erroring (#1520)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2025-04-20 21:17:05 -05:00
Aaron 656a9b365f docs(Content Script UI): Add additional details about when onRemove is called 2025-04-20 21:15:08 -05:00
Namu 67fa3db921 docs: Fix import in unit-testing.md (#1598) 2025-04-20 20:36:06 -05:00
github-actions[bot] f2d3061e97 chore(release): wxt v0.20.3
📼 VHS / Create VHS (push) Cancelled after 0s
2025-04-19 17:17:39 +00:00
Aaron 760c34e416 feat: Automatically place document-level CSS outside shadow root (#1594) 2025-04-19 12:12:04 -05:00
Bang·_· 48398b315c docs: Added "[Blens - Time Tracker and AI Insight]" to the homepage (#1587) 2025-04-18 21:53:19 -05:00
techlism 45d0d9d7f1 docs: Fix entrypoints.md examples (#1586) 2025-04-18 21:11:04 -05:00
Aaron b0f4ac8221 fix: Fix double hashing of inline script keys
Additionally, update the vite plugin name to better align with purpose
2025-04-18 20:50:02 -05:00
90 changed files with 4527 additions and 2604 deletions
+1
View File
@@ -14,6 +14,7 @@ on:
- module-solid
- module-svelte
- module-vue
- runner
- storage
- unocss
- webextension-polyfill
+1
View File
@@ -14,6 +14,7 @@ on:
- module-solid
- module-svelte
- module-vue
- runner
- storage
- webextension-polyfill
- wxt
+1
View File
@@ -5,6 +5,7 @@
.output
.webextrc
.wxt
.wxt-runner
*.log
/docs/.vitepress/cache
docs/.vitepress/.temp
@@ -71,6 +71,20 @@ const chromeExtensionIds = [
'bmoggiinmnodjphdjnmpcnlleamkfedj', // AliasVault - Open-Source Password & (Email) Alias Manager
'hlnhhamckimoaiekbglafiebkfimhapb', // SnapThePrice: AI-Powered Real-time Lowest Price Finder
'gdjampjdgjmbifnhldgcnccdjkcoicmg', // radiofrance - news & broadcasts (French), music (international)
'jlnhphlghikichhgbnkepenehbmloenb', // Blens - Time Tracker and AI Insight
'njnammmpdodmfkodnfpammnpdcbhnlcm', // Always Light Mode - Setting website always in light mode
'lblmfclcfniabobmamfkdogcgdagbhhb', // DesignPicker - Color Picker & Font Detector
'pamnlaoeobcmhkliljfaofekeddpmfoh', // Web to PDF
'jmbcbeepjfenihlocplnbmbhimcoooka', // Online CSV Viewer
'nkjcoophmpcmmgadnljnlpbpfdfacgbo', // YouTube Video Transcript
'lcaieahkjgeggeiihblhcjbbjlppgieh', // NetSuite Record Scripts
'gmocfknjllodfiomnljmaehcplnekhlo', // VueTracker
'ggcfemmoabhhelfkhknhbnkmeahloiod', // CanCopy - A web extension that allow you to copy any content from website
'modkelfkcfjpgbfmnbnllalkiogfofhb', // Language Learning with AI
'npfopljnjbamegincfjelhjhnonnjloo', // Bilibili Feed History Helper
'edkhpdceeinkcacjdgebjehipmnbomce', // NZBDonkey - The ultimate NZB file download tool
'cckggnbnimdbbpmdinkkgbbncopbloob', // WeChat Markdown Editor(微信 Markdown 编辑器)
'jcblcjolcojmfopefcighfmkkefbaofg', // Tab Grab
];
const { data, err, isLoading } = useListExtensionDetails(chromeExtensionIds);
+2
View File
@@ -15,6 +15,7 @@ import { version as autoIconsVersion } from '../../packages/auto-icons/package.j
import { version as unocssVersion } from '../../packages/unocss/package.json';
import { version as storageVersion } from '../../packages/storage/package.json';
import { version as analyticsVersion } from '../../packages/analytics/package.json';
import { version as runnerVersion } from '../../packages/runner/package.json';
import addKnowledge from 'vitepress-knowledge';
import {
groupIconMdPlugin,
@@ -41,6 +42,7 @@ const otherPackages = {
i18n: i18nVersion,
storage: storageVersion,
unocss: unocssVersion,
runner: runnerVersion,
};
const knowledge = addKnowledge<DefaultTheme.Config>({
@@ -8,6 +8,16 @@ outline: deep
During development, WXT uses [`web-ext` by Mozilla](https://www.npmjs.com/package/web-ext) to automatically open a browser window with your extension installed.
:::danger
Chrome 137 removed support for the `--load-extension` CLI flag, which WXT relied on to open the browser with an extension installed. So this feature will not work for Chrome.
You have two options:
1. Install [Chrome for Testing](https://developer.chrome.com/blog/chrome-for-testing/) (which still supports the `--load-extension` flag) and [point the `chrome` binary to it](#set-browser-binaries), or
2. [Disable this feature](#disable-opening-browser) and manually load your extension
:::
## Config Files
You can configure browser startup in 3 places:
+2 -2
View File
@@ -211,12 +211,12 @@ When defining your background entrypoint, keep in mind that WXT will import this
<!-- prettier-ignore -->
```ts
browser.action.onClick.addListener(() => { // [!code --]
browser.action.onClicked.addListener(() => { // [!code --]
// ... // [!code --]
}); // [!code --]
export default defineBackground(() => {
browser.action.onClick.addListener(() => { // [!code ++]
browser.action.onClicked.addListener(() => { // [!code ++]
// ... // [!code ++]
}); // [!code ++]
});
+1 -1
View File
@@ -85,7 +85,7 @@ import { injectScript, createShadowRootUi } from '#imports';
But Vitest sees this:
```ts
import { injectScript } from 'wxt/browser';
import { injectScript } from 'wxt/utils/inject-script';
import { createShadowRootUi } from 'wxt/utils/content-script-ui/shadow-root';
```
+1
View File
@@ -0,0 +1 @@
<!--@include: ../packages/runner/README.md-->
+28 -28
View File
@@ -4,7 +4,7 @@
"engines": {
"node": ">=18.20.3"
},
"packageManager": "pnpm@10.8.0",
"packageManager": "pnpm@10.10.0",
"scripts": {
"check": "check && pnpm -r --sequential run check",
"test": "pnpm -r --sequential run test run",
@@ -17,33 +17,33 @@
"docs:preview": "pnpm -s docs:gen && vitepress preview docs"
},
"devDependencies": {
"@aklinker1/buildc": "catalog:",
"@aklinker1/check": "catalog:",
"@commitlint/config-conventional": "catalog:",
"@commitlint/types": "catalog:",
"@types/fs-extra": "catalog:",
"@vitest/coverage-v8": "catalog:",
"changelogen": "catalog:",
"consola": "catalog:",
"fast-glob": "catalog:",
"feed": "catalog:",
"fs-extra": "catalog:",
"lint-staged": "catalog:",
"markdown-it-footnote": "catalog:",
"nano-spawn": "catalog:",
"prettier": "catalog:",
"simple-git-hooks": "catalog:",
"tsx": "catalog:",
"typedoc": "catalog:",
"typedoc-plugin-frontmatter": "catalog:",
"typedoc-plugin-markdown": "catalog:",
"typedoc-vitepress-theme": "catalog:",
"typescript": "catalog:",
"vitepress": "catalog:",
"vitepress-knowledge": "catalog:",
"vitepress-plugin-group-icons": "catalog:",
"vitest-mock-extended": "catalog:",
"vue": "catalog:",
"@aklinker1/buildc": "^1.1.4",
"@aklinker1/check": "2.0.0",
"@commitlint/config-conventional": "^19.8.0",
"@commitlint/types": "^19.8.0",
"@types/fs-extra": "^11.0.4",
"@vitest/coverage-v8": "^3.1.2",
"changelogen": "^0.6.1",
"consola": "^3.4.2",
"fast-glob": "^3.3.3",
"feed": "^4.2.2",
"fs-extra": "^11.3.0",
"lint-staged": "^15.5.1",
"markdown-it-footnote": "^4.0.0",
"nano-spawn": "^0.2.0",
"prettier": "^3.5.3",
"simple-git-hooks": "^2.13.0",
"tsx": "4.19.4",
"typedoc": "^0.25.4",
"typedoc-plugin-frontmatter": "^1.3.0",
"typedoc-plugin-markdown": "4.0.0-next.23",
"typedoc-vitepress-theme": "1.0.0-next.3",
"typescript": "^5.8.3",
"vitepress": "^1.6.3",
"vitepress-knowledge": "^0.4.1",
"vitepress-plugin-group-icons": "^1.5.2",
"vitest-mock-extended": "^3.1.0",
"vue": "^3.5.13",
"wxt": "workspace:*"
},
"simple-git-hooks": {
@@ -9,11 +9,12 @@ import type {
AnalyticsEventMetadata,
AnalyticsProvider,
} from './types';
import { browser } from '@wxt-dev/browser';
const ANALYTICS_PORT = '@wxt-dev/analytics';
export function createAnalytics(config?: AnalyticsConfig): Analytics {
if (typeof chrome === 'undefined' || !chrome?.runtime?.id)
if (!browser?.runtime?.id)
throw Error(
'Cannot use WXT analytics in contexts without access to the browser.runtime APIs',
);
@@ -51,13 +52,13 @@ function createBackgroundAnalytics(
defineStorageItem<boolean>('local:wxt-analytics:enabled', false);
// Cached values
const platformInfo = chrome.runtime.getPlatformInfo();
const platformInfo = browser.runtime.getPlatformInfo();
const userAgent = UAParser();
let userId = Promise.resolve(userIdStorage.getValue()).then(
(id) => id ?? globalThis.crypto.randomUUID(),
);
let userProperties = userPropertiesStorage.getValue();
const manifest = chrome.runtime.getManifest();
const manifest = browser.runtime.getManifest();
const getBackgroundMeta = () => ({
timestamp: Date.now(),
@@ -178,7 +179,7 @@ function createBackgroundAnalytics(
config?.providers?.map((provider) => provider(analytics, config)) ?? [];
// Listen for messages from the rest of the extension
chrome.runtime.onConnect.addListener((port) => {
browser.runtime.onConnect.addListener((port) => {
if (port.name === ANALYTICS_PORT) {
port.onMessage.addListener(({ fn, args }) => {
// @ts-expect-error: Untyped fn key
@@ -194,7 +195,7 @@ function createBackgroundAnalytics(
* Creates an analytics client for non-background contexts.
*/
function createFrontendAnalytics(): Analytics {
const port = chrome.runtime.connect({ name: ANALYTICS_PORT });
const port = browser.runtime.connect({ name: ANALYTICS_PORT });
const sessionId = Date.now();
const getFrontendMetadata = (): AnalyticsEventMetadata => ({
sessionId,
@@ -252,8 +253,8 @@ function defineStorageItem<T>(
): AnalyticsStorageItem<T> {
return {
getValue: async () =>
(await chrome.storage.local.get(key))[key] ?? defaultValue,
setValue: (newValue) => chrome.storage.local.set({ [key]: newValue }),
(await browser.storage.local.get(key))[key] ?? defaultValue,
setValue: (newValue) => browser.storage.local.set({ [key]: newValue }),
};
}
+7 -7
View File
@@ -51,15 +51,15 @@
"wxt": ">=0.20.0"
},
"devDependencies": {
"@aklinker1/check": "catalog:",
"@types/chrome": "catalog:",
"@types/ua-parser-js": "catalog:",
"publint": "catalog:",
"typescript": "catalog:",
"unbuild": "catalog:",
"@aklinker1/check": "2.0.0",
"@types/ua-parser-js": "^0.7.39",
"publint": "^0.3.12",
"typescript": "^5.8.3",
"unbuild": "^3.5.0",
"wxt": "workspace:*"
},
"dependencies": {
"ua-parser-js": "catalog:"
"@wxt-dev/browser": "workspace:^",
"ua-parser-js": "^1.0.40"
}
}
+1 -2
View File
@@ -3,8 +3,7 @@
"compilerOptions": {
"paths": {
"#analytics": ["./.wxt/analytics/index.ts"]
},
"types": ["chrome"]
}
},
"exclude": ["node_modules", "dist"]
}
+8 -8
View File
@@ -45,16 +45,16 @@
"wxt": ">=0.19.0"
},
"dependencies": {
"defu": "catalog:",
"fs-extra": "catalog:",
"sharp": "catalog:"
"defu": "^6.1.4",
"fs-extra": "^11.3.0",
"sharp": "^0.34.1"
},
"devDependencies": {
"@aklinker1/check": "catalog:",
"oxlint": "catalog:",
"publint": "catalog:",
"typescript": "catalog:",
"unbuild": "catalog:",
"@aklinker1/check": "2.0.0",
"oxlint": "^0.16.8",
"publint": "^0.3.12",
"typescript": "^5.8.3",
"unbuild": "^3.5.0",
"wxt": "workspace:*"
}
}
+7 -7
View File
@@ -1,7 +1,7 @@
{
"name": "@wxt-dev/browser",
"description": "Provides a cross-browser API for using extension APIs and types based on @types/chrome",
"version": "0.0.315",
"version": "0.0.326",
"type": "module",
"main": "src/index.mjs",
"types": "src/index.d.ts",
@@ -23,12 +23,12 @@
"src"
],
"devDependencies": {
"@types/chrome": "0.0.315",
"fs-extra": "catalog:",
"nano-spawn": "catalog:",
"tsx": "catalog:",
"typescript": "catalog:",
"vitest": "catalog:"
"@types/chrome": "0.0.326",
"fs-extra": "^11.3.0",
"nano-spawn": "^0.2.0",
"tsx": "4.19.4",
"typescript": "^5.8.3",
"vitest": "^3.1.2"
},
"dependencies": {
"@types/filesystem": "*",
+2
View File
@@ -76,5 +76,7 @@ function transformFile(file: string, content: string): string {
.replaceAll('declare namespace chrome', 'export namespace Browser')
// Update references to `chrome` namespace to `Browser`
.replaceAll('chrome.', 'Browser.')
// Fix links to developer.chrome.com
.replaceAll('developer.Browser.com', 'developer.chrome.com')
);
}
+26 -25
View File
@@ -1,5 +1,6 @@
/* DO NOT EDIT - generated by scripts/generate.ts */
/* eslint-disable @typescript-eslint/no-wrapper-object-types */
export namespace Browser {
////////////////////
// Cast
@@ -112,7 +113,7 @@ export namespace Browser {
*/
export function initialize(
apiConfig: Browser.cast.ApiConfig,
successCallback: Function,
successCallback: () => void,
errorCallback: (error: Browser.cast.Error) => void,
): void;
@@ -160,7 +161,7 @@ export namespace Browser {
*/
export function setCustomReceivers(
receivers: Browser.cast.Receiver[],
successCallback: Function,
successCallback: () => void,
errorCallback: (error: Browser.cast.Error) => void,
): void;
@@ -171,7 +172,7 @@ export namespace Browser {
*/
export function setReceiverDisplayStatus(
receiver: Browser.cast.Receiver,
successCallback: Function,
successCallback: () => void,
errorCallback: (error: Browser.cast.Error) => void,
): void;
@@ -294,7 +295,7 @@ export namespace Browser {
*/
setReceiverVolumeLevel(
newLevel: number,
successCallback: Function,
successCallback: () => void,
errorCallback: (error: Browser.cast.Error) => void,
): void;
@@ -305,7 +306,7 @@ export namespace Browser {
*/
setReceiverMuted(
muted: boolean,
successCallback: Function,
successCallback: () => void,
errorCallback: (error: Browser.cast.Error) => void,
): void;
@@ -313,13 +314,13 @@ export namespace Browser {
* @param successCallback
* @param errorCallback
*/
leave(successCallback: Function, errorCallback: (error: Browser.cast.Error) => void): void;
leave(successCallback: () => void, errorCallback: (error: Browser.cast.Error) => void): void;
/**
* @param successCallback
* @param errorCallback
*/
stop(successCallback: Function, errorCallback: (error: Browser.cast.Error) => void): void;
stop(successCallback: () => void, errorCallback: (error: Browser.cast.Error) => void): void;
/**
* @param namespace
@@ -330,7 +331,7 @@ export namespace Browser {
sendMessage(
namespace: string,
message: string | object,
successCallback: Function,
successCallback: () => void,
errorCallback: (error: Browser.cast.Error) => void,
): void;
@@ -887,7 +888,7 @@ export namespace Browser {
*/
getStatus(
getStatusRequest: Browser.cast.media.GetStatusRequest,
successCallback: Function,
successCallback: () => void,
errorCallback: (error: Browser.cast.Error) => void,
): void;
@@ -898,7 +899,7 @@ export namespace Browser {
*/
play(
playRequest: Browser.cast.media.PlayRequest,
successCallback: Function,
successCallback: () => void,
errorCallback: (error: Browser.cast.Error) => void,
): void;
@@ -909,7 +910,7 @@ export namespace Browser {
*/
pause(
pauseRequest: Browser.cast.media.PauseRequest,
successCallback: Function,
successCallback: () => void,
errorCallback: (error: Browser.cast.Error) => void,
): void;
@@ -920,7 +921,7 @@ export namespace Browser {
*/
seek(
seekRequest: Browser.cast.media.SeekRequest,
successCallback: Function,
successCallback: () => void,
errorCallback: (error: Browser.cast.Error) => void,
): void;
@@ -931,7 +932,7 @@ export namespace Browser {
*/
stop(
stopRequest: Browser.cast.media.StopRequest,
successCallback: Function,
successCallback: () => void,
errorCallback: (error: Browser.cast.Error) => void,
): void;
@@ -942,7 +943,7 @@ export namespace Browser {
*/
setVolume(
volumeRequest: Browser.cast.media.VolumeRequest,
successCallback: Function,
successCallback: () => void,
errorCallback: (error: Browser.cast.Error) => void,
): void;
@@ -953,7 +954,7 @@ export namespace Browser {
*/
editTracksInfo(
editTracksInfoRequest: Browser.cast.media.EditTracksInfoRequest,
successCallback: Function,
successCallback: () => void,
errorCallback: (error: Browser.cast.Error) => void,
): void;
@@ -986,7 +987,7 @@ export namespace Browser {
*/
queueAppendItem(
item: Browser.cast.media.QueueItem,
successCallback: Function,
successCallback: () => void,
errorCallback: (error: Browser.cast.Error) => void,
): void;
@@ -997,7 +998,7 @@ export namespace Browser {
*/
queueInsertItems(
queueInsertItemsRequest: Browser.cast.media.QueueInsertItemsRequest,
successCallback: Function,
successCallback: () => void,
errorCallback: (error: Browser.cast.Error) => void,
): void;
@@ -1008,7 +1009,7 @@ export namespace Browser {
*/
queueJumpToItem(
itemId: number,
successCallback: Function,
successCallback: () => void,
errorCallback: (error: Browser.cast.Error) => void,
): void;
@@ -1021,7 +1022,7 @@ export namespace Browser {
queueMoveItemToNewIndex(
itemId: number,
newIndex: number,
successCallback: Function,
successCallback: () => void,
errorCallback: (error: Browser.cast.Error) => void,
): void;
@@ -1029,13 +1030,13 @@ export namespace Browser {
* @param successCallback
* @param errorCallback
*/
queueNext(successCallback: Function, errorCallback: (error: Browser.cast.Error) => void): void;
queueNext(successCallback: () => void, errorCallback: (error: Browser.cast.Error) => void): void;
/**
* @param successCallback
* @param errorCallback
*/
queuePrev(successCallback: Function, errorCallback: (error: Browser.cast.Error) => void): void;
queuePrev(successCallback: () => void, errorCallback: (error: Browser.cast.Error) => void): void;
/**
* @param itemId
@@ -1044,7 +1045,7 @@ export namespace Browser {
*/
queueRemoveItem(
itemId: number,
successCallback: Function,
successCallback: () => void,
errorCallback: (error: Browser.cast.Error) => void,
): void;
@@ -1055,7 +1056,7 @@ export namespace Browser {
*/
queueReorderItems(
queueReorderItemsRequest: Browser.cast.media.QueueReorderItemsRequest,
successCallback: Function,
successCallback: () => void,
errorCallback: (error: Browser.cast.Error) => void,
): void;
@@ -1066,7 +1067,7 @@ export namespace Browser {
*/
queueSetRepeatMode(
repeatMode: Browser.cast.media.RepeatMode,
successCallback: Function,
successCallback: () => void,
errorCallback: (error: Browser.cast.Error) => void,
): void;
@@ -1077,7 +1078,7 @@ export namespace Browser {
*/
queueUpdateItems(
queueUpdateItemsRequest: Browser.cast.media.QueueUpdateItemsRequest,
successCallback: Function,
successCallback: () => void,
errorCallback: (error: Browser.cast.Error) => void,
): void;
}
+1188 -710
View File
File diff suppressed because it is too large Load Diff
+5 -5
View File
@@ -24,10 +24,10 @@
],
"devDependencies": {
"@types/chrome": "{{chromeTypesVersion}}",
"fs-extra": "catalog:",
"nano-spawn": "catalog:",
"tsx": "catalog:",
"typescript": "catalog:",
"vitest": "catalog:"
"fs-extra": "^11.3.0",
"nano-spawn": "^0.2.0",
"tsx": "4.19.4",
"typescript": "^5.8.3",
"vitest": "^3.1.2"
}
}
+31
View File
@@ -1,5 +1,36 @@
# Changelog
## v0.2.4
[compare changes](https://github.com/wxt-dev/wxt/compare/i18n-v0.2.3...i18n-v0.2.4)
### 🩹 Fixes
- Standardize locale codes and warn about unsupported ones ([#1617](https://github.com/wxt-dev/wxt/pull/1617))
- Use `@wxt-dev/browser` instead of `@types/chrome` ([#1645](https://github.com/wxt-dev/wxt/pull/1645))
### 📖 Documentation
- Add react language ID to README ([#1347](https://github.com/wxt-dev/wxt/pull/1347))
- Fix public path reference ([bcb20874](https://github.com/wxt-dev/wxt/commit/bcb20874))
### 🏡 Chore
- **deps:** Upgrade all non-major dependencies ([#1164](https://github.com/wxt-dev/wxt/pull/1164))
- **deps:** Bump dev and non-breaking major dependencies ([#1167](https://github.com/wxt-dev/wxt/pull/1167))
- Add funding links to `package.json` files ([#1446](https://github.com/wxt-dev/wxt/pull/1446))
- Use PNPM 10's new catelog feature ([#1493](https://github.com/wxt-dev/wxt/pull/1493))
- Move production dependencies to PNPM 10 catelog ([#1494](https://github.com/wxt-dev/wxt/pull/1494))
- Stop using PNPM catalog ([#1644](https://github.com/wxt-dev/wxt/pull/1644))
- Upgrade `@aklinker1/check` to v2 ([#1647](https://github.com/wxt-dev/wxt/pull/1647))
- Change browser workspace dependency to `^` ([c7335add](https://github.com/wxt-dev/wxt/commit/c7335add))
### ❤️ Contributors
- Aaron ([@aklinker1](https://github.com/aklinker1))
- Okinea Dev ([@okineadev](https://github.com/okineadev))
- Redwoodlid ([@redwoodlid](https://github.com/redwoodlid))
## v0.2.3
[compare changes](https://github.com/wxt-dev/wxt/compare/i18n-v0.2.2...i18n-v0.2.3)
+13 -13
View File
@@ -1,7 +1,7 @@
{
"name": "@wxt-dev/i18n",
"description": "Type-safe wrapper around browser.i18n.getMessage with additional features",
"version": "0.2.3",
"version": "0.2.4",
"type": "module",
"repository": {
"type": "git",
@@ -26,9 +26,10 @@
"test": "buildc --deps-only -- vitest"
},
"dependencies": {
"chokidar": "catalog:",
"confbox": "catalog:",
"fast-glob": "catalog:"
"@wxt-dev/browser": "workspace:^",
"chokidar": "^4.0.3",
"confbox": "^0.1.8 || ^0.2.2",
"fast-glob": "^3.3.3"
},
"peerDependencies": {
"wxt": ">=0.19.7"
@@ -39,15 +40,14 @@
}
},
"devDependencies": {
"@aklinker1/check": "catalog:",
"@types/chrome": "catalog:",
"@types/node": "catalog:",
"oxlint": "catalog:",
"publint": "catalog:",
"typescript": "catalog:",
"unbuild": "catalog:",
"vitest": "catalog:",
"vitest-plugin-random-seed": "catalog:",
"@aklinker1/check": "2.0.0",
"@types/node": "^20.17.6",
"oxlint": "^0.16.8",
"publint": "^0.3.12",
"typescript": "^5.8.3",
"unbuild": "^3.5.0",
"vitest": "^3.1.2",
"vitest-plugin-random-seed": "^1.1.1",
"wxt": "workspace:*"
},
"main": "./dist/index.cjs",
+11 -6
View File
@@ -1,13 +1,18 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { createI18n } from '../index';
import { browser } from '@wxt-dev/browser';
const getMessageMock = vi.fn();
vi.stubGlobal('chrome', {
i18n: {
getMessage: getMessageMock,
},
vi.mock('@wxt-dev/browser', async () => {
const { vi } = await import('vitest');
return {
browser: {
i18n: {
getMessage: vi.fn(),
},
},
};
});
const getMessageMock = vi.mocked(browser.i18n.getMessage);
describe('createI18n', () => {
beforeEach(() => {
+11 -6
View File
@@ -1,13 +1,18 @@
import { beforeEach, describe, it, vi } from 'vitest';
import { createI18n } from '..';
import { browser } from '@wxt-dev/browser';
const getMessageMock = vi.fn();
vi.stubGlobal('chrome', {
i18n: {
getMessage: getMessageMock,
},
vi.mock('@wxt-dev/browser', async () => {
const { vi } = await import('vitest');
return {
browser: {
i18n: {
getMessage: vi.fn(),
},
},
};
});
const getMessageMock = vi.mocked(browser.i18n.getMessage);
const n: number = 1;
+32 -2
View File
@@ -1,10 +1,14 @@
import { describe, it, expect } from 'vitest';
import { ChromeMessage } from '../build';
import { applyChromeMessagePlaceholders, getSubstitutionCount } from '../utils';
import {
applyChromeMessagePlaceholders,
getSubstitutionCount,
standardizeLocale,
} from '../utils';
describe('Utils', () => {
describe('applyChromeMessagePlaceholders', () => {
it('should return the combined stirng', () => {
it('should return the combined string', () => {
const input = {
message: 'Hello $username$, welcome to $appName$',
placeholders: {
@@ -60,4 +64,30 @@ describe('Utils', () => {
expect(getSubstitutionCount('Hello $10')).toBe(1);
});
});
describe('standardizeLocale', () => {
it('should convert two-letter locale codes to lowercase', () => {
expect(standardizeLocale('en')).toEqual('en');
expect(standardizeLocale('EN')).toEqual('en');
});
it('should convert locale code extensions to uppercase', () => {
expect(standardizeLocale('en_US')).toEqual('en_US');
expect(standardizeLocale('en_us')).toEqual('en_US');
expect(standardizeLocale('es_419')).toEqual('es_419');
});
it('should convert dashes to underscores', () => {
expect(standardizeLocale('en_US')).toEqual('en_US');
expect(standardizeLocale('en-US')).toEqual('en_US');
});
it('should return the input string as-is for unknown formats', () => {
expect(standardizeLocale('en_USSS')).toEqual('en_USSS');
expect(standardizeLocale('en-')).toEqual('en-');
expect(standardizeLocale('------')).toEqual('------');
expect(standardizeLocale('test')).toEqual('test');
expect(standardizeLocale('hello-world')).toEqual('hello-world');
});
});
});
+2
View File
@@ -10,6 +10,8 @@ import { parseYAML, parseJSON5, parseTOML } from 'confbox';
import { dirname, extname } from 'node:path';
import { applyChromeMessagePlaceholders, getSubstitutionCount } from './utils';
export { SUPPORTED_LOCALES } from './supported-locales';
//
// TYPES
//
+3 -2
View File
@@ -7,6 +7,7 @@ import {
I18n,
Substitution,
} from './types';
import { browser } from '@wxt-dev/browser';
export function createI18n<
T extends I18nStructure = DefaultI18nStructure,
@@ -39,9 +40,9 @@ export function createI18n<
if (sub?.length) {
// Convert all substitutions to strings
const stringSubs = sub?.map((sub) => String(sub));
message = chrome.i18n.getMessage(key.replaceAll('.', '_'), stringSubs);
message = browser.i18n.getMessage(key.replaceAll('.', '_'), stringSubs);
} else {
message = chrome.i18n.getMessage(key.replaceAll('.', '_'));
message = browser.i18n.getMessage(key.replaceAll('.', '_'));
}
if (!message) {
console.warn(`[i18n] Message not found: "${key}"`);
+18 -4
View File
@@ -16,12 +16,14 @@ import {
generateChromeMessagesText,
parseMessagesFile,
generateTypeText,
SUPPORTED_LOCALES,
} from './build';
import glob from 'fast-glob';
import { basename, extname, join, resolve } from 'node:path';
import { watch } from 'chokidar';
import { GeneratedPublicFile, WxtDirFileEntry } from 'wxt';
import { writeFile } from 'node:fs/promises';
import { standardizeLocale } from './utils';
export default defineWxtModule<I18nOptions>({
name: '@wxt-dev/i18n',
@@ -46,10 +48,22 @@ export default defineWxtModule<I18nOptions>({
cwd: localesDir,
absolute: true,
});
return files.map((file) => ({
file,
locale: basename(file).replace(extname(file), ''),
}));
const unsupportedLocales: string[] = [];
const res = files.map((file) => {
const rawLocale = basename(file).replace(extname(file), '');
const locale = standardizeLocale(rawLocale);
if (!SUPPORTED_LOCALES.has(locale)) unsupportedLocales.push(locale);
return { file, locale };
});
if (unsupportedLocales.length > 0)
wxt.logger.warn(
`Unsupported locales: [${unsupportedLocales.join(', ')}].\n\nWeb extensions only support a limited set of locales as described here: https://developer.chrome.com/docs/extensions/reference/api/i18n#locales`,
);
return res;
};
const generateOutputJsonFiles = async (): Promise<
+58
View File
@@ -0,0 +1,58 @@
/** From https://developer.chrome.com/docs/extensions/reference/api/i18n#locales */
export const SUPPORTED_LOCALES = new Set([
'ar',
'am',
'bg',
'bn',
'ca',
'cs',
'da',
'de',
'el',
'en',
'en_AU',
'en_GB',
'en_US',
'es',
'es_419',
'et',
'fa',
'fi',
'fil',
'fr',
'gu',
'he',
'hi',
'hr',
'hu',
'id',
'it',
'ja',
'kn',
'ko',
'lt',
'lv',
'ml',
'mr',
'ms',
'nl',
'no',
'pl',
'pt_BR',
'pt_PT',
'ro',
'ru',
'sk',
'sl',
'sr',
'sv',
'sw',
'ta',
'te',
'th',
'tr',
'uk',
'vi',
'zh_CN',
'zh_TW',
]);
+13
View File
@@ -21,3 +21,16 @@ export function getSubstitutionCount(message: string): number {
}
const MAX_SUBSTITUTIONS = 9;
/** Given a string, standardize it to the format `xx_YY`. */
export function standardizeLocale(locale: string): string {
if (locale.length === 2) return locale.toLowerCase();
const [is_match, prefix, suffix] =
locale.match(/^([a-z]{2})[-_]([a-z]{2,3})$/i) ?? [];
if (is_match) {
return `${prefix.toLowerCase()}_${suffix.toUpperCase()}`;
}
return locale;
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"types": ["chrome", "node"]
"types": ["node"]
},
"exclude": ["node_modules/**", "dist/**"]
}
+9 -9
View File
@@ -48,17 +48,17 @@
"wxt": ">=0.19.16"
},
"dependencies": {
"@vitejs/plugin-react": "catalog:"
"@vitejs/plugin-react": "^4.4.1"
},
"devDependencies": {
"@aklinker1/check": "catalog:",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"publint": "catalog:",
"react": "catalog:",
"react-dom": "catalog:",
"typescript": "catalog:",
"unbuild": "catalog:",
"@aklinker1/check": "2.0.0",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.3",
"publint": "^0.3.12",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"typescript": "^5.8.3",
"unbuild": "^3.5.0",
"wxt": "workspace:*"
}
}
+6 -6
View File
@@ -48,14 +48,14 @@
"wxt": ">=0.19.16"
},
"dependencies": {
"vite-plugin-solid": "catalog:"
"vite-plugin-solid": "^2.11.6"
},
"devDependencies": {
"@aklinker1/check": "catalog:",
"publint": "catalog:",
"solid-js": "catalog:",
"typescript": "catalog:",
"unbuild": "catalog:",
"@aklinker1/check": "2.0.0",
"publint": "^0.3.12",
"solid-js": "^1.9.6",
"typescript": "^5.8.3",
"unbuild": "^3.5.0",
"wxt": "workspace:*"
}
}
+5 -5
View File
@@ -47,13 +47,13 @@
"svelte": ">=5"
},
"dependencies": {
"@sveltejs/vite-plugin-svelte": "catalog:"
"@sveltejs/vite-plugin-svelte": "^4.0.0 || ^5.0.0"
},
"devDependencies": {
"@aklinker1/check": "catalog:",
"publint": "catalog:",
"typescript": "catalog:",
"unbuild": "catalog:",
"@aklinker1/check": "2.0.0",
"publint": "^0.3.12",
"typescript": "^5.8.3",
"unbuild": "^3.5.0",
"wxt": "workspace:*"
}
}
+5 -5
View File
@@ -46,13 +46,13 @@
"wxt": ">=0.19.16"
},
"dependencies": {
"@vitejs/plugin-vue": "catalog:"
"@vitejs/plugin-vue": "^5.2.3"
},
"devDependencies": {
"@aklinker1/check": "catalog:",
"publint": "catalog:",
"typescript": "catalog:",
"unbuild": "catalog:",
"@aklinker1/check": "2.0.0",
"publint": "^0.3.12",
"typescript": "^5.8.3",
"unbuild": "^3.5.0",
"wxt": "workspace:*"
}
}
+225
View File
@@ -0,0 +1,225 @@
# `@wxt-dev/runner`
Programmatically open a browser and install a web extension from a local directory.
###### With WXT
> [!WARNING]
> This package is intended to replace [`web-ext`](https://github.com/mozilla/web-ext) in the future, but it is not ready at the moment. Once it's ready for testing in WXT, more details will be added here.
```ts
// ~/wxt.runner.config.ts OR <project>/wxt.runner.config.ts
import { defineRunnerConfig } from 'wxt';
export default defineRunnerConfig({
// Options go here
});
```
###### JS API
```ts
import { run } from '@wxt-dev/runner';
await run({
extensionDir: '/path/to/extension',
// Other options...
});
```
## Features
- Supports all Chromium and Firefox based browsers
- Zero dependencies
- One-line config for persisting data between launches
## Requirements
`@wxt-dev/runner` requires a JS runtime that implements the `WebSocket` standard:
| JS Runtime | Version |
| ---------- | ----------- |
| NodeJS | &ge; 22.4.0 |
| Bun | &ge; 1.2.0 |
You also need to have a specific version of the browser installed that supports the latest features so extensions can be loaded:
| Browser | Version |
| -------- | -------- |
| Chromium | Unknown |
| Firefox | &ge; 139 |
## TODO
- [x] Provide install functions to allow hooking into already running instances of Chrome/Firefox
- [ ] Try to setup E2E tests on Firefox with Puppeteer using this approach
- [ ] Try to setup E2E tests on Chrome with Puppeteer using this approach
## Options
### Target
To open a specific browser, use the `target` option:
```ts
import { run } from '@wxt-dev/runner';
await run({
extensionDir: 'path/to/extension',
target: 'firefox',
});
```
Defaults to opening `chrome`. You may see type-hints for a list of popular browsers, but you can enter any string you want here.
### Data Persistence
Browsers block you from using your normal browser profiles when using the [BiDi and CDP protocols](#implementation-details) for security reasons.
To change how the new profile's data is saved between sessions, use the `dataPersistence` option:
```ts
import { run } from '@wxt-dev/runner';
await run({
dataPersistence: 'user',
});
```
- `"none"` (default): Use a brand new browser profile every time the browser is opened (stored in the system's tmp directory)
- `"project"`: Create a new profile that is re-used for your current directory (by default stored in `.wxt-runner` or `.wxt/runner` for WXT projects)
- `"user"`: Create a new profile that is re-used for all projects using `@wxt-dev/runner` (by default stored in `$HOME/.wxt-runner`)
These presets configure different flags for different operating systems when spawning the browser process.
If you want to customize your data persistence beyond what these presets define, [you can override the browser flags yourself](#Arguments) to configure persistence.
### Browser Binaries
`@wxt-dev/runner` will look for browser binaries/executables in [a hard-coded list of paths](https://github.com/wxt-dev/wxt/blob/main/packages/runner/src/browser-paths.ts). It does not and will not explore your filesystem/`$PATH` to find where the browser is installed. That means there are times you will need to specify the path to a browser's binary on your system:
- Your browser's path is non-standard or missing from the hard-coded list.
- You want to use a specific version/release of the browser.
- You're using a less popular browser and `@wxt-dev/runner` doesn't have hard-coded paths for it.
To do this, use the `browserBinaries` option and set the path to the browser's binary:
```ts
import { run } from '@wxt-dev/runner';
await run({
extensionDir: 'path/to/extension',
browserBinaries: {
chrome: '/path/to/chrome',
firefox: '/path/to/firefox',
},
});
```
### Arguments
To pass custom arguments to the browser on startup, use the `chromiumArgs` or `firefoxArgs` options:
```ts
import { run } from '@wxt-dev/runner';
await run({
extensionDir: 'path/to/extension',
chromiumArgs: ['--window-size=1920,1080'],
firefoxArgs: ['--window-size', '1920,1080'],
});
```
### Start URLs
To open specific URLs in tabs by default, you also use the `chromiumArgs` or `firefoxArgs` options.
Any URLs passed as a CLI argument will be opened in the browser when it starts.
```ts
import { run } from '@wxt-dev/runner';
await run({
extensionDir: 'path/to/extension',
chromiumArgs: ['https://example.com'],
firefoxArgs: ['https://example.com'],
});
```
### Debugging
To see debug logs, set the `DEBUG` env var to `"@wxt-dev/runner"`. This will print the resolved config, commands used to spawn the browser, any messages sent on the browser's communication protocol, and more for you to debug.
<details>
<summary>Example debug output</summary>
```
@wxt-dev/runner:options User options: { extensionDir: 'demo-extension', target: undefined }
@wxt-dev/runner:options Resolved options: {
browserBinary: '/usr/bin/chromium',
chromiumArgs: [
'--disable-features=Translate,OptimizationHints,MediaRouter,DialMediaRouteProvider,CalculateNativeWinOcclusion,InterestFeedContentSuggestions,CertificateTransparencyComponentUpdater,AutofillServerCommunication,PrivacySandboxSettings4',
'--disable-component-extensions-with-background-pages',
'--disable-background-networking',
'--disable-component-update',
'--disable-client-side-phishing-detection',
'--disable-sync',
'--metrics-recording-only',
'--disable-default-apps',
'--no-default-browser-check',
'--no-first-run',
'--disable-background-timer-throttling',
'--disable-ipc-flooding-protection',
'--password-store=basic',
'--use-mock-keychain',
'--force-fieldtrials=*BackgroundTracing/default/',
'--disable-hang-monitor',
'--disable-prompt-on-repost',
'--disable-domain-reliability',
'--propagate-iph-for-testing',
'--remote-debugging-port=0',
'--remote-debugging-pipe',
'--user-data-dir=/tmp/wxt-runner-pWXLO1',
'--enable-unsafe-extension-debugging'
],
dataDir: '/tmp/wxt-runner-pWXLO1',
dataPersistence: 'none',
chromiumRemoteDebuggingPort: 0,
extensionDir: '/home/aklinker1/Development/github.com/wxt-dev/wxt/packages/runner/demo-extension',
firefoxArgs: [
'--new-instance',
'--no-remote',
'--profile',
'/tmp/wxt-runner-pWXLO1',
'--remote-debugging-port=0',
'about:debugging#/runtime/this-firefox'
],
firefoxRemoteDebuggingPort: 0,
target: 'chrome'
}
@wxt-dev/runner:chrome:stderr DevTools listening on ws://127.0.0.1:38397/devtools/browser/93dc4de5-64cb-4e0b-a9d3-7549527015f0
@wxt-dev/runner:cdp Sending command: {
id: 1,
method: 'Extensions.loadUnpacked',
params: {
path: '/home/aklinker1/Development/github.com/wxt-dev/wxt/packages/runner/demo-extension'
}
}
@wxt-dev/runner:cdp Received response: { id: 1, result: { id: 'hckhakegfgenefhikdcfkaaonnclljmf' } }
```
</details>
## Implementation Details
All this package does is spawn a child process to open the browser with some default flags before using remote protocols to install the extension.
### Firefox
We use the new [WebDriver BiDi protocol](https://www.w3.org/TR/webdriver-bidi) to install the extension. This just involves connecting to a web socket and sending a few messages.
### Chrome
We use the [CDP](https://chromedevtools.github.io/devtools-protocol/) with `--remote-debugging-pipe` and `--enable-unsafe-extension-debugging` to install the extension by sending a message via IO pipes 3 and 4.
We don't use Webdriver Bidi because it's not built into Chrome yet. It requires us instantiating a separate child process for `chromedriver`. This is slower and more difficult than just using the CDP built into Chrome.
@@ -0,0 +1 @@
console.log('Hello background!');
@@ -0,0 +1,9 @@
{
"name": "Test",
"version": "1.0.0",
"manifest_version": 3,
"background": {
"service_worker": "background.js",
"scripts": ["background.js"]
}
}
+16
View File
@@ -0,0 +1,16 @@
//
// USAGE:
// pnpm dev
// pnpm dev firefox-nightly
// pnpm dev <target>
//
import { run } from './src';
// Uncomment to enable debug logs
process.env.DEBUG = '@wxt-dev/runner';
await run({
extensionDir: 'demo-extension',
target: process.argv[2],
});
+51
View File
@@ -0,0 +1,51 @@
{
"name": "@wxt-dev/runner",
"description": "Launch Chrome and Firefox with a web extension installed",
"version": "0.1.0",
"type": "module",
"repository": {
"type": "git",
"url": "git+https://github.com/wxt-dev/wxt.git",
"directory": "packages/runner"
},
"homepage": "https://github.com/wxt-dev/wxt/tree/main/packages/runner#readme",
"keywords": [
"web-extension",
"chrome-extension",
"wxt"
],
"author": {
"name": "Aaron Klinker",
"email": "aaronklinker1+wxt@gmail.com"
},
"license": "MIT",
"funding": "https://github.com/sponsors/wxt-dev",
"scripts": {
"check": "pnpm build && check",
"test": "buildc --deps-only -- vitest",
"dev": "tsx --trace-warnings dev.ts",
"build": "buildc -- unbuild",
"prepublishOnly": "pnpm build"
},
"dependencies": {},
"devDependencies": {
"@aklinker1/check": "2.0.0",
"oxlint": "^0.16.8",
"publint": "^0.3.12",
"tsx": "4.19.4",
"typescript": "^5.8.3",
"unbuild": "^3.5.0",
"vitest": "^3.1.2"
},
"main": "dist/index.mjs",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
}
},
"files": [
"dist"
]
}
@@ -0,0 +1,80 @@
import { describe, it, expect, vi } from 'vitest';
import { mock } from 'vitest-mock-extended';
import { createCdpConnection, type CDPConnection } from '../cdp';
import { createBidiConnection, type BidiConnection } from '../bidi';
import type { ChildProcess } from 'node:child_process';
import { installChromium, installFirefox } from '../install';
vi.mock('../cdp');
const createCdpConnectionMock = vi.mocked(createCdpConnection);
vi.mock('../bidi');
const createBidiConnectionMock = vi.mocked(createBidiConnection);
describe('Install', () => {
describe('Chromium', () => {
it('Should send the install command to the process', async () => {
const browserProcess = mock<ChildProcess>();
const connection = mock<CDPConnection>({
[Symbol.dispose]: vi.fn(),
});
const extensionDir = '/path/to/extension';
const expectedExtensionId = 'chromium-extension-id';
createCdpConnectionMock.mockReturnValue(connection);
connection.send.mockImplementation(async (method) => {
if (method === 'Extensions.loadUnpacked')
return { id: expectedExtensionId };
throw Error('Unknown method');
});
const res = await installChromium(browserProcess, extensionDir);
expect(createCdpConnectionMock).toBeCalledTimes(1);
expect(createCdpConnectionMock).toBeCalledWith(browserProcess);
expect(connection.send).toBeCalledTimes(1);
expect(connection.send).toBeCalledWith('Extensions.loadUnpacked', {
path: extensionDir,
});
expect(res).toEqual({ id: expectedExtensionId });
});
});
describe('Firefox', () => {
it('Should connect to the server, start a session, then install the extension', async () => {
const debuggerUrl = 'http://127.0.0.1:9222';
const extensionDir = '/path/to/extension';
const expectedExtensionId = 'firefox-extension-id';
const connection = mock<BidiConnection>({
[Symbol.dispose]: vi.fn(),
});
createBidiConnectionMock.mockResolvedValue(connection);
connection.send.mockImplementation(async (method) => {
if (method === 'session.new') return { sessionId: 'session-id' };
if (method === 'webExtension.install')
return { extension: expectedExtensionId };
});
const res = await installFirefox(debuggerUrl, extensionDir);
expect(createBidiConnectionMock).toBeCalledTimes(1);
expect(createBidiConnectionMock).toBeCalledWith(debuggerUrl);
expect(connection.send).toBeCalledTimes(2);
expect(connection.send).toBeCalledWith('session.new', {
capabilities: {},
});
expect(connection.send).toBeCalledWith('webExtension.install', {
extensionData: {
type: 'path',
path: extensionDir,
},
});
expect(res).toEqual({ extension: expectedExtensionId });
});
});
});
@@ -0,0 +1,306 @@
import { describe, it, expect, vi, beforeAll } from 'vitest';
import { ResolvedRunOptions, resolveRunOptions } from '../options';
import { resolve, join } from 'node:path';
import { tmpdir, homedir } from 'node:os';
import { mkdir } from 'node:fs/promises';
vi.mock('node:os', async () => {
const { vi } = await import('vitest');
const os: any = await vi.importActual('node:os');
const { join } = await import('node:path');
return {
...os,
tmpdir: () => join(os.tmpdir(), 'tmpdir-mock'),
homedir: () => join(os.tmpdir(), 'homedir-mock'),
};
});
describe('Options', () => {
beforeAll(async () => {
// Make sure mock test directories exist
await mkdir(tmpdir(), { recursive: true });
await mkdir(homedir(), { recursive: true });
});
describe('extensionDir', () => {
it('should default to the current working directory', async () => {
const actual = await resolveRunOptions({});
expect(actual).toMatchObject<Partial<ResolvedRunOptions>>({
extensionDir: process.cwd(),
});
});
it('should resolve relative to the current working directory', async () => {
const actual = await resolveRunOptions({
extensionDir: './path/to/extension',
});
expect(actual).toMatchObject<Partial<ResolvedRunOptions>>({
extensionDir: resolve(process.cwd(), './path/to/extension'),
});
});
});
describe('target', () => {
it('should be "chrome" by default', async () => {
const actual = await resolveRunOptions({
extensionDir: 'path/to/extension',
});
expect(actual).toMatchObject<Partial<ResolvedRunOptions>>({
target: 'chrome',
});
});
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',
});
});
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.');
});
});
describe('browserBinary', () => {
it('should denormalize the browserBinary', async () => {
const path =
process.platform === 'win32'
? 'C:/path/to/custom/browser.exe'
: '/path/to/custom/browser';
const expectedPath =
process.platform === 'win32'
? 'C:\\path\\to\\custom\\browser.exe'
: path;
const actual = await resolveRunOptions({
extensionDir: 'path/to/extension',
target: 'custom',
browserBinaries: {
custom: path,
},
});
expect(actual).toMatchObject<Partial<ResolvedRunOptions>>({
browserBinary: expectedPath,
});
});
});
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(
'Custom Chromium --user-data-dir argument ignored',
),
);
});
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(
'Custom Chromium --remote-debugging-port argument ignored',
),
);
});
it('should combine default args with user provided ones', async () => {
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',
'--disable-component-extensions-with-background-pages',
'--disable-background-networking',
'--disable-component-update',
'--disable-client-side-phishing-detection',
'--disable-sync',
'--metrics-recording-only',
'--disable-default-apps',
'--no-default-browser-check',
'--no-first-run',
'--disable-background-timer-throttling',
'--disable-ipc-flooding-protection',
'--password-store=basic',
'--use-mock-keychain',
'--force-fieldtrials=*BackgroundTracing/default/',
'--disable-hang-monitor',
'--disable-prompt-on-repost',
'--disable-domain-reliability',
'--propagate-iph-for-testing',
// Debugging
expect.stringContaining('--remote-debugging-port='),
'--remote-debugging-pipe',
expect.stringContaining('--user-data-dir='), // See dataPersistence tests
'--enable-unsafe-extension-debugging',
// User provided
'--window-size=1920,1080',
]);
});
});
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'),
);
});
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(
'Custom Firefox --remote-debugging-port argument ignored',
),
);
});
it('should combine default args with user provided ones', async () => {
const actual = await resolveRunOptions({
firefoxArgs: ['--window-size=1920,1080'],
});
expect(actual.firefoxArgs).toEqual([
// Defaults
'--new-instance',
'--no-remote',
'--profile',
expect.any(String), // See dataPersistence tests
expect.stringContaining('--remote-debugging-port='),
'about:debugging#/runtime/this-firefox',
// User provided
'--window-size=1920,1080',
]);
});
});
describe('chromiumRemoteDebuggingPort', () => {
it('should default to 0', async () => {
const actual = await resolveRunOptions({});
expect(actual).toMatchObject<Partial<ResolvedRunOptions>>({
chromiumRemoteDebuggingPort: 0,
chromiumArgs: expect.arrayContaining([`--remote-debugging-port=0`]),
});
});
it('should respect user provided port', async () => {
const actual = await resolveRunOptions({
chromiumRemoteDebuggingPort: 9222,
});
expect(actual).toMatchObject<Partial<ResolvedRunOptions>>({
chromiumRemoteDebuggingPort: 9222,
chromiumArgs: expect.arrayContaining([`--remote-debugging-port=9222`]),
});
});
});
describe('firefoxRemoteDebuggingPort', () => {
it('should default to 0', async () => {
const actual = await resolveRunOptions({});
expect(actual).toMatchObject<Partial<ResolvedRunOptions>>({
firefoxRemoteDebuggingPort: 0,
firefoxArgs: expect.arrayContaining([`--remote-debugging-port=0`]),
});
});
it('should respect user provided port', async () => {
const actual = await resolveRunOptions({
firefoxRemoteDebuggingPort: 9222,
});
expect(actual).toMatchObject<Partial<ResolvedRunOptions>>({
firefoxRemoteDebuggingPort: 9222,
firefoxArgs: expect.arrayContaining([`--remote-debugging-port=9222`]),
});
});
});
describe('dataPersistence', () => {
it('should default to "none"', async () => {
const actual = await resolveRunOptions({});
expect(actual).toMatchObject<Partial<ResolvedRunOptions>>({
dataPersistence: 'none',
});
});
it('should use a temporary directory when set to "none"', async () => {
const actual = await resolveRunOptions({
dataPersistence: 'none',
});
expect(actual).toMatchObject<Partial<ResolvedRunOptions>>({
dataPersistence: 'none',
dataDir: expect.stringContaining(join(tmpdir(), 'wxt-runner-')),
chromiumArgs: expect.arrayContaining([
expect.stringContaining(
`--user-data-dir=${join(tmpdir(), 'wxt-runner-')}`,
),
]),
firefoxArgs: expect.arrayContaining([
expect.stringContaining(join(tmpdir(), 'wxt-runner-')),
]),
});
});
it('should use a directory in the current working directory when set to "project"', async () => {
const actual = await resolveRunOptions({
dataPersistence: 'project',
});
expect(actual).toMatchObject<Partial<ResolvedRunOptions>>({
dataPersistence: 'project',
dataDir: expect.stringContaining(join(process.cwd(), '.wxt-runner')),
chromiumArgs: expect.arrayContaining([
expect.stringContaining(
`--user-data-dir=${join(process.cwd(), '.wxt-runner')}`,
),
]),
firefoxArgs: expect.arrayContaining([
expect.stringContaining(join(process.cwd(), '.wxt-runner')),
]),
});
});
it('should use a directory in the user\'s home directory when set to "user"', async () => {
const actual = await resolveRunOptions({
dataPersistence: 'user',
});
expect(actual).toMatchObject<Partial<ResolvedRunOptions>>({
dataPersistence: 'user',
dataDir: expect.stringContaining(join(homedir(), '.wxt-runner')),
chromiumArgs: expect.arrayContaining([
expect.stringContaining(
`--user-data-dir=${join(homedir(), '.wxt-runner')}`,
),
]),
firefoxArgs: expect.arrayContaining([
expect.stringContaining(join(homedir(), '.wxt-runner')),
]),
});
});
});
});
+75
View File
@@ -0,0 +1,75 @@
import { openWebSocket } from './web-socket';
import { debug } from './debug';
const debugBidi = debug.scoped('bidi');
export interface BidiConnection extends Disposable {
send<T>(method: string, params: any, timeout?: number): Promise<T>;
close(): void;
}
export async function createBidiConnection(
baseUrl: string,
): Promise<BidiConnection> {
const url = new URL('/session', baseUrl);
debugBidi('Connecting to BiDi server @', url.href);
const webSocket = await openWebSocket(url.href);
debugBidi('Connected');
let requestId = 0;
return {
send(method, params, timeout = 10e3) {
const id = ++requestId;
const command = { id, method, params };
debugBidi('Sending command:', command);
return new Promise((resolve, reject) => {
const cleanup = () => {
webSocket.removeEventListener('message', onMessage);
webSocket.removeEventListener('error', onError);
};
setTimeout(() => {
cleanup();
reject(
new Error(
`Timed out after ${timeout}ms waiting for ${method} response`,
),
);
}, timeout);
const onMessage = (event: MessageEvent) => {
const data = JSON.parse(event.data);
if (data.id === id) {
debugBidi('Received response:', data);
cleanup();
if (data.type === 'success') resolve(data.result);
else reject(Error(data.message, { cause: data }));
}
};
const onError = (error: any) => {
cleanup();
reject(new Error('Error sending request', { cause: error }));
};
webSocket.addEventListener('message', onMessage);
webSocket.addEventListener('error', onError);
webSocket.send(JSON.stringify(command));
});
},
close() {
debugBidi('Closing connection...');
webSocket.close();
debugBidi('Closed connection');
},
[Symbol.dispose]() {
debugBidi('Disposing connection...');
webSocket.close();
debugBidi('Disposed connection');
},
};
}
+120
View File
@@ -0,0 +1,120 @@
export type BrowserPlatform = 'windows' | 'mac' | 'linux';
export type KnownTarget =
| 'chromium'
| 'chrome'
| 'chrome-beta'
| 'chrome-dev'
| 'chrome-canary'
| 'edge'
| 'edge-beta'
| 'edge-dev'
| 'edge-canary'
| 'firefox'
| 'firefox-nightly'
| 'firefox-developer-edition'
| 'zen';
export const KNOWN_BROWSER_PATHS: Record<
KnownTarget,
Record<BrowserPlatform, string[]>
> = {
// Chromium based targets
chromium: {
mac: [],
linux: [
// Arch
'/usr/bin/chromium',
],
windows: [],
},
chrome: {
mac: ['/Applications/Chrome.app/Contents/MacOS/Google Chrome'],
linux: [],
windows: ['C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'],
},
'chrome-beta': {
mac: [],
linux: [],
windows: [],
},
'chrome-canary': {
mac: [
'/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
],
linux: [],
windows: [],
},
'chrome-dev': {
mac: [],
linux: [],
windows: [],
},
edge: {
mac: [],
linux: [],
windows: [],
},
'edge-beta': {
mac: [],
linux: [],
windows: [],
},
'edge-canary': {
mac: [],
linux: [],
windows: [],
},
'edge-dev': {
mac: [],
linux: [],
windows: [],
},
// Firefox based targets
firefox: {
mac: ['/Applications/Firefox.app/Contents/MacOS/firefox'],
linux: [
// Arch
'/usr/bin/firefox',
],
windows: [],
},
'firefox-nightly': {
mac: ['/Applications/Firefox Nightly.app/Contents/MacOS/firefox'],
linux: [],
windows: ['C:\\Program Files\\Firefox Nightly\\firefox.exe'],
},
'firefox-developer-edition': {
mac: ['/Applications/Firefox Developer Edition.app/Contents/MacOS/firefox'],
linux: [
// Arch
'/usr/bin/firefox-developer-edition',
],
windows: [],
},
zen: {
mac: ['/Applications/Zen Browser.app/Contents/MacOS/zen'],
linux: [],
windows: [],
},
};
/**
* When targeting a browser, this map contains the other targets to fall back on when a binary could not be found for the primary target.
*/
export const FALLBACK_TARGETS: Partial<Record<KnownTarget, KnownTarget[]>> = {
chrome: [
'chromium',
'chrome-canary',
'chrome-beta',
'chrome-dev',
'edge',
'edge-canary',
'edge-beta',
'edge-dev',
],
firefox: ['firefox-developer-edition', 'firefox-nightly', 'zen'],
};
+57
View File
@@ -0,0 +1,57 @@
import type { ChildProcess } from 'node:child_process';
import type { Readable, Writable } from 'node:stream';
import { debug } from './debug';
const debugCdp = debug.scoped('cdp');
export interface CDPConnection extends Disposable {
send<T>(method: string, params: any, timeout?: number): Promise<T>;
close(): void;
}
export function createCdpConnection(
browserProcess: ChildProcess,
): CDPConnection {
const inputStream = browserProcess.stdio[3] as Writable;
const outputStream = browserProcess.stdio[4] as Readable;
let requestId = 0;
return {
send(method, params, timeout = 10e3) {
const id = ++requestId;
const command = { id, method, params };
debugCdp('Sending command:', command);
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error(`CDP command timed out: ${method}`));
}, timeout);
const onData = (data: Buffer) => {
// Trim the trailing null character
const text = data.toString().slice(0, -1);
const res = JSON.parse(text);
if (res.id !== id) return;
debugCdp('Received response:', res);
clearTimeout(timer);
outputStream.removeListener('data', onData);
if ('error' in res) {
reject(new Error(res.error.message, { cause: res.error }));
} else {
resolve(res.result);
}
};
outputStream.addListener('data', onData);
inputStream.write(JSON.stringify(command) + '\0');
});
},
close() {},
[Symbol.dispose]() {},
};
}
+24
View File
@@ -0,0 +1,24 @@
export interface Debug {
(...args: any[]): void;
scoped: (scope: string) => Debug;
}
function createDebug(scopes: string[]): Debug {
const debug = (...args: any[]) => {
const scope = scopes.join(':');
if (
process.env.DEBUG === '1' ||
process.env.DEBUG === 'true' ||
scope.startsWith(process.env.DEBUG ?? '@NOT')
) {
const params = scope ? [`\x1b[31m${scope}\x1b[0m`, ...args] : args;
console.log(...params);
}
};
debug.scoped = (scope: string) => createDebug([...scopes, scope]);
return debug;
}
export const debug = createDebug(['@wxt-dev/runner']);
+3
View File
@@ -0,0 +1,3 @@
export * from './run';
export * from './options';
export * from './install';
+59
View File
@@ -0,0 +1,59 @@
import type { ChildProcess } from 'node:child_process';
import { createBidiConnection } from './bidi';
import { createCdpConnection } from './cdp';
/**
* Install an extension to an already running instance of Firefox.
* @param debuggerUrl The URL of the Firefox BiDi server (ex: `ws://127.0.0.1:45912`).
* @param extensionDir Absolute path to the directory containing the extension to be installed.
*/
export async function installFirefox(
debuggerUrl: string,
extensionDir: string,
): Promise<BidiWebExtensionInstallResponse> {
using bidi = await createBidiConnection(debuggerUrl);
// Start a session
await bidi.send<unknown>('session.new', { capabilities: {} });
// Install the extension
return await bidi.send<BidiWebExtensionInstallResponse>(
'webExtension.install',
{
extensionData: {
type: 'path',
path: extensionDir,
},
},
);
}
export type BidiWebExtensionInstallResponse = {
extension: string;
};
/**
* Given a child process of Chrome, install an extension. The process must be started with the following flags:
*
* - `--remote-debugging-pipe`
* - `--user-data-dir=...`
* - `--enable-unsafe-extension-debugging`
*
* Otherwise it the CDP doesn't have permission to install extensions.
*/
export async function installChromium(
browserProcess: ChildProcess,
extensionDir: string,
): Promise<CdpExtensionsLoadUnpackedResponse> {
using cdp = createCdpConnection(browserProcess);
return await cdp.send<CdpExtensionsLoadUnpackedResponse>(
'Extensions.loadUnpacked',
{
path: extensionDir,
},
);
}
export type CdpExtensionsLoadUnpackedResponse = {
id: string;
};
+286
View File
@@ -0,0 +1,286 @@
import {
FALLBACK_TARGETS,
KNOWN_BROWSER_PATHS,
KnownTarget,
type BrowserPlatform,
} from './browser-paths';
import { resolve, join } from 'node:path';
import { homedir, tmpdir } from 'node:os';
import { debug } from './debug';
import { mkdtemp, open } from 'node:fs/promises';
const debugOptions = debug.scoped('options');
export type UnknownTarget = string & {};
export type Target = KnownTarget | UnknownTarget;
export type RunOptions = {
/** Paths to binaries to use for each target. */
browserBinaries?: Record<string, string>;
/** Customize the arguments passed to the chromium binary. Conflicting arguments with required ones to install extensions are ignored. */
chromiumArgs?: string[];
/** Control how data is persisted between launches. Either save data at a user level, project level, or don't persist data at all. Defaults to `project`. */
dataPersistence?: 'user' | 'project' | 'none';
/** Customize where your profile's data is stored when using `dataPersistence: 'project'`. Can be absolute or relative to the current working directory. */
projectDataDir?: string;
/** Customize the port Chrome's debugger is listening on. Defaults to a random open port. */
chromiumRemoteDebuggingPort?: number;
/** Directory where the extension will be installed from. Should contain a `manifest.json` file. Can be relative to the current working directory. Defaults to the current working directory. */
extensionDir?: string;
/** Customize the arguments passed to the firefox binary. Conflicting arguments with required ones to install extensions are ignored. */
firefoxArgs?: string[];
/** Customize the port Firefox's debugger is listening on. Defaults to a random open port. */
firefoxRemoteDebuggingPort?: number;
/** Specify the browser to open. Defaults to `"chrome"`, but you can pass any string. */
target?: Target;
};
export type ResolvedRunOptions = {
/** Absolute path to the browser binary. */
browserBinary: string;
chromiumArgs: string[];
chromiumRemoteDebuggingPort: number;
/** Absolute path to the directory where browser data will be stored. */
dataDir: string;
dataPersistence: 'user' | 'project' | 'none';
/** Absolute path to the extension directory. */
extensionDir: string;
firefoxArgs: string[];
firefoxRemoteDebuggingPort: number;
target: string;
};
export async function resolveRunOptions(
options: RunOptions | undefined,
): Promise<ResolvedRunOptions> {
debugOptions('User options:', options);
const target = options?.target || 'chrome';
const _browserBinary =
options?.browserBinaries?.[target] ?? (await findBrowserBinary(target));
if (!_browserBinary)
throw Error(
`Could not find "${target}" binary.\n\nIf it is installed in a custom location, you can specify the path with the browserPaths option.`,
);
// Denormalize the path so it uses the correct path separator for the OS
const browserBinary = resolve(_browserBinary);
const chromiumRemoteDebuggingPort = options?.chromiumRemoteDebuggingPort ?? 0;
const firefoxRemoteDebuggingPort = options?.firefoxRemoteDebuggingPort ?? 0;
const dataPersistence = options?.dataPersistence ?? 'none';
const dataDir =
dataPersistence === 'user'
? join(homedir(), '.wxt-runner', target)
: dataPersistence === 'project'
? options?.projectDataDir
? resolve(options.projectDataDir)
: resolve('.wxt-runner', target)
: dataPersistence === 'none'
? await mkdtemp(join(tmpdir(), 'wxt-runner-'))
: resolve(dataPersistence);
const resolved: ResolvedRunOptions = {
browserBinary,
chromiumArgs: resolveChromiumArgs(
options?.chromiumArgs,
chromiumRemoteDebuggingPort,
dataDir,
),
dataDir,
dataPersistence,
chromiumRemoteDebuggingPort,
extensionDir: resolve(options?.extensionDir ?? '.'),
firefoxArgs: resolveFirefoxArgs(
options?.firefoxArgs,
firefoxRemoteDebuggingPort,
dataDir,
),
firefoxRemoteDebuggingPort,
target,
};
debugOptions('Resolved options:', resolved);
return resolved;
}
async function findBrowserBinary(target: string): Promise<string | undefined> {
const targets = new Set<KnownTarget>([target as KnownTarget]);
FALLBACK_TARGETS[target as KnownTarget]?.forEach((fallback) =>
targets.add(fallback),
);
const platform = getPlatform();
for (const target of targets) {
const potentialPaths = KNOWN_BROWSER_PATHS[target]?.[platform] ?? [];
for (const path of potentialPaths) {
if (await exists(path)) return path;
}
}
}
function getPlatform(): BrowserPlatform {
switch (process.platform) {
case 'win32':
return 'windows';
case 'darwin':
return 'mac';
default:
return 'linux';
}
}
function resolveChromiumArgs(
userArgs: string[] | undefined,
chromiumRemoteDebuggingPort: ResolvedRunOptions['chromiumRemoteDebuggingPort'],
dataDir: string,
): string[] {
return deduplicateArgs(
[
// Limit features to improve performance
...CHROME_LAUNCHER_DEFAULT_FLAGS,
// Enable debugging
`--remote-debugging-port=${chromiumRemoteDebuggingPort}`,
// Required for installing extensions
`--remote-debugging-pipe`,
`--user-data-dir=${dataDir}`,
`--enable-unsafe-extension-debugging`,
],
userArgs,
{
'--remote-debugging-port':
'\x1b[1m\x1b[33mCustom Chromium --remote-debugging-port argument ignored.\x1b[0m Use \x1b[36mchromiumRemoteDebuggingPort\x1b[0m option instead.',
'--user-data-dir':
'\x1b[1m\x1b[33mCustom Chromium --user-data-dir argument ignored.\x1b[0m Use \x1b[36mdataPersistence\x1b[0m option instead.',
},
);
}
function resolveFirefoxArgs(
userArgs: string[] | undefined,
firefoxRemoteDebuggingPort: ResolvedRunOptions['firefoxRemoteDebuggingPort'],
dataDir: string,
): string[] {
return deduplicateArgs(
[
// Allows opening multiple instances of Firefox at the same time
`--new-instance`,
`--no-remote`,
`--profile`,
dataDir,
// Required for installing extensions
`--remote-debugging-port=${firefoxRemoteDebuggingPort}`,
// Default URL to start with
`about:debugging#/runtime/this-firefox`,
],
userArgs,
{
'--remote-debugging-port':
'\x1b[1m\x1b[33mCustom Firefox --remote-debugging-port argument ignored.\x1b[0m Use \x1b[36mfirefoxDebuggerPort\x1b[0m option instead.',
'--profile':
'\x1b[1m\x1b[33mCustom Firefox --profile argument ignored.\x1b[0m Use \x1b[36mdataPersistence\x1b[0m option instead.',
},
);
}
function deduplicateArgs(
requiredArgs: string[],
userArgs: string[] | undefined,
warnings: Record<string, string>,
): string[] {
const getKey = (arg: string) => {
return arg.startsWith('--') ? arg.split('=')[0] : arg;
};
const alreadyAdded = new Set<string>(requiredArgs.map(getKey));
const args = [...requiredArgs];
userArgs?.forEach((arg) => {
const key = getKey(arg);
if (alreadyAdded.has(key)) {
if (warnings[key]) console.warn(`[@wxt-dev/runner] ${warnings[key]}`);
} else {
alreadyAdded.add(key);
args.push(arg);
}
});
return args;
}
async function exists(path: string): Promise<boolean> {
try {
await open(path, 'r');
return true;
} catch (err) {
// @ts-expect-error: Unknown error type
if (err?.code === 'ENOENT') return false;
throw err;
}
}
/**
* Copied from https://github.com/GoogleChrome/chrome-launcher/blob/main/src/flags.ts
* with some flags commented out. Run tests after updating to compare.
*/
const CHROME_LAUNCHER_DEFAULT_FLAGS = [
'--disable-features=' +
[
// Disable built-in Google Translate service
'Translate',
// Disable the Chrome Optimization Guide background networking
'OptimizationHints',
// Disable the Chrome Media Router (cast target discovery) background networking
'MediaRouter',
/// Avoid the startup dialog for _Do you want the application “Chromium.app” to accept incoming network connections?_. This is a sub-component of the MediaRouter.
'DialMediaRouteProvider',
// Disable the feature of: Calculate window occlusion on Windows will be used in the future to throttle and potentially unload foreground tabs in occluded windows.
'CalculateNativeWinOcclusion',
// Disables the Discover feed on NTP
'InterestFeedContentSuggestions',
// Don't update the CT lists
'CertificateTransparencyComponentUpdater',
// Disables autofill server communication. This feature isn't disabled via other 'parent' flags.
'AutofillServerCommunication',
// Disables "Enhanced ad privacy in Chrome" dialog (though as of 2024-03-20 it shouldn't show up if the profile has no stored country).
'PrivacySandboxSettings4',
].join(','),
// Disable some extensions that aren't affected by --disable-extensions
'--disable-component-extensions-with-background-pages',
// Disable various background network services, including extension updating,
// safe browsing service, upgrade detector, translate, UMA
'--disable-background-networking',
// Don't update the browser 'components' listed at chrome://components/
'--disable-component-update',
// Disables client-side phishing detection.
'--disable-client-side-phishing-detection',
// Disable syncing to a Google account
'--disable-sync',
// Disable reporting to UMA, but allows for collection
'--metrics-recording-only',
// Disable installation of default apps on first run
'--disable-default-apps',
// Disable the default browser check, do not prompt to set it as such
'--no-default-browser-check',
// Skip first run wizards
'--no-first-run',
// Disable task throttling of timer tasks from background pages.
'--disable-background-timer-throttling',
// Disable the default throttling of IPC between renderer & browser processes.
'--disable-ipc-flooding-protection',
// Avoid potential instability of using Gnome Keyring or KDE wallet. crbug.com/571003 crbug.com/991424
'--password-store=basic',
// Use mock keychain on Mac to prevent blocking permissions dialogs
'--use-mock-keychain',
// Disable background tracing (aka slow reports & deep reports) to avoid 'Tracing already started'
'--force-fieldtrials=*BackgroundTracing/default/',
// Suppresses hang monitor dialogs in renderer processes. This flag may allow slow unload handlers on a page to prevent the tab from closing.
'--disable-hang-monitor',
// Reloading a page that came from a POST normally prompts the user.
'--disable-prompt-on-repost',
// Disables Domain Reliability Monitoring, which tracks whether the browser has difficulty contacting Google-owned sites and uploads reports to Google.
'--disable-domain-reliability',
// Disable the in-product Help (IPH) system.
'--propagate-iph-for-testing',
];
+19
View File
@@ -0,0 +1,19 @@
export function promiseWithResolvers<T>(): {
promise: Promise<T>;
resolve: (value: T) => void;
reject: (error: unknown) => void;
} {
let resolve: (value: T) => void;
let reject: (error: unknown) => void;
const promise = new Promise<T>((res, rej) => {
resolve = res;
reject = rej;
});
return {
promise,
resolve: resolve!,
reject: reject!,
};
}
+125
View File
@@ -0,0 +1,125 @@
import { debug } from './debug';
import {
resolveRunOptions,
type ResolvedRunOptions,
type RunOptions,
} from './options';
import { spawn } from 'node:child_process';
import { installChromium, installFirefox } from './install';
import { promiseWithResolvers } from './promises';
const debugFirefox = debug.scoped('firefox');
const debugChrome = debug.scoped('chrome');
export interface Runner {
stop(): void;
}
export async function run(options: RunOptions): Promise<Runner> {
const resolvedOptions = await resolveRunOptions(options);
if (
resolvedOptions.target.includes('firefox') ||
resolvedOptions.target.includes('zen')
) {
return runFirefox(resolvedOptions);
} else {
return runChromium(resolvedOptions);
}
}
async function runFirefox(options: ResolvedRunOptions): Promise<Runner> {
const urlRes = promiseWithResolvers<string>();
const urlTimeout = setTimeout(() => {
urlRes.reject(Error('Timed out after 10s waiting for the browser to open'));
}, 10e3);
// Firefox notifies the user if an instance is already running, so we don't add any logs for it.
const browserProcess = spawn(
`"${options.browserBinary}"`,
options.firefoxArgs,
{
stdio: ['ignore', 'pipe', 'pipe'],
shell: true,
},
);
const debugFirefoxStderr = debugFirefox.scoped('stderr');
browserProcess.stderr.on('data', (data: string) => {
const message = data.toString().trim();
debugFirefoxStderr(message);
if (message.startsWith('WebDriver BiDi listening on ws://')) {
clearTimeout(urlTimeout);
urlRes.resolve(message.slice(28));
}
});
const debugFirefoxStdout = debugFirefox.scoped('stdout');
browserProcess.stdout.on('data', (data: string) => {
const message = data.toString().trim();
debugFirefoxStdout(message);
});
const baseUrl = await urlRes.promise;
await installFirefox(baseUrl, options.extensionDir);
return {
stop() {
browserProcess.kill('SIGINT');
},
};
}
async function runChromium(options: ResolvedRunOptions): Promise<Runner> {
const browserProcess = spawn(
`"${options.browserBinary}"`,
options.chromiumArgs,
{
stdio: ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'],
shell: true,
},
);
const opened = promiseWithResolvers<void>();
const openedTimeout = setTimeout(() => {
opened.reject(Error('Timed out after 10s waiting for browser to open.'));
}, 10e3);
const debugChromeStderr = debugChrome.scoped('stderr');
browserProcess.stderr!.on('data', (data: string) => {
const message = data.toString().trim();
debugChromeStderr(message);
// This message signifies Chrome started up correctly.
if (message.startsWith('DevTools listening on')) {
clearTimeout(openedTimeout);
opened.resolve();
}
});
const debugChromeStdout = debugChrome.scoped('stdout');
browserProcess.stdout!.on('data', (data: string) => {
const message = data.toString().trim();
debugChromeStdout(message);
// This message signifies Chrome was already open, and thus we couldn't open the required new instance.
if (message === 'Opening in existing browser session.') {
clearTimeout(openedTimeout);
opened.reject(
Error(
'An instance of the browser is already running. Close it and try again.',
),
);
}
});
// Wait for the browser to open before proceeding.
await opened.promise;
await installChromium(browserProcess, options.extensionDir);
return {
stop() {
browserProcess.kill('SIGINT');
},
};
}
+37
View File
@@ -0,0 +1,37 @@
export function openWebSocket(url: string): Promise<WebSocket> {
if (typeof WebSocket === 'undefined') {
throw new Error(
'To open Firefox, your JS runtime must support the standard WebSocket API (NodeJS >=22.4.0, Bun, etc).',
);
}
return new Promise<WebSocket>((resolve, reject) => {
const webSocket = new WebSocket(url);
const cleanup = () => {
webSocket.removeEventListener('open', onOpen);
webSocket.removeEventListener('error', onError);
webSocket.removeEventListener('close', onClose);
};
const onOpen = async () => {
cleanup();
resolve(webSocket);
};
const onClose = (event: CloseEvent) => {
cleanup();
reject(
new Error(
`Connection closed: code=${event.code}, reason=${event.reason}`,
),
);
};
const onError = (error: any) => {
cleanup();
reject(new Error('Error connecting to WebSocket', { cause: error }));
};
webSocket.addEventListener('open', onOpen);
webSocket.addEventListener('error', onError);
webSocket.addEventListener('close', onClose);
});
}
+4
View File
@@ -0,0 +1,4 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {}
}
+8
View File
@@ -0,0 +1,8 @@
import { defineProject } from 'vitest/config';
export default defineProject({
test: {
mockReset: true,
restoreMocks: true,
},
});
+10 -10
View File
@@ -30,18 +30,18 @@
"test": "buildc --deps-only -- vitest"
},
"dependencies": {
"async-mutex": "catalog:",
"dequal": "catalog:"
"@wxt-dev/browser": "workspace:^",
"async-mutex": "^0.5.0",
"dequal": "^2.0.3"
},
"devDependencies": {
"@aklinker1/check": "catalog:",
"@types/chrome": "catalog:",
"@webext-core/fake-browser": "catalog:",
"oxlint": "catalog:",
"publint": "catalog:",
"typescript": "catalog:",
"unbuild": "catalog:",
"vitest": "catalog:"
"@aklinker1/check": "2.0.0",
"@webext-core/fake-browser": "^1.3.2",
"oxlint": "^0.16.8",
"publint": "^0.3.12",
"typescript": "^5.8.3",
"unbuild": "^3.5.0",
"vitest": "^3.1.2"
},
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
+22 -3
View File
@@ -1,6 +1,7 @@
import { fakeBrowser } from '@webext-core/fake-browser';
import { describe, it, expect, beforeEach, vi, expectTypeOf } from 'vitest';
import { MigrationError, type WxtStorageItem, storage } from '../index';
import { browser } from '@wxt-dev/browser';
/**
* This works because fakeBrowser is synchronous, and is will finish any number of chained
@@ -222,7 +223,7 @@ describe('Storage Utils', () => {
describe('setMeta', () => {
it('should set metadata at key+$', async () => {
const existing = { v: 1 };
await chrome.storage[storageArea].set({ count$: existing });
await browser.storage[storageArea].set({ count$: existing });
const newValues = {
date: Date.now(),
};
@@ -238,7 +239,7 @@ describe('Storage Utils', () => {
'should remove any properties set to %s',
async (version) => {
const existing = { v: 1 };
await chrome.storage[storageArea].set({ count$: existing });
await browser.storage[storageArea].set({ count$: existing });
const expected = {};
await storage.setMeta(`${storageArea}:count`, { v: version });
@@ -1265,7 +1266,7 @@ describe('Storage Utils', () => {
await item.removeValue();
// Make sure it's actually blank before running the test
expect(await chrome.storage.local.get()).toEqual({});
expect(await browser.storage.local.get()).toEqual({});
init.mockClear();
const [value1, value2] = await Promise.all([
@@ -1281,6 +1282,19 @@ describe('Storage Utils', () => {
it('should define a nullable value when options are not passed', () => {
const item = storage.defineItem<number>(`local:test`);
expectTypeOf(item).toEqualTypeOf<WxtStorageItem<number | null, {}>>();
const item2 = storage.defineItem<number>(`local:test`, {});
expectTypeOf(item2).toEqualTypeOf<WxtStorageItem<number | null, {}>>();
const item3 = storage.defineItem<number>(`local:test`, {
fallback: undefined,
});
expectTypeOf(item3).toEqualTypeOf<WxtStorageItem<number | null, {}>>();
const item4 = storage.defineItem<number>(`local:test`, {
defaultValue: undefined,
});
expectTypeOf(item4).toEqualTypeOf<WxtStorageItem<number | null, {}>>();
});
it('should define a non-null value when options are passed with a nullish default value', () => {
@@ -1288,6 +1302,11 @@ describe('Storage Utils', () => {
defaultValue: 123,
});
expectTypeOf(item).toEqualTypeOf<WxtStorageItem<number, {}>>();
const item2 = storage.defineItem(`local:test`, {
fallback: 123,
});
expectTypeOf(item2).toEqualTypeOf<WxtStorageItem<number, {}>>();
});
it('should define a nullable value when options are passed with null default value', () => {
+11 -10
View File
@@ -1,4 +1,3 @@
/// <reference types="chrome" />
/**
* Simplified storage APIs with support for versioned fields, snapshots, metadata, and item definitions.
*
@@ -7,13 +6,7 @@
*/
import { dequal } from 'dequal/lite';
import { Mutex } from 'async-mutex';
const browser: typeof chrome =
// @ts-expect-error
globalThis.browser?.runtime?.id == null
? globalThis.chrome
: // @ts-expect-error
globalThis.browser;
import { browser, type Browser } from '@wxt-dev/browser';
export const storage = createStorage();
@@ -750,8 +743,16 @@ export interface WxtStorage {
): WxtStorageItem<TValue | null, TMetadata>;
defineItem<TValue, TMetadata extends Record<string, unknown> = {}>(
key: StorageItemKey,
options: WxtStorageItemOptions<TValue>,
options: WxtStorageItemOptions<TValue> & { fallback: TValue },
): WxtStorageItem<TValue, TMetadata>;
defineItem<TValue, TMetadata extends Record<string, unknown> = {}>(
key: StorageItemKey,
options: WxtStorageItemOptions<TValue> & { defaultValue: TValue },
): WxtStorageItem<TValue, TMetadata>;
defineItem<TValue, TMetadata extends Record<string, unknown> = {}>(
key: StorageItemKey,
options: WxtStorageItemOptions<TValue>,
): WxtStorageItem<TValue | null, TMetadata>;
}
interface WxtStorageDriver {
@@ -889,7 +890,7 @@ export interface WxtStorageItemOptions<T> {
}
export type StorageAreaChanges = {
[key: string]: chrome.storage.StorageChange;
[key: string]: Browser.storage.StorageChange;
};
/**
+1 -2
View File
@@ -1,8 +1,7 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"verbatimModuleSyntax": true,
"types": ["chrome"]
"verbatimModuleSyntax": true
},
"exclude": ["node_modules/**", "dist/**"]
}
+8 -8
View File
@@ -40,16 +40,16 @@
"wxt": ">=0.19.0"
},
"devDependencies": {
"@aklinker1/check": "catalog:",
"oxlint": "catalog:",
"publint": "catalog:",
"typescript": "catalog:",
"unbuild": "catalog:",
"unocss": "catalog:",
"@aklinker1/check": "2.0.0",
"oxlint": "^0.16.8",
"publint": "^0.3.12",
"typescript": "^5.8.3",
"unbuild": "^3.5.0",
"unocss": "^0.64.0 || ^0.65.0 || ^65.0.0 || ^66.0.0",
"wxt": "workspace:*"
},
"dependencies": {
"defu": "catalog:",
"fast-glob": "catalog:"
"defu": "^6.1.4",
"fast-glob": "^3.3.3"
}
}
+6 -6
View File
@@ -45,12 +45,12 @@
"wxt": ">=0.20.0"
},
"devDependencies": {
"@aklinker1/check": "catalog:",
"@types/webextension-polyfill": "catalog:",
"publint": "catalog:",
"typescript": "catalog:",
"unbuild": "catalog:",
"webextension-polyfill": "catalog:",
"@aklinker1/check": "2.0.0",
"@types/webextension-polyfill": "^0.12.3",
"publint": "^0.3.12",
"typescript": "^5.8.3",
"unbuild": "^3.5.0",
"webextension-polyfill": "^0.12.0",
"wxt": "workspace:*"
}
}
+9 -9
View File
@@ -18,19 +18,19 @@
},
"dependencies": {
"@wxt-dev/i18n": "workspace:*",
"react": "catalog:",
"react-dom": "catalog:"
"react": "^19.1.0",
"react-dom": "^19.1.0"
},
"devDependencies": {
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.3",
"@wxt-dev/auto-icons": "workspace:*",
"@wxt-dev/unocss": "workspace:*",
"sass": "catalog:",
"typescript": "catalog:",
"unocss": "catalog:",
"vitest": "catalog:",
"vitest-plugin-random-seed": "catalog:",
"sass": "^1.87.0",
"typescript": "^5.8.3",
"unocss": "^0.64.0 || ^0.65.0 || ^65.0.0 || ^66.0.0",
"vitest": "^3.1.2",
"vitest-plugin-random-seed": "^1.1.1",
"wxt": "workspace:*"
},
"buildc": {
+58
View File
@@ -1,5 +1,63 @@
# Changelog
## v0.20.6
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.5...wxt-v0.20.6)
## v0.20.5
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.4...wxt-v0.20.5)
### 🩹 Fixes
- Don't use crypto.randUUID for shadow root UIs ([3577c0b](https://github.com/wxt-dev/wxt/commit/3577c0b))
### ❤️ Contributors
- Aaron ([@aklinker1](https://github.com/aklinker1))
## v0.20.4
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.3...wxt-v0.20.4)
### 🚀 Enhancements
- Ignore popup/index.ts instead of erroring ([#1520](https://github.com/wxt-dev/wxt/pull/1520))
- Ignore elements with a `vite-ignore` or `wxt-ignore` attribute ([#1603](https://github.com/wxt-dev/wxt/pull/1603))
- Add `{{packageVersion}}` as template variable ([#1604](https://github.com/wxt-dev/wxt/pull/1604))
### 🩹 Fixes
- Adding missing `""` to `PublicPath` and `browser.runtime.getUrl` ([#1597](https://github.com/wxt-dev/wxt/pull/1597))
- Fix CORS error in Firefox ([#1607](https://github.com/wxt-dev/wxt/pull/1607))
### 📖 Documentation
- **Content Script UI:** Add additional details about when `onRemove` is called ([656a9b3](https://github.com/wxt-dev/wxt/commit/656a9b3))
### ❤️ Contributors
- Yunsup Sim ([@SimYunSup](https://github.com/SimYunSup))
- ТΞNSΛI ([@Tensai75](https://github.com/Tensai75))
- Nishu ([@nishu-murmu](https://github.com/nishu-murmu))
- Aaron ([@aklinker1](https://github.com/aklinker1))
## v0.20.3
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.2...wxt-v0.20.3)
### 🚀 Enhancements
- Automatically place document-level CSS outside shadow root ([#1594](https://github.com/wxt-dev/wxt/pull/1594))
### 🩹 Fixes
- Fix double hashing of inline script keys ([b0f4ac8](https://github.com/wxt-dev/wxt/commit/b0f4ac8))
### ❤️ Contributors
- Aaron ([@aklinker1](https://github.com/aklinker1))
## v0.20.2
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.1...wxt-v0.20.2)
@@ -48,6 +48,7 @@ describe('TypeScript Project', () => {
declare module "wxt/browser" {
export type PublicPath =
| ""
| "/"
| "/options.html"
| "/popup.html"
+65 -61
View File
@@ -1,7 +1,7 @@
{
"name": "wxt",
"type": "module",
"version": "0.20.2",
"version": "0.20.6",
"description": "⚡ Next-gen Web Extension Framework",
"license": "MIT",
"scripts": {
@@ -16,69 +16,69 @@
"prepack": "pnpm build"
},
"dependencies": {
"@1natsu/wait-element": "catalog:",
"@aklinker1/rollup-plugin-visualizer": "catalog:",
"@webext-core/fake-browser": "catalog:",
"@webext-core/isolated-element": "catalog:",
"@webext-core/match-patterns": "catalog:",
"@wxt-dev/browser": "workspace:*",
"@1natsu/wait-element": "^4.1.2",
"@aklinker1/rollup-plugin-visualizer": "5.12.0",
"@webext-core/fake-browser": "^1.3.2",
"@webext-core/isolated-element": "^1.1.2",
"@webext-core/match-patterns": "^1.0.3",
"@wxt-dev/browser": "workspace:^",
"@wxt-dev/storage": "workspace:^1.0.0",
"async-mutex": "catalog:",
"c12": "catalog:",
"cac": "catalog:",
"chokidar": "catalog:",
"ci-info": "catalog:",
"consola": "catalog:",
"defu": "catalog:",
"dotenv": "catalog:",
"dotenv-expand": "catalog:",
"esbuild": "catalog:",
"fast-glob": "catalog:",
"filesize": "catalog:",
"fs-extra": "catalog:",
"get-port-please": "catalog:",
"giget": "catalog:",
"hookable": "catalog:",
"import-meta-resolve": "catalog:",
"is-wsl": "catalog:",
"json5": "catalog:",
"jszip": "catalog:",
"linkedom": "catalog:",
"magicast": "catalog:",
"minimatch": "catalog:",
"nano-spawn": "catalog:",
"normalize-path": "catalog:",
"nypm": "catalog:",
"ohash": "catalog:",
"open": "catalog:",
"ora": "catalog:",
"perfect-debounce": "catalog:",
"picocolors": "catalog:",
"prompts": "catalog:",
"publish-browser-extension": "catalog:",
"scule": "catalog:",
"unimport": "catalog:",
"vite": "catalog:",
"vite-node": "catalog:",
"web-ext-run": "catalog:"
"async-mutex": "^0.5.0",
"c12": "^3.0.3",
"cac": "^6.7.14",
"chokidar": "^4.0.3",
"ci-info": "^4.2.0",
"consola": "^3.4.2",
"defu": "^6.1.4",
"dotenv": "^16.5.0",
"dotenv-expand": "^12.0.2",
"esbuild": "^0.25.0",
"fast-glob": "^3.3.3",
"filesize": "^10.1.6",
"fs-extra": "^11.3.0",
"get-port-please": "^3.1.2",
"giget": "^1.2.3 || ^2.0.0",
"hookable": "^5.5.3",
"import-meta-resolve": "^4.1.0",
"is-wsl": "^3.1.0",
"json5": "^2.2.3",
"jszip": "^3.10.1",
"linkedom": "^0.18.10",
"magicast": "^0.3.5",
"minimatch": "^10.0.1",
"nano-spawn": "^0.2.0",
"normalize-path": "^3.0.0",
"nypm": "^0.6.0",
"ohash": "^2.0.11",
"open": "^10.1.2",
"ora": "^8.2.0",
"perfect-debounce": "^1.0.0",
"picocolors": "^1.1.1",
"prompts": "^2.4.2",
"publish-browser-extension": "^2.3.0 || ^3.0.0",
"scule": "^1.3.0",
"unimport": "^3.13.1 || ^4.0.0 || ^5.0.0",
"vite": "^5.4.19 || ^6.3.4",
"vite-node": "^2.1.4 || ^3.1.2",
"web-ext-run": "^0.2.3"
},
"devDependencies": {
"@aklinker1/check": "catalog:",
"@faker-js/faker": "catalog:",
"@types/fs-extra": "catalog:",
"@types/lodash.merge": "catalog:",
"@types/node": "catalog:",
"@types/normalize-path": "catalog:",
"@types/prompts": "catalog:",
"extract-zip": "catalog:",
"happy-dom": "catalog:",
"lodash.merge": "catalog:",
"oxlint": "catalog:",
"publint": "catalog:",
"typescript": "catalog:",
"unbuild": "catalog:",
"vitest": "catalog:",
"vitest-plugin-random-seed": "catalog:"
"@aklinker1/check": "2.0.0",
"@faker-js/faker": "^9.7.0",
"@types/fs-extra": "^11.0.4",
"@types/lodash.merge": "^4.6.9",
"@types/node": "^20.17.6",
"@types/normalize-path": "^3.0.2",
"@types/prompts": "^2.4.9",
"extract-zip": "^2.0.1",
"happy-dom": "^17.4.6",
"lodash.merge": "^4.6.2",
"oxlint": "^0.16.8",
"publint": "^0.3.12",
"typescript": "^5.8.3",
"unbuild": "^3.5.0",
"vitest": "^3.1.2",
"vitest-plugin-random-seed": "^1.1.1"
},
"peerDependenciesMeta": {},
"repository": {
@@ -167,6 +167,10 @@
"types": "./dist/utils/match-patterns.d.ts",
"default": "./dist/utils/match-patterns.mjs"
},
"./utils/split-shadow-root-css": {
"types": "./dist/utils/split-shadow-root-css.d.ts",
"default": "./dist/utils/split-shadow-root-css.mjs"
},
"./utils/storage": {
"types": "./dist/utils/storage.d.ts",
"default": "./dist/utils/storage.mjs"
@@ -85,13 +85,13 @@ export function devHtmlPrerender(
inlineScripts.forEach((script) => {
// Save the text content for later
const textContent = script.textContent ?? '';
const textHash = hash(textContent);
inlineScriptContents[textHash] = textContent;
const key = hash(textContent);
inlineScriptContents[key] = textContent;
// Replace unsafe inline script
const virtualScript = document.createElement('script');
virtualScript.type = 'module';
virtualScript.src = `${server.origin}/@id/${virtualInlineScript}?${textHash}`;
virtualScript.src = `${server.origin}/@id/${virtualInlineScript}?${key}`;
script.replaceWith(virtualScript);
});
@@ -111,7 +111,7 @@ export function devHtmlPrerender(
},
},
{
name: 'wxt:virtualize-react-refresh',
name: 'wxt:virtualize-inline-scripts',
apply: 'serve',
resolveId(id) {
// Resolve inline scripts
@@ -127,9 +127,9 @@ export function devHtmlPrerender(
load(id) {
// Resolve virtualized inline scripts
if (id.startsWith(resolvedVirtualInlineScript)) {
// id="virtual:wxt-inline-script?<hash>"
const newHash = hash(id.substring(id.indexOf('?')));
return inlineScriptContents[newHash];
// id="virtual:wxt-inline-script?<key>"
const key = id.substring(id.indexOf('?') + 1);
return inlineScriptContents[key];
}
// Ignore chunks during HTML file pre-rendering
@@ -150,6 +150,13 @@ export function pointToDevServer(
attr: string,
) {
document.querySelectorAll(querySelector).forEach((element) => {
if (
element.hasAttribute('vite-ignore') ||
element.hasAttribute('wxt-ignore')
) {
element.removeAttribute('wxt-ignore');
return;
}
const src = element.getAttribute(attr);
if (!src || isUrl(src)) return;
+8 -6
View File
@@ -73,16 +73,18 @@ async function getPathsDeclarationEntry(
isHtmlEntrypoint(entry) ? '.html' : '.js',
),
)
.concat([''])
.concat(await getPublicFiles());
await wxt.hooks.callHook('prepare:publicPaths', wxt, paths);
const unions = paths
.map(normalizePath)
.sort()
.map((path) => ` | "/${path}"`)
.join('\n');
const unions = [
` | ""`,
` | "/"`,
...paths
.map(normalizePath)
.sort()
.map((path) => ` | "/${path}"`),
].join('\n');
const template = `// Generated by wxt
import "wxt/browser";
@@ -1503,7 +1503,7 @@ describe('Manifest Utils', () => {
expect(actual).toMatchObject({
content_security_policy:
"script-src 'self' http://localhost:3000; object-src 'self';",
permissions: ['http://localhost:3000/*', 'tabs'],
permissions: ['http://localhost/*', 'tabs'],
});
});
@@ -1535,7 +1535,7 @@ describe('Manifest Utils', () => {
sandbox:
"script-src 'self' 'unsafe-inline' 'unsafe-eval' http://localhost:3000; sandbox allow-scripts allow-forms allow-popups allow-modals; child-src 'self';",
},
host_permissions: ['http://localhost:3000/*'],
host_permissions: ['http://localhost/*'],
permissions: ['tabs', 'scripting'],
});
});
@@ -668,27 +668,46 @@ describe('findEntrypoints', () => {
expect(entrypoints[0]).toEqual(expected);
});
it('should not allow multiple entrypoints with the same name', async () => {
it('should ignore other index files in the same directory when index.html exists', async () => {
globMock.mockResolvedValueOnce([
'content/index.ts',
'popup/index.html',
'popup/index.ts',
'popup/index.css',
]);
const entrypoints = await findEntrypoints();
expect(entrypoints).toHaveLength(2);
expect(entrypoints[0]).toMatchObject({
type: 'content-script',
name: 'content',
});
expect(entrypoints[1]).toMatchObject({
type: 'popup',
name: 'popup',
});
});
it('should not allow a file entrypoint and directory entrypoint to have the same name', async () => {
globMock.mockResolvedValueOnce([
'options/index.html',
'options/index.jsx',
'popup.html',
'popup/index.html',
'popup/index.ts',
'ui.html',
'other.ts',
'other/index.ts',
]);
await expect(() => findEntrypoints()).rejects.toThrowError(
[
'Multiple entrypoints with the same name detected, only one entrypoint for each name is allowed.',
'',
'- options',
` - ${unnormalizePath('src/entrypoints/options/index.html')}`,
` - ${unnormalizePath('src/entrypoints/options/index.jsx')}`,
'- other',
` - ${unnormalizePath('src/entrypoints/other.ts')}`,
` - ${unnormalizePath('src/entrypoints/other/index.ts')}`,
'- popup',
` - ${unnormalizePath('src/entrypoints/popup.html')}`,
` - ${unnormalizePath('src/entrypoints/popup/index.html')}`,
` - ${unnormalizePath('src/entrypoints/popup/index.ts')}`,
].join('\n'),
);
});
@@ -53,20 +53,34 @@ export async function findEntrypoints(): Promise<Entrypoint[]> {
relativePaths.sort();
const pathGlobs = Object.keys(PATH_GLOB_TO_TYPE_MAP);
const entrypointInfos: EntrypointInfo[] = relativePaths.reduce<
EntrypointInfo[]
>((results, relativePath) => {
const inputPath = resolve(wxt.config.entrypointsDir, relativePath);
const name = getEntrypointName(wxt.config.entrypointsDir, inputPath);
const matchingGlob = pathGlobs.find((glob) =>
minimatch(relativePath, glob),
);
if (matchingGlob) {
const type = PATH_GLOB_TO_TYPE_MAP[matchingGlob];
results.push({ name, inputPath, type });
}
return results;
}, []);
const entrypointInfos: EntrypointInfo[] = relativePaths
.reduce<EntrypointInfo[]>((results, relativePath) => {
const inputPath = resolve(wxt.config.entrypointsDir, relativePath);
const name = getEntrypointName(wxt.config.entrypointsDir, inputPath);
const matchingGlob = pathGlobs.find((glob) =>
minimatch(relativePath, glob),
);
if (matchingGlob) {
const type = PATH_GLOB_TO_TYPE_MAP[matchingGlob];
results.push({ name, inputPath, type });
}
return results;
}, [])
.filter(({ name, inputPath }, _, entrypointInfos) => {
// Remove <name>/index.* if <name>/index.html exists
if (inputPath.endsWith('.html')) return true;
const isIndexFile = /index\..+$/.test(inputPath);
if (!isIndexFile) return true;
const hasIndexHtml = entrypointInfos.some(
(entry) =>
entry.name === name && entry.inputPath.endsWith('index.html'),
);
if (hasIndexHtml) return false;
return true;
});
await wxt.hooks.callHook('entrypoints:found', wxt, entrypointInfos);
+7 -1
View File
@@ -459,7 +459,13 @@ function discoverIcons(
}
function addDevModeCsp(manifest: Browser.runtime.Manifest): void {
const permission = `${wxt.server?.origin ?? ''}/*`;
let permissonUrl = wxt.server?.origin;
if (permissonUrl) {
const permissionUrlInstance = new URL(permissonUrl);
permissionUrlInstance.port = '';
permissonUrl = permissionUrlInstance.toString();
}
const permission = `${permissonUrl}*`;
const allowedCsp = wxt.server?.origin ?? 'http://localhost:*';
if (manifest.manifest_version === 3) {
+3 -3
View File
@@ -26,11 +26,10 @@ export async function zip(config?: InlineConfig): Promise<string[]> {
wxt.logger.info('Zipping extension...');
const zipFiles: string[] = [];
const packageJson = await getPackageJson();
const projectName =
wxt.config.zip.name ??
safeFilename(
(await getPackageJson())?.name || path.basename(process.cwd()),
);
safeFilename(packageJson?.name || path.basename(process.cwd()));
const applyTemplate = (template: string): string =>
template
.replaceAll('{{name}}', projectName)
@@ -39,6 +38,7 @@ export async function zip(config?: InlineConfig): Promise<string[]> {
'{{version}}',
output.manifest.version_name ?? output.manifest.version,
)
.replaceAll('{{packageVersion}}', packageJson?.version)
.replaceAll('{{mode}}', wxt.config.mode)
.replaceAll('{{manifestVersion}}', `mv${wxt.config.manifestVersion}`);
+2
View File
@@ -147,6 +147,7 @@ export interface InlineConfig {
*
* - <span v-pre>`{{name}}`</span> - The project's name converted to kebab-case
* - <span v-pre>`{{version}}`</span> - The version_name or version from the manifest
* - <span v-pre>`{{packageVersion}}`</span> - The version from the package.json
* - <span v-pre>`{{browser}}`</span> - The target browser from the `--browser` CLI flag
* - <span v-pre>`{{mode}}`</span> - The current mode
* - <span v-pre>`{{manifestVersion}}`</span> - Either "2" or "3"
@@ -171,6 +172,7 @@ export interface InlineConfig {
*
* - <span v-pre>`{{name}}`</span> - The project's name converted to kebab-case
* - <span v-pre>`{{version}}`</span> - The version_name or version from the manifest
* - <span v-pre>`{{packageVersion}}`</span> - The version from the package.json
* - <span v-pre>`{{browser}}`</span> - The target browser from the `--browser` CLI flag
* - <span v-pre>`{{mode}}`</span> - The current mode
* - <span v-pre>`{{manifestVersion}}`</span> - Either "2" or "3"
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -5,6 +5,7 @@ import type { ContentScriptUi, ContentScriptUiOptions } from './types';
import { createIsolatedElement } from '@webext-core/isolated-element';
import { applyPosition, createMountFunctions, mountUi } from './shared';
import { logger } from '../internal/logger';
import { splitShadowRootCss } from '../split-shadow-root-css';
/**
* Create a content script UI inside a [`ShadowRoot`](https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot).
@@ -17,10 +18,11 @@ export async function createShadowRootUi<TMounted>(
ctx: ContentScriptContext,
options: ShadowRootContentScriptUiOptions<TMounted>,
): Promise<ShadowRootContentScriptUi<TMounted>> {
const instanceId = Math.random().toString(36).substring(2, 15);
const css: string[] = [];
if (!options.inheritStyles) {
css.push(`/* WXT Shadow Root Reset */ body{all:initial;}`);
css.push(`/* WXT Shadow Root Reset */ :host{all:initial !important;}`);
}
if (options.css) {
css.push(options.css);
@@ -31,6 +33,9 @@ export async function createShadowRootUi<TMounted>(
css.push(entryCss.replaceAll(':root', ':host'));
}
// Some rules must be applied outside the shadow root, so split the CSS apart
const { shadowCss, documentCss } = splitShadowRootCss(css.join('\n').trim());
const {
isolatedElement: uiContainer,
parentElement: shadowHost,
@@ -38,7 +43,7 @@ export async function createShadowRootUi<TMounted>(
} = await createIsolatedElement({
name: options.name,
css: {
textContent: css.join('\n').trim(),
textContent: shadowCss,
},
mode: options.mode ?? 'open',
isolateEvents: options.isolateEvents,
@@ -51,6 +56,20 @@ export async function createShadowRootUi<TMounted>(
// Add shadow root element to DOM
mountUi(shadowHost, options);
applyPosition(shadowHost, shadow.querySelector('html'), options);
// Add document CSS
if (
documentCss &&
!document.querySelector(
`style[wxt-shadow-root-document-styles="${instanceId}"]`,
)
) {
const style = document.createElement('style');
style.textContent = documentCss;
style.setAttribute('wxt-shadow-root-document-styles', instanceId);
(document.head ?? document.body).append(style);
}
// Mount UI inside shadow root
mounted = options.onMount(uiContainer, shadow, shadowHost);
};
@@ -58,11 +77,20 @@ export async function createShadowRootUi<TMounted>(
const remove = () => {
// Cleanup mounted state
options.onRemove?.(mounted);
// Detach shadow root from DOM
shadowHost.remove();
// Remove document CSS
const documentStyle = document.querySelector(
`style[wxt-shadow-root-document-styles="${instanceId}"]`,
);
documentStyle?.remove();
// Remove children from uiContainer
while (uiContainer.lastChild)
uiContainer.removeChild(uiContainer.lastChild);
// Clear mounted value
mounted = undefined;
};
@@ -9,6 +9,9 @@ export type ContentScriptUiOptions<TMounted> = ContentScriptPositioningOptions &
/**
* Callback called before the UI is removed from the webpage. Use to cleanup your UI, like
* unmounting your Vue or React apps.
*
* Note that this callback is called only when `ui.remove` is called - that means it is
* not called automatically when the anchor is removed, unless you use `autoMount`.
*/
onRemove?: (mounted: TMounted | undefined) => void;
};
@@ -0,0 +1,25 @@
/**
* Given a CSS string that will be loaded into a shadow root, split it into two parts:
* - `documentCss`: CSS that needs to be applied to the document (like `@property`)
* - `shadowCss`: CSS that needs to be applied to the shadow root
* @param css
*/
export function splitShadowRootCss(css: string): {
documentCss: string;
shadowCss: string;
} {
let shadowCss = css;
let documentCss = '';
const rulesRegex = /(\s*@(property|font-face)[\s\S]*?{[\s\S]*?})/gm;
let match;
while ((match = rulesRegex.exec(css)) !== null) {
documentCss += match[1];
shadowCss = shadowCss.replace(match[1], '');
}
return {
documentCss: documentCss.trim(),
shadowCss: shadowCss.trim(),
};
}
+1
View File
@@ -14,6 +14,7 @@
"src/utils/define-unlisted-script.ts",
"src/utils/define-wxt-plugin.ts",
"src/utils/match-patterns.ts",
"src/utils/split-shadow-root-css.ts",
"src/utils/storage.ts",
"src/testing/index.ts",
"src/modules.ts"
+1058 -1481
View File
File diff suppressed because it is too large Load Diff
-99
View File
@@ -8,102 +8,3 @@ onlyBuiltDependencies:
- sharp
- simple-git-hooks
- spawn-sync
catalog:
'@1natsu/wait-element': ^4.1.2
'@aklinker1/buildc': ^1.1.4
'@aklinker1/check': ^1.4.5
'@aklinker1/rollup-plugin-visualizer': 5.12.0
'@commitlint/config-conventional': ^19.8.0
'@commitlint/types': ^19.8.0
'@faker-js/faker': ^9.6.0
'@sveltejs/vite-plugin-svelte': ^4.0.0 || ^5.0.0
'@types/chrome': ^0.0.313
'@types/fs-extra': ^11.0.4
'@types/lodash.merge': ^4.6.9
'@types/node': ^20.17.6
'@types/normalize-path': ^3.0.2
'@types/prompts': ^2.4.9
'@types/react': ^19.1.0
'@types/react-dom': ^19.1.2
'@types/ua-parser-js': ^0.7.39
'@types/webextension-polyfill': ^0.12.3
'@vitejs/plugin-react': ^4.3.4
'@vitejs/plugin-vue': ^5.2.3
'@vitest/coverage-v8': ^3.1.1
'@webext-core/fake-browser': ^1.3.2
'@webext-core/isolated-element': ^1.1.2
'@webext-core/match-patterns': ^1.0.3
async-mutex: ^0.5.0
c12: ^3.0.3
cac: ^6.7.14
changelogen: ^0.6.1
chokidar: ^4.0.3
ci-info: ^4.2.0
confbox: ^0.1.8 || ^0.2.2
consola: ^3.4.2
defu: ^6.1.4
dequal: ^2.0.3
dotenv: ^16.4.7
dotenv-expand: ^12.0.1
esbuild: ^0.25.0
extract-zip: ^2.0.1
fast-glob: ^3.3.3
feed: ^4.2.2
filesize: ^10.1.6
fs-extra: ^11.3.0
get-port-please: ^3.1.2
giget: ^1.2.3 || ^2.0.0
happy-dom: ^17.4.4
hookable: ^5.5.3
import-meta-resolve: ^4.1.0
is-wsl: ^3.1.0
json5: ^2.2.3
jszip: ^3.10.1
linkedom: ^0.18.9
lint-staged: ^15.5.0
lodash.merge: ^4.6.2
magicast: ^0.3.5
markdown-it-footnote: ^4.0.0
minimatch: ^10.0.1
nano-spawn: ^0.2.0
normalize-path: ^3.0.0
nypm: ^0.6.0
ohash: ^2.0.11
open: ^10.1.0
ora: ^8.2.0
oxlint: ^0.16.5
perfect-debounce: ^1.0.0
picocolors: ^1.1.1
prettier: ^3.5.3
prompts: ^2.4.2
publint: ^0.3.10
publish-browser-extension: ^2.3.0 || ^3.0.0
react: ^19.1.0
react-dom: ^19.1.0
sass: ^1.86.3
scule: ^1.3.0
sharp: ^0.34.1
simple-git-hooks: ^2.12.1
solid-js: ^1.9.5
tsx: 4.19.3
typedoc: ^0.25.4
typedoc-plugin-frontmatter: ^1.3.0
typedoc-plugin-markdown: 4.0.0-next.23
typedoc-vitepress-theme: 1.0.0-next.3
typescript: ^5.8.3
ua-parser-js: ^1.0.40
unbuild: ^3.5.0
unimport: ^3.13.1 || ^4.0.0 || ^5.0.0
unocss: ^0.64.0 || ^0.65.0 || ^65.0.0 || ^66.0.0
vite: ^5.4.17 || ^6.2.5
vite-node: ^2.1.4 || ^3.0.0
vite-plugin-solid: ^2.11.6
vitepress: ^1.6.3
vitepress-knowledge: ^0.4.1
vitepress-plugin-group-icons: ^1.4.1
vitest: ^3.1.1
vitest-mock-extended: ^3.1.0
vitest-plugin-random-seed: ^1.1.1
vue: ^3.5.13
web-ext-run: ^0.2.2
webextension-polyfill: ^0.12.0
+3 -3
View File
@@ -19,10 +19,10 @@
"react-dom": "^19.1.0"
},
"devDependencies": {
"@types/react": "^19.1.0",
"@types/react-dom": "^19.1.2",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.3",
"@wxt-dev/module-react": "^1.1.3",
"typescript": "^5.8.3",
"wxt": "^0.20.0"
"wxt": "^0.20.6"
}
}
+2 -2
View File
@@ -15,11 +15,11 @@
"postinstall": "wxt prepare"
},
"dependencies": {
"solid-js": "^1.9.5"
"solid-js": "^1.9.6"
},
"devDependencies": {
"@wxt-dev/module-solid": "^1.1.3",
"typescript": "^5.8.3",
"wxt": "^0.20.0"
"wxt": "^0.20.6"
}
}
+3 -3
View File
@@ -17,10 +17,10 @@
"devDependencies": {
"@tsconfig/svelte": "^5.0.4",
"@wxt-dev/module-svelte": "^2.0.3",
"svelte": "^5.25.9",
"svelte-check": "^4.1.5",
"svelte": "^5.28.2",
"svelte-check": "^4.1.6",
"tslib": "^2.8.1",
"typescript": "^5.8.3",
"wxt": "^0.20.0"
"wxt": "^0.20.6"
}
}

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Before

Width:  |  Height:  |  Size: 559 B

After

Width:  |  Height:  |  Size: 559 B

Before

Width:  |  Height:  |  Size: 916 B

After

Width:  |  Height:  |  Size: 916 B

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

+1 -1
View File
@@ -16,6 +16,6 @@
},
"devDependencies": {
"typescript": "^5.8.3",
"wxt": "^0.20.0"
"wxt": "^0.20.6"
}
}
+2 -2
View File
@@ -20,7 +20,7 @@
"devDependencies": {
"@wxt-dev/module-vue": "^1.0.2",
"typescript": "5.6.3",
"vue-tsc": "^2.2.8",
"wxt": "^0.20.0"
"vue-tsc": "^2.2.10",
"wxt": "^0.20.6"
}
}