Compare commits

...

42 Commits

Author SHA1 Message Date
github-actions[bot] dd549dc686 chore(release): wxt v0.20.15
📼 VHS / Create VHS (push) Waiting to run
2026-02-11 07:13:31 +00:00
Nick Doan 3417bd3ed2 feat: Add log --level cli option (#1973)
Co-authored-by: nickbar01234 <nickbar01234gmail.com>
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2026-02-11 04:57:27 +00:00
Patryk Kuniczak 9470d2b696 fix: rename name to names prop of rollup to fix deprecated warning (#2106) 2026-02-10 12:23:19 -06:00
Aaron f523a7d31d docs: Add notes about chrome services and feature flags (#2110) 2026-02-10 12:22:18 -06:00
Patryk Kuniczak ea4c2b035b fix: Remove prepublish script of root package.json (#2109) 2026-02-10 10:11:47 +01:00
willow c5bfba837f fix(storage): Set version number on init (#1996) 2026-02-10 01:41:00 +00:00
aklinker1 c5ad957b25 fix: Upgrade @wxt-dev/browser to latest @types/chrome version 2026-02-10 00:47:54 +00:00
Patryk Kuniczak cc849e7cea docs: rename keys name of getMetas() to be proper (#2105) 2026-02-09 22:28:49 +01:00
Patryk Kuniczak 22428da54d chore: simplify imports of cli/__tests__/index.ts (#2104) 2026-02-09 20:59:46 +00:00
Patryk Kuniczak f6071f7e2d fix: Change deprecated function presetUno to presetWind3 (#2103) 2026-02-09 21:55:28 +01:00
Aaron 08e8560865 chore: Upgrade PNPM to 10.29.2 (#2102) 2026-02-09 12:46:47 -06:00
Aaron 75f01db543 refactor: Simplify import paths (#2093)
Co-authored-by: Patryk Kuniczak <p.kuniczak@gmail.com>
2026-02-09 02:09:51 -06:00
Aaron 5a028d2551 chore: Remove unused code (#2087)
Co-authored-by: Patryk Kuniczak <p.kuniczak@gmail.com>
2026-02-09 01:45:11 -06:00
Aaron 07a4ba8bc5 fix: Make ExtensionRunner#closeBrowser optional (#2092)
Co-authored-by: Patryk Kuniczak <p.kuniczak@gmail.com>
2026-02-09 01:44:35 -06:00
Aaron f84921f735 chore: Fix typos (#2086)
Co-authored-by: Patryk Kuniczak <p.kuniczak@gmail.com>
2026-02-09 01:39:46 -06:00
Aaron 872494e490 chore: Update JSDoc to use @internal (#2091)
Co-authored-by: Patryk Kuniczak <p.kuniczak@gmail.com>
2026-02-09 01:39:07 -06:00
Aaron d0848b0be5 chore: Cleanup @ts-* comments (#2094)
Co-authored-by: Patryk Kuniczak <p.kuniczak@gmail.com>
2026-02-09 01:38:42 -06:00
Aaron c848956777 chore: Cleanup comment line lengths (#2090)
Co-authored-by: Patryk Kuniczak <p.kuniczak@gmail.com>
2026-02-09 01:38:09 -06:00
Aaron 296997d10e refactor: Cleanup unnecssary checks (#2089)
Co-authored-by: Patryk Kuniczak <p.kuniczak@gmail.com>
2026-02-09 01:37:47 -06:00
Aaron ffdb22238c refactor: async/await cleanup (#2088)
Co-authored-by: Patryk Kuniczak <p.kuniczak@gmail.com>
2026-02-09 01:37:16 -06:00
Aaron a63c320403 refactor: Code cleanup for storage package (#2085)
Co-authored-by: Patryk Kuniczak <p.kuniczak@gmail.com>
2026-02-09 01:36:40 -06:00
Aaron aa86ed78a3 refactor: Code cleanup in analytics package (#2084)
Co-authored-by: Patryk Kuniczak <p.kuniczak@gmail.com>
2026-02-09 01:36:15 -06:00
wotan-allfather 19456ec855 feat(popup): Add Firefox default_area and theme_icons support (#2097)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2026-02-09 07:28:26 +00:00
Aaron 2c57840c44 revert: Use uiContainer to apply position when creating shadow root ui (#2036)
This reverts commit 26b7927f62.
2026-02-09 00:44:07 -06:00
Namu 26b7927f62 fix: Use uiContainer to apply position when creating shadow root ui (#2036)
Co-authored-by: Namu <535248+namukang@users.noreply.github.com>
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2026-02-09 06:32:35 +00:00
Eli d4267af725 refactor: cleanup esm (#1950)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2026-02-09 04:27:51 +00:00
ariasuni 16a2aedcd0 docs: Small improvements for hooks (#2095)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2026-02-09 02:46:40 +00:00
Puzant 755f04f777 docs: Added KeyFloat (#2082)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2026-02-09 02:43:58 +00:00
Namu cf9539ddab fix: content script is incorrectly invalidated when injected multiple times (#2035)
Co-authored-by: Namu <535248+namukang@users.noreply.github.com>
2026-02-09 02:38:54 +00:00
Aaron 99d558f33c refactor: Standardize file existence checks to pathExists (#2083)
Co-authored-by: Patryk Kuniczak <p.kuniczak@gmail.com>
2026-02-08 15:21:45 -06:00
Patryk Kuniczak 1fe073004b fix: normalize path for createAnalytics of analytics/index.ts (#2013) 2026-02-08 08:05:09 -06:00
Aaron 6ae5fb5edb docs: Fix JSDoc 2026-02-08 08:03:46 -06:00
Patryk Kuniczak 3a3953edf6 feat: Export normalizePath from wxt module (#2080) 2026-02-08 08:18:49 +00:00
rxliuli c8e788eaf8 docs: Add blog post resource for building browser extensions (#2077) 2026-02-07 13:40:14 +00:00
Aaron 088a44496d docs: Add llms.txt to documentation site (#1725) 2026-02-06 16:38:37 -06:00
Aaron 60a57c5abc chore: Fix markdownlint issues 2026-02-06 16:34:08 -06:00
Aaron 482466bbbf docs: Update MAINTAINERS.md
Added an example of how to ask for a bug reproduction.
2026-02-06 15:10:11 -06:00
Aaron 65deb7317b chore: Upgrade github actions (#2073) 2026-02-06 14:14:19 -06:00
Patryk Kuniczak 00ae061822 chore: fix recognize lack of argument for scripts (#2015) 2026-02-06 14:04:14 -06:00
Aaron 9bd7fc1bd5 chore: Don't report unreleased chore/doc commits 2026-02-06 13:36:04 -06:00
Aaron 98ddb83f1d docs: Add PR guidelines for small and targeted changes 2026-02-06 10:55:00 -06:00
Aaron 6dd8b9d072 docs: Add MAINTAINERS.md file for notes specific to maintainers 2026-02-06 10:41:57 -06:00
104 changed files with 1736 additions and 364 deletions
@@ -10,7 +10,7 @@ jobs:
if: github.repository_owner == 'wxt-dev'
steps:
- name: Checkout
uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
if: ${{ github.repository == 'wxt-dev/wxt' }}
steps:
- name: Checkout
uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup
uses: ./.github/actions/setup
+1 -1
View File
@@ -10,7 +10,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# Only fetch the config file from the repository
sparse-checkout-cone-mode: false
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
contents: write
steps:
- name: Checkout
uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup
uses: ./.github/actions/setup
+1 -1
View File
@@ -39,7 +39,7 @@ jobs:
- validate
steps:
- name: Checkout
uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
ssh-key: ${{ secrets.DEPLOY_KEY }} # https://github.com/sbellone/release-workflow-example
+1 -1
View File
@@ -30,7 +30,7 @@ jobs:
contents: write
steps:
- name: Checkout
uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup
uses: ./.github/actions/setup
+2 -2
View File
@@ -17,7 +17,7 @@ jobs:
id-token: write # OIDC for NPM publishing
steps:
- name: Checkout
uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ssh-key: ${{ secrets.DEPLOY_KEY }}
@@ -36,7 +36,7 @@ jobs:
- name: Commit Changes
id: commit
uses: stefanzweifel/git-auto-commit-action@01d77ca6cb089da1360e540865f7d035c95aa199 # v6.0.1
uses: stefanzweifel/git-auto-commit-action@04702edda442b2e678b25b537cec683a1493fcb9 # v7.1.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
+7 -7
View File
@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup
uses: ./.github/actions/setup
@@ -28,7 +28,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup
uses: ./.github/actions/setup
@@ -41,7 +41,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup
uses: ./.github/actions/setup
@@ -68,13 +68,13 @@ jobs:
coverage: false
steps:
- name: Checkout
uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup
uses: ./.github/actions/setup
- name: Setup Bun
uses: oven-sh/setup-bun@22457c87c1b161cf7dde222c3e82b2b5f8d2bed2 # v2.0.2
uses: oven-sh/setup-bun@3d267786b128fe76c2f16a390aa2448b815359f3 # v2.1.2
- name: Run Tests
if: ${{ ! matrix.coverage }}
@@ -86,7 +86,7 @@ jobs:
- name: Upload Coverage
if: matrix.coverage
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
@@ -104,7 +104,7 @@ jobs:
- vue
steps:
- name: Checkout
uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup
uses: ./.github/actions/setup
+2 -2
View File
@@ -17,7 +17,7 @@ jobs:
contents: write
steps:
- name: Checkout
uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493 # v5.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup
uses: ./.github/actions/setup
@@ -36,7 +36,7 @@ jobs:
path: 'docs/tapes/init-demo.tape'
- name: Save recorded GIF
uses: stefanzweifel/git-auto-commit-action@01d77ca6cb089da1360e540865f7d035c95aa199 # v6.0.1
uses: stefanzweifel/git-auto-commit-action@04702edda442b2e678b25b537cec683a1493fcb9 # v7.1.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
+2 -1
View File
@@ -10,5 +10,6 @@
// Additional guidelines for Copilot
"github.copilot.chat.codeGeneration.instructions": [
{ "file": "CONTRIBUTING.md" }
]
],
"oxc.enable": true
}
+49
View File
@@ -0,0 +1,49 @@
# Maintainers
A couple of things for you to consider before merging a PR or giving the go-ahead on a feature proposal.
## Be picky about new features and packages
We are responsible for maintaining them and fixing related bugs after the PR is merged. The community can always release their own WXT modules or packages, not everything needs to be built into WXT.
## Prefer standards over customization
Don't merge PRs that just add another way to do something, like [this one](https://github.com/wxt-dev/wxt/pull/2053#issuecomment-3857010196).
WXT is opinionated, if you have questions about what is WXT's opinion or we need to create a new one, create an issue and @ me to discuss.
## PRs should be small and targeted
A PR should make one change. They should not make any unrelated changes outside of accomplishing the one thing. This makes PRs easier to review and they get merged more quickly - a win-win for everyone.
## `@wxt-dev/*` packages are separate
We can't make changes to these packages assuming people are using them only with WXT. For example, I almost missed [this PR](https://github.com/wxt-dev/wxt/pull/2049#issuecomment-3861251599).
I want these packages to be usable on their own so if people don't like WXT's build tool, they can still use our other awesome packages.
## Minimize dependencies
I don't like how heavy lots of frameworks are. It's unavoidable to a certain extent, but if you can do something without another dependency, try to.
If you need a dependency, look for one with zero dependencies and that's respectable.
## Look at milestones
I've organized WXT's long term plans into [milestones](https://github.com/wxt-dev/wxt/milestones), check those out to get an idea of my priorities for some of the larger features.
## Require tests
If someone opens a PR to fix a bug but doesn't include tests, don't merge the PR. Tests are required to prevent regressions and maintain a codebase long term.
## Ask for reproductions for bugs
You don't need to triage bugs if someone doesn't give you enough information. You can always ask for a repo with a reproduction or wait for more details.
If there's not an easy way to reproduce a bug, you're wasting your time triaging it. Just be mindful of your own time!
Here's an example of how to ask for a reproduction: <https://github.com/wxt-dev/wxt/issues/2064#issuecomment-3862579110>. You could be more blunt than this.
## Add yourself as a code owner
If you want to be responsible for a specific package or directory, add yourself to the [`.github/CODEOWNERS`](https://github.com/wxt-dev/wxt/blob/main/.github/CODEOWNERS) file to get added as a reviewer to PRs automatically. You can also add yourself to the default list to be added as a reviewer on all PRs.
@@ -116,6 +116,7 @@ const chromeExtensionIds = [
'jmghclbfbbapimhbgnpffbimphlpolnm', // Teams Chat Exporter
'jdcppdokgfbnhiacbeplahgnciahnhck', // Lofi BGM Player - Free lofi focus music for work & study
'cgpmbiiagnehkikhcbnhiagfomajncpa', // Margin - Annotate and highlight any webpage, with your notes saved to the decentralized AT Protocol.
'mfjdonmgmgcijagclnkfhmjiblbfjaid', // KeyFloat - Floating multilingual keyboard with native key mappings, drag, dark mode, sounds, and dynamic layouts for macOS & Windows
];
const { data, err, isLoading } = useListExtensionDetails(chromeExtensionIds);
+2
View File
@@ -25,6 +25,7 @@ import {
import { Feed } from 'feed';
import { writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import llmstxt from 'vitepress-plugin-llms';
const origin = 'https://wxt.dev';
@@ -71,6 +72,7 @@ export default defineConfig({
vite: {
clearScreen: false,
plugins: [
llmstxt(),
groupIconVitePlugin({
customIcon: {
'wxt.config.ts': localIconLoader(
@@ -105,3 +105,24 @@ export default defineWebExtConfig({
disabled: true,
});
```
### Enabling Chrome Features
Some APIs are disabled in Chrome during development because of the default flags `web-ext` uses to launch Chrome, like the [Prompt API](https://developer.chrome.com/docs/ai/prompt-api).
If your extension depends on new features or services, you can enable them via `chromiumArgs`:
```ts
import { defineWebExtConfig } from 'wxt';
export default defineWebExtConfig({
chromiumArgs: [
// For example, this flag enables the Prompt API
'--disable-features=DisableLoadExtensionCommandLineSwitch',
],
});
```
There is no comprehensive list of what feature flags enable what APIs and services.
Alternatively, if you can't find a flag that enables a feature you're looking for, [disable the opening the browser during development](#disable-opening-browser) and use your regular chrome profile for development.
+2
View File
@@ -20,6 +20,8 @@ export default defineConfig({
Most hooks provide the `wxt` object as the first argument. It contains the resolved config and other info about the current build. The other arguments can be modified by reference to change different parts of the build system.
You can find the [list of all available hooks](/api/reference/wxt/interfaces/WxtHooks) in the API reference.
Putting one-off hooks like this in your config file is simple, but if you find yourself writing lots of hooks, you should extract them into [WXT Modules](/guide/essentials/wxt-modules) instead.
## Execution Order
+13
View File
@@ -470,6 +470,19 @@ When you define a Newtab entrypoint, WXT will automatically update the manifest
/>
<meta name="manifest.type" content="page_action|browser_action" />
<meta name="manifest.browser_style" content="true|false" />
<!-- Firefox only: where to place the action button -->
<meta
name="manifest.default_area"
content="navbar|menupanel|tabstrip|personaltoolbar"
/>
<!-- Firefox only: icons for light/dark themes -->
<meta
name="manifest.theme_icons"
content="[
{ light: '/icon-light-16.png', dark: '/icon-dark-16.png', size: 16 },
{ light: '/icon-light-32.png', dark: '/icon-dark-32.png', size: 32 }
]"
/>
<!-- Set include/exclude if the page should be removed from some builds -->
<meta name="manifest.include" content="['chrome', ...]" />
+1 -1
View File
@@ -66,7 +66,7 @@ Each module's setup function is executed after the `wxt.config.ts` file is loade
Refer to the [API reference](/api/reference/wxt/interfaces/Wxt) for a complete list of properties and functions available.
Also to make sure and read about all the [hooks that are available](https://wxt.dev/api/reference/wxt/interfaces/WxtHooks) - they are essential to writing modules.
Also make sure to read about [all the hooks that are available](/api/reference/wxt/interfaces/WxtHooks) - they are essential to writing modules.
### Recipes
+1
View File
@@ -7,6 +7,7 @@ This page is dedicated to all the awesome people how have made something for WXT
## Blog Posts
- [Building Modern Cross Browser Web Extensions](https://aabidk.dev/tags/wxt/) by Aabid ([@aabidk20](https://github.com/aabidk20))
- [Building Browser Extensions with WXT](https://rxliuli.com/blog/browser-extension-dev-01-introduction-to-basic-concepts/) by rxliuli ([@rxliuli](https://github.com/rxliuli))
## NPM Packages
+34
View File
@@ -164,6 +164,10 @@ Both issues have the same cause: the library puts something outside the `ShadowR
Both issues have the same fix: tell the library to put elements inside the `ShadowRoot`, not outside it. See the details above for more information and example fixes for each problem.
## Does WXT provide docs for LLMs?
Yes, WXT's documentation provides markdown files based on the [the /llms.txt proposal](https://llmstxt.org/).
## Is there an LLM trained on WXT's docs that I chat with?
Yes! There's a "Ask AI" button in the bottom right of the page, try it out! Or visit <https://knowledge.wxt.dev/> for a fullscreen experience.
@@ -186,3 +190,33 @@ To run the WXT dev server in a devcontainer, but load the dev build of your exte
3. **Tell WXT to listen on all network interfaces**
To enable hot-reloading, your extension has to connect to the WXT dev server running inside your container. WXT will only listen on `localhost` by default, which prevents connections from outside the devcontainer. To fix this you can instruct WXT to listen on all interfaces with `wxt --host 0.0.0.0`.
## How do I use the new Prompt API in Chrome?
The service responsible for the [Prompt API](https://developer.chrome.com/docs/ai/prompt-api) is not enabled by default if you let WXT open the browser during dev mode. When checking `LanguageModel.availability`, you will always receive "unavailable".
You have two options:
1. Pass the `--disable-features=DisableLoadExtensionCommandLineSwitch` feature flag to enable the service in the browser WXT opens:
```ts
// wxt.config.ts
export default defineConfig({
webExt: {
chromiumArgs: [
'--disable-features=DisableLoadExtensionCommandLineSwitch',
],
},
});
```
2. Disable the runner and install your extension in your regular chrome profile manually:
```ts
// wxt.config.ts
export default defineConfig({
webExt: {
disabled: true,
},
});
```
+2 -2
View File
@@ -4,13 +4,12 @@
"engines": {
"node": ">=18.20.3"
},
"packageManager": "pnpm@10.15.1",
"packageManager": "pnpm@10.29.2+sha512.bef43fa759d91fd2da4b319a5a0d13ef7a45bb985a3d7342058470f9d2051a3ba8674e629672654686ef9443ad13a82da2beb9eeb3e0221c87b8154fff9d74b8",
"scripts": {
"check": "check && pnpm -r --sequential run check",
"test": "pnpm -r --sequential run test run",
"test:coverage": "pnpm -r --sequential run test:coverage",
"prepare": "simple-git-hooks",
"prepublish": "pnpm -s build",
"docs:gen": "typedoc --options docs/typedoc.json",
"docs:dev": "pnpm -s docs:gen && vitepress dev docs",
"docs:build": "pnpm -s docs:gen && vitepress build docs",
@@ -47,6 +46,7 @@
"vitepress": "^1.6.4",
"vitepress-knowledge": "^0.4.1",
"vitepress-plugin-group-icons": "^1.7.1",
"vitepress-plugin-llms": "^1.11.0",
"vitest-mock-extended": "^3.1.0",
"vue": "^3.5.27",
"wxt": "workspace:*"
+21 -21
View File
@@ -13,6 +13,22 @@ import { browser } from '@wxt-dev/browser';
const ANALYTICS_PORT = '@wxt-dev/analytics';
const INTERACTIVE_TAGS = new Set([
'A',
'BUTTON',
'INPUT',
'SELECT',
'TEXTAREA',
]);
const INTERACTIVE_ROLES = new Set([
'button',
'link',
'checkbox',
'menuitem',
'tab',
'radio',
]);
export function createAnalytics(config?: AnalyticsConfig): Analytics {
if (!browser?.runtime?.id)
throw Error(
@@ -62,8 +78,8 @@ function createBackgroundAnalytics(
const getBackgroundMeta = () => ({
timestamp: Date.now(),
// Don't track sessions for the background, it can be running
// indefinitely, and will inflate session duration stats.
// Don't track sessions for the background, it can be running indefinitely
// and will inflate session duration stats.
sessionId: undefined,
language: navigator.language,
referrer: undefined,
@@ -75,7 +91,7 @@ function createBackgroundAnalytics(
const getBaseEvent = async (
meta: AnalyticsEventMetadata,
): Promise<BaseAnalyticsEvent> => {
const platform = await platformInfo;
const { arch, os } = await platformInfo;
return {
meta,
user: {
@@ -84,8 +100,8 @@ function createBackgroundAnalytics(
version: config?.version ?? manifest.version_name ?? manifest.version,
wxtMode: import.meta.env.MODE,
wxtBrowser: import.meta.env.BROWSER,
arch: platform.arch,
os: platform.os,
arch,
os,
browser: userAgent.browser.name,
browserVersion: userAgent.browser.version,
...(await userProperties),
@@ -259,22 +275,6 @@ function defineStorageItem<T>(
};
}
const INTERACTIVE_TAGS = new Set([
'A',
'BUTTON',
'INPUT',
'SELECT',
'TEXTAREA',
]);
const INTERACTIVE_ROLES = new Set([
'button',
'link',
'checkbox',
'menuitem',
'tab',
'radio',
]);
export function defineAnalyticsProvider<T = never>(
definition: (
/** The analytics object. */
+10 -11
View File
@@ -8,6 +8,7 @@ import {
} from 'wxt/modules';
import { relative, resolve } from 'node:path';
import type { AnalyticsConfig } from './types';
import { normalizePath } from 'wxt';
declare module 'wxt/utils/define-app-config' {
export interface WxtAppConfig {
@@ -38,17 +39,15 @@ export default defineWxtModule({
});
// Generate #analytics module
const wxtAnalyticsCode = [
`import { createAnalytics } from '${
process.env.NPM
? clientModuleId
: relative(wxtAnalyticsFolder, clientModuleId)
}';`,
`import { useAppConfig } from '#imports';`,
``,
`export const analytics = createAnalytics(useAppConfig().analytics);`,
``,
].join('\n');
const wxtAnalyticsCode = `import { createAnalytics } from '${
process.env.NPM
? clientModuleId
: normalizePath(relative(wxtAnalyticsFolder, clientModuleId))
}';
import { useAppConfig } from '#imports';
export const analytics = createAnalytics(useAppConfig().analytics);
`;
addAlias(wxt, '#analytics', wxtAnalyticsIndex);
wxt.hook('prepare:types', async (_, entries) => {
entries.push({
+2 -2
View File
@@ -3,7 +3,7 @@ import { defineWxtModule } from 'wxt/modules';
import { resolve, relative } from 'node:path';
import defu from 'defu';
import sharp from 'sharp';
import { ensureDir, exists } from 'fs-extra';
import { ensureDir, pathExists } from 'fs-extra';
export default defineWxtModule<AutoIconsOptions>({
name: '@wxt-dev/auto-icons',
@@ -37,7 +37,7 @@ export default defineWxtModule<AutoIconsOptions>({
if (!parsedOptions.enabled)
return wxt.logger.warn(`\`[auto-icons]\` ${this.name} disabled`);
if (!(await exists(resolvedPath))) {
if (!(await pathExists(resolvedPath))) {
return wxt.logger.warn(
`\`[auto-icons]\` Skipping icon generation, no base icon found at ${relative(process.cwd(), resolvedPath)}`,
);
+2 -2
View File
@@ -114,7 +114,7 @@ async function findBrowserBinary(target: string): Promise<string | undefined> {
for (const target of targets) {
const potentialPaths = KNOWN_BROWSER_PATHS[target]?.[platform] ?? [];
for (const path of potentialPaths) {
if (await exists(path)) return path;
if (await pathExists(path)) return path;
}
}
}
@@ -207,7 +207,7 @@ function deduplicateArgs(
return args;
}
async function exists(path: string): Promise<boolean> {
async function pathExists(path: string): Promise<boolean> {
try {
await open(path, 'r');
return true;
@@ -706,6 +706,47 @@ describe('Storage Utils', () => {
expect(migrateToV3).toBeCalledWith(4);
});
it('should set the version without running migrations for empty storage items', async () => {
const migrate = vi.fn((n: number) => n * 2);
const item = storage.defineItem<number>('local:key', {
init: () => 1,
version: 2,
migrations: {
2: migrate,
},
});
const value = await item.getValue();
const meta = await item.getMeta();
expect(value).toEqual(1);
expect(meta).toEqual({ v: 2 });
expect(migrate).toBeCalledTimes(0);
});
it('should set the version and run migrations if a storage value already exists and v2 is added', async () => {
const key = 'local:key';
// Mimic as if an initial value was set by a previous version. We expect
// this value to be migrated and doubled to V2.
await storage.setItem(key, 1);
const migrate = vi.fn((n: number) => n * 2);
const item = storage.defineItem<number>(key, {
init: () => 1,
version: 2,
migrations: {
2: migrate,
},
});
const value = await item.getValue();
const meta = await item.getMeta();
expect(value).toEqual(2);
expect(meta).toEqual({ v: 2 });
expect(migrate).toBeCalledTimes(1);
});
it('should call onMigrationComplete callback function if defined', async () => {
await fakeBrowser.storage.local.set({
count: 2,
+135 -21
View File
@@ -17,6 +17,7 @@ function createStorage(): WxtStorage {
sync: createDriver('sync'),
managed: createDriver('managed'),
};
const getDriver = (area: StorageArea) => {
const driver = drivers[area];
if (driver == null) {
@@ -25,14 +26,17 @@ function createStorage(): WxtStorage {
}
return driver;
};
const resolveKey = (key: StorageItemKey) => {
const deliminatorIndex = key.indexOf(':');
const driverArea = key.substring(0, deliminatorIndex) as StorageArea;
const driverKey = key.substring(deliminatorIndex + 1);
if (driverKey == null)
if (driverKey == null) {
throw Error(
`Storage key should be in the form of "area:key", but received "${key}"`,
);
}
return {
driverArea,
@@ -40,17 +44,23 @@ function createStorage(): WxtStorage {
driver: getDriver(driverArea),
};
};
const getMetaKey = (key: string) => key + '$';
const mergeMeta = (oldMeta: any, newMeta: any): any => {
const newFields = { ...oldMeta };
Object.entries(newMeta).forEach(([key, value]) => {
if (value == null) delete newFields[key];
else newFields[key] = value;
});
return newFields;
};
const getValueOrFallback = (value: any, fallback: any) =>
value ?? fallback ?? null;
const getMetaValue = (properties: any) =>
typeof properties === 'object' && !Array.isArray(properties)
? properties
@@ -64,11 +74,13 @@ function createStorage(): WxtStorage {
const res = await driver.getItem<any>(driverKey);
return getValueOrFallback(res, opts?.fallback ?? opts?.defaultValue);
};
const getMeta = async (driver: WxtStorageDriver, driverKey: string) => {
const metaKey = getMetaKey(driverKey);
const res = await driver.getItem<any>(metaKey);
return getMetaValue(res);
};
const setItem = async (
driver: WxtStorageDriver,
driverKey: string,
@@ -76,6 +88,7 @@ function createStorage(): WxtStorage {
) => {
await driver.setItem(driverKey, value ?? null);
};
const setMeta = async (
driver: WxtStorageDriver,
driverKey: string,
@@ -85,23 +98,27 @@ function createStorage(): WxtStorage {
const existingFields = getMetaValue(await driver.getItem(metaKey));
await driver.setItem(metaKey, mergeMeta(existingFields, properties));
};
const removeItem = async (
driver: WxtStorageDriver,
driverKey: string,
opts: RemoveItemOptions | undefined,
) => {
await driver.removeItem(driverKey);
if (opts?.removeMeta) {
const metaKey = getMetaKey(driverKey);
await driver.removeItem(metaKey);
}
};
const removeMeta = async (
driver: WxtStorageDriver,
driverKey: string,
properties: string | string[] | undefined,
) => {
const metaKey = getMetaKey(driverKey);
if (properties == null) {
await driver.removeItem(metaKey);
} else {
@@ -110,19 +127,19 @@ function createStorage(): WxtStorage {
await driver.setItem(metaKey, newFields);
}
};
const watch = (
driver: WxtStorageDriver,
driverKey: string,
cb: WatchCallback<any>,
) => {
return driver.watch(driverKey, cb);
};
) => driver.watch(driverKey, cb);
const storage: WxtStorage = {
return {
getItem: async (key, opts) => {
const { driver, driverKey } = resolveKey(key);
return await getItem(driver, driverKey, opts);
},
getItems: async (keys) => {
const areaToKeyMap = new Map<StorageArea, string[]>();
const keyToOptsMap = new Map<string, GetItemOptions<any> | undefined>();
@@ -131,6 +148,7 @@ function createStorage(): WxtStorage {
keys.forEach((key) => {
let keyStr: StorageItemKey;
let opts: GetItemOptions<any> | undefined;
if (typeof key === 'string') {
// key: string
keyStr = key;
@@ -143,9 +161,11 @@ function createStorage(): WxtStorage {
keyStr = key.key;
opts = key.options;
}
orderedKeys.push(keyStr);
const { driverArea, driverKey } = resolveKey(keyStr);
const areaKeys = areaToKeyMap.get(driverArea) ?? [];
areaToKeyMap.set(driverArea, areaKeys.concat(driverKey));
keyToOptsMap.set(keyStr, opts);
});
@@ -154,6 +174,7 @@ function createStorage(): WxtStorage {
await Promise.all(
Array.from(areaToKeyMap.entries()).map(async ([driverArea, keys]) => {
const driverResults = await drivers[driverArea].getItems(keys);
driverResults.forEach((driverResult) => {
const key = `${driverArea}:${driverResult.key}` as StorageItemKey;
const opts = keyToOptsMap.get(key);
@@ -161,6 +182,7 @@ function createStorage(): WxtStorage {
driverResult.value,
opts?.fallback ?? opts?.defaultValue,
);
resultsMap.set(key, value);
});
}),
@@ -171,14 +193,17 @@ function createStorage(): WxtStorage {
value: resultsMap.get(key),
}));
},
getMeta: async (key) => {
const { driver, driverKey } = resolveKey(key);
return await getMeta(driver, driverKey);
},
getMetas: async (args) => {
const keys = args.map((arg) => {
const key = typeof arg === 'string' ? arg : arg.key;
const { driverArea, driverKey } = resolveKey(key);
return {
key,
driverArea,
@@ -186,6 +211,7 @@ function createStorage(): WxtStorage {
driverMetaKey: getMetaKey(driverKey),
};
});
const areaToDriverMetaKeysMap = keys.reduce<
Partial<Record<StorageArea, (typeof keys)[number][]>>
>((map, key) => {
@@ -211,10 +237,12 @@ function createStorage(): WxtStorage {
meta: resultsMap[key.key],
}));
},
setItem: async (key, value) => {
const { driver, driverKey } = resolveKey(key);
await setItem(driver, driverKey, value);
},
setItems: async (items) => {
const areaToKeyValueMap: Partial<
Record<StorageArea, Array<{ key: string; value: any }>>
@@ -229,6 +257,7 @@ function createStorage(): WxtStorage {
value: item.value,
});
});
await Promise.all(
Object.entries(areaToKeyValueMap).map(async ([driverArea, values]) => {
const driver = getDriver(driverArea as StorageArea);
@@ -236,10 +265,12 @@ function createStorage(): WxtStorage {
}),
);
},
setMeta: async (key, properties) => {
const { driver, driverKey } = resolveKey(key);
await setMeta(driver, driverKey, properties);
},
setMetas: async (items) => {
const areaToMetaUpdatesMap: Partial<
Record<StorageArea, { key: string; properties: any }[]>
@@ -278,16 +309,19 @@ function createStorage(): WxtStorage {
),
);
},
removeItem: async (key, opts) => {
const { driver, driverKey } = resolveKey(key);
await removeItem(driver, driverKey, opts);
},
removeItems: async (keys) => {
const areaToKeysMap: Partial<Record<StorageArea, string[]>> = {};
keys.forEach((key) => {
let keyStr: StorageItemKey;
let opts: RemoveItemOptions | undefined;
if (typeof key === 'string') {
// key: string
keyStr = key;
@@ -303,9 +337,11 @@ function createStorage(): WxtStorage {
keyStr = key.key;
opts = key.options;
}
const { driverArea, driverKey } = resolveKey(keyStr);
areaToKeysMap[driverArea] ??= [];
areaToKeysMap[driverArea].push(driverKey);
if (opts?.removeMeta) {
areaToKeysMap[driverArea].push(getMetaKey(driverKey));
}
@@ -318,36 +354,45 @@ function createStorage(): WxtStorage {
}),
);
},
clear: async (base) => {
const driver = getDriver(base);
await driver.clear();
},
removeMeta: async (key, properties) => {
const { driver, driverKey } = resolveKey(key);
await removeMeta(driver, driverKey, properties);
},
snapshot: async (base, opts) => {
const driver = getDriver(base);
const data = await driver.snapshot();
opts?.excludeKeys?.forEach((key) => {
delete data[key];
delete data[getMetaKey(key)];
});
return data;
},
restoreSnapshot: async (base, data) => {
const driver = getDriver(base);
await driver.restoreSnapshot(data);
},
watch: (key, cb) => {
const { driver, driverKey } = resolveKey(key);
return watch(driver, driverKey, cb);
},
unwatch() {
Object.values(drivers).forEach((driver) => {
driver.unwatch();
});
},
defineItem: (key, opts?: WxtStorageItemOptions<any>) => {
const { driver, driverKey } = resolveKey(key);
@@ -357,12 +402,14 @@ function createStorage(): WxtStorage {
onMigrationComplete,
debug = false,
} = opts ?? {};
if (targetVersion < 1) {
throw Error(
'Storage item version cannot be less than 1. Initial versions should be set to 1, not 0.',
);
}
const migrate = async () => {
const migrate: WxtStorageItem<any, any>['migrate'] = async () => {
const driverMetaKey = getMetaKey(driverKey);
const [{ value }, { value: meta }] = await driver.getItems([
driverKey,
@@ -376,11 +423,12 @@ function createStorage(): WxtStorage {
`Version downgrade detected (v${currentVersion} -> v${targetVersion}) for "${key}"`,
);
}
if (currentVersion === targetVersion) {
return;
}
if (debug === true) {
if (debug) {
console.debug(
`[@wxt-dev/storage] Running storage migration for ${key}: v${currentVersion} -> v${targetVersion}`,
);
@@ -395,7 +443,7 @@ function createStorage(): WxtStorage {
migratedValue =
(await migrations?.[migrateToVersion]?.(migratedValue)) ??
migratedValue;
if (debug === true) {
if (debug) {
console.debug(
`[@wxt-dev/storage] Storage migration processed for version: v${migrateToVersion}`,
);
@@ -411,14 +459,16 @@ function createStorage(): WxtStorage {
{ key: driverMetaKey, value: { ...meta, v: targetVersion } },
]);
if (debug === true) {
if (debug) {
console.debug(
`[@wxt-dev/storage] Storage migration completed for ${key} v${targetVersion}`,
{ migratedValue },
);
}
onMigrationComplete?.(migratedValue, targetVersion);
};
const migrationsDone =
opts?.migrations == null
? Promise.resolve()
@@ -441,6 +491,9 @@ function createStorage(): WxtStorage {
const newValue = await opts.init();
await driver.setItem<any>(driverKey, newValue);
if (value == null && targetVersion > 1) {
await setMeta(driver, driverKey, { v: targetVersion });
}
return newValue;
});
@@ -449,62 +502,75 @@ function createStorage(): WxtStorage {
return {
key,
get defaultValue() {
return getFallback();
},
get fallback() {
return getFallback();
},
getValue: async () => {
await migrationsDone;
if (opts?.init) {
return await getOrInitValue();
} else {
return await getItem(driver, driverKey, opts);
}
},
getMeta: async () => {
await migrationsDone;
return await getMeta(driver, driverKey);
},
setValue: async (value) => {
await migrationsDone;
return await setItem(driver, driverKey, value);
},
setMeta: async (properties) => {
await migrationsDone;
return await setMeta(driver, driverKey, properties);
},
removeValue: async (opts) => {
await migrationsDone;
return await removeItem(driver, driverKey, opts);
},
removeMeta: async (properties) => {
await migrationsDone;
return await removeMeta(driver, driverKey, properties);
},
watch: (cb) =>
watch(driver, driverKey, (newValue, oldValue) =>
cb(newValue ?? getFallback(), oldValue ?? getFallback()),
),
migrate,
};
},
};
return storage;
}
function createDriver(storageArea: StorageArea): WxtStorageDriver {
const getStorageArea = () => {
if (browser.runtime == null) {
throw Error(
[
"'wxt/storage' must be loaded in a web extension environment",
'\n - If thrown during a build, see https://github.com/wxt-dev/wxt/issues/371',
" - If thrown during tests, mock 'wxt/browser' correctly. See https://wxt.dev/guide/go-further/testing.html\n",
].join('\n'),
);
throw Error(`'wxt/storage' must be loaded in a web extension environment
- If thrown during a build, see https://github.com/wxt-dev/wxt/issues/371
- If thrown during tests, mock 'wxt/browser' correctly. See https://wxt.dev/guide/go-further/testing.html
`);
}
if (browser.storage == null) {
throw Error(
"You must add the 'storage' permission to your manifest to use 'wxt/storage'",
@@ -514,18 +580,23 @@ function createDriver(storageArea: StorageArea): WxtStorageDriver {
const area = browser.storage[storageArea];
if (area == null)
throw Error(`"browser.storage.${storageArea}" is undefined`);
return area;
};
const watchListeners = new Set<(changes: StorageAreaChanges) => void>();
return {
getItem: async (key) => {
const res = await getStorageArea().get<Record<string, any>>(key);
return res[key];
},
getItems: async (keys) => {
const result = await getStorageArea().get(keys);
return keys.map((key) => ({ key, value: result[key] ?? null }));
},
setItem: async (key, value) => {
if (value == null) {
await getStorageArea().remove(key);
@@ -533,6 +604,7 @@ function createDriver(storageArea: StorageArea): WxtStorageDriver {
await getStorageArea().set({ [key]: value });
}
},
setItems: async (values) => {
const map = values.reduce<Record<string, unknown>>(
(map, { key, value }) => {
@@ -541,40 +613,51 @@ function createDriver(storageArea: StorageArea): WxtStorageDriver {
},
{},
);
await getStorageArea().set(map);
},
removeItem: async (key) => {
await getStorageArea().remove(key);
},
removeItems: async (keys) => {
await getStorageArea().remove(keys);
},
clear: async () => {
await getStorageArea().clear();
},
snapshot: async () => {
return await getStorageArea().get();
},
restoreSnapshot: async (data) => {
await getStorageArea().set(data);
},
watch(key, cb) {
const listener = (changes: StorageAreaChanges) => {
const change = changes[key] as {
newValue?: any;
oldValue?: any | null;
} | null;
if (change == null) return;
if (dequal(change.newValue, change.oldValue)) return;
if (change == null || dequal(change.newValue, change.oldValue)) return;
cb(change.newValue ?? null, change.oldValue ?? null);
};
getStorageArea().onChanged.addListener(listener);
watchListeners.add(listener);
return () => {
getStorageArea().onChanged.removeListener(listener);
watchListeners.delete(listener);
};
},
unwatch() {
watchListeners.forEach((listener) => {
getStorageArea().onChanged.removeListener(listener);
@@ -615,6 +698,7 @@ export interface WxtStorage {
| { key: StorageItemKey; options?: GetItemOptions<any> }
>,
): Promise<Array<{ key: StorageItemKey; value: any }>>;
/**
* Return an object containing metadata about the key. Object is stored at `key + "$"`. If value
* is not an object, it returns an empty object.
@@ -623,15 +707,17 @@ export interface WxtStorage {
* await storage.getMeta("local:installDate");
*/
getMeta<T extends Record<string, unknown>>(key: StorageItemKey): Promise<T>;
/**
* Get the metadata of multiple storage items.
*
* @param items List of keys or items to get the metadata of.
* @param keys List of keys or items to get the metadata of.
* @returns An array containing storage keys and their metadata.
*/
getMetas(
keys: Array<StorageItemKey | WxtStorageItem<any, any>>,
): Promise<Array<{ key: StorageItemKey; meta: any }>>;
/**
* Set a value in storage. Setting a value to `null` or `undefined` is equivalent to calling
* `removeItem`.
@@ -640,6 +726,7 @@ export interface WxtStorage {
* await storage.setItem<number>("local:installDate", Date.now());
*/
setItem<T>(key: StorageItemKey, value: T | null): Promise<void>;
/**
* Set multiple values in storage. If a value is set to `null` or `undefined`, the key is removed.
*
@@ -655,6 +742,7 @@ export interface WxtStorage {
| { item: WxtStorageItem<any, any>; value: any }
>,
): Promise<void>;
/**
* Sets metadata properties. If some properties are already set, but are not included in the
* `properties` parameter, they will not be removed.
@@ -666,10 +754,11 @@ export interface WxtStorage {
key: StorageItemKey,
properties: T | null,
): Promise<void>;
/**
* Set the metadata of multiple storage items.
*
* @param items List of storage keys or items and metadata to set for each.
* @param metas List of storage keys or items and metadata to set for each.
*/
setMetas(
metas: Array<
@@ -677,6 +766,7 @@ export interface WxtStorage {
| { item: WxtStorageItem<any, any>; meta: Record<string, any> }
>,
): Promise<void>;
/**
* Removes an item from storage.
*
@@ -684,6 +774,7 @@ export interface WxtStorage {
* await storage.removeItem("local:installDate");
*/
removeItem(key: StorageItemKey, opts?: RemoveItemOptions): Promise<void>;
/**
* Remove a list of keys from storage.
*/
@@ -715,6 +806,7 @@ export interface WxtStorage {
key: StorageItemKey,
properties?: string | string[],
): Promise<void>;
/**
* Return all the items in storage.
*/
@@ -722,15 +814,18 @@ export interface WxtStorage {
base: StorageArea,
opts?: SnapshotOptions,
): Promise<Record<string, unknown>>;
/**
* Restores the results of `snapshot`. If new properties have been saved since the snapshot, they are
* not overridden. Only values existing in the snapshot are overridden.
*/
restoreSnapshot(base: StorageArea, data: any): Promise<void>;
/**
* Watch for changes to a specific key in storage.
*/
watch<T>(key: StorageItemKey, cb: WatchCallback<T | null>): Unwatch;
/**
* Remove all watch listeners.
*/
@@ -786,42 +881,52 @@ export interface WxtStorageItem<
* The storage key passed when creating the storage item.
*/
key: StorageItemKey;
/**
* @deprecated Renamed to fallback, use it instead.
*/
defaultValue: TValue;
/**
* The value provided by the `fallback` option.
*/
fallback: TValue;
/**
* Get the latest value from storage.
*/
getValue(): Promise<TValue>;
/**
* Get metadata.
*/
getMeta(): Promise<NullablePartial<TMetadata>>;
/**
* Set the value in storage.
*/
setValue(value: TValue): Promise<void>;
/**
* Set metadata properties.
*/
setMeta(properties: NullablePartial<TMetadata>): Promise<void>;
/**
* Remove the value from storage.
*/
removeValue(opts?: RemoveItemOptions): Promise<void>;
/**
* Remove all metadata or certain properties from metadata.
*/
removeMeta(properties?: string[]): Promise<void>;
/**
* Listen for changes to the value in storage.
*/
watch(cb: WatchCallback<TValue>): Unwatch;
/**
* If there are migrations defined on the storage item, migrate to the latest version.
*
@@ -839,6 +944,7 @@ export interface GetItemOptions<T> {
* @deprecated Renamed to `fallback`, use it instead.
*/
defaultValue?: T;
/**
* Default value returned when `getItem` would otherwise return `null`.
*/
@@ -867,10 +973,12 @@ export interface WxtStorageItemOptions<T> {
* @deprecated Renamed to `fallback`, use it instead.
*/
defaultValue?: T;
/**
* Default value returned when `getValue` would otherwise return `null`.
*/
fallback?: T;
/**
* If passed, a value in storage will be initialized immediately after
* defining the storage item. This function returns the value that will be
@@ -878,20 +986,24 @@ export interface WxtStorageItemOptions<T> {
* already exist.
*/
init?: () => T | Promise<T>;
/**
* Provide a version number for the storage item to enable migrations. When changing the version
* in the future, migration functions will be ran on application startup.
*/
version?: number;
/**
* A map of version numbers to the functions used to migrate the data to that version.
*/
migrations?: Record<number, (oldValue: any) => any>;
/**
* Print debug logs, such as migration process.
* @default false
*/
debug?: boolean;
/**
* A callback function that runs on migration complete.
*/
@@ -909,10 +1021,12 @@ export type StorageAreaChanges = {
type NullablePartial<T> = {
[key in keyof T]+?: T[key] | undefined | null;
};
/**
* Callback called when a value in storage is changed.
*/
export type WatchCallback<T> = (newValue: T, oldValue: T) => void;
/**
* Call to remove a watch listener
*/
+2 -2
View File
@@ -1,5 +1,5 @@
import { defineConfig } from 'wxt';
import { presetUno } from 'unocss';
import { presetWind3 } from 'unocss';
export default defineConfig({
srcDir: 'src',
@@ -53,7 +53,7 @@ export default defineConfig({
],
},
},
presets: [presetUno()],
presets: [presetWind3()],
},
},
});
+47
View File
@@ -1,5 +1,52 @@
# Changelog
## v0.20.15
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.14...wxt-v0.20.15)
### 🚀 Enhancements
- Export `normalizePath` from `wxt` module ([#2080](https://github.com/wxt-dev/wxt/pull/2080))
- **popup:** Add Firefox `default_area` and `theme_icons` support ([#2097](https://github.com/wxt-dev/wxt/pull/2097))
- Add log `--level` cli option ([#1973](https://github.com/wxt-dev/wxt/pull/1973))
### 🩹 Fixes
- Content script is incorrectly invalidated when injected multiple times ([#2035](https://github.com/wxt-dev/wxt/pull/2035))
- Use `uiContainer` to apply position when creating shadow root ui ([#2036](https://github.com/wxt-dev/wxt/pull/2036))
- Make `ExtensionRunner#closeBrowser` optional ([#2092](https://github.com/wxt-dev/wxt/pull/2092))
- Rename `name` to `names` prop of `rollup` to fix deprecated warning ([#2106](https://github.com/wxt-dev/wxt/pull/2106))
### 💅 Refactors
- Standardize file existence checks to `pathExists` ([#2083](https://github.com/wxt-dev/wxt/pull/2083))
- Cleanup esm ([#1950](https://github.com/wxt-dev/wxt/pull/1950))
- `async`/`await` cleanup ([#2088](https://github.com/wxt-dev/wxt/pull/2088))
- Cleanup unnecssary checks ([#2089](https://github.com/wxt-dev/wxt/pull/2089))
- Simplify import paths ([#2093](https://github.com/wxt-dev/wxt/pull/2093))
### 📖 Documentation
- Fix JSDoc ([6ae5fb5e](https://github.com/wxt-dev/wxt/commit/6ae5fb5e))
### 🏡 Chore
- Cleanup comment line lengths ([#2090](https://github.com/wxt-dev/wxt/pull/2090))
- Cleanup `@ts-*` comments ([#2094](https://github.com/wxt-dev/wxt/pull/2094))
- Update JSDoc to use `@internal` ([#2091](https://github.com/wxt-dev/wxt/pull/2091))
- Fix typos ([#2086](https://github.com/wxt-dev/wxt/pull/2086))
- Remove unused code ([#2087](https://github.com/wxt-dev/wxt/pull/2087))
- Simplify imports of `cli/__tests__/index.ts` ([#2104](https://github.com/wxt-dev/wxt/pull/2104))
### ❤️ Contributors
- Nick Doan ([@nickbar01234](https://github.com/nickbar01234))
- Patryk Kuniczak ([@PatrykKuniczak](https://github.com/PatrykKuniczak))
- Aaron ([@aklinker1](https://github.com/aklinker1))
- Wotan-allfather <wotan-ai@proton.me>
- Namu ([@namuorg](https://github.com/namuorg))
- Eli ([@lishaduck](https://github.com/lishaduck))
## v0.20.14
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.13...wxt-v0.20.14)
+9 -9
View File
@@ -30,8 +30,8 @@ describe('Analysis', () => {
},
});
expect(await project.fileExists('stats.html')).toBe(true);
expect(await project.fileExists('.output/chrome-mv3/stats-0.json')).toBe(
expect(await project.pathExists('stats.html')).toBe(true);
expect(await project.pathExists('.output/chrome-mv3/stats-0.json')).toBe(
false,
);
});
@@ -52,9 +52,9 @@ describe('Analysis', () => {
},
});
expect(await project.fileExists('stats.html')).toBe(true);
expect(await project.fileExists('stats-0.json')).toBe(true);
expect(await project.fileExists('stats-1.json')).toBe(true);
expect(await project.pathExists('stats.html')).toBe(true);
expect(await project.pathExists('stats-0.json')).toBe(true);
expect(await project.pathExists('stats-1.json')).toBe(true);
});
it('should support customizing the stats output directory', async () => {
@@ -73,7 +73,7 @@ describe('Analysis', () => {
},
});
expect(await project.fileExists('stats/bundle.html')).toBe(true);
expect(await project.pathExists('stats/bundle.html')).toBe(true);
});
it('should place artifacts next to the custom output file', async () => {
@@ -93,9 +93,9 @@ describe('Analysis', () => {
},
});
expect(await project.fileExists('stats/bundle.html')).toBe(true);
expect(await project.fileExists('stats/bundle-0.json')).toBe(true);
expect(await project.fileExists('stats/bundle-1.json')).toBe(true);
expect(await project.pathExists('stats/bundle.html')).toBe(true);
expect(await project.pathExists('stats/bundle-0.json')).toBe(true);
expect(await project.pathExists('stats/bundle-1.json')).toBe(true);
});
it('should open the stats in the browser when requested', async () => {
+3 -3
View File
@@ -142,7 +142,7 @@ describe('Auto Imports', () => {
await project.prepare();
expect(await project.fileExists('.wxt/types/imports.d.ts')).toBe(false);
expect(await project.pathExists('.wxt/types/imports.d.ts')).toBe(false);
});
it('should only include imports-module.d.ts in the the project', async () => {
@@ -280,10 +280,10 @@ describe('Auto Imports', () => {
},
});
expect(await project.fileExists('.wxt/eslint-auto-imports.mjs')).toBe(
expect(await project.pathExists('.wxt/eslint-auto-imports.mjs')).toBe(
false,
);
expect(await project.fileExists('.wxt/eslintrc-auto-import.json')).toBe(
expect(await project.pathExists('.wxt/eslintrc-auto-import.json')).toBe(
false,
);
});
+3 -3
View File
@@ -77,7 +77,7 @@ describe('Module Helpers', () => {
await project.build(config);
expect(await project.fileExists('.output/chrome-mv3/injected.js')).toBe(
expect(await project.pathExists('.output/chrome-mv3/injected.js')).toBe(
true,
);
});
@@ -118,10 +118,10 @@ describe('Module Helpers', () => {
fileName: 'module.txt',
});
await expect(
project.fileExists('.output/chrome-mv3/module.txt'),
project.pathExists('.output/chrome-mv3/module.txt'),
).resolves.toBe(true);
await expect(
project.fileExists('.output/chrome-mv3/example/generated.txt'),
project.pathExists('.output/chrome-mv3/example/generated.txt'),
).resolves.toBe(true);
});
+10 -10
View File
@@ -126,7 +126,7 @@ describe('Output Directory Structure', () => {
await project.build({ browser: 'firefox' });
expect(await project.fileExists('.output/firefox-mv2/background.js')).toBe(
expect(await project.pathExists('.output/firefox-mv2/background.js')).toBe(
false,
);
});
@@ -146,7 +146,7 @@ describe('Output Directory Structure', () => {
await project.build({ browser: 'chrome' });
expect(await project.fileExists('.output/firefox-mv2/background.js')).toBe(
expect(await project.pathExists('.output/firefox-mv2/background.js')).toBe(
false,
);
});
@@ -174,7 +174,7 @@ describe('Output Directory Structure', () => {
await project.build();
expect(await project.fileExists('stats.html')).toBe(true);
expect(await project.pathExists('stats.html')).toBe(true);
});
it('should support JavaScript entrypoints', async () => {
@@ -210,14 +210,14 @@ describe('Output Directory Structure', () => {
----------------------------------------
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","background":{"service_worker":"background.js"},"content_scripts":[{"matches":["*://*.google.com/*"],"js":["content-scripts/content.js"]},{"matches":["*://*.duckduckgo.com/*"],"js":["content-scripts/named.js"]}]}"
`);
expect(await project.fileExists('.output/chrome-mv3/background.js'));
expect(await project.pathExists('.output/chrome-mv3/background.js'));
expect(
await project.fileExists('.output/chrome-mv3/content-scripts/content.js'),
await project.pathExists('.output/chrome-mv3/content-scripts/content.js'),
);
expect(
await project.fileExists('.output/chrome-mv3/content-scripts/named.js'),
await project.pathExists('.output/chrome-mv3/content-scripts/named.js'),
);
expect(await project.fileExists('.output/chrome-mv3/unlisted.js'));
expect(await project.pathExists('.output/chrome-mv3/unlisted.js'));
});
it('should support CSS entrypoints', async () => {
@@ -300,7 +300,7 @@ describe('Output Directory Structure', () => {
await project.build();
expect(await project.fileExists('dist/chrome-mv3/manifest.json')).toBe(
expect(await project.pathExists('dist/chrome-mv3/manifest.json')).toBe(
true,
);
});
@@ -335,7 +335,7 @@ describe('Output Directory Structure', () => {
await project.build({
vite: () => ({
build: {
// Make output for snapshot readible
// Make output for the snapshot readable
minify: false,
},
}),
@@ -408,7 +408,7 @@ describe('Output Directory Structure', () => {
await project.build({
vite: () => ({
build: {
// Make output for snapshot readible
// Make output for the snapshot readable
minify: false,
},
}),
+1 -1
View File
@@ -31,7 +31,7 @@ describe('React', () => {
await project.build();
expect(
await project.fileExists('.output/chrome-mv3/content-scripts/demo.js'),
await project.pathExists('.output/chrome-mv3/content-scripts/demo.js'),
).toBe(true);
expect(await project.serializeFile('.output/chrome-mv3/manifest.json'))
.toMatchInlineSnapshot(`
+1 -1
View File
@@ -20,7 +20,7 @@ describe('Remote Code', () => {
);
expect(output).not.toContain(url);
expect(
await project.fileExists(`.wxt/cache/${encodeURIComponent(url)}`),
await project.pathExists(`.wxt/cache/${encodeURIComponent(url)}`),
).toBe(true);
});
});
+3 -3
View File
@@ -108,7 +108,7 @@ describe('User Config', () => {
await project.build({ configFile: 'test.config.ts' });
expect(
await project.fileExists('.custom-output/chrome-mv3/background.js'),
await project.pathExists('.custom-output/chrome-mv3/background.js'),
).toBe(true);
});
@@ -129,13 +129,13 @@ describe('User Config', () => {
await project.build();
expect(
await project.fileExists('.output/test-chrome-mv3-production-build'),
await project.pathExists('.output/test-chrome-mv3-production-build'),
).toBe(true);
await project.build({ mode: 'development' });
expect(
await project.fileExists('.output/test-chrome-mv3-development-dev-build'),
await project.pathExists('.output/test-chrome-mv3-development-dev-build'),
).toBe(true);
});
+18 -18
View File
@@ -26,7 +26,7 @@ describe('Zipping', () => {
browser: 'firefox',
zip: { downloadPackages: ['flatten'] },
});
expect(await project.fileExists('.output/')).toBe(true);
expect(await project.pathExists('.output/')).toBe(true);
await extract(sourcesZip, { dir: unzipDir });
// Update package json wxt path
@@ -51,7 +51,7 @@ describe('Zipping', () => {
}),
).resolves.not.toHaveProperty('exitCode');
await expect(project.fileExists(unzipDir, '.output')).resolves.toBe(true);
await expect(project.pathExists(unzipDir, '.output')).resolves.toBe(true);
expect(
await project.serializeFile(
project.resolvePath(unzipDir, 'package.json'),
@@ -95,8 +95,8 @@ describe('Zipping', () => {
},
});
expect(await project.fileExists(artifactZip)).toBe(true);
expect(await project.fileExists(sourcesZip)).toBe(true);
expect(await project.pathExists(artifactZip)).toBe(true);
expect(await project.pathExists(sourcesZip)).toBe(true);
});
it('should not zip hidden files into sources by default', async () => {
@@ -117,8 +117,8 @@ describe('Zipping', () => {
browser: 'firefox',
});
await extract(sourcesZip, { dir: unzipDir });
expect(await project.fileExists(unzipDir, '.env')).toBe(false);
expect(await project.fileExists(unzipDir, '.hidden-dir/file')).toBe(false);
expect(await project.pathExists(unzipDir, '.env')).toBe(false);
expect(await project.pathExists(unzipDir, '.hidden-dir/file')).toBe(false);
});
it('should not zip files inside hidden directories if only the directory is specified', async () => {
@@ -142,8 +142,8 @@ describe('Zipping', () => {
},
});
await extract(sourcesZip, { dir: unzipDir });
expect(await project.fileExists(unzipDir, '.hidden-dir/file')).toBe(false);
expect(await project.fileExists(unzipDir, '.hidden-dir/nested/file')).toBe(
expect(await project.pathExists(unzipDir, '.hidden-dir/file')).toBe(false);
expect(await project.pathExists(unzipDir, '.hidden-dir/nested/file')).toBe(
false,
);
});
@@ -171,12 +171,12 @@ describe('Zipping', () => {
},
});
await extract(sourcesZip, { dir: unzipDir });
expect(await project.fileExists(unzipDir, '.env')).toBe(true);
expect(await project.fileExists(unzipDir, '.hidden-dir/file')).toBe(true);
expect(await project.fileExists(unzipDir, '.hidden-dir/nested/file1')).toBe(
expect(await project.pathExists(unzipDir, '.env')).toBe(true);
expect(await project.pathExists(unzipDir, '.hidden-dir/file')).toBe(true);
expect(await project.pathExists(unzipDir, '.hidden-dir/nested/file1')).toBe(
true,
);
expect(await project.fileExists(unzipDir, '.hidden-dir/nested/file2')).toBe(
expect(await project.pathExists(unzipDir, '.hidden-dir/nested/file2')).toBe(
true,
);
});
@@ -210,10 +210,10 @@ describe('Zipping', () => {
});
await extract(sourcesZip, { dir: unzipDir });
expect(
await project.fileExists(unzipDir, 'entrypoints/not-firefox.content.ts'),
await project.pathExists(unzipDir, 'entrypoints/not-firefox.content.ts'),
).toBe(false);
expect(
await project.fileExists(unzipDir, 'entrypoints/all.content.ts'),
await project.pathExists(unzipDir, 'entrypoints/all.content.ts'),
).toBe(true);
});
@@ -234,7 +234,7 @@ describe('Zipping', () => {
browser,
});
expect(await project.fileExists(sourcesZip)).toBe(true);
expect(await project.pathExists(sourcesZip)).toBe(true);
},
);
@@ -258,7 +258,7 @@ describe('Zipping', () => {
},
});
expect(await project.fileExists(sourcesZip)).toBe(true);
expect(await project.pathExists(sourcesZip)).toBe(true);
},
);
@@ -282,7 +282,7 @@ describe('Zipping', () => {
},
});
expect(await project.fileExists(sourcesZip)).toBe(false);
expect(await project.pathExists(sourcesZip)).toBe(false);
},
);
@@ -305,6 +305,6 @@ describe('Zipping', () => {
});
await extract(sourcesZip, { dir: unzipDir });
expect(await project.fileExists(unzipDir, 'manifest.json')).toBe(true);
expect(await project.pathExists(unzipDir, 'manifest.json')).toBe(true);
});
});
+5 -13
View File
@@ -10,7 +10,7 @@ import {
prepare,
zip,
} from '../src';
import { normalizePath } from '../src/core/utils/paths';
import { normalizePath } from '../src/core/utils';
import merge from 'lodash.merge';
// Run "pnpm wxt" to use the "wxt" dev script, not the "wxt" binary from the
@@ -138,14 +138,6 @@ export class TestProject {
return this.serializeDir('.output', ignoreContentsOfFilenames);
}
/**
* Read all the files from the test project's `.wxt` directory and combine them into a string
* that can be used in a snapshot.
*/
serializeWxtDir(): Promise<string> {
return this.serializeDir(resolve(this.root, '.wxt/types'));
}
/**
* Deeply print the filename and contents of all files in a directory.
*
@@ -185,13 +177,13 @@ export class TestProject {
].join(`\n${''.padEnd(40, '-')}\n`);
}
fileExists(...path: string[]): Promise<boolean> {
return fs.exists(this.resolvePath(...path));
pathExists(...path: string[]): Promise<boolean> {
return fs.pathExists(this.resolvePath(...path));
}
async getOutputManifest(
getOutputManifest(
path: string = '.output/chrome-mv3/manifest.json',
): Promise<any> {
return await fs.readJson(this.resolvePath(path));
return fs.readJson(this.resolvePath(path));
}
}
+10 -2
View File
@@ -1,7 +1,7 @@
{
"name": "wxt",
"type": "module",
"version": "0.20.14",
"version": "0.20.15",
"description": "⚡ Next-gen Web Extension Framework",
"license": "MIT",
"scripts": {
@@ -62,6 +62,14 @@
"vite-node": "^3.2.4 || ^5.0.0",
"web-ext-run": "^0.2.4"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0"
},
"peerDependenciesMeta": {
"eslint": {
"optional": true
}
},
"devDependencies": {
"@faker-js/faker": "^10.2.0",
"@types/fs-extra": "^11.0.4",
@@ -69,6 +77,7 @@
"@types/node": "^20.17.6",
"@types/normalize-path": "^3.0.2",
"@types/prompts": "^2.4.9",
"eslint": "^9.38.0",
"extract-zip": "^2.0.1",
"happy-dom": "^20.5.0",
"lodash.merge": "^4.6.2",
@@ -78,7 +87,6 @@
"vitest": "^4.0.18",
"vitest-plugin-random-seed": "^1.1.2"
},
"peerDependenciesMeta": {},
"repository": {
"type": "git",
"url": "git+https://github.com/wxt-dev/wxt.git"
+1 -1
View File
@@ -49,7 +49,7 @@ export default defineWxtModule({
// Only create global types when user has enabled auto-imports
entries.push(await getImportsDeclarationEntry(unimport));
if (wxt.config.imports.eslintrc.enabled === false) return;
if (!wxt.config.imports.eslintrc.enabled) return;
// Only generate ESLint config if that feature is enabled
entries.push(
+84 -7
View File
@@ -1,12 +1,14 @@
import { describe, it, vi, beforeEach, expect } from 'vitest';
import { build } from '../../core/build';
import { createServer } from '../../core/create-server';
import { zip } from '../../core/zip';
import { prepare } from '../../core/prepare';
import { clean } from '../../core/clean';
import { initialize } from '../../core/initialize';
import {
build,
createServer,
zip,
prepare,
clean,
initialize,
} from '../../core';
import { mock } from 'vitest-mock-extended';
import consola from 'consola';
import consola, { LogLevels } from 'consola';
vi.mock('../../core/build');
const buildMock = vi.mocked(build);
@@ -137,6 +139,21 @@ describe('CLI', () => {
expect(createServerMock).toBeCalledWith({
debug: true,
});
expect(consola.level).toBe(LogLevels.debug);
});
it('should set log --level', async () => {
mockArgv('--level', 'warn');
await importCli();
expect(consola.level).toBe(LogLevels.warn);
});
it('--debug should override --level', async () => {
mockArgv('--debug', '--level', 'silent');
await importCli();
expect(consola.level).toBe(LogLevels.debug);
});
});
@@ -229,6 +246,21 @@ describe('CLI', () => {
expect(buildMock).toBeCalledWith({
debug: true,
});
expect(consola.level).toBe(LogLevels.debug);
});
it('should set log --level', async () => {
mockArgv('build', '--level', 'warn');
await importCli();
expect(consola.level).toBe(LogLevels.warn);
});
it('--debug should override --level', async () => {
mockArgv('build', '--debug', '--level', 'silent');
await importCli();
expect(consola.level).toBe(LogLevels.debug);
});
});
@@ -310,6 +342,21 @@ describe('CLI', () => {
debug: true,
zip: {},
});
expect(consola.level).toBe(LogLevels.debug);
});
it('should set log --level', async () => {
mockArgv('zip', '--level', 'warn');
await importCli();
expect(consola.level).toBe(LogLevels.warn);
});
it('--debug should override --level', async () => {
mockArgv('zip', '--debug', '--level', 'silent');
await importCli();
expect(consola.level).toBe(LogLevels.debug);
});
it('should pass undefined for zipSources when --sources is not passed', async () => {
@@ -379,6 +426,21 @@ describe('CLI', () => {
expect(prepareMock).toBeCalledWith({
debug: true,
});
expect(consola.level).toBe(LogLevels.debug);
});
it('should set log --level', async () => {
mockArgv('prepare', '--level', 'warn');
await importCli();
expect(consola.level).toBe(LogLevels.warn);
});
it('--debug should override --level', async () => {
mockArgv('prepare', '--debug', '--level', 'silent');
await importCli();
expect(consola.level).toBe(LogLevels.debug);
});
});
@@ -413,6 +475,21 @@ describe('CLI', () => {
expect(cleanMock).toBeCalledWith({
debug: true,
});
expect(consola.level).toBe(LogLevels.debug);
});
it('should set log --level', async () => {
mockArgv('clean', '--level', 'warn');
await importCli();
expect(consola.level).toBe(LogLevels.warn);
});
it('--debug should override --level', async () => {
mockArgv('clean', '--debug', '--level', 'silent');
await importCli();
expect(consola.level).toBe(LogLevels.debug);
});
});
+9 -6
View File
@@ -1,5 +1,5 @@
import { CAC, Command } from 'cac';
import consola, { LogLevels } from 'consola';
import consola, { LogLevels, LogType } from 'consola';
import { filterTruthy, toArray } from '../core/utils/arrays';
import { printHeader } from '../core/utils/log';
import { formatDuration } from '../core/utils/time';
@@ -19,8 +19,13 @@ export function wrapAction(
},
) {
return async (...args: any[]) => {
// Enable consola's debug mode globally at the start of all commands when the `--debug` flag is
// passed
const level: LogType | undefined = args.find((arg) => arg?.level)?.level;
if (level && Object.keys(LogLevels).includes(level)) {
consola.level = LogLevels[level];
}
// Enable consola's debug mode globally at the start of all commands when
// the `--debug` flag is passed
const isDebug = !!args.find((arg) => arg?.debug);
if (isDebug) {
consola.level = LogLevels.debug;
@@ -40,9 +45,7 @@ export function wrapAction(
consola.fail(
`Command failed after ${formatDuration(Date.now() - startTime)}`,
);
if (err instanceof ValidationError) {
// Don't log these errors, they've already been logged
} else {
if (!(err instanceof ValidationError)) {
consola.error(err);
}
process.exit(1);
+4
View File
@@ -9,6 +9,10 @@ import {
const cli = cac('wxt');
cli.option('--debug', 'enable debug mode');
cli.option(
'--level <level>',
'specify log level ("silent" | "fatal" | "error" | "warn" | "log" | "info" | "success" | "fail" | "ready" | "start" | "box" | "debug" | "trace" | "verbose")',
);
// DEV
cli
+4 -4
View File
@@ -27,7 +27,7 @@ import { installSourcemapsSupport } from 'vite-node/source-map';
import { createExtensionEnvironment } from '../../utils/environments';
import { dirname, extname, join, relative } from 'node:path';
import fs from 'fs-extra';
import { normalizePath } from '../../utils/paths';
import { normalizePath } from '../../utils';
export async function createViteBuilder(
wxtConfig: ResolvedConfig,
@@ -146,10 +146,10 @@ export async function createViteBuilder(
),
// Output content script CSS to `content-scripts/`, but all other scripts are written to
// `assets/`.
assetFileNames: ({ name }) => {
assetFileNames: ({ names }) => {
if (
entrypoint.type === 'content-script' &&
name?.endsWith('css')
names[0].endsWith('css')
) {
return `content-scripts/${entrypoint.name}.[ext]`;
} else {
@@ -453,7 +453,7 @@ async function moveHtmlFiles(
);
// TODO: Optimize and only delete old path directories
removeEmptyDirs(config.outDir);
await removeEmptyDirs(config.outDir);
return movedChunks;
}
@@ -4,7 +4,7 @@ import {
fakeDevServer,
fakeResolvedConfig,
} from '../../../../utils/testing/fake-objects';
import { normalizePath } from '../../../../utils/paths';
import { normalizePath } from '../../../../utils';
import { resolve } from 'node:path';
import { parseHTML } from 'linkedom';
@@ -16,7 +16,7 @@ export function bundleAnalysis(config: ResolvedConfig): vite.Plugin {
}
/**
* @deprecated FOR TESTING ONLY.
* @internal FOR TESTING ONLY.
*/
export function resetBundleIncrement() {
increment = 0;
@@ -3,7 +3,7 @@ import { ResolvedConfig, WxtDevServer } from '../../../../types';
import { getEntrypointName } from '../../../utils/entrypoints';
import { parseHTML } from 'linkedom';
import { dirname, relative, resolve } from 'node:path';
import { normalizePath } from '../../../utils/paths';
import { normalizePath } from '../../../utils';
import { hash } from 'ohash';
// Stored outside the plugin to effect all instances of the devHtmlPrerender plugin.
@@ -1,5 +1,5 @@
import { Plugin } from 'vite';
import { ResolvedConfig, WxtDevServer } from '../../../../types';
import type { Plugin } from 'vite';
import type { ResolvedConfig, WxtDevServer } from '../../../../types';
/**
* Defines global constants about the dev server. Helps scripts connect to the server's web socket.
@@ -1,5 +1,5 @@
import { Plugin } from 'vite';
import { ResolvedConfig } from '../../../../types';
import type { Plugin } from 'vite';
import type { ResolvedConfig } from '../../../../types';
import { fetchCached } from '../../../utils/network';
/**
@@ -15,12 +15,12 @@ export function download(config: ResolvedConfig): Plugin {
resolveId(id) {
if (id.startsWith('url:')) return '\0' + id;
},
async load(id) {
load(id) {
if (!id.startsWith('\0url:')) return;
// Load file from network or cache
const url = id.replace('\0url:', '');
return await fetchCached(url, config);
return fetchCached(url, config);
},
};
}
@@ -27,7 +27,7 @@ export function extensionApiMock(config: ResolvedConfig): vite.PluginOption {
],
},
ssr: {
// Inline all WXT modules subdependencies can be mocked
// Inline all WXT modules sub-dependencies can be mocked
noExternal: ['wxt'],
},
};
@@ -1,4 +1,4 @@
import { Plugin } from 'vite';
import type { Plugin } from 'vite';
import { VIRTUAL_NOOP_BACKGROUND_MODULE_ID } from '../../../utils/constants';
/**
@@ -1,6 +1,6 @@
import { ResolvedConfig } from '../../../../types';
import * as vite from 'vite';
import { normalizePath } from '../../../utils/paths';
import type * as vite from 'vite';
import { normalizePath } from '../../../utils';
import { removeMainFunctionCode } from '../../../utils/transform';
import { resolve } from 'node:path';
@@ -1,4 +1,4 @@
import { exists } from 'fs-extra';
import { pathExists } from 'fs-extra';
import { resolve } from 'node:path';
import type * as vite from 'vite';
import { ResolvedConfig } from '../../../../types';
@@ -25,7 +25,7 @@ export function resolveAppConfig(config: ResolvedConfig): vite.Plugin {
async resolveId(id) {
if (id !== virtualModuleId) return;
return (await exists(appConfigFile))
return (await pathExists(appConfigFile))
? appConfigFile
: resolvedVirtualModuleId;
},
@@ -1,6 +1,6 @@
import { Plugin } from 'vite';
import type { Plugin } from 'vite';
import { ResolvedConfig } from '../../../../types';
import { normalizePath } from '../../../utils/paths';
import { normalizePath } from '../../../utils';
import {
VirtualModuleId,
virtualModuleNames,
@@ -1,6 +1,6 @@
import { parseHTML } from 'linkedom';
import type * as vite from 'vite';
import { normalizePath } from '../../../utils/paths';
import { normalizePath } from '../../../utils';
import { ResolvedConfig } from '../../../../types';
/**
+2 -2
View File
@@ -126,7 +126,7 @@ async function createServerInternal(): Promise<WxtDevServer> {
async stop() {
wasStopped = true;
keyboardShortcuts.stop();
await runner.closeBrowser();
await runner.closeBrowser?.();
await builderServer.close();
await wxt.hooks.callHook('server:closed', wxt, server);
@@ -150,7 +150,7 @@ async function createServerInternal(): Promise<WxtDevServer> {
server.ws.send('wxt:reload-extension');
},
async restartBrowser() {
await runner.closeBrowser();
await runner.closeBrowser?.();
keyboardShortcuts.stop();
await wxt.reloadConfig();
runner = await createExtensionRunner();
+6 -4
View File
@@ -3,7 +3,7 @@ import fs from 'fs-extra';
import { dirname, relative, resolve } from 'node:path';
import { getEntrypointBundlePath, isHtmlEntrypoint } from './utils/entrypoints';
import { getEntrypointGlobals, getGlobals } from './utils/globals';
import { normalizePath } from './utils/paths';
import { normalizePath } from './utils';
import path from 'node:path';
import { Message, parseI18nMessages } from './utils/i18n';
import { writeFileIfDifferent, getPublicFiles } from './utils/fs';
@@ -136,7 +136,7 @@ declare module "wxt/browser" {
'messages.json',
);
let messages: Message[];
if (await fs.exists(defaultLocalePath)) {
if (await fs.pathExists(defaultLocalePath)) {
const content = JSON.parse(await fs.readFile(defaultLocalePath, 'utf-8'));
messages = parseI18nMessages(content);
} else {
@@ -166,7 +166,8 @@ declare module "wxt/browser" {
};
const overrides = [
// Generate individual overloads for each message so JSDoc contains description and base translation.
// Generate individual overloads for each message so JSDoc contains
// description and base translation.
...messages.map((message) =>
renderGetMessageOverload(
`"${message.name}"`,
@@ -174,7 +175,8 @@ declare module "wxt/browser" {
message.message,
),
),
// Include a final union-based override so TS accepts valid string templates or concatenations
// Include a final union-based override so TS accepts valid string
// templates or concatenations.
// ie: browser.i18n.getMessage(`some_enum_${enumValue}`)
renderGetMessageOverload(
messages.map((message) => `"${message.name}"`).join(' | '),
+1
View File
@@ -6,3 +6,4 @@ export * from './create-server';
export * from './initialize';
export * from './prepare';
export * from './zip';
export * from './utils';
@@ -2,7 +2,7 @@ import { beforeAll, describe, expect, it } from 'vitest';
import path from 'node:path';
import { npm } from '../npm';
import spawn from 'nano-spawn';
import { exists } from 'fs-extra';
import { pathExists } from 'fs-extra';
describe('NPM Package Management Utils', () => {
describe('listDependencies', () => {
@@ -41,7 +41,7 @@ describe('NPM Package Management Utils', () => {
const actual = await npm.downloadDependency(id, downloadDir);
expect(actual).toEqual(expected);
expect(await exists(actual)).toBe(true);
expect(await pathExists(actual)).toBe(true);
});
});
});
+5 -12
View File
@@ -21,7 +21,7 @@ import consola, { LogLevels } from 'consola';
import defu from 'defu';
import { NullablyRequired } from './utils/types';
import fs from 'fs-extra';
import { normalizePath } from './utils/paths';
import { normalizePath } from './utils';
import glob from 'fast-glob';
import { builtinModules } from '../builtin-modules';
import { getEslintVersion } from './utils/eslint';
@@ -532,17 +532,10 @@ async function getUnimportEslintOptions(
* Returns the path to `node_modules/wxt`.
*/
function resolveWxtModuleDir() {
// TODO: Drop the __filename expression once we're fully running in ESM
// (see https://github.com/wxt-dev/wxt/issues/277)
const importer =
typeof __filename === 'string'
? pathToFileURL(__filename).href
: import.meta.url;
// TODO: Switch to import.meta.resolve() once the parent argument is unflagged
// (e.g. --experimental-import-meta-resolve) and all Node.js versions we support
// have it.
const url = esmResolve('wxt', importer);
const url = esmResolve('wxt', import.meta.url);
// esmResolve() returns the "wxt/dist/index.mjs" file, not the package's root
// directory, which we want to return from this function.
@@ -550,7 +543,7 @@ function resolveWxtModuleDir() {
}
async function isDirMissing(dir: string) {
return !(await fs.exists(dir));
return !(await fs.pathExists(dir));
}
function logMissingDir(logger: Logger, name: string, expected: string) {
@@ -580,8 +573,8 @@ export async function mergeBuilderConfig(
if (vite) {
return {
vite: async (env) => {
const resolvedInlineConfig = (await inlineConfig.vite?.(env)) ?? {};
const resolvedUserConfig = (await userConfig.vite?.(env)) ?? {};
const [resolvedInlineConfig = {}, resolvedUserConfig = {}] =
await Promise.all([inlineConfig.vite?.(env), userConfig.vite?.(env)]);
return vite.mergeConfig(resolvedUserConfig, resolvedInlineConfig);
},
};
@@ -38,7 +38,7 @@ describe('createExtensionRunner', () => {
});
it('should return a WSL runner when `is-wsl` is true', async () => {
isWslMock.mockResolvedValueOnce(true);
isWslMock.mockReturnValueOnce(true);
setFakeWxt({
config: {
browser: 'chrome',
@@ -51,7 +51,7 @@ describe('createExtensionRunner', () => {
});
it('should return a manual runner when `runner.disabled` is true', async () => {
isWslMock.mockResolvedValueOnce(false);
isWslMock.mockReturnValueOnce(false);
setFakeWxt({
config: {
browser: 'chrome',
+6 -6
View File
@@ -1,15 +1,15 @@
import { ExtensionRunner } from '../../types';
import { createWslRunner } from './wsl';
import { createWebExtRunner } from './web-ext';
import { createSafariRunner } from './safari';
import { createManualRunner } from './manual';
import type { ExtensionRunner } from '../../types';
import { isWsl } from '../utils/wsl';
import { wxt } from '../wxt';
import { createManualRunner } from './manual';
import { createSafariRunner } from './safari';
import { createWebExtRunner } from './web-ext';
import { createWslRunner } from './wsl';
export async function createExtensionRunner(): Promise<ExtensionRunner> {
if (wxt.config.browser === 'safari') return createSafariRunner();
if (await isWsl()) return createWslRunner();
if (isWsl()) return createWslRunner();
if (wxt.config.runnerConfig.config?.disabled) return createManualRunner();
return createWebExtRunner();
-3
View File
@@ -15,8 +15,5 @@ export function createManualRunner(): ExtensionRunner {
)}" as an unpacked extension manually`,
);
},
async closeBrowser() {
// noop
},
};
}
-3
View File
@@ -15,8 +15,5 @@ export function createSafariRunner(): ExtensionRunner {
)}" as an unpacked extension manually`,
);
},
async closeBrowser() {
// noop
},
};
}
+1 -1
View File
@@ -88,7 +88,7 @@ export function createWebExtRunner(): ExtensionRunner {
},
async closeBrowser() {
return await runner?.exit();
await runner?.exit();
},
};
}
-3
View File
@@ -15,8 +15,5 @@ export function createWslRunner(): ExtensionRunner {
)}" as an unpacked extension manually`,
);
},
async closeBrowser() {
// noop
},
};
}
@@ -106,6 +106,88 @@ describe('Manifest Utils', () => {
expect(actual[expectedType]).toEqual(expected);
},
);
it('should include default_area for Firefox in mv3', async () => {
const popup = fakePopupEntrypoint({
options: {
// @ts-expect-error: Force this to be undefined
mv2Key: null,
defaultArea: 'navbar',
},
outputDir: outDir,
skipped: false,
});
const buildOutput = fakeBuildOutput();
setFakeWxt({
config: {
manifestVersion: 3,
outDir,
},
});
const { manifest: actual } = await generateManifest(
[popup],
buildOutput,
);
expect((actual.action as any).default_area).toBe('navbar');
});
it('should include theme_icons for Firefox in mv3', async () => {
const themeIcons = [
{ light: '/icon-light-16.png', dark: '/icon-dark-16.png', size: 16 },
{ light: '/icon-light-32.png', dark: '/icon-dark-32.png', size: 32 },
];
const popup = fakePopupEntrypoint({
options: {
// @ts-expect-error: Force this to be undefined
mv2Key: null,
themeIcons,
},
outputDir: outDir,
skipped: false,
});
const buildOutput = fakeBuildOutput();
setFakeWxt({
config: {
manifestVersion: 3,
outDir,
},
});
const { manifest: actual } = await generateManifest(
[popup],
buildOutput,
);
expect((actual.action as any).theme_icons).toEqual(themeIcons);
});
it('should include default_area for Firefox in mv2', async () => {
const popup = fakePopupEntrypoint({
options: {
// @ts-expect-error: Force this to be undefined
mv2Key: null,
defaultArea: 'menupanel',
},
outputDir: outDir,
skipped: false,
});
const buildOutput = fakeBuildOutput();
setFakeWxt({
config: {
manifestVersion: 2,
outDir,
},
});
const { manifest: actual } = await generateManifest(
[popup],
buildOutput,
);
expect((actual.browser_action as any).default_area).toBe('menupanel');
});
});
describe('action without popup', () => {
@@ -1,5 +1,5 @@
import { beforeEach, describe, expect, it } from 'vitest';
import { DevModeChange, detectDevChanges } from '../../../utils/building';
import { DevModeChange, detectDevChanges } from '../detect-dev-changes';
import {
fakeBackgroundEntrypoint,
fakeContentScriptEntrypoint,
@@ -12,7 +12,7 @@ import {
fakeOutputChunk,
fakeWxt,
setFakeWxt,
} from '../../../utils/testing/fake-objects';
} from '../../testing/fake-objects';
import { BuildOutput, BuildStepOutput } from '../../../../types';
import { setWxtForTesting } from '../../../wxt';
@@ -13,11 +13,8 @@ import { resolve } from 'path';
import { findEntrypoints } from '../find-entrypoints';
import fs from 'fs-extra';
import glob from 'fast-glob';
import {
fakeResolvedConfig,
setFakeWxt,
} from '../../../utils/testing/fake-objects';
import { unnormalizePath } from '../../../utils/paths';
import { fakeResolvedConfig, setFakeWxt } from '../../testing/fake-objects';
import { unnormalizePath } from '../../paths';
import { wxt } from '../../../wxt';
vi.mock('fast-glob');
@@ -4,8 +4,8 @@ import {
EntrypointGroup,
OutputFile,
} from '../../../types';
import { every, some } from '../../utils/arrays';
import { normalizePath } from '../../utils/paths';
import { every, some } from '../arrays';
import { normalizePath } from '../paths';
import { wxt } from '../../wxt';
/**
@@ -207,10 +207,6 @@ interface ExtensionReload extends RebuildChange {
type: 'extension-reload';
}
// interface BrowserRestart extends RebuildChange {
// type: 'browser-restart';
// }
interface ContentScriptReload extends RebuildChange {
type: 'content-script-reload';
changedSteps: BuildStepOutput[];
@@ -21,9 +21,9 @@ import {
isHtmlEntrypoint,
isJsEntrypoint,
resolvePerBrowserOptions,
} from '../../utils/entrypoints';
import { VIRTUAL_NOOP_BACKGROUND_MODULE_ID } from '../../utils/constants';
import { CSS_EXTENSIONS_PATTERN } from '../../utils/paths';
} from '../entrypoints';
import { VIRTUAL_NOOP_BACKGROUND_MODULE_ID } from '../constants';
import { CSS_EXTENSIONS_PATTERN } from '../paths';
import pc from 'picocolors';
import { wxt } from '../../wxt';
import { camelCase } from 'scule';
@@ -77,9 +77,8 @@ export async function findEntrypoints(): Promise<Entrypoint[]> {
(entry) =>
entry.name === name && entry.inputPath.endsWith('index.html'),
);
if (hasIndexHtml) return false;
return true;
return !hasIndexHtml;
});
await wxt.hooks.callHook('entrypoints:found', wxt, entrypointInfos);
@@ -184,8 +183,7 @@ async function importEntrypoints(infos: EntrypointInfo[]) {
await Promise.all([
// HTML
...htmlInfos.map(async (info) => {
const res = await importHtmlEntrypoint(info);
resMap[info.inputPath] = res;
resMap[info.inputPath] = await importHtmlEntrypoint(info);
}),
// JS
(async () => {
@@ -268,7 +266,7 @@ async function getPopupEntrypoint(
info: EntrypointInfo,
options: Record<string, any>,
): Promise<PopupEntrypoint> {
const stictOptions: PopupEntrypoint['options'] = resolvePerBrowserOptions(
const strictOptions: PopupEntrypoint['options'] = resolvePerBrowserOptions(
{
browserStyle: options.browserStyle,
exclude: options.exclude,
@@ -276,16 +274,20 @@ async function getPopupEntrypoint(
defaultIcon: options.defaultIcon,
defaultTitle: options.title,
mv2Key: options.type,
defaultArea: options.defaultArea,
},
wxt.config.browser,
);
if (stictOptions.mv2Key && stictOptions.mv2Key !== 'page_action')
stictOptions.mv2Key = 'browser_action';
if (strictOptions.mv2Key && strictOptions.mv2Key !== 'page_action')
strictOptions.mv2Key = 'browser_action';
// themeIcons is an array of objects, not a per-browser option
const themeIcons = options.themeIcons;
return {
type: 'popup',
name: 'popup',
options: stictOptions,
options: { ...strictOptions, themeIcons },
inputPath: info.inputPath,
outputDir: wxt.config.outDir,
};
@@ -3,10 +3,10 @@ import { BuildOutput, Entrypoint } from '../../../types';
import pc from 'picocolors';
import fs from 'fs-extra';
import { groupEntrypoints } from './group-entrypoints';
import { formatDuration } from '../../utils/time';
import { printBuildSummary } from '../../utils/log';
import { formatDuration } from '../time';
import { printBuildSummary } from '../log';
import glob from 'fast-glob';
import { unnormalizePath } from '../../utils/paths';
import { unnormalizePath } from '../paths';
import { rebuild } from './rebuild';
import { relative } from 'node:path';
import {
@@ -87,7 +87,7 @@ export async function internalBuild(): Promise<BuildOutput> {
} else {
wxt.logger.info(`Opening ${pc.yellow(statsPath)} in browser...`);
const { default: open } = await import('open');
open(wxt.config.analysis.outputFile);
await open(wxt.config.analysis.outputFile);
}
}
}
@@ -133,12 +133,12 @@ function printValidationResults({
}, new Map<Entrypoint, ValidationResult[]>());
Array.from(entrypointErrors.entries()).forEach(([entrypoint, errors]) => {
wxt.logger.log(relative(cwd, entrypoint.inputPath));
console.log();
wxt.logger.log(relative(cwd, entrypoint.inputPath) + '\n');
errors.forEach((err) => {
const type = err.type === 'error' ? pc.red('ERROR') : pc.yellow('WARN');
const recieved = pc.dim(`(recieved: ${JSON.stringify(err.value)})`);
wxt.logger.log(` - ${type} ${err.message} ${recieved}`);
const received = pc.dim(`(received: ${JSON.stringify(err.value)})`);
wxt.logger.log(` - ${type} ${err.message} ${received}`);
});
console.log();
});
+2 -2
View File
@@ -9,8 +9,8 @@ export function defineCommand<TArgs extends any[]>(
},
) {
return async (...args: TArgs) => {
// Enable consola's debug mode globally at the start of all commands when the `--debug` flag is
// passed
// Enable consola's debug mode globally at the start of all commands when
// the `--debug` flag is passed
const isDebug = !!args.find((arg) => arg?.debug);
if (isDebug) {
consola.level = LogLevels.debug;
+1 -4
View File
@@ -1,9 +1,6 @@
export async function getEslintVersion(): Promise<string[]> {
try {
const require = (await import('node:module')).default.createRequire(
import.meta.url,
);
const { ESLint } = require('eslint');
const { ESLint } = await import('eslint');
return ESLint.version?.split('.') ?? [];
} catch {
// Return an empty version when there's an error importing ESLint
+1 -1
View File
@@ -28,7 +28,7 @@ export async function writeFileIfDifferent(
* `config.publicDir`.
*/
export async function getPublicFiles(): Promise<string[]> {
if (!(await fs.exists(wxt.config.publicDir))) return [];
if (!(await fs.pathExists(wxt.config.publicDir))) return [];
const files = await glob('**/*', { cwd: wxt.config.publicDir });
return files.map(unnormalizePath);
+1
View File
@@ -0,0 +1 @@
export { normalizePath } from './paths';
@@ -3,6 +3,5 @@ import { version } from '../../../version';
import { consola } from 'consola';
export function printHeader() {
console.log();
consola.log(`${pc.gray('WXT')} ${pc.gray(pc.bold(version))}`);
consola.log(`\n${pc.gray('WXT')} ${pc.gray(pc.bold(version))}`);
}
+12 -6
View File
@@ -281,6 +281,12 @@ function addEntrypoints(
if (popup.options.browserStyle)
// @ts-expect-error: Not typed by @wxt-dev/browser, but supported by Firefox
options.browser_style = popup.options.browserStyle;
if (popup.options.defaultArea)
// @ts-expect-error: Not typed by @wxt-dev/browser, but supported by Firefox
options.default_area = popup.options.defaultArea;
if (popup.options.themeIcons)
// @ts-expect-error: Not typed by @wxt-dev/browser, but supported by Firefox
options.theme_icons = popup.options.themeIcons;
if (manifest.manifest_version === 3) {
manifest.action = {
...manifest.action,
@@ -459,13 +465,13 @@ function discoverIcons(
}
function addDevModeCsp(manifest: Browser.runtime.Manifest): void {
let permissonUrl = wxt.server?.origin;
if (permissonUrl) {
const permissionUrlInstance = new URL(permissonUrl);
let permissionUrl = wxt.server?.origin;
if (permissionUrl) {
const permissionUrlInstance = new URL(permissionUrl);
permissionUrlInstance.port = '';
permissonUrl = permissionUrlInstance.toString();
permissionUrl = permissionUrlInstance.toString();
}
const permission = `${permissonUrl}*`;
const permission = `${permissionUrl}*`;
const allowedCsp = wxt.server?.origin ?? 'http://localhost:*';
if (manifest.manifest_version === 3) {
@@ -669,7 +675,7 @@ function convertCspToMv2(manifest: Browser.runtime.Manifest): void {
}
/**
* Make sure all resources are in MV3 format. If not, add a wanring
* Make sure all resources are in MV3 format. If not, add a warning.
*/
function validateMv3WebAccessibleResources(
manifest: Browser.runtime.Manifest,
+1 -7
View File
@@ -4,13 +4,7 @@ import { withTimeout } from './time';
function isOffline(): Promise<boolean> {
const isOffline = new Promise<boolean>((res) => {
dns.resolve('google.com', (err) => {
if (err == null) {
res(false);
} else {
res(true);
}
});
dns.resolve('google.com', (err) => res(err != null));
});
return withTimeout(isOffline, 1e3).catch(() => true);
}
+2 -2
View File
@@ -2,8 +2,8 @@ import systemPath from 'node:path';
import normalize from 'normalize-path';
/**
* Converts system paths to normalized bundler path. On windows and unix, this returns paths with /
* instead of \.
* Converts system paths to normalized bundler path.
* On Windows, this returns paths with `/` instead of `\`.
*/
export function normalizePath(path: string): string {
return normalize(path);
@@ -14,7 +14,6 @@ import {
OptionsEntrypoint,
PopupEntrypoint,
OutputChunk,
OutputFile,
OutputAsset,
BuildOutput,
BuildStepOutput,
@@ -25,7 +24,7 @@ import {
} from '../../../types';
import { mock } from 'vitest-mock-extended';
import { vi } from 'vitest';
import { setWxtForTesting } from '../../../core/wxt';
import { setWxtForTesting } from '../../wxt';
import type { Browser } from '@wxt-dev/browser';
faker.seed(import.meta.test.SEED);
@@ -146,6 +145,10 @@ export const fakePopupEntrypoint = fakeObjectCreator<PopupEntrypoint>(() => ({
'page_action',
undefined,
]),
// Firefox-specific options - kept undefined by default to avoid breaking existing tests
browserStyle: undefined,
defaultArea: undefined,
themeIcons: undefined,
},
skipped: faker.datatype.boolean(),
}));
@@ -204,10 +207,6 @@ export const fakeOutputAsset = fakeObjectCreator<OutputAsset>(() => ({
fileName: fakeFileName(),
}));
export function fakeOutputFile(): OutputFile {
return faker.helpers.arrayElement([fakeOutputAsset(), fakeOutputChunk()]);
}
export const fakeManifest = fakeObjectCreator<Browser.runtime.Manifest>(() => ({
manifest_version: faker.helpers.arrayElement([2, 3]),
name: faker.string.alphanumeric(),
+5 -3
View File
@@ -1,7 +1,9 @@
// TODO: Someone smarter than me should just mock this module instead.
import isWsl_ from 'is-wsl';
/**
* Returns true when running on WSL or WSL2.
*/
export async function isWsl(): Promise<boolean> {
const { default: isWsl } = await import('is-wsl'); // ESM only, requires dynamic import
return isWsl;
export function isWsl(): boolean {
return isWsl_;
}
+4 -2
View File
@@ -26,8 +26,10 @@ export async function registerWxt(
const hooks = createHooks<WxtHooks>();
const config = await resolveConfig(inlineConfig, command);
const builder = await createViteBuilder(config, hooks, () => wxt.server);
const pm = await createWxtPackageManager(config.root);
const [builder, pm] = await Promise.all([
createViteBuilder(config, hooks, () => wxt.server),
createWxtPackageManager(config.root),
]);
wxt = {
config,
+2 -2
View File
@@ -4,12 +4,12 @@ import fs from 'fs-extra';
import { safeFilename } from './utils/strings';
import { getPackageJson } from './utils/package';
import { formatDuration } from './utils/time';
import { printFileList } from './utils/log/printFileList';
import { printFileList } from './utils/log';
import { findEntrypoints, internalBuild } from './utils/building';
import { registerWxt, wxt } from './wxt';
import JSZip from 'jszip';
import glob from 'fast-glob';
import { normalizePath } from './utils/paths';
import { normalizePath } from './utils';
import { minimatchMultiple } from './utils/minimatch-multiple';
/**
+6 -6
View File
@@ -10,7 +10,7 @@ import type {
WxtModuleOptions,
WxtModuleSetup,
} from './types';
import * as vite from 'vite';
import type * as vite from 'vite';
import glob from 'fast-glob';
import { resolve } from 'node:path';
import type { UnimportOptions } from 'unimport';
@@ -109,7 +109,10 @@ export function addViteConfig(
wxt.hooks.hook('config:resolved', (wxt) => {
const userVite = wxt.config.vite;
wxt.config.vite = async (env) => {
const fromUser = await userVite(env);
const [vite, fromUser] = await Promise.all([
import('vite'),
userVite(env),
]);
const fromModule = viteConfig(env) ?? {};
return vite.mergeConfig(fromModule, fromUser);
};
@@ -167,11 +170,8 @@ export function addImportPreset(
preset: UnimportOptions['presets'][0],
): void {
wxt.hooks.hook('config:resolved', (wxt) => {
// In older versions of WXT, `wxt.config.imports` could be false
if (!wxt.config.imports) return;
wxt.config.imports.presets ??= [];
// De-dupelicate built-in named presets
// De-duplicate built-in named presets
if (wxt.config.imports.presets.includes(preset)) return;
wxt.config.imports.presets.push(preset);
+29 -2
View File
@@ -21,7 +21,7 @@ export interface InlineConfig {
* Directory containing all source code. Set to `"src"` to move all source code to a `src/`
* directory.
*
* After changing, don't forget to move the `public/` and `entrypoints/` directories into the new
* After changing, remember to move the `public/` and `entrypoints/` directories into the new
* source dir.
*
* @default config.root
@@ -648,6 +648,19 @@ export interface IsolatedWorldContentScriptEntrypointOptions extends BaseContent
world?: 'ISOLATED';
}
/**
* Firefox theme icon definition for light/dark mode support.
* @see https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/browser_action#theme_icons
*/
export interface ThemeIcon {
/** Path to the icon shown when the browser uses a light theme. */
light: string;
/** Path to the icon shown when the browser uses a dark theme. */
dark: string;
/** Icon size in pixels. */
size: number;
}
export interface PopupEntrypointOptions extends BaseEntrypointOptions {
/**
* Defaults to "browser_action" to be equivalent to MV3's "action" key
@@ -656,6 +669,18 @@ export interface PopupEntrypointOptions extends BaseEntrypointOptions {
defaultIcon?: Record<string, string>;
defaultTitle?: PerBrowserOption<string>;
browserStyle?: PerBrowserOption<boolean>;
/**
* Firefox only. Defines the part of the browser in which the button is initially placed.
* @see https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/action#default_area
*/
defaultArea?: PerBrowserOption<
'navbar' | 'menupanel' | 'tabstrip' | 'personaltoolbar'
>;
/**
* Firefox only. Icons for light and dark themes.
* @see https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/action#theme_icons
*/
themeIcons?: ThemeIcon[];
}
export interface OptionsEntrypointOptions extends BaseEntrypointOptions {
@@ -1403,7 +1428,9 @@ export interface FsCache {
export interface ExtensionRunner {
openBrowser(): Promise<void>;
closeBrowser(): Promise<void>;
closeBrowser?(): Promise<void>;
/** Whether or not this runner actually opens the browser. */
canOpen?(): boolean;
}
@@ -24,7 +24,7 @@ describe('Content Script Context', () => {
const onInvalidated = vi.fn();
ctx.onInvalidated(onInvalidated);
// @ts-ignore
// @ts-expect-error Deleting `runtime.id` to simulate disconnection
delete fakeBrowser.runtime.id;
const isValid = ctx.isValid;
@@ -1,7 +1,7 @@
/** @module wxt/utils/content-script-context */
import { ContentScriptDefinition } from '../types';
import { browser } from 'wxt/browser';
import { logger } from '../utils/internal/logger';
import { logger } from './internal/logger';
import {
WxtLocationChangeEvent,
getUniqueEventName,
@@ -43,23 +43,19 @@ export class ContentScriptContext implements AbortController {
'wxt:content-script-started',
);
private isTopFrame = window.self === window.top;
private id: string;
private abortController: AbortController;
private locationWatcher = createLocationWatcher(this);
private receivedMessageIds = new Set<string>();
constructor(
private readonly contentScriptName: string,
public readonly options?: Omit<ContentScriptDefinition, 'main'>,
) {
this.id = Math.random().toString(36).slice(2);
this.abortController = new AbortController();
if (this.isTopFrame) {
this.listenForNewerScripts({ ignoreFirstEvent: true });
this.stopOldScripts();
} else {
this.listenForNewerScripts();
}
this.stopOldScripts();
this.listenForNewerScripts();
}
get signal() {
@@ -243,43 +239,56 @@ export class ContentScriptContext implements AbortController {
}
stopOldScripts() {
// Use postMessage so it get's sent to all the frames of the page.
document.dispatchEvent(
new CustomEvent(ContentScriptContext.SCRIPT_STARTED_MESSAGE_TYPE, {
detail: {
contentScriptName: this.contentScriptName,
messageId: this.id,
},
}),
);
// Send message using `window.postMessage` for backwards compatibility to invalidate old versions before WXT changed to `document.dispatchEvent`
// TODO: Remove this once WXT version using `document.dispatchEvent` has been released for a while
window.postMessage(
{
type: ContentScriptContext.SCRIPT_STARTED_MESSAGE_TYPE,
contentScriptName: this.contentScriptName,
messageId: Math.random().toString(36).slice(2),
messageId: this.id,
},
'*',
);
}
verifyScriptStartedEvent(event: MessageEvent) {
const isScriptStartedEvent =
event.data?.type === ContentScriptContext.SCRIPT_STARTED_MESSAGE_TYPE;
verifyScriptStartedEvent(event: CustomEvent) {
const isSameContentScript =
event.data?.contentScriptName === this.contentScriptName;
const isNotDuplicate = !this.receivedMessageIds.has(event.data?.messageId);
return isScriptStartedEvent && isSameContentScript && isNotDuplicate;
event.detail?.contentScriptName === this.contentScriptName;
// Handle case where website dispatches the event again for some reason
const isFromSelf = event.detail?.messageId === this.id;
return isSameContentScript && !isFromSelf;
}
listenForNewerScripts(options?: { ignoreFirstEvent?: boolean }) {
let isFirst = true;
const cb = (event: MessageEvent) => {
if (this.verifyScriptStartedEvent(event)) {
this.receivedMessageIds.add(event.data.messageId);
const wasFirst = isFirst;
isFirst = false;
if (wasFirst && options?.ignoreFirstEvent) return;
this.notifyInvalidated();
listenForNewerScripts() {
const cb: EventListener = (event) => {
if (
!(event instanceof CustomEvent) ||
!this.verifyScriptStartedEvent(event)
) {
return;
}
this.notifyInvalidated();
};
addEventListener('message', cb);
this.onInvalidated(() => removeEventListener('message', cb));
document.addEventListener(
ContentScriptContext.SCRIPT_STARTED_MESSAGE_TYPE,
cb,
);
this.onInvalidated(() =>
document.removeEventListener(
ContentScriptContext.SCRIPT_STARTED_MESSAGE_TYPE,
cb,
),
);
}
}
@@ -19,7 +19,7 @@ export function createIframeUi<TMounted>(
iframe.src = browser.runtime.getURL(options.page);
wrapper.appendChild(iframe);
let mounted: TMounted | undefined = undefined;
let mounted: TMounted | undefined;
const mount = () => {
applyPosition(wrapper, iframe, options);
options.onBeforeMount?.(wrapper, iframe);
@@ -14,7 +14,7 @@ export function createIntegratedUi<TMounted>(
): IntegratedContentScriptUi<TMounted> {
const wrapper = document.createElement(options.tag || 'div');
let mounted: TMounted | undefined = undefined;
let mounted: TMounted | undefined;
const mount = () => {
applyPosition(wrapper, undefined, options);
mountUi(wrapper, options);
@@ -12,7 +12,7 @@ import {
isExist as mountDetector,
isNotExist as removeDetector,
} from '@1natsu/wait-element/detectors';
import { logger } from '../../utils/internal/logger';
import { logger } from '../internal/logger';
export function applyPosition(
root: HTMLElement,
@@ -108,7 +108,6 @@ export function mountUi(
break;
default:
options.append(anchor, root);
break;
}
}
@@ -116,7 +115,7 @@ export function createMountFunctions<TMounted>(
baseFunctions: BaseMountFunctions,
options: ContentScriptUiOptions<TMounted>,
): MountFunctions {
let autoMountInstance: AutoMount | undefined = undefined;
let autoMountInstance: AutoMount | undefined;
const stopAutoMount = () => {
autoMountInstance?.stopAutoMount();
@@ -213,7 +212,7 @@ function autoMountUi(
}
}
}
observeElement(resolvedAnchor);
void observeElement(resolvedAnchor);
return { stopAutoMount: _stopAutoMount };
}
@@ -29,9 +29,6 @@ let ws: WxtWebSocket | undefined;
/**
* Connect to the websocket and listen for messages.
*
* @param onMessage Optional callback that is called when a message is recieved and we've verified
* it's structure is what we expect.
*/
export function getDevServerWebSocket(): WxtWebSocket {
if (import.meta.env.COMMAND !== 'serve')
@@ -41,7 +41,7 @@ let result;
try {
initPlugins();
result = definition.main();
// @ts-expect-error: res shouldn't be a promise, but we're checking it anyways
// @ts-expect-error: Res shouldn't be a promise, but we're checking it anyways
if (result instanceof Promise) {
console.warn(
"The background's main() function return a promise, but it must be synchronous",
+2 -2
View File
@@ -1,4 +1,4 @@
import { exists, rm } from 'fs-extra';
import { pathExists, rm } from 'fs-extra';
let setupHappened = false;
@@ -13,7 +13,7 @@ export async function setup() {
globalThis.__ENTRYPOINT__ = 'test';
const e2eDistPath = './e2e/dist/';
if (await exists(e2eDistPath)) {
if (await pathExists(e2eDistPath)) {
await rm(e2eDistPath, { recursive: true, force: true });
}
}
+873
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -1,9 +1,9 @@
import {
determineSemverChange,
loadChangelogConfig,
parseCommits,
generateMarkDown,
loadChangelogConfig,
parseChangelogMarkdown,
parseCommits,
} from 'changelogen';
import spawn from 'nano-spawn';
import { getPkgTag, grabPackageDetails, listCommitsInDir } from './git';
@@ -12,7 +12,7 @@ import fs from 'fs-extra';
import path from 'node:path';
const pkg = process.argv[2];
if (pkg == null) {
if (!pkg) {
throw Error(
'Package name missing. Usage: tsx bump-package-version.ts <package-name>',
);
+3 -3
View File
@@ -1,5 +1,6 @@
import {
createGithubRelease,
GithubRelease,
loadChangelogConfig,
parseChangelogMarkdown,
} from 'changelogen';
@@ -8,7 +9,7 @@ import { grabPackageDetails } from './git';
import consola from 'consola';
const pkg = process.argv[2];
if (pkg == null) {
if (!pkg) {
throw Error(
'Package name missing. Usage: tsx create-github-release.ts <package-name>',
);
@@ -29,7 +30,6 @@ await createGithubRelease(config, {
tag_name: prevTag,
name: `${pkgName} v${currentVersion}`,
body: releases[0].body,
// @ts-expect-error: Not typed in changelogen, but present: https://docs.github.com/en/rest/releases/releases?apiVersion=2022-11-28#create-a-release
make_latest: String(pkg === 'wxt'),
});
} as GithubRelease & { make_latest: string });
consola.success('Created release');
+2 -2
View File
@@ -34,11 +34,11 @@ for dir in "$PACKAGES_DIR"/*; do
if [ -n "$last_tag" ]; then
# If a tag is found, show commits since that tag for the specific package directory
echo "Commits since last tag ($last_tag):"
git log "${last_tag}..HEAD" --oneline -- "$dir"
git log "${last_tag}..HEAD" --oneline -- "$dir" | grep -v -E "^[a-f0-9]* (chore|docs)"
else
# If no tag is found, show all commits for that package directory
echo "No tags found for this package. Listing all commits:"
git log --oneline -- "$dir"
git log --oneline -- "$dir" | grep -v -E "^[a-f0-9]* (chore|docs)"
fi
echo ""
fi
+1 -1
View File
@@ -9,7 +9,7 @@ import fs from 'fs-extra';
import consola from 'consola';
const pkg = process.argv[2];
if (pkg == null) {
if (!pkg) {
throw Error(
'Package name missing. Usage: tsx sync-releases.ts <package-name>',
);
+6 -12
View File
@@ -62,27 +62,22 @@ async function main(): Promise<never> {
const isMajor = ['-m', '--major'].some((arg) => args.includes(arg));
const upgrades = await detectUpgrades(dependencies, isMajor);
if (upgrades.length === 0) {
console.log();
consola.info("No upgrades found, you're up to date!");
console.log();
if (!upgrades.length) {
consola.info("\nNo upgrades found, you're up to date!\n");
process.exit(0);
}
printUpgrades(upgrades);
if (!isWrite) {
consola.info('Run with `-w` to write changes to package.json files');
console.log();
consola.info('Run with `-w` to write changes to package.json files\n');
process.exit(0);
}
consola.start('Writing new versions to package.json files...');
await writeUpgrades(packageJsonFiles, upgrades);
consola.success('Done!');
console.log();
consola.info('Run `pnpm i` to install new dependencies');
console.log();
consola.info('\nRun `pnpm i` to install new dependencies\n');
process.exit(0);
}
@@ -285,7 +280,6 @@ async function detectUpgrades(
}
if (upgradeToRange === currentRange) continue;
// if (currentVersion === latestVersion) continue;
results.push({
name: dep.name,
@@ -315,8 +309,8 @@ function printUpgrades(upgrades: UpgradeDetails[]): void {
);
const numberPadding = String(upgrades.length + 1).length + 1;
consola.info(`Found ${upgrades.length} upgrades:`);
console.log();
consola.info(`Found ${upgrades.length} upgrades:\n`);
for (let i = 0; i < upgrades.length; i++) {
const upgrade = upgrades[i];
const num = `\x1b[2m${(i + 1).toString().padStart(numberPadding)}.\x1b[0m`;
+1 -1
View File
@@ -23,6 +23,6 @@
"@types/react-dom": "^19.2.3",
"@wxt-dev/module-react": "^1.1.5",
"typescript": "^5.9.3",
"wxt": "^0.20.14"
"wxt": "^0.20.15"
}
}

Some files were not shown because too many files have changed in this diff Show More