Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 38345481df | |||
| da92449e0f | |||
| 23b3980645 | |||
| 81b4b629c9 | |||
| 4f7e9cb050 | |||
| b226d9b947 | |||
| aabba41e86 | |||
| f19e6908b0 | |||
| 5f651cc3fe | |||
| 8a6acbaaf2 | |||
| 13ab8e4d01 | |||
| bd039dcbab | |||
| 9cd4e8342b | |||
| c9633cefec | |||
| 1dd1370ed7 | |||
| 9ff90d0acf | |||
| b754435589 | |||
| 195d2cceaf | |||
| 420d78b638 | |||
| b35e499355 | |||
| 41c3d6d015 | |||
| 64610bad41 | |||
| 45b7d7c67f | |||
| 0bb2746869 | |||
| f9b0aa45f8 | |||
| 6f6ce33bbd | |||
| e1c6020997 | |||
| 012bd7e67f | |||
| 35778f70cb | |||
| cb4f9aa752 | |||
| f80fb42483 | |||
| 02c79ca244 | |||
| 50e8c86ce9 | |||
| 6fbfc15be7 | |||
| c793d3cf6b | |||
| b5f4d8cc1b | |||
| 77e6d1f5a4 | |||
| 3f149ac4fa | |||
| c0dbffaa82 | |||
| 707f03a3eb |
@@ -1,2 +1,5 @@
|
||||
* text=auto eol=lf
|
||||
pnpm-lock.yaml linguist-generated
|
||||
package-lock.json linguist-generated
|
||||
bun.lockb linguist-generated
|
||||
yarn.lock linguist-generated
|
||||
|
||||
@@ -43,6 +43,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup
|
||||
- uses: oven-sh/setup-bun@v1
|
||||
- name: pnpm test:coverage
|
||||
run: pnpm test:coverage --reporter=default --reporter=hanging-process
|
||||
- uses: codecov/codecov-action@v4
|
||||
|
||||
+111
-1
@@ -1,5 +1,115 @@
|
||||
# Changelog
|
||||
|
||||
## v0.17.7
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.17.6...v0.17.7)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- **zip:** List `.wxt/local_modules` overrides relative to the `package.json` they're added to ([#526](https://github.com/wxt-dev/wxt/pull/526))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Bump templates to v0.17 ([#524](https://github.com/wxt-dev/wxt/pull/524))
|
||||
- Increase test timeout for windows NPM tests ([#525](https://github.com/wxt-dev/wxt/pull/525))
|
||||
|
||||
## v0.17.6
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.17.5...v0.17.6)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Add warnings when important directories are missing ([#516](https://github.com/wxt-dev/wxt/pull/516))
|
||||
- Automatically remove top-level MV2-only or MV3-only keys ([#518](https://github.com/wxt-dev/wxt/pull/518))
|
||||
- Automatically generate `browser_action` based on `action` for MV2 ([#519](https://github.com/wxt-dev/wxt/pull/519))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- **zip:** List all private packages correctly in a PNPM workspace ([#520](https://github.com/wxt-dev/wxt/pull/520))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Mentions moving folders into `srcDir` ([9cd4e83](https://github.com/wxt-dev/wxt/commit/9cd4e83))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- **deps-dev:** Bump @types/react from 18.2.34 to 18.2.61 ([#510](https://github.com/wxt-dev/wxt/pull/510))
|
||||
|
||||
## v0.17.5
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.17.4...v0.17.5)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Expose package management utils under `wxt.pm` ([#502](https://github.com/wxt-dev/wxt/pull/502))
|
||||
- Download and override private packages for Firefox code review ([#507](https://github.com/wxt-dev/wxt/pull/507))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Fix typos ([#503](https://github.com/wxt-dev/wxt/pull/503))
|
||||
- Add docs about configuring the manifest as a function ([195d2cc](https://github.com/wxt-dev/wxt/commit/195d2cc))
|
||||
- Fix CLI generation ([b754435](https://github.com/wxt-dev/wxt/commit/b754435))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Use JSZip for `wxt zip`, enabling future features ([#501](https://github.com/wxt-dev/wxt/pull/501))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Btea ([@btea](http://github.com/btea))
|
||||
|
||||
## v0.17.4
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.17.3...v0.17.4)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Add basic content script to templates ([#495](https://github.com/wxt-dev/wxt/pull/495))
|
||||
- Add `ResolvedConfig.wxtModuleDir`, resolving the directory once ([#497](https://github.com/wxt-dev/wxt/pull/497))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Resolve the path to `node_modules/wxt` correctly ([#498](https://github.com/wxt-dev/wxt/pull/498))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Added DocVersionRedirector to "Using WXT" section ([#492](https://github.com/wxt-dev/wxt/pull/492))
|
||||
- Fix typos ([f80fb42](https://github.com/wxt-dev/wxt/commit/f80fb42))
|
||||
- Add CRXJS to comparison page ([cb4f9aa](https://github.com/wxt-dev/wxt/commit/cb4f9aa))
|
||||
- Update comparison page ([35778f7](https://github.com/wxt-dev/wxt/commit/35778f7))
|
||||
- Update context usage ([012bd7e](https://github.com/wxt-dev/wxt/commit/012bd7e))
|
||||
- Add testing example for `ContentScriptContext` ([e1c6020](https://github.com/wxt-dev/wxt/commit/e1c6020))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Fix tests after template change ([f9b0aa4](https://github.com/wxt-dev/wxt/commit/f9b0aa4))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Btea ([@btea](http://github.com/btea))
|
||||
- Leo Shklovskii <leo@thermopylae.net>
|
||||
|
||||
## v0.17.3
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.17.2...v0.17.3)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- **storage:** Guarantee `storage.getItems` returns values in the same order as requested ([b5f4d8c](https://github.com/wxt-dev/wxt/commit/b5f4d8c))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Content scripts crash when using `storage.defineItem` ([77e6d1f](https://github.com/wxt-dev/wxt/commit/77e6d1f))
|
||||
- **storage:** Revert #478 and run migrations when item is defined and properly wait for migrations before allowing read/writes ([#487](https://github.com/wxt-dev/wxt/pull/487), [#478](https://github.com/wxt-dev/wxt/issues/478))
|
||||
|
||||
## v0.17.2
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.17.1...v0.17.2)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Don't use sub-dependency binaries directly ([#482](https://github.com/wxt-dev/wxt/pull/482))
|
||||
|
||||
## v0.17.1
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.17.0...v0.17.1)
|
||||
@@ -14,7 +124,7 @@
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Leo Shklovskii <leo@thermopylae.net>
|
||||
- Leo Shklovskii ([@leos](https://github.com/leos))
|
||||
|
||||
## v0.17.0
|
||||
|
||||
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* A alias around `publish-extension` that is always installed on the path without having to install
|
||||
* `publish-browser-extension` as a direct dependency (like for PNPM, which doesn't link
|
||||
* sub-dependency binaries to "node_modules/.bin")
|
||||
*/
|
||||
require('publish-browser-extension/cli');
|
||||
@@ -15,4 +15,7 @@ export default defineConfig({
|
||||
alias: {
|
||||
public: 'src/public',
|
||||
},
|
||||
zip: {
|
||||
downloadPackages: ['sass'],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -18,6 +18,8 @@ const chromeExtensionIds = [
|
||||
'lknmjhcajhfbbglglccadlfdjbaiifig', // tl;dv - Record, Transcribe & ChatGPT for Google Meet
|
||||
'youtube中文配音/oglffgiaiekgeicdgkdlnlkhliajdlja', // Youtube中文配音
|
||||
'agjnjboanicjcpenljmaaigopkgdnihi', // PreMiD
|
||||
'aiakblgmlabokilgljkglggnpflljdgp', // Markdown Sticky Notes
|
||||
'nomnkbngkijpffepcgbbofhcnafpkiep', // DocVersionRedirector
|
||||
];
|
||||
|
||||
const { data, err, isLoading } = useListExtensionDetails(chromeExtensionIds);
|
||||
|
||||
@@ -19,7 +19,7 @@ const title = 'Next-gen Web Extension Framework';
|
||||
const titleSuffix = ' – WXT';
|
||||
|
||||
const description =
|
||||
"WXT provides the best developer experience, making it quick, easy, and fun to develop chrome extensions for all browsers. With built-in utilties for building, zipping, and publishing your extension, it's easy to get started.";
|
||||
"WXT provides the best developer experience, making it quick, easy, and fun to develop chrome extensions for all browsers. With built-in utilities for building, zipping, and publishing your extension, it's easy to get started.";
|
||||
const ogTitle = `${title}${titleSuffix}`;
|
||||
const ogUrl = 'https://wxt.dev';
|
||||
const ogImage = 'https://wxt.dev/social-preview.png';
|
||||
@@ -124,7 +124,7 @@ export default defineConfig({
|
||||
{ text: 'Options', link: '/entrypoints/options.md' },
|
||||
{ text: 'Popup', link: '/entrypoints/popup.md' },
|
||||
{ text: 'Sandbox', link: '/entrypoints/sandbox.md' },
|
||||
{ text: 'Sidepanel', link: '/entrypoints/sidepanel.md' },
|
||||
{ text: 'Side Panel', link: '/entrypoints/sidepanel.md' },
|
||||
{ text: 'Unlisted Pages', link: '/entrypoints/unlisted-pages.md' },
|
||||
{
|
||||
text: 'Unlisted Scripts',
|
||||
|
||||
@@ -32,14 +32,20 @@ export function generateCliDocs() {
|
||||
'```',
|
||||
];
|
||||
|
||||
const commands = await Promise.all(
|
||||
const commands = await Promise.allSettled(
|
||||
extractCommands(dev.info).map(async (command) => {
|
||||
const res = await execaCommand(`pnpm -s wxt ${command} --help`);
|
||||
const { rest: docs } = splitInfo(res.stdout);
|
||||
return [`## \`wxt ${command}\``, '```sh', docs, '```'];
|
||||
}),
|
||||
);
|
||||
lines.push(...commands);
|
||||
lines.push(
|
||||
...commands
|
||||
.filter((res) => res.status === 'fulfilled')
|
||||
.map(
|
||||
(res) => (res as unknown as PromiseFulfilledResult<string>).value,
|
||||
),
|
||||
);
|
||||
|
||||
const text = await format(
|
||||
PREFACE +
|
||||
|
||||
@@ -52,9 +52,11 @@ When defining multiple content scripts, content script entrypoints that have the
|
||||
|
||||
## Context
|
||||
|
||||
Old content scripts are not automatically stopped when an extension updates and reloads. Often, this leads to "Invalidated context" errors in production when a content script from an old version of your extension tries to use a extension API.
|
||||
Old content scripts are not automatically stopped when an extension updates and reloads. Often, this leads to "Invalidated context" errors in production when a content script from an old version of your extension tries to use a web extension API (ie, the `browser` or `chrome` globals).
|
||||
|
||||
WXT provides a utility for managing this process: `ContentScriptContext`. An instance of this class is provided to you automatically inside the `main` function of your content script.
|
||||
WXT provides a utility for handling this process: `ContentScriptContext`. An instance of this class is provided to you automatically inside the `main` function of your content script.
|
||||
|
||||
When your extension updates or is uninstalled, the context will become invalidated, and will trigger any `ctx.onInvalidated` listeners you add:
|
||||
|
||||
```ts
|
||||
export default defineContentScript({
|
||||
@@ -65,26 +67,33 @@ export default defineContentScript({
|
||||
// ...
|
||||
});
|
||||
|
||||
// Stop fetch requests
|
||||
fetch('...url', { signal: ctx.signal });
|
||||
|
||||
// Timeout utilities
|
||||
// Timeout utilities that are automatically cleared when invalidated
|
||||
ctx.setTimeout(() => {
|
||||
// ...
|
||||
}, 5e3);
|
||||
ctx.setInterval(() => {
|
||||
// ...
|
||||
}, 60e3);
|
||||
|
||||
// Or add event listeners that get removed when invalidated
|
||||
ctx.addEventListener(document, 'visibilitychange', (event) => {
|
||||
// ...
|
||||
});
|
||||
|
||||
// You can also stop fetch requests
|
||||
fetch('...url', { signal: ctx.signal });
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
The class extends [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) and provides other utilities for stopping a content script's logic once it becomes invalidated.
|
||||
|
||||
:::tip
|
||||
:::warning
|
||||
When working with content scripts, **you should always use the `ctx` object to stop any async or future work.**
|
||||
|
||||
This prevents old content scripts from interfering with new content scripts, and prevents error messages from the console in production.
|
||||
|
||||
If you're using a framework like React, Vue, Svelte, etc., make sure you're unmounting your UI properly in the `onRemove` option of [`createShadowRootUi`](https://wxt.dev/guide/content-script-ui.html#shadow-root).
|
||||
:::
|
||||
|
||||
## CSS
|
||||
|
||||
+29
-28
@@ -1,39 +1,40 @@
|
||||
# Compare
|
||||
|
||||
Lets compare the features of WXT vs [Plasmo](https://docs.plasmo.com/framework), another web extension framework.
|
||||
Lets compare the features of WXT vs [Plasmo](https://docs.plasmo.com/framework) (another web extension framework) and [CRXJS](https://crxjs.dev/vite-plugin) (the most popular bundler plugin).
|
||||
|
||||
## Overview
|
||||
|
||||
| Features | WXT | Plasmo |
|
||||
| ---------------------------------------------------- | :-------------------------: | :--------------------------------------: |
|
||||
| Supports all browsers | ✅ | ✅ |
|
||||
| MV2 Support | ✅ | ✅ |
|
||||
| MV3 Support | ✅ | ✅ |
|
||||
| Create Extension ZIPs | ✅ | ✅ |
|
||||
| Create Firefox Sources ZIP | ✅ | ❌ |
|
||||
| First-class TypeScript support | ✅ | ✅ |
|
||||
| File based entrypoint discovery | ✅ | ✅ |
|
||||
| Inline entrypoint config | ✅ | ✅ |
|
||||
| Auto-imports | ✅ | ❌ |
|
||||
| Supports all frontend frameworks | ✅ | 🟡 Only React, Vue, and Svelte |
|
||||
| Framework specific entrypoints (like `Popup.tsx`) | 🟡 `.html` `.ts` `.tsx` | ✅ `.html` `.ts` `.tsx` `.vue` `.svelte` |
|
||||
| Automated publishing | 🟡 Coming soon | ✅ |
|
||||
| Remote Code Bundling (Google Analytics) | ✅ | ✅ |
|
||||
| Features | WXT | Plasmo | CRXJS |
|
||||
| ---------------------------------------------------- | :-------------------------: | :--------------------------------------: | :---------------------------------------------------------------------: |
|
||||
| Supports all browsers | ✅ | ✅ | ❌ See [#56](https://github.com/crxjs/chrome-extension-tools/issues/56) |
|
||||
| MV2 Support | ✅ | ✅ | 🟡 Either MV2 or MV3 |
|
||||
| MV3 Support | ✅ | ✅ | 🟡 Either MV2 or MV3 |
|
||||
| Create Extension ZIPs | ✅ | ✅ | ❌ |
|
||||
| Create Firefox Sources ZIP | ✅ | ❌ | ❌ |
|
||||
| First-class TypeScript support | ✅ | ✅ | ✅ |
|
||||
| Entrypoint discovery | File based | File based | ❌ |
|
||||
| Inline entrypoint config | ✅ | ✅ | Manifest based |
|
||||
| Auto-imports | ✅ | ❌ | ❌ |
|
||||
| Supports all frontend frameworks | ✅ | 🟡 Only React, Vue, and Svelte | ✅ |
|
||||
| Framework specific entrypoints (like `Popup.tsx`) | 🟡 `.html` `.ts` `.tsx` | ✅ `.html` `.ts` `.tsx` `.vue` `.svelte` | ❌ |
|
||||
| Automated publishing | ✅ | ✅ | ❌ |
|
||||
| Remote Code Bundling (Google Analytics) | ✅ | ✅ | ❌ |
|
||||
| <strong style="opacity: 50%">Dev Mode</strong> | | |
|
||||
| `.env` Files | ✅ | ✅ |
|
||||
| Opens browser and install extension | ✅ | ❌ |
|
||||
| HMR for UIs | ✅ | 🟡 React only |
|
||||
| Reload HTML Files on Change | ✅ | 🟡 Reloads entire extension |
|
||||
| Reload Content Scripts on Change | ✅ | 🟡 Reloads entire extension |
|
||||
| Reload Background on Change | 🟡 Reloads entire extension | 🟡 Reloads entire extension |
|
||||
| <strong style="opacity: 50%">Built-in Utils</strong> | | |
|
||||
| Storage | ✅ | ✅ |
|
||||
| Messaging | 🟡 Coming soon | ✅ |
|
||||
| Content Script UI | ✅ | ✅ |
|
||||
| `.env` Files | ✅ | ✅ | ✅ |
|
||||
| Opens browser and install extension | ✅ | ❌ | ❌ |
|
||||
| HMR for UIs | ✅ | 🟡 React only | ✅ |
|
||||
| Reload HTML Files on Change | ✅ | 🟡 Reloads entire extension | ✅ |
|
||||
| Reload Content Scripts on Change | ✅ | 🟡 Reloads entire extension | ✅ |
|
||||
| Reload Background on Change | 🟡 Reloads entire extension | 🟡 Reloads entire extension | 🟡 Reloads entire extension |
|
||||
| Respects Content Script `run_at` | ✅ | ✅ | ❌ ESM-style loaders run asynchronously |
|
||||
| <strong style="opacity: 50%">Built-in Utils</strong> | | | |
|
||||
| Storage | ✅ | ✅ | ❌ |
|
||||
| Messaging | ❌ | ✅ | ❌ |
|
||||
| Content Script UI | ✅ | ✅ | ❌ |
|
||||
|
||||
## Dev Mode
|
||||
## Why use WXT?
|
||||
|
||||
WXT's main goal is improving the development experience (DX) of creating web extensions. There are two things WXT does differently:
|
||||
WXT's main goal is improving the development experience (DX) of creating web extensions, while not sacrificing support. There are two things WXT does differently:
|
||||
|
||||
1. Automatically opens a browser with the extension installed when starting development
|
||||
2. Reload each part of the extension individually rather than reloading the entire extension
|
||||
|
||||
@@ -114,7 +114,7 @@ WXT 0.14.1
|
||||
[8:55:54 AM] ERROR entrypoints/content.ts: Cannot use imported variable "GOOGLE_MATCHES" before main function. See https://wxt.dev/guide/entrypoints.html#side-effects
|
||||
```
|
||||
|
||||
This throws an error because WXT needs to import each entrypoint during the build process to extract its definition (containing the `match`, `run_at`, `include`/`exclude`, etc.) to render the `manifest.json` correctly. Before loading an entrypoint, a transformation is applied to remove all imports. This prevents imported modules (local or NPM) with side-effects from running during the build process, potentially throwing an error.
|
||||
This throws an error because WXT needs to import each entrypoint during the build process to extract its definition (containing the `match`, `runAt`, `include`/`exclude`, etc.) to render the `manifest.json` correctly. Before loading an entrypoint, a transformation is applied to remove all imports. This prevents imported modules (local or NPM) with side-effects from running during the build process, potentially throwing an error.
|
||||
|
||||
:::details Why?
|
||||
|
||||
|
||||
+115
-5
@@ -1,3 +1,7 @@
|
||||
---
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# Manifest.json
|
||||
|
||||
The manifest.json is generated at build-time based on files in the `entrypoints/` directory and `wxt.config.ts`.
|
||||
@@ -20,11 +24,11 @@ export default defineConfig({
|
||||
});
|
||||
```
|
||||
|
||||
## `name`
|
||||
### `name`
|
||||
|
||||
If not provided via the `manifest` config, the [manifest's `name`](https://developer.chrome.com/docs/extensions/mv3/manifest/name/) defaults to your package.json's `name` property.
|
||||
|
||||
## `version` and `version_name`
|
||||
### `version` and `version_name`
|
||||
|
||||
The [manifest's `version` and `version_name`](https://developer.chrome.com/docs/extensions/mv3/manifest/version/) properties are based on the `version` field listed in your `package.json` or `wxt.config.ts`.
|
||||
|
||||
@@ -33,7 +37,7 @@ The [manifest's `version` and `version_name`](https://developer.chrome.com/docs/
|
||||
|
||||
If a version is not found, a warning is logged and the version defaults to `"0.0.0"`.
|
||||
|
||||
### Example
|
||||
#### Example
|
||||
|
||||
```json
|
||||
// package.json
|
||||
@@ -50,7 +54,7 @@ If a version is not found, a warning is logged and the version defaults to `"0.0
|
||||
}
|
||||
```
|
||||
|
||||
## `icons`
|
||||
### `icons`
|
||||
|
||||
By default, WXT will discover icons in your [`public` directory](/guide/assets#public-directory) and use them for the [manifest's `icons`](https://developer.chrome.com/docs/extensions/mv3/manifest/icons/).
|
||||
|
||||
@@ -83,7 +87,7 @@ export default defineConfig({
|
||||
});
|
||||
```
|
||||
|
||||
## `permissions`
|
||||
### `permissions`
|
||||
|
||||
[Permissions](https://developer.chrome.com/docs/extensions/reference/permissions/) must be listed in the manifest config.
|
||||
|
||||
@@ -125,3 +129,109 @@ export default defineConfig({
|
||||
See the official localization examples for more details:
|
||||
|
||||
<ExampleList tag="i18n" />
|
||||
|
||||
## Per-Manifest Version Config
|
||||
|
||||
WXT applies several transformations to your manifest to simplify managing both MV2 and MV3 keys in your `wxt.config.ts` file:
|
||||
|
||||
1. Top level MV2-only or MV3-only keys are stripped from the final manifest when targeting the other manifest version
|
||||
2. Some keys, are automatically converted between versions when possible:
|
||||
- Define `web_accessible_resources` in it's MV3 style and it will be converted to the MV2 style automatically
|
||||
- `action` will automatically be converted to `browser_action` for MV3. To use `page_action` instead, add both `action` and `page_action` entries to your manifest
|
||||
|
||||
For example, a `wxt.config.ts` file that looks like this:
|
||||
|
||||
```ts
|
||||
import { defineConfig } from 'wxt';
|
||||
|
||||
export default defineConfig({
|
||||
mainfest: {
|
||||
action: {
|
||||
default_title: 'Some Title',
|
||||
},
|
||||
web_accessible_resources: [
|
||||
{
|
||||
matches: ['*://*.google.com/*'],
|
||||
resources: ['icon/*.png'],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Will be output differently for each manifest version:
|
||||
|
||||
:::code-group
|
||||
|
||||
```json [MV2]
|
||||
{
|
||||
"manifest_version": 2,
|
||||
// ...
|
||||
"browser_action": {
|
||||
"default_title": "Some Title"
|
||||
},
|
||||
"web_accessible_resources": ["icon/*.png"]
|
||||
}
|
||||
```
|
||||
|
||||
```json [MV3]
|
||||
{
|
||||
"manifest_version": 3,
|
||||
// ...
|
||||
"action": {
|
||||
"default_title": "Some Title"
|
||||
},
|
||||
"web_accessible_resources": [
|
||||
{
|
||||
"matches": ["*://*.google.com/*"],
|
||||
"resources": ["icon/*.png"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
:::tip
|
||||
If this isn't enough control for your use-case, remember you can use a function for the `manifest` key and generate it however you'd like, or you can use the `build:manifestGenerated` hook to apply additional transformations.
|
||||
:::
|
||||
|
||||
## Per-Browser Configuration
|
||||
|
||||
The `manifest` field can be a function. If you are building and extension for multiple browsers, and need to modify the manifest per browser, using a function instead of an object is very useful.
|
||||
|
||||
```ts
|
||||
export default defineConfig({
|
||||
manifest: ({ browser, manifestVersion, mode, command }) => {
|
||||
return {
|
||||
// Your manifest
|
||||
};
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
:::info
|
||||
The first argument is of type `ConfigEnv`. See the [API reference](/api/wxt/interfaces/ConfigEnv) for info about each property.
|
||||
:::
|
||||
|
||||
For example, say you use OAuth, and you need to provide a different `oauth.client_id` for each browser:
|
||||
|
||||
```ts
|
||||
const clientIds = {
|
||||
chrome: '<your-chrome-client-id>',
|
||||
edge: '<your-edge-client-id>',
|
||||
firefox: '<your-firefox-client-id>',
|
||||
opera: '<your-opera-client-id>',
|
||||
};
|
||||
|
||||
export default defineConfig({
|
||||
manifest: ({ browser }) => ({
|
||||
oauth: {
|
||||
client_id: clientIds[browser],
|
||||
scopes: [
|
||||
// ...
|
||||
],
|
||||
},
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
@@ -136,6 +136,28 @@ See Issue [#377](https://github.com/wxt-dev/wxt/issues/377) for more details.
|
||||
|
||||
Ensure that you have a `README.md` or `SOURCE_CODE_REVIEW.md` file with the above commands so that the Firefox team knows how to build your extension.
|
||||
|
||||
#### Private Packages
|
||||
|
||||
If you use private packages and you don't want to provide your auth token to the Firefox team during the review process, you can use `zip.downloadPackages` to download any private packages and include them in the zip.
|
||||
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
export default defineConfig({
|
||||
zip: {
|
||||
downloadPackages: [
|
||||
'@mycompany/some-package',
|
||||
//...
|
||||
],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Depending on your package manager, the `package.json` in the sources zip will be modified to use the downloaded dependencies via the `overrides` or `resolutions` field.
|
||||
|
||||
:::warning
|
||||
WXT uses the command `npm pack <package-name>` to download the package. That means regardless of your package manager, you need to properly setup a `.npmrc` file. NPM and PNPM both respect `.npmrc` files, but Yarn and Bun have their own ways of authorizing private registries, so you'll need to add an `.npmrc` file.
|
||||
:::
|
||||
|
||||
### Safari
|
||||
|
||||
> 🚧 Not supported yet
|
||||
|
||||
+1
-24
@@ -263,27 +263,4 @@ export const ignoredWebsites = storage.defineItem<IgnoredWebsiteV2[]>( // [!code
|
||||
|
||||
### Running Migrations
|
||||
|
||||
To run migrations, you have two options:
|
||||
|
||||
1. Import all versioned storage items into your background script and they will run automatically whenever your extension updates
|
||||
2. Manually call `item.migrate()`
|
||||
|
||||
The first approach is recommended. To make importing all your storage items easy, you can define all of them in a single file, `utils/storage.ts`, and import that file into your background entrypoint:
|
||||
|
||||
```ts
|
||||
// utils/storage.ts
|
||||
export countStorage = storage.defineItem(...);
|
||||
export themeStorage = storage.defineItem(...);
|
||||
export someOtherStorage = storage.defineItem(...);
|
||||
```
|
||||
|
||||
```ts
|
||||
// entrypoints/background.ts
|
||||
import '@/utils/storage'; // This import runs migrations on updates
|
||||
|
||||
export default defineBackground({
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
> When you call `storage.defineItem`, a `browser.runtime.onInstalled` listener is added. `onInstalled` listeners are only triggered in the background, which is why you must import them into the background.
|
||||
As soon as `storage.defineItem` is called, WXT checks if migrations need to be ran, and if so, runs them. Calls to get or update the storage item's value or metadata (`getValue`, `setValue`, `removeValue`, `getMeta`, etc) will automatically wait for the migration process to finish before actually reading or writing values.
|
||||
|
||||
@@ -25,6 +25,7 @@ describe('Init command', () => {
|
||||
"assets/vue.svg",
|
||||
"components/HelloWorld.vue",
|
||||
"entrypoints/background.ts",
|
||||
"entrypoints/content.ts",
|
||||
"entrypoints/popup/App.vue",
|
||||
"entrypoints/popup/index.html",
|
||||
"entrypoints/popup/main.ts",
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { TestProject } from '../utils';
|
||||
import extract from 'extract-zip';
|
||||
import { execaCommand } from 'execa';
|
||||
import { readFile, writeFile } from 'fs-extra';
|
||||
|
||||
process.env.WXT_PNPM_IGNORE_WORKSPACE = 'true';
|
||||
|
||||
describe('Zipping', () => {
|
||||
it('should download packages and produce a valid build when zipping sources', async () => {
|
||||
const project = new TestProject({
|
||||
name: 'test',
|
||||
version: '1.0.0',
|
||||
dependencies: {
|
||||
flatten: '1.0.3',
|
||||
},
|
||||
});
|
||||
project.addFile(
|
||||
'entrypoints/background.ts',
|
||||
'export default defineBackground(() => {});',
|
||||
);
|
||||
const unzipDir = project.resolvePath('.output/test-1.0.0-sources');
|
||||
const sourcesZip = project.resolvePath('.output/test-1.0.0-sources.zip');
|
||||
|
||||
await project.zip({
|
||||
browser: 'firefox',
|
||||
zip: { downloadPackages: ['flatten'] },
|
||||
});
|
||||
expect(await project.fileExists('.output/')).toBe(true);
|
||||
|
||||
await extract(sourcesZip, { dir: unzipDir });
|
||||
// Update package json wxt path
|
||||
const packageJsonPath = project.resolvePath(unzipDir, 'package.json');
|
||||
const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf-8'));
|
||||
packageJson.dependencies.wxt = '../../../../..';
|
||||
await writeFile(
|
||||
packageJsonPath,
|
||||
JSON.stringify(packageJson, null, 2),
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
// Build zipped extension
|
||||
await expect(
|
||||
execaCommand('pnpm i --ignore-workspace --frozen-lockfile false', {
|
||||
cwd: unzipDir,
|
||||
}),
|
||||
).resolves.toMatchObject({ exitCode: 0 });
|
||||
await expect(
|
||||
execaCommand('pnpm wxt build -b firefox', { cwd: unzipDir }),
|
||||
).resolves.toMatchObject({ exitCode: 0 });
|
||||
|
||||
await expect(project.fileExists(unzipDir, '.output')).resolves.toBe(true);
|
||||
expect(
|
||||
await project.serializeFile(
|
||||
project.resolvePath(unzipDir, 'package.json'),
|
||||
),
|
||||
).toMatchInlineSnapshot(`
|
||||
".output/test-1.0.0-sources/package.json
|
||||
----------------------------------------
|
||||
{
|
||||
"name": "test",
|
||||
"description": "Example description",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"wxt": "../../../../..",
|
||||
"flatten": "1.0.3"
|
||||
},
|
||||
"resolutions": {
|
||||
"flatten@1.0.3": "file://./.wxt/local_modules/flatten-1.0.3.tgz"
|
||||
}
|
||||
}"
|
||||
`);
|
||||
});
|
||||
});
|
||||
+2
-2
@@ -175,8 +175,8 @@ export class TestProject {
|
||||
].join(`\n${''.padEnd(40, '-')}\n`);
|
||||
}
|
||||
|
||||
fileExists(path: string): Promise<boolean> {
|
||||
return fs.exists(this.resolvePath(path));
|
||||
fileExists(...path: string[]): Promise<boolean> {
|
||||
return fs.exists(this.resolvePath(...path));
|
||||
}
|
||||
|
||||
async getOutputManifest(
|
||||
|
||||
+11
-7
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "wxt",
|
||||
"type": "module",
|
||||
"version": "0.17.1",
|
||||
"version": "0.17.7",
|
||||
"description": "Next gen framework for developing web extensions",
|
||||
"engines": {
|
||||
"node": ">=18",
|
||||
@@ -30,7 +30,10 @@
|
||||
"bin",
|
||||
"dist"
|
||||
],
|
||||
"bin": "./bin/wxt.mjs",
|
||||
"bin": {
|
||||
"wxt": "./bin/wxt.mjs",
|
||||
"wxt-publish-extension": "./bin/wxt-publish-extension.cjs"
|
||||
},
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
@@ -103,6 +106,7 @@
|
||||
"sync-releases": "pnpx changelogen@latest gh release"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aklinker1/rollup-plugin-visualizer": "5.12.0",
|
||||
"@types/webextension-polyfill": "^0.10.5",
|
||||
"@webext-core/fake-browser": "^1.3.1",
|
||||
"@webext-core/isolated-element": "^1.1.1",
|
||||
@@ -124,21 +128,20 @@
|
||||
"is-wsl": "^3.0.0",
|
||||
"jiti": "^1.21.0",
|
||||
"json5": "^2.2.3",
|
||||
"jszip": "^3.10.1",
|
||||
"linkedom": "^0.16.1",
|
||||
"manage-path": "^2.0.0",
|
||||
"minimatch": "^9.0.3",
|
||||
"natural-compare": "^1.4.0",
|
||||
"normalize-path": "^3.0.0",
|
||||
"nypm": "^0.3.6",
|
||||
"ora": "^7.0.1",
|
||||
"picocolors": "^1.0.0",
|
||||
"prompts": "^2.4.2",
|
||||
"publish-browser-extension": "^2.1.2",
|
||||
"rollup-plugin-visualizer": "^5.9.2",
|
||||
"publish-browser-extension": "^2.1.3",
|
||||
"unimport": "^3.4.0",
|
||||
"vite": "^5.1.3",
|
||||
"web-ext-run": "^0.2.0",
|
||||
"webextension-polyfill": "^0.10.0",
|
||||
"zip-dir": "^2.0.0"
|
||||
"webextension-polyfill": "^0.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^8.3.1",
|
||||
@@ -150,6 +153,7 @@
|
||||
"@types/prompts": "^2.4.9",
|
||||
"@vitest/coverage-v8": "^1.0.1",
|
||||
"execa": "^8.0.1",
|
||||
"extract-zip": "^2.0.1",
|
||||
"happy-dom": "^13.3.8",
|
||||
"lint-staged": "^15.2.0",
|
||||
"lodash.merge": "^4.6.2",
|
||||
|
||||
Generated
+78
-60
@@ -8,6 +8,9 @@ importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@aklinker1/rollup-plugin-visualizer':
|
||||
specifier: 5.12.0
|
||||
version: 5.12.0
|
||||
'@types/webextension-polyfill':
|
||||
specifier: ^0.10.5
|
||||
version: 0.10.7
|
||||
@@ -31,7 +34,7 @@ importers:
|
||||
version: 6.7.14
|
||||
chokidar:
|
||||
specifier: ^3.5.3
|
||||
version: 3.5.3
|
||||
version: 3.6.0
|
||||
consola:
|
||||
specifier: ^3.2.3
|
||||
version: 3.2.3
|
||||
@@ -71,12 +74,12 @@ importers:
|
||||
json5:
|
||||
specifier: ^2.2.3
|
||||
version: 2.2.3
|
||||
jszip:
|
||||
specifier: ^3.10.1
|
||||
version: 3.10.1
|
||||
linkedom:
|
||||
specifier: ^0.16.1
|
||||
version: 0.16.1
|
||||
manage-path:
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0
|
||||
version: 0.16.8
|
||||
minimatch:
|
||||
specifier: ^9.0.3
|
||||
version: 9.0.3
|
||||
@@ -86,6 +89,9 @@ importers:
|
||||
normalize-path:
|
||||
specifier: ^3.0.0
|
||||
version: 3.0.0
|
||||
nypm:
|
||||
specifier: ^0.3.6
|
||||
version: 0.3.6
|
||||
ora:
|
||||
specifier: ^7.0.1
|
||||
version: 7.0.1
|
||||
@@ -96,11 +102,8 @@ importers:
|
||||
specifier: ^2.4.2
|
||||
version: 2.4.2
|
||||
publish-browser-extension:
|
||||
specifier: ^2.1.2
|
||||
version: 2.1.2
|
||||
rollup-plugin-visualizer:
|
||||
specifier: ^5.9.2
|
||||
version: 5.12.0
|
||||
specifier: ^2.1.3
|
||||
version: 2.1.3
|
||||
unimport:
|
||||
specifier: ^3.4.0
|
||||
version: 3.4.0
|
||||
@@ -113,9 +116,6 @@ importers:
|
||||
webextension-polyfill:
|
||||
specifier: ^0.10.0
|
||||
version: 0.10.0
|
||||
zip-dir:
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0
|
||||
devDependencies:
|
||||
'@faker-js/faker':
|
||||
specifier: ^8.3.1
|
||||
@@ -144,6 +144,9 @@ importers:
|
||||
execa:
|
||||
specifier: ^8.0.1
|
||||
version: 8.0.1
|
||||
extract-zip:
|
||||
specifier: ^2.0.1
|
||||
version: 2.0.1
|
||||
happy-dom:
|
||||
specifier: ^13.3.8
|
||||
version: 13.3.8
|
||||
@@ -226,6 +229,22 @@ importers:
|
||||
|
||||
packages:
|
||||
|
||||
/@aklinker1/rollup-plugin-visualizer@5.12.0:
|
||||
resolution: {integrity: sha512-X24LvEGw6UFmy0lpGJDmXsMyBD58XmX1bbwsaMLhNoM+UMQfQ3b2RtC+nz4b/NoRK5r6QJSKJHBNVeUdwqybaQ==}
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
rollup: 2.x || 3.x || 4.x
|
||||
peerDependenciesMeta:
|
||||
rollup:
|
||||
optional: true
|
||||
dependencies:
|
||||
open: 8.4.2
|
||||
picomatch: 2.3.1
|
||||
source-map: 0.7.4
|
||||
yargs: 17.7.2
|
||||
dev: false
|
||||
|
||||
/@algolia/autocomplete-core@1.9.3(algoliasearch@4.20.0):
|
||||
resolution: {integrity: sha512-009HdfugtGCdC4JdXUbVJClA0q0zh24yyePn+KUGk3rP7j8FEe/m5Yo/z65gn6nP/cM39PxpzqKrL7A6fP6PPw==}
|
||||
dependencies:
|
||||
@@ -1136,7 +1155,7 @@ packages:
|
||||
/@types/react-dom@18.2.14:
|
||||
resolution: {integrity: sha512-V835xgdSVmyQmI1KLV2BEIUgqEuinxp9O4G6g3FqO/SqLac049E53aysv0oEFD2kHfejeKU+ZqL2bcFWj9gLAQ==}
|
||||
dependencies:
|
||||
'@types/react': 18.2.34
|
||||
'@types/react': 18.2.61
|
||||
dev: true
|
||||
|
||||
/@types/react@18.2.34:
|
||||
@@ -1147,6 +1166,14 @@ packages:
|
||||
csstype: 3.1.2
|
||||
dev: true
|
||||
|
||||
/@types/react@18.2.61:
|
||||
resolution: {integrity: sha512-NURTN0qNnJa7O/k4XUkEW2yfygA+NxS0V5h1+kp9jPwhzZy95q3ADoGMP0+JypMhrZBTTgjKAUlTctde1zzeQA==}
|
||||
dependencies:
|
||||
'@types/prop-types': 15.7.9
|
||||
'@types/scheduler': 0.16.5
|
||||
csstype: 3.1.3
|
||||
dev: true
|
||||
|
||||
/@types/scheduler@0.16.5:
|
||||
resolution: {integrity: sha512-s/FPdYRmZR8SjLWGMCuax7r3qCWQw9QKHzXVukAuuIJkXkDRwp+Pu5LMIVFi0Fxbav35WURicYr8u1QsoybnQw==}
|
||||
dev: true
|
||||
@@ -1164,7 +1191,6 @@ packages:
|
||||
requiresBuild: true
|
||||
dependencies:
|
||||
'@types/node': 20.10.3
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@vitejs/plugin-vue@5.0.2(vite@5.1.3)(vue@3.4.3):
|
||||
@@ -1356,7 +1382,7 @@ packages:
|
||||
dependencies:
|
||||
'@vue/runtime-core': 3.3.10
|
||||
'@vue/shared': 3.3.10
|
||||
csstype: 3.1.2
|
||||
csstype: 3.1.3
|
||||
dev: true
|
||||
|
||||
/@vue/runtime-dom@3.4.3:
|
||||
@@ -1699,7 +1725,6 @@ packages:
|
||||
|
||||
/buffer-crc32@0.2.13:
|
||||
resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
|
||||
dev: false
|
||||
|
||||
/buffer-from@1.1.2:
|
||||
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
||||
@@ -1743,7 +1768,7 @@ packages:
|
||||
/c12@1.5.1:
|
||||
resolution: {integrity: sha512-BWZRJgDEveT8uI+cliCwvYSSSSvb4xKoiiu5S0jaDbKBopQLQF7E+bq9xKk1pTcG+mUa3yXuFO7bD9d8Lr9Xxg==}
|
||||
dependencies:
|
||||
chokidar: 3.5.3
|
||||
chokidar: 3.6.0
|
||||
defu: 6.1.4
|
||||
dotenv: 16.3.1
|
||||
giget: 1.1.3
|
||||
@@ -1829,8 +1854,8 @@ packages:
|
||||
dependencies:
|
||||
get-func-name: 2.0.2
|
||||
|
||||
/chokidar@3.5.3:
|
||||
resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==}
|
||||
/chokidar@3.6.0:
|
||||
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
||||
engines: {node: '>= 8.10.0'}
|
||||
dependencies:
|
||||
anymatch: 3.1.3
|
||||
@@ -1866,6 +1891,12 @@ packages:
|
||||
engines: {node: '>=8'}
|
||||
dev: false
|
||||
|
||||
/citty@0.1.6:
|
||||
resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==}
|
||||
dependencies:
|
||||
consola: 3.2.3
|
||||
dev: false
|
||||
|
||||
/cli-boxes@3.0.0:
|
||||
resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -2247,7 +2278,6 @@ packages:
|
||||
resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==}
|
||||
dependencies:
|
||||
once: 1.4.0
|
||||
dev: false
|
||||
|
||||
/entities@4.5.0:
|
||||
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
|
||||
@@ -2469,7 +2499,6 @@ packages:
|
||||
'@types/yauzl': 2.10.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/fast-glob@3.3.2:
|
||||
resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==}
|
||||
@@ -2490,7 +2519,6 @@ packages:
|
||||
resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==}
|
||||
dependencies:
|
||||
pend: 1.2.0
|
||||
dev: false
|
||||
|
||||
/filesize@10.1.0:
|
||||
resolution: {integrity: sha512-GTLKYyBSDz3nPhlLVPjPWZCnhkd9TrrRArNcy8Z+J2cqScB7h2McAzR6NBX6nYOoWafql0roY8hrocxnZBv9CQ==}
|
||||
@@ -2647,7 +2675,6 @@ packages:
|
||||
engines: {node: '>=8'}
|
||||
dependencies:
|
||||
pump: 3.0.0
|
||||
dev: false
|
||||
|
||||
/get-stream@6.0.1:
|
||||
resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==}
|
||||
@@ -3375,8 +3402,8 @@ packages:
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
dev: false
|
||||
|
||||
/linkedom@0.16.1:
|
||||
resolution: {integrity: sha512-kbK4txFSjGstS8aHkYo3sRSD7viQbE1TAlYRqiqU13oihzzQFp23D1OwW3VdAQJuzqzBB+1qo9Qvp0xOeoVKig==}
|
||||
/linkedom@0.16.8:
|
||||
resolution: {integrity: sha512-+HtHVHBb3yZKlP9pgcJdi1AIG9tsAuo+Qtlz+79cCTsxgQwDzajsZjYvpp+DEckCK/zoGVhzkADniYZQ57KcFQ==}
|
||||
dependencies:
|
||||
css-select: 5.1.0
|
||||
cssom: 0.5.0
|
||||
@@ -3544,10 +3571,6 @@ packages:
|
||||
resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
|
||||
dev: false
|
||||
|
||||
/manage-path@2.0.0:
|
||||
resolution: {integrity: sha512-NJhyB+PJYTpxhxZJ3lecIGgh4kwIY2RAh44XvAz9UlqthlQwtPBf62uBVR8XaD8CRuSjQ6TnZH2lNJkbLPZM2A==}
|
||||
dev: false
|
||||
|
||||
/mark.js@8.11.1:
|
||||
resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==}
|
||||
dev: true
|
||||
@@ -3664,9 +3687,9 @@ packages:
|
||||
resolution: {integrity: sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg==}
|
||||
dependencies:
|
||||
acorn: 8.11.2
|
||||
pathe: 1.1.1
|
||||
pathe: 1.1.2
|
||||
pkg-types: 1.0.3
|
||||
ufo: 1.3.1
|
||||
ufo: 1.4.0
|
||||
|
||||
/moment@2.29.4:
|
||||
resolution: {integrity: sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==}
|
||||
@@ -3839,6 +3862,17 @@ packages:
|
||||
boolbase: 1.0.0
|
||||
dev: false
|
||||
|
||||
/nypm@0.3.6:
|
||||
resolution: {integrity: sha512-2CATJh3pd6CyNfU5VZM7qSwFu0ieyabkEdnogE30Obn1czrmOYiZ8DOZLe1yBdLKWoyD3Mcy2maUs+0MR3yVjQ==}
|
||||
engines: {node: ^14.16.0 || >=16.10.0}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
citty: 0.1.6
|
||||
execa: 8.0.1
|
||||
pathe: 1.1.2
|
||||
ufo: 1.4.0
|
||||
dev: false
|
||||
|
||||
/object-assign@4.1.1:
|
||||
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -3867,7 +3901,7 @@ packages:
|
||||
dependencies:
|
||||
destr: 2.0.2
|
||||
node-fetch-native: 1.4.1
|
||||
ufo: 1.3.1
|
||||
ufo: 1.4.0
|
||||
dev: false
|
||||
|
||||
/ohash@1.1.3:
|
||||
@@ -4044,12 +4078,14 @@ packages:
|
||||
/pathe@1.1.1:
|
||||
resolution: {integrity: sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==}
|
||||
|
||||
/pathe@1.1.2:
|
||||
resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==}
|
||||
|
||||
/pathval@1.1.1:
|
||||
resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==}
|
||||
|
||||
/pend@1.2.0:
|
||||
resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==}
|
||||
dev: false
|
||||
|
||||
/perfect-debounce@1.0.0:
|
||||
resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==}
|
||||
@@ -4089,7 +4125,7 @@ packages:
|
||||
dependencies:
|
||||
jsonc-parser: 3.2.0
|
||||
mlly: 1.4.2
|
||||
pathe: 1.1.1
|
||||
pathe: 1.1.2
|
||||
|
||||
/postcss-load-config@4.0.1:
|
||||
resolution: {integrity: sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==}
|
||||
@@ -4175,8 +4211,8 @@ packages:
|
||||
sade: 1.8.1
|
||||
dev: true
|
||||
|
||||
/publish-browser-extension@2.1.2:
|
||||
resolution: {integrity: sha512-g6+mtdR4Z+GYHPIrfaAwC7Kbt1oQlpJ8r0x1PAytScy33OFdK+HVUeDDYorBpPAiQlmYJRQga7rY9QVTyTw34g==}
|
||||
/publish-browser-extension@2.1.3:
|
||||
resolution: {integrity: sha512-qisnXUUwjvu5kMvObfG7UQ9rPU3t0XfbKdCdCrwAXaLUySdC25nlM4gxi+CBvL7LiwvK494GJyEK/weQBhIyTQ==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
@@ -4204,7 +4240,6 @@ packages:
|
||||
dependencies:
|
||||
end-of-stream: 1.4.4
|
||||
once: 1.4.0
|
||||
dev: false
|
||||
|
||||
/punycode@2.3.0:
|
||||
resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==}
|
||||
@@ -4391,22 +4426,6 @@ packages:
|
||||
glob: 7.1.6
|
||||
dev: false
|
||||
|
||||
/rollup-plugin-visualizer@5.12.0:
|
||||
resolution: {integrity: sha512-8/NU9jXcHRs7Nnj07PF2o4gjxmm9lXIrZ8r175bT9dK8qoLlvKTwRMArRCMgpMGlq8CTLugRvEmyMeMXIU2pNQ==}
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
rollup: 2.x || 3.x || 4.x
|
||||
peerDependenciesMeta:
|
||||
rollup:
|
||||
optional: true
|
||||
dependencies:
|
||||
open: 8.4.2
|
||||
picomatch: 2.3.1
|
||||
source-map: 0.7.4
|
||||
yargs: 17.7.2
|
||||
dev: false
|
||||
|
||||
/rollup@4.6.1:
|
||||
resolution: {integrity: sha512-jZHaZotEHQaHLgKr8JnQiDT1rmatjgKlMekyksz+yk9jt/8z9quNjnKNRoaM0wd9DC2QKXjmWWuDYtM3jfF8pQ==}
|
||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||
@@ -4468,7 +4487,7 @@ packages:
|
||||
engines: {node: '>=14.0.0'}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
chokidar: 3.5.3
|
||||
chokidar: 3.6.0
|
||||
immutable: 4.3.4
|
||||
source-map-js: 1.0.2
|
||||
|
||||
@@ -4978,7 +4997,7 @@ packages:
|
||||
dependencies:
|
||||
bundle-require: 4.0.1(esbuild@0.19.8)
|
||||
cac: 6.7.14
|
||||
chokidar: 3.5.3
|
||||
chokidar: 3.6.0
|
||||
debug: 4.3.4
|
||||
esbuild: 0.19.8
|
||||
execa: 5.1.1
|
||||
@@ -5079,8 +5098,8 @@ packages:
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/ufo@1.3.1:
|
||||
resolution: {integrity: sha512-uY/99gMLIOlJPwATcMVYfqDSxUR9//AUcgZMzwfSTJPDKzA1S8mX4VLqa+fiAtveraQUBCz4FFcwVZBGbwBXIw==}
|
||||
/ufo@1.4.0:
|
||||
resolution: {integrity: sha512-Hhy+BhRBleFjpJ2vchUNN40qgkh0366FWJGqVLYBHev0vpHTrXSA0ryT+74UiW6KWsldNurQMKGqCm1M2zBciQ==}
|
||||
|
||||
/uhyphen@0.2.0:
|
||||
resolution: {integrity: sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==}
|
||||
@@ -5137,7 +5156,7 @@ packages:
|
||||
resolution: {integrity: sha512-9ZdRwbh/4gcm1JTOkp9lAkIDrtOyOxgHmY7cjuwI8L/2RTikMcVG25GsZwNAgRuap3iDw2jeq7eoqtAsz5rW3A==}
|
||||
dependencies:
|
||||
acorn: 8.11.2
|
||||
chokidar: 3.5.3
|
||||
chokidar: 3.6.0
|
||||
webpack-sources: 3.2.3
|
||||
webpack-virtual-modules: 0.5.0
|
||||
dev: false
|
||||
@@ -5698,7 +5717,6 @@ packages:
|
||||
dependencies:
|
||||
buffer-crc32: 0.2.13
|
||||
fd-slicer: 1.1.0
|
||||
dev: false
|
||||
|
||||
/yocto-queue@1.0.0:
|
||||
resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==}
|
||||
|
||||
@@ -3,11 +3,12 @@ import { describe, it, expect, beforeEach, vi, expectTypeOf } from 'vitest';
|
||||
import { browser } from '~/browser';
|
||||
import { WxtStorageItem, storage } from '~/storage';
|
||||
|
||||
async function triggerUpdate() {
|
||||
await fakeBrowser.runtime.onInstalled.trigger({
|
||||
reason: 'update',
|
||||
temporary: false,
|
||||
});
|
||||
/**
|
||||
* This works because fakeBrowser is synchronous, and is will finish any number of chained
|
||||
* calls within a single tick of the event loop, ie: a timeout of 0.
|
||||
*/
|
||||
async function waitForMigrations() {
|
||||
return new Promise((res) => setTimeout(res));
|
||||
}
|
||||
|
||||
describe('Storage Utils', () => {
|
||||
@@ -75,10 +76,7 @@ describe('Storage Utils', () => {
|
||||
|
||||
const actual = await storage.getItems(params);
|
||||
|
||||
expect(actual).toHaveLength(3);
|
||||
expected.forEach((item) => {
|
||||
expect(actual).toContainEqual(item);
|
||||
});
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -412,7 +410,7 @@ describe('Storage Utils', () => {
|
||||
3: migrateToV3,
|
||||
},
|
||||
});
|
||||
await triggerUpdate();
|
||||
await waitForMigrations();
|
||||
|
||||
const actualValue = await item.getValue();
|
||||
const actualMeta = await item.getMeta();
|
||||
@@ -439,7 +437,7 @@ describe('Storage Utils', () => {
|
||||
3: migrateToV3,
|
||||
},
|
||||
});
|
||||
await triggerUpdate();
|
||||
await waitForMigrations();
|
||||
|
||||
const actualValue = await item.getValue();
|
||||
const actualMeta = await item.getMeta();
|
||||
@@ -464,7 +462,7 @@ describe('Storage Utils', () => {
|
||||
2: migrateToV2,
|
||||
},
|
||||
});
|
||||
await triggerUpdate();
|
||||
await waitForMigrations();
|
||||
|
||||
const actualValue = await item.getValue();
|
||||
const actualMeta = await item.getMeta();
|
||||
@@ -492,7 +490,7 @@ describe('Storage Utils', () => {
|
||||
3: migrateToV3,
|
||||
},
|
||||
});
|
||||
await triggerUpdate();
|
||||
await waitForMigrations();
|
||||
|
||||
expect(migrateToV2).not.toBeCalled();
|
||||
expect(migrateToV3).not.toBeCalled();
|
||||
@@ -514,7 +512,7 @@ describe('Storage Utils', () => {
|
||||
3: migrateToV3,
|
||||
},
|
||||
});
|
||||
await triggerUpdate();
|
||||
await waitForMigrations();
|
||||
|
||||
const actualValue = await item.getValue();
|
||||
const actualMeta = await item.getMeta();
|
||||
@@ -541,7 +539,7 @@ describe('Storage Utils', () => {
|
||||
defaultValue: 0,
|
||||
version: nextVersion,
|
||||
});
|
||||
await triggerUpdate();
|
||||
await waitForMigrations();
|
||||
|
||||
await expect(item.migrate()).rejects.toThrow(
|
||||
'Version downgrade detected (v2 -> v1) for "local:count"',
|
||||
|
||||
+1
-1
@@ -51,7 +51,7 @@ export type {
|
||||
Identity,
|
||||
Idle,
|
||||
Management,
|
||||
Manifest,
|
||||
Manifest, // TODO: Export custom manifest types that are valid for both Chrome and Firefox.
|
||||
ContextMenus,
|
||||
Menus,
|
||||
NetworkStatus,
|
||||
|
||||
+10
-2
@@ -1,6 +1,5 @@
|
||||
import { CAC, Command } from 'cac';
|
||||
import consola, { LogLevels } from 'consola';
|
||||
import { exec } from '~/core/utils/exec';
|
||||
import { printHeader } from '~/core/utils/log';
|
||||
import { formatDuration } from '~/core/utils/time';
|
||||
import { ValidationError } from '~/core/utils/validation';
|
||||
@@ -63,10 +62,18 @@ export function getArrayFromFlags<T>(
|
||||
}
|
||||
|
||||
const aliasCommandNames = new Set<string>();
|
||||
/**
|
||||
* @param base Command to add this one to
|
||||
* @param name The command name to add
|
||||
* @param alias The CLI tool being aliased
|
||||
* @param bin The CLI tool binary name. Usually the same as the alias
|
||||
* @param docsUrl URL to the docs for the aliased CLI tool
|
||||
*/
|
||||
export function createAliasedCommand(
|
||||
base: CAC,
|
||||
name: string,
|
||||
alias: string,
|
||||
bin: string,
|
||||
docsUrl: string,
|
||||
) {
|
||||
const aliasedCommand = base
|
||||
@@ -79,7 +86,8 @@ export function createAliasedCommand(
|
||||
const args = process.argv.slice(
|
||||
process.argv.indexOf(aliasedCommand.name) + 1,
|
||||
);
|
||||
await exec(alias, args, {
|
||||
const { execa } = await import('execa');
|
||||
await execa(bin, args, {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
} catch {
|
||||
|
||||
@@ -140,6 +140,7 @@ createAliasedCommand(
|
||||
cli,
|
||||
'submit',
|
||||
'publish-extension',
|
||||
'wxt-publish-extension',
|
||||
'https://www.npmjs.com/publish-browser-extension',
|
||||
);
|
||||
|
||||
|
||||
@@ -10,6 +10,29 @@ import { createLocationWatcher } from './location-watcher';
|
||||
*
|
||||
* It also provides several utilities like `ctx.setTimeout` and `ctx.setInterval` that should be used in
|
||||
* content scripts instead of `window.setTimeout` or `window.setInterval`.
|
||||
*
|
||||
* To create context for testing, you can use the class's constructor:
|
||||
*
|
||||
* ```ts
|
||||
* import { ContentScriptContext } from 'wxt/client';
|
||||
*
|
||||
* test("storage listener should be removed when context is invalidated", () => {
|
||||
* const ctx = new ContentScriptContext('test');
|
||||
* const item = storage.defineItem("local:count", { defaultValue: 0 });
|
||||
* const watcher = vi.fn();
|
||||
*
|
||||
* const unwatch = item.watch(watcher);
|
||||
* ctx.onInvalidated(unwatch); // Listen for invalidate here
|
||||
*
|
||||
* await item.setValue(1);
|
||||
* expect(watcher).toBeCalledTimes(1);
|
||||
* expect(watcher).toBeCalledWith(1, 0);
|
||||
*
|
||||
* ctx.notifyInvalidated(); // Use this function to invalidate the context
|
||||
* await item.setValue(2);
|
||||
* expect(watcher).toBeCalledTimes(1);
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export class ContentScriptContext implements AbortController {
|
||||
private static SCRIPT_STARTED_MESSAGE_TYPE = 'wxt:content-script-started';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type * as vite from 'vite';
|
||||
import { visualizer } from 'rollup-plugin-visualizer';
|
||||
import { visualizer } from '@aklinker1/rollup-plugin-visualizer';
|
||||
import { ResolvedConfig } from '~/types';
|
||||
import path from 'node:path';
|
||||
|
||||
|
||||
@@ -16,8 +16,8 @@ export function devHtmlPrerender(
|
||||
): vite.PluginOption {
|
||||
const htmlReloadId = '@wxt/reload-html';
|
||||
const resolvedHtmlReloadId = resolve(
|
||||
config.root,
|
||||
'node_modules/wxt/dist/virtual/reload-html.js',
|
||||
config.wxtModuleDir,
|
||||
'dist/virtual/reload-html.js',
|
||||
);
|
||||
const virtualReactRefreshId = '@wxt/virtual-react-refresh';
|
||||
const resolvedVirtualReactRefreshId = '\0' + virtualReactRefreshId;
|
||||
|
||||
@@ -30,10 +30,7 @@ export function virtualEntrypoint(
|
||||
|
||||
const inputPath = id.replace(resolvedVirtualId, '');
|
||||
const template = await fs.readFile(
|
||||
resolve(
|
||||
config.root,
|
||||
`node_modules/wxt/dist/virtual/${type}-entrypoint.js`,
|
||||
),
|
||||
resolve(config.wxtModuleDir, `dist/virtual/${type}-entrypoint.js`),
|
||||
'utf-8',
|
||||
);
|
||||
return template.replace(`virtual:user-${type}`, inputPath);
|
||||
|
||||
@@ -26,8 +26,8 @@ export function webextensionPolyfillMock(
|
||||
alias: {
|
||||
// Alias to use a mocked version of the polyfill
|
||||
'webextension-polyfill': path.resolve(
|
||||
config.root,
|
||||
'node_modules/wxt/dist/virtual/mock-browser',
|
||||
config.wxtModuleDir,
|
||||
'dist/virtual/mock-browser',
|
||||
),
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import path from 'node:path';
|
||||
import { bun } from '../bun';
|
||||
|
||||
const cwd = path.resolve(__dirname, 'fixtures/bun-project');
|
||||
|
||||
describe.skipIf(() => process.platform === 'win32')(
|
||||
'Bun Package Management Utils',
|
||||
() => {
|
||||
describe('listDependencies', () => {
|
||||
it('should list direct dependencies', async () => {
|
||||
const actual = await bun.listDependencies({ cwd });
|
||||
expect(actual).toEqual([
|
||||
{ name: 'flatten', version: '1.0.3' },
|
||||
{ name: 'mime-types', version: '2.1.35' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should list all dependencies', async () => {
|
||||
const actual = await bun.listDependencies({ cwd, all: true });
|
||||
expect(actual).toEqual([
|
||||
{ name: 'flatten', version: '1.0.3' },
|
||||
{ name: 'mime-db', version: '1.52.0' },
|
||||
{ name: 'mime-types', version: '2.1.35' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
BIN
Binary file not shown.
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "bun-ls",
|
||||
"dependencies": {
|
||||
"mime-types": "2.1.35"
|
||||
},
|
||||
"devDependencies": {
|
||||
"flatten": "1.0.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "npm-ls",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "npm-ls",
|
||||
"dependencies": {
|
||||
"mime-types": "2.1.35"
|
||||
},
|
||||
"devDependencies": {
|
||||
"flatten": "1.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/flatten": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/flatten/-/flatten-1.0.3.tgz",
|
||||
"integrity": "sha512-dVsPA/UwQ8+2uoFe5GHtiBMu48dWLTdsuEd7CKGlZlD78r1TTWBvDuFaFGKCo/ZfEr95Uk56vZoX86OsHkUeIg==",
|
||||
"deprecated": "flatten is deprecated in favor of utility frameworks such as lodash.",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "npm-ls",
|
||||
"dependencies": {
|
||||
"mime-types": "2.1.35"
|
||||
},
|
||||
"devDependencies": {
|
||||
"flatten": "1.0.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "pnpm-ls",
|
||||
"dependencies": {
|
||||
"mime-types": "2.1.35"
|
||||
},
|
||||
"devDependencies": {
|
||||
"flatten": "1.0.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
lockfileVersion: '6.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
dependencies:
|
||||
mime-types:
|
||||
specifier: 2.1.35
|
||||
version: 2.1.35
|
||||
|
||||
devDependencies:
|
||||
flatten:
|
||||
specifier: 1.0.3
|
||||
version: 1.0.3
|
||||
|
||||
packages:
|
||||
|
||||
/flatten@1.0.3:
|
||||
resolution: {integrity: sha512-dVsPA/UwQ8+2uoFe5GHtiBMu48dWLTdsuEd7CKGlZlD78r1TTWBvDuFaFGKCo/ZfEr95Uk56vZoX86OsHkUeIg==}
|
||||
deprecated: flatten is deprecated in favor of utility frameworks such as lodash.
|
||||
dev: true
|
||||
|
||||
/mime-db@1.52.0:
|
||||
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: false
|
||||
|
||||
/mime-types@2.1.35:
|
||||
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dependencies:
|
||||
mime-db: 1.52.0
|
||||
dev: false
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "yarn-ls",
|
||||
"packageManager": "yarn@1.22.19",
|
||||
"dependencies": {
|
||||
"mime-types": "2.1.35"
|
||||
},
|
||||
"devDependencies": {
|
||||
"flatten": "1.0.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
flatten@1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.3.tgz#c1283ac9f27b368abc1e36d1ff7b04501a30356b"
|
||||
integrity sha512-dVsPA/UwQ8+2uoFe5GHtiBMu48dWLTdsuEd7CKGlZlD78r1TTWBvDuFaFGKCo/ZfEr95Uk56vZoX86OsHkUeIg==
|
||||
|
||||
mime-db@1.52.0:
|
||||
version "1.52.0"
|
||||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
|
||||
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
|
||||
|
||||
mime-types@2.1.35:
|
||||
version "2.1.35"
|
||||
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
|
||||
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
|
||||
dependencies:
|
||||
mime-db "1.52.0"
|
||||
@@ -0,0 +1,50 @@
|
||||
import { beforeAll, describe, expect, it } from 'vitest';
|
||||
import path from 'node:path';
|
||||
import { npm } from '../npm';
|
||||
import { execaCommand } from 'execa';
|
||||
import { exists } from 'fs-extra';
|
||||
|
||||
const cwd = path.resolve(__dirname, 'fixtures/npm-project');
|
||||
|
||||
describe(
|
||||
'NPM Package Management Utils',
|
||||
() => {
|
||||
beforeAll(async () => {
|
||||
// NPM needs the modules installed for 'npm ls' to work
|
||||
await execaCommand('npm i', { cwd });
|
||||
});
|
||||
|
||||
describe('listDependencies', () => {
|
||||
it('should list direct dependencies', async () => {
|
||||
const actual = await npm.listDependencies({ cwd });
|
||||
expect(actual).toEqual([
|
||||
{ name: 'flatten', version: '1.0.3' },
|
||||
{ name: 'mime-types', version: '2.1.35' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should list all dependencies', async () => {
|
||||
const actual = await npm.listDependencies({ cwd, all: true });
|
||||
expect(actual).toEqual([
|
||||
{ name: 'flatten', version: '1.0.3' },
|
||||
{ name: 'mime-types', version: '2.1.35' },
|
||||
{ name: 'mime-db', version: '1.52.0' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('downloadDependency', () => {
|
||||
it('should download the dependency as a tarball', async () => {
|
||||
const downloadDir = path.resolve(cwd, 'dist');
|
||||
const id = 'mime-db@1.52.0';
|
||||
const expected = path.resolve(downloadDir, 'mime-db-1.52.0.tgz');
|
||||
|
||||
const actual = await npm.downloadDependency(id, downloadDir);
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
expect(await exists(actual)).toBe(true);
|
||||
});
|
||||
});
|
||||
},
|
||||
{ timeout: 20e3 },
|
||||
);
|
||||
@@ -0,0 +1,34 @@
|
||||
import { beforeAll, describe, expect, it } from 'vitest';
|
||||
import path from 'node:path';
|
||||
import { pnpm } from '../pnpm';
|
||||
import { execaCommand } from 'execa';
|
||||
|
||||
const cwd = path.resolve(__dirname, 'fixtures/pnpm-project');
|
||||
|
||||
process.env.WXT_PNPM_IGNORE_WORKSPACE = 'true';
|
||||
|
||||
describe('PNPM Package Management Utils', () => {
|
||||
beforeAll(async () => {
|
||||
// PNPM needs the modules installed, or 'pnpm ls' will return a blank list.
|
||||
await execaCommand('pnpm i --ignore-workspace', { cwd });
|
||||
});
|
||||
|
||||
describe('listDependencies', () => {
|
||||
it('should list direct dependencies', async () => {
|
||||
const actual = await pnpm.listDependencies({ cwd });
|
||||
expect(actual).toEqual([
|
||||
{ name: 'flatten', version: '1.0.3' },
|
||||
{ name: 'mime-types', version: '2.1.35' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should list all dependencies', async () => {
|
||||
const actual = await pnpm.listDependencies({ cwd, all: true });
|
||||
expect(actual).toEqual([
|
||||
{ name: 'flatten', version: '1.0.3' },
|
||||
{ name: 'mime-types', version: '2.1.35' },
|
||||
{ name: 'mime-db', version: '1.52.0' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import path from 'node:path';
|
||||
import { yarn } from '../yarn';
|
||||
|
||||
const cwd = path.resolve(__dirname, 'fixtures/yarn-project');
|
||||
|
||||
describe('Yarn Package Management Utils', () => {
|
||||
describe('listDependencies', () => {
|
||||
it('should list direct dependencies', async () => {
|
||||
const actual = await yarn.listDependencies({ cwd });
|
||||
expect(actual).toEqual([
|
||||
{ name: 'mime-db', version: '1.52.0' },
|
||||
{ name: 'flatten', version: '1.0.3' },
|
||||
{ name: 'mime-types', version: '2.1.35' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should list all dependencies', async () => {
|
||||
const actual = await yarn.listDependencies({ cwd, all: true });
|
||||
expect(actual).toEqual([
|
||||
{ name: 'mime-db', version: '1.52.0' },
|
||||
{ name: 'flatten', version: '1.0.3' },
|
||||
{ name: 'mime-types', version: '2.1.35' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
import { dedupeDependencies, npm } from './npm';
|
||||
import { WxtPackageManagerImpl } from './types';
|
||||
|
||||
export const bun: WxtPackageManagerImpl = {
|
||||
overridesKey: 'overrides', // But also supports "resolutions"
|
||||
downloadDependency(...args) {
|
||||
return npm.downloadDependency(...args);
|
||||
},
|
||||
async listDependencies(options) {
|
||||
const args = ['pm', 'ls'];
|
||||
if (options?.all) {
|
||||
args.push('--all');
|
||||
}
|
||||
const { execa } = await import('execa');
|
||||
const res = await execa('bun', args, { cwd: options?.cwd });
|
||||
return dedupeDependencies(
|
||||
res.stdout
|
||||
.split('\n')
|
||||
.slice(1) // Skip the first line, is not a dependency
|
||||
.map((line) => line.trim())
|
||||
.map((line) => /.* (@?\S+)@(\S+)$/.exec(line))
|
||||
// @ts-expect-error: Filtering to known non-null matches
|
||||
.filter<RegExpExecArray>((match) => !!match)
|
||||
.map(([_, name, version]) => ({ name, version })),
|
||||
);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
import {
|
||||
detectPackageManager,
|
||||
addDependency,
|
||||
addDevDependency,
|
||||
ensureDependencyInstalled,
|
||||
installDependencies,
|
||||
removeDependency,
|
||||
PackageManager,
|
||||
PackageManagerName,
|
||||
} from 'nypm';
|
||||
import { WxtPackageManager } from '~/types';
|
||||
import { bun } from './bun';
|
||||
import { WxtPackageManagerImpl } from './types';
|
||||
import { yarn } from './yarn';
|
||||
import { pnpm } from './pnpm';
|
||||
import { npm } from './npm';
|
||||
|
||||
export async function createWxtPackageManager(
|
||||
root: string,
|
||||
): Promise<WxtPackageManager> {
|
||||
const pm = await detectPackageManager(root, {
|
||||
includeParentDirs: true,
|
||||
});
|
||||
|
||||
// Use requirePm to prevent throwing errors before the package manager utils are used.
|
||||
const requirePm = <T>(cb: (pm: PackageManager) => T) => {
|
||||
if (pm == null) throw Error('Could not detect package manager');
|
||||
return cb(pm);
|
||||
};
|
||||
|
||||
return {
|
||||
get name() {
|
||||
return requirePm((pm) => pm.name);
|
||||
},
|
||||
get command() {
|
||||
return requirePm((pm) => pm.command);
|
||||
},
|
||||
get version() {
|
||||
return requirePm((pm) => pm.version);
|
||||
},
|
||||
get majorVersion() {
|
||||
return requirePm((pm) => pm.majorVersion);
|
||||
},
|
||||
get lockFile() {
|
||||
return requirePm((pm) => pm.lockFile);
|
||||
},
|
||||
get files() {
|
||||
return requirePm((pm) => pm.files);
|
||||
},
|
||||
addDependency,
|
||||
addDevDependency,
|
||||
ensureDependencyInstalled,
|
||||
installDependencies,
|
||||
removeDependency,
|
||||
get overridesKey() {
|
||||
return requirePm((pm) => packageManagers[pm.name].overridesKey);
|
||||
},
|
||||
downloadDependency(...args) {
|
||||
return requirePm((pm) =>
|
||||
packageManagers[pm.name].downloadDependency(...args),
|
||||
);
|
||||
},
|
||||
listDependencies(...args) {
|
||||
return requirePm((pm) =>
|
||||
packageManagers[pm.name].listDependencies(...args),
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const packageManagers: Record<PackageManagerName, WxtPackageManagerImpl> = {
|
||||
npm,
|
||||
pnpm,
|
||||
bun,
|
||||
yarn,
|
||||
};
|
||||
@@ -0,0 +1,85 @@
|
||||
import { Dependency } from '~/types';
|
||||
import { WxtPackageManagerImpl } from './types';
|
||||
import path from 'node:path';
|
||||
import { ensureDir } from 'fs-extra';
|
||||
|
||||
export const npm: WxtPackageManagerImpl = {
|
||||
overridesKey: 'overrides',
|
||||
async downloadDependency(id, downloadDir) {
|
||||
await ensureDir(downloadDir);
|
||||
const { execa } = await import('execa');
|
||||
const res = await execa('npm', ['pack', id, '--json'], {
|
||||
cwd: downloadDir,
|
||||
});
|
||||
const packed: PackedDependency[] = JSON.parse(res.stdout);
|
||||
return path.resolve(downloadDir, packed[0].filename);
|
||||
},
|
||||
async listDependencies(options) {
|
||||
const args = ['ls', '--json'];
|
||||
if (options?.all) {
|
||||
args.push('--depth', 'Infinity');
|
||||
}
|
||||
const { execa } = await import('execa');
|
||||
const res = await execa('npm', args, { cwd: options?.cwd });
|
||||
const project: NpmListProject = JSON.parse(res.stdout);
|
||||
|
||||
return flattenNpmListOutput([project]);
|
||||
},
|
||||
};
|
||||
|
||||
export function flattenNpmListOutput(projects: NpmListProject[]): Dependency[] {
|
||||
const queue: Record<string, NpmListDependency>[] = projects.flatMap(
|
||||
(project) => {
|
||||
const acc: Record<string, NpmListDependency>[] = [];
|
||||
if (project.dependencies) acc.push(project.dependencies);
|
||||
if (project.devDependencies) acc.push(project.devDependencies);
|
||||
return acc;
|
||||
},
|
||||
);
|
||||
const dependencies: Dependency[] = [];
|
||||
while (queue.length > 0) {
|
||||
Object.entries(queue.pop()!).forEach(([name, meta]) => {
|
||||
dependencies.push({
|
||||
name,
|
||||
version: meta.version,
|
||||
});
|
||||
if (meta.dependencies) queue.push(meta.dependencies);
|
||||
if (meta.devDependencies) queue.push(meta.devDependencies);
|
||||
});
|
||||
}
|
||||
return dedupeDependencies(dependencies);
|
||||
}
|
||||
|
||||
export function dedupeDependencies(dependencies: Dependency[]): Dependency[] {
|
||||
const hashes = new Set<string>();
|
||||
return dependencies.filter((dep) => {
|
||||
const hash = `${dep.name}@${dep.version}`;
|
||||
if (hashes.has(hash)) {
|
||||
return false;
|
||||
} else {
|
||||
hashes.add(hash);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export interface NpmListProject {
|
||||
name: string;
|
||||
dependencies?: Record<string, NpmListDependency>;
|
||||
devDependencies?: Record<string, NpmListDependency>;
|
||||
}
|
||||
|
||||
export interface NpmListDependency {
|
||||
version: string;
|
||||
resolved?: string;
|
||||
overridden?: boolean;
|
||||
dependencies?: Record<string, NpmListDependency>;
|
||||
devDependencies?: Record<string, NpmListDependency>;
|
||||
}
|
||||
|
||||
interface PackedDependency {
|
||||
id: string;
|
||||
name: string;
|
||||
version: string;
|
||||
filename: string;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { NpmListProject, flattenNpmListOutput, npm } from './npm';
|
||||
import { WxtPackageManagerImpl } from './types';
|
||||
|
||||
export const pnpm: WxtPackageManagerImpl = {
|
||||
overridesKey: 'resolutions', // "pnpm.overrides" has a higher priority, but I don't want to deal with nesting
|
||||
downloadDependency(...args) {
|
||||
return npm.downloadDependency(...args);
|
||||
},
|
||||
async listDependencies(options) {
|
||||
const args = ['ls', '-r', '--json'];
|
||||
if (options?.all) {
|
||||
args.push('--depth', 'Infinity');
|
||||
}
|
||||
// Helper for testing - since WXT uses pnpm workspaces, folders inside it don't behave like
|
||||
// standalone projects unless you pass the --ignore-workspace flag.
|
||||
if (
|
||||
typeof process !== 'undefined' &&
|
||||
process.env.WXT_PNPM_IGNORE_WORKSPACE === 'true'
|
||||
) {
|
||||
args.push('--ignore-workspace');
|
||||
}
|
||||
const { execa } = await import('execa');
|
||||
const res = await execa('pnpm', args, { cwd: options?.cwd });
|
||||
const projects: NpmListProject[] = JSON.parse(res.stdout);
|
||||
|
||||
return flattenNpmListOutput(projects);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
import { WxtPackageManager } from '~/types';
|
||||
|
||||
export type WxtPackageManagerImpl = Pick<
|
||||
WxtPackageManager,
|
||||
'downloadDependency' | 'listDependencies' | 'overridesKey'
|
||||
>;
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Dependency } from '~/types';
|
||||
import { WxtPackageManagerImpl } from './types';
|
||||
import { dedupeDependencies, npm } from './npm';
|
||||
|
||||
export const yarn: WxtPackageManagerImpl = {
|
||||
overridesKey: 'resolutions',
|
||||
downloadDependency(...args) {
|
||||
return npm.downloadDependency(...args);
|
||||
},
|
||||
async listDependencies(options) {
|
||||
const args = ['list', '--json'];
|
||||
if (options?.all) {
|
||||
args.push('--depth', 'Infinity');
|
||||
}
|
||||
const { execa } = await import('execa');
|
||||
const res = await execa('yarn', args, { cwd: options?.cwd });
|
||||
const tree = res.stdout
|
||||
.split('\n')
|
||||
.map<JsonLine>((line) => JSON.parse(line))
|
||||
.find((line) => line.type === 'tree')?.data as JsonLineTree | undefined;
|
||||
if (tree == null) throw Error("'yarn list --json' did not output a tree");
|
||||
|
||||
const queue = [...tree.trees];
|
||||
const dependencies: Dependency[] = [];
|
||||
|
||||
while (queue.length > 0) {
|
||||
const { name: treeName, children } = queue.pop()!;
|
||||
const match = /(@?\S+)@(\S+)$/.exec(treeName);
|
||||
if (match) {
|
||||
const [_, name, version] = match;
|
||||
dependencies.push({ name, version });
|
||||
}
|
||||
if (children != null) {
|
||||
queue.push(...children);
|
||||
}
|
||||
}
|
||||
|
||||
return dedupeDependencies(dependencies);
|
||||
},
|
||||
};
|
||||
|
||||
type JsonLine =
|
||||
| { type: unknown; data: unknown }
|
||||
| { type: 'tree'; data: JsonLineTree };
|
||||
|
||||
interface JsonLineTree {
|
||||
type: 'list';
|
||||
trees: Tree[];
|
||||
}
|
||||
|
||||
interface Tree {
|
||||
name: string;
|
||||
children?: Tree[];
|
||||
}
|
||||
@@ -111,6 +111,7 @@ describe('Manifest Utils', () => {
|
||||
setFakeWxt({
|
||||
config: {
|
||||
outDir,
|
||||
manifestVersion: 3,
|
||||
manifest: {
|
||||
action: {
|
||||
default_icon: 'icon-16.png',
|
||||
@@ -119,13 +120,83 @@ describe('Manifest Utils', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
const expected: Partial<Manifest.WebExtensionManifest> = {
|
||||
action: wxt.config.manifest.action,
|
||||
};
|
||||
|
||||
const { manifest: actual } = await generateManifest([], buildOutput);
|
||||
|
||||
expect(actual).toMatchObject(expected);
|
||||
expect(actual.action).toEqual(wxt.config.manifest.action);
|
||||
expect(actual.browser_action).toBeUndefined();
|
||||
expect(actual.page_action).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should generate `browser_action` for MV2 when only `action` is defined', async () => {
|
||||
const buildOutput = fakeBuildOutput();
|
||||
setFakeWxt({
|
||||
config: {
|
||||
outDir,
|
||||
manifestVersion: 2,
|
||||
manifest: {
|
||||
action: {
|
||||
default_title: 'Action',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { manifest: actual } = await generateManifest([], buildOutput);
|
||||
|
||||
expect(actual.action).toBeUndefined();
|
||||
expect(actual.browser_action).toEqual(wxt.config.manifest.action);
|
||||
expect(actual.page_action).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should keep the `page_action` for MV2 when both `action` and `page_action` are defined', async () => {
|
||||
const buildOutput = fakeBuildOutput();
|
||||
setFakeWxt({
|
||||
config: {
|
||||
outDir,
|
||||
manifestVersion: 2,
|
||||
manifest: {
|
||||
action: {
|
||||
default_title: 'Action',
|
||||
},
|
||||
page_action: {
|
||||
default_title: 'Page Action',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { manifest: actual } = await generateManifest([], buildOutput);
|
||||
|
||||
expect(actual.action).toBeUndefined();
|
||||
expect(actual.browser_action).toBeUndefined();
|
||||
expect(actual.page_action).toEqual(wxt.config.manifest.page_action);
|
||||
});
|
||||
|
||||
it('should keep the custom `browser_action` for MV2 when both `action` and `browser_action` are defined', async () => {
|
||||
const buildOutput = fakeBuildOutput();
|
||||
setFakeWxt({
|
||||
config: {
|
||||
outDir,
|
||||
manifestVersion: 2,
|
||||
manifest: {
|
||||
action: {
|
||||
default_title: 'Action',
|
||||
},
|
||||
browser_action: {
|
||||
default_title: 'Browser Action',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { manifest: actual } = await generateManifest([], buildOutput);
|
||||
|
||||
expect(actual.action).toBeUndefined();
|
||||
expect(actual.browser_action).toEqual(
|
||||
wxt.config.manifest.browser_action,
|
||||
);
|
||||
expect(actual.page_action).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1306,6 +1377,76 @@ describe('Manifest Utils', () => {
|
||||
expect(actual.commands).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Stripping keys', () => {
|
||||
const mv2Manifest = {
|
||||
page_action: {},
|
||||
browser_action: {},
|
||||
automation: {},
|
||||
content_capabilities: {},
|
||||
converted_from_user_script: {},
|
||||
current_locale: {},
|
||||
differential_fingerprint: {},
|
||||
event_rules: {},
|
||||
file_browser_handlers: {},
|
||||
file_system_provider_capabilities: {},
|
||||
input_components: {},
|
||||
nacl_modules: {},
|
||||
natively_connectable: {},
|
||||
offline_enabled: {},
|
||||
platforms: {},
|
||||
replacement_web_app: {},
|
||||
system_indicator: {},
|
||||
user_scripts: {},
|
||||
};
|
||||
const mv3Manifest = {
|
||||
action: {},
|
||||
export: {},
|
||||
optional_host_permissions: {},
|
||||
side_panel: {},
|
||||
};
|
||||
const hostPermissionsManifest = {
|
||||
host_permissions: {},
|
||||
};
|
||||
const manifest: any = {
|
||||
...mv2Manifest,
|
||||
...mv3Manifest,
|
||||
...hostPermissionsManifest,
|
||||
};
|
||||
|
||||
it.each([
|
||||
['firefox', 2, mv2Manifest],
|
||||
['chrome', 2, { ...mv2Manifest, ...hostPermissionsManifest }],
|
||||
['safari', 2, { ...mv2Manifest, ...hostPermissionsManifest }],
|
||||
['edge', 2, { ...mv2Manifest, ...hostPermissionsManifest }],
|
||||
['firefox', 3, { ...mv3Manifest, ...hostPermissionsManifest }],
|
||||
['chrome', 3, { ...mv3Manifest, ...hostPermissionsManifest }],
|
||||
['safari', 3, { ...mv3Manifest, ...hostPermissionsManifest }],
|
||||
['edge', 3, { ...mv3Manifest, ...hostPermissionsManifest }],
|
||||
] as const)(
|
||||
"%s MV%s should only include that version's keys",
|
||||
async (browser, manifestVersion, expected) => {
|
||||
setFakeWxt({
|
||||
config: {
|
||||
browser,
|
||||
manifest,
|
||||
manifestVersion,
|
||||
command: 'build',
|
||||
},
|
||||
});
|
||||
const output = fakeBuildOutput();
|
||||
|
||||
const { manifest: actual } = await generateManifest([], output);
|
||||
|
||||
expect(actual).toEqual({
|
||||
name: expect.any(String),
|
||||
version: expect.any(String),
|
||||
manifest_version: manifestVersion,
|
||||
...expected,
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('stripPathFromMatchPattern', () => {
|
||||
|
||||
@@ -52,8 +52,8 @@ export async function importEntrypointFile<T>(path: string): Promise<T> {
|
||||
esmResolve: true,
|
||||
alias: {
|
||||
'webextension-polyfill': resolve(
|
||||
wxt.config.root,
|
||||
'node_modules/wxt/dist/virtual/mock-browser.js',
|
||||
wxt.config.wxtModuleDir,
|
||||
'dist/virtual/mock-browser.js',
|
||||
),
|
||||
},
|
||||
// Continue using node to load TS files even if `bun run --bun` is detected. Jiti does not
|
||||
|
||||
@@ -16,8 +16,8 @@ import {
|
||||
validateEntrypoints,
|
||||
} from '../validation';
|
||||
import consola from 'consola';
|
||||
import { exec } from '../exec';
|
||||
import { wxt } from '../../wxt';
|
||||
import { mergeJsonOutputs } from '@aklinker1/rollup-plugin-visualizer';
|
||||
|
||||
/**
|
||||
* Builds the extension based on an internal config. No more config discovery is performed, the
|
||||
@@ -93,17 +93,11 @@ async function combineAnalysisStats(): Promise<void> {
|
||||
});
|
||||
const absolutePaths = unixFiles.map(unnormalizePath);
|
||||
|
||||
await exec(
|
||||
'rollup-plugin-visualizer',
|
||||
[
|
||||
...absolutePaths,
|
||||
'--template',
|
||||
wxt.config.analysis.template,
|
||||
'--filename',
|
||||
wxt.config.analysis.outputFile,
|
||||
],
|
||||
{ cwd: wxt.config.root, stdio: 'inherit' },
|
||||
);
|
||||
await mergeJsonOutputs({
|
||||
inputs: absolutePaths,
|
||||
template: wxt.config.analysis.template,
|
||||
filename: wxt.config.analysis.outputFile,
|
||||
});
|
||||
|
||||
if (!wxt.config.analysis.keepArtifacts) {
|
||||
await Promise.all(absolutePaths.map((statsFile) => fs.remove(statsFile)));
|
||||
|
||||
@@ -18,6 +18,8 @@ import { createViteBuilder } from '~/core/builders/vite';
|
||||
import defu from 'defu';
|
||||
import { NullablyRequired } from '../types';
|
||||
import { isModuleInstalled } from '../package';
|
||||
import fs from 'fs-extra';
|
||||
import { normalizePath } from '../paths';
|
||||
|
||||
/**
|
||||
* Given an inline config, discover the config file if necessary, merge the results, resolve any
|
||||
@@ -71,15 +73,22 @@ export async function resolveConfig(
|
||||
inlineConfig.root ?? userConfig.root ?? process.cwd(),
|
||||
);
|
||||
const wxtDir = path.resolve(root, '.wxt');
|
||||
const wxtModuleDir = await resolveWxtModuleDir();
|
||||
const srcDir = path.resolve(root, mergedConfig.srcDir ?? root);
|
||||
const entrypointsDir = path.resolve(
|
||||
srcDir,
|
||||
mergedConfig.entrypointsDir ?? 'entrypoints',
|
||||
);
|
||||
if (await isDirMissing(entrypointsDir)) {
|
||||
logMissingDir(logger, 'Entrypoints', entrypointsDir);
|
||||
}
|
||||
const filterEntrypoints = !!mergedConfig.filterEntrypoints?.length
|
||||
? new Set(mergedConfig.filterEntrypoints)
|
||||
: undefined;
|
||||
const publicDir = path.resolve(srcDir, mergedConfig.publicDir ?? 'public');
|
||||
if (await isDirMissing(publicDir)) {
|
||||
logMissingDir(logger, 'Public', publicDir);
|
||||
}
|
||||
const typesDir = path.resolve(wxtDir, 'types');
|
||||
const outBaseDir = path.resolve(root, mergedConfig.outDir ?? '.output');
|
||||
const outDir = path.resolve(outBaseDir, `${browser}-mv${manifestVersion}`);
|
||||
@@ -127,6 +136,7 @@ export async function resolveConfig(
|
||||
outBaseDir,
|
||||
outDir,
|
||||
publicDir,
|
||||
wxtModuleDir,
|
||||
root,
|
||||
runnerConfig,
|
||||
srcDir,
|
||||
@@ -256,6 +266,7 @@ function resolveInternalZipConfig(
|
||||
root: string,
|
||||
mergedConfig: InlineConfig,
|
||||
): NullablyRequired<ResolvedConfig['zip']> {
|
||||
const downloadedPackagesDir = path.resolve(root, '.wxt/local_modules');
|
||||
return {
|
||||
name: undefined,
|
||||
sourcesTemplate: '{{name}}-{{version}}-sources.zip',
|
||||
@@ -275,6 +286,8 @@ function resolveInternalZipConfig(
|
||||
// From user
|
||||
...(mergedConfig.zip?.excludeSources ?? []),
|
||||
],
|
||||
downloadPackages: mergedConfig.zip?.downloadPackages ?? [],
|
||||
downloadedPackagesDir,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -322,3 +335,27 @@ async function getUnimportOptions(
|
||||
defaultOptions,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to `node_modules/wxt`.
|
||||
*/
|
||||
async function resolveWxtModuleDir() {
|
||||
const requireResolve =
|
||||
require?.resolve ??
|
||||
(await import('node:module')).default.createRequire(import.meta.url)
|
||||
.resolve;
|
||||
// require.resolve returns the wxt/dist/index file, not the package's root directory, which we want to return
|
||||
return path.resolve(requireResolve('wxt'), '../..');
|
||||
}
|
||||
|
||||
async function isDirMissing(dir: string) {
|
||||
return !(await fs.exists(dir));
|
||||
}
|
||||
|
||||
function logMissingDir(logger: Logger, name: string, expected: string) {
|
||||
logger.warn(
|
||||
`${name} directory not found: ./${normalizePath(
|
||||
path.relative(process.cwd(), expected),
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import type { Options } from 'execa';
|
||||
import managePath from 'manage-path';
|
||||
import { resolve } from 'node:path';
|
||||
import { wxt } from '../wxt';
|
||||
|
||||
const managedPath = managePath(process.env);
|
||||
|
||||
/**
|
||||
* Wrapper around `execa` with a modified `PATH` variable containing CLI tools from WXT's dependencies.
|
||||
*/
|
||||
export const exec = async (
|
||||
file: string,
|
||||
args?: readonly string[],
|
||||
options?: Options,
|
||||
) => {
|
||||
// Reset so the same path isn't added multiple times
|
||||
managedPath.restore();
|
||||
|
||||
// Add subdependency path for PNPM shamefully-hoist=false
|
||||
managedPath.push(
|
||||
resolve(wxt.config.root, 'node_modules/wxt/node_modules/.bin'),
|
||||
);
|
||||
|
||||
const { execa } = await import('execa');
|
||||
return await execa(file, args, options);
|
||||
};
|
||||
@@ -114,13 +114,17 @@ export async function generateManifest(
|
||||
wxt.config.transformManifest(manifest);
|
||||
await wxt.hooks.callHook('build:manifestGenerated', wxt, manifest);
|
||||
|
||||
if (wxt.config.manifestVersion === 2)
|
||||
if (wxt.config.manifestVersion === 2) {
|
||||
convertWebAccessibleResourcesToMv2(manifest);
|
||||
convertActionToMv2(manifest);
|
||||
}
|
||||
|
||||
if (wxt.config.manifestVersion === 3) {
|
||||
validateMv3WebAccessbileResources(manifest);
|
||||
}
|
||||
|
||||
stripKeys(manifest);
|
||||
|
||||
if (manifest.name == null)
|
||||
throw Error(
|
||||
"Manifest 'name' is missing. Either:\n1. Set the name in your <rootDir>/package.json\n2. Set a name via the manifest option in your wxt.config.ts",
|
||||
@@ -613,6 +617,17 @@ export function convertWebAccessibleResourcesToMv2(
|
||||
);
|
||||
}
|
||||
|
||||
function convertActionToMv2(manifest: Manifest.WebExtensionManifest): void {
|
||||
if (
|
||||
manifest.action == null ||
|
||||
manifest.browser_action != null ||
|
||||
manifest.page_action != null
|
||||
)
|
||||
return;
|
||||
|
||||
manifest.browser_action = manifest.action;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure all resources are in MV3 format. If not, add a wanring
|
||||
*/
|
||||
@@ -632,3 +647,50 @@ export function validateMv3WebAccessbileResources(
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove keys from the manifest based on the build target.
|
||||
*/
|
||||
function stripKeys(manifest: Manifest.WebExtensionManifest): void {
|
||||
let keysToRemove: string[] = [];
|
||||
if (wxt.config.manifestVersion === 2) {
|
||||
keysToRemove.push(...mv3OnlyKeys);
|
||||
if (wxt.config.browser === 'firefox')
|
||||
keysToRemove.push(...firefoxMv3OnlyKeys);
|
||||
} else {
|
||||
keysToRemove.push(...mv2OnlyKeys);
|
||||
}
|
||||
|
||||
keysToRemove.forEach((key) => {
|
||||
delete manifest[key as keyof Manifest.WebExtensionManifest];
|
||||
});
|
||||
}
|
||||
|
||||
const mv2OnlyKeys = [
|
||||
'page_action',
|
||||
'browser_action',
|
||||
'automation',
|
||||
'content_capabilities',
|
||||
'converted_from_user_script',
|
||||
'current_locale',
|
||||
'differential_fingerprint',
|
||||
'event_rules',
|
||||
'file_browser_handlers',
|
||||
'file_system_provider_capabilities',
|
||||
'input_components',
|
||||
'nacl_modules',
|
||||
'natively_connectable',
|
||||
'offline_enabled',
|
||||
'platforms',
|
||||
'replacement_web_app',
|
||||
'system_indicator',
|
||||
'user_scripts',
|
||||
];
|
||||
|
||||
const mv3OnlyKeys = [
|
||||
'action',
|
||||
'export',
|
||||
'optional_host_permissions',
|
||||
'side_panel',
|
||||
];
|
||||
const firefoxMv3OnlyKeys = ['host_permissions'];
|
||||
|
||||
@@ -262,6 +262,7 @@ export const fakeResolvedConfig = fakeObjectCreator<ResolvedConfig>(() => {
|
||||
outDir: fakeDir(),
|
||||
publicDir: fakeDir(),
|
||||
root: fakeDir(),
|
||||
wxtModuleDir: fakeDir(),
|
||||
runnerConfig: {
|
||||
config: {},
|
||||
},
|
||||
@@ -285,6 +286,8 @@ export const fakeResolvedConfig = fakeObjectCreator<ResolvedConfig>(() => {
|
||||
sourcesRoot: fakeDir(),
|
||||
sourcesTemplate: '{{name}}-sources.zip',
|
||||
name: faker.person.firstName().toLowerCase(),
|
||||
downloadedPackagesDir: fakeDir(),
|
||||
downloadPackages: [],
|
||||
},
|
||||
transformManifest: () => {},
|
||||
userConfigMetadata: {},
|
||||
@@ -305,11 +308,13 @@ export const fakeWxt = fakeObjectCreator<Wxt>(() => ({
|
||||
hooks: mock(),
|
||||
logger: mock(),
|
||||
reloadConfig: vi.fn(),
|
||||
pm: mock(),
|
||||
}));
|
||||
|
||||
export function setFakeWxt(overrides?: DeepPartial<Wxt>) {
|
||||
const wxt = fakeWxt(overrides);
|
||||
setWxtForTesting(wxt);
|
||||
return wxt;
|
||||
}
|
||||
|
||||
export const fakeBuildOutput = fakeObjectCreator<BuildOutput>(() => ({
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { InlineConfig, Wxt, WxtDevServer, WxtHooks } from '~/types';
|
||||
import { resolveConfig } from './utils/building';
|
||||
import { createHooks } from 'hookable';
|
||||
import { createWxtPackageManager } from './package-managers';
|
||||
|
||||
/**
|
||||
* Global variable set once `createWxt` is called once. Since this variable is used everywhere, this
|
||||
@@ -18,6 +19,7 @@ export async function registerWxt(
|
||||
): Promise<void> {
|
||||
const config = await resolveConfig(inlineConfig, command, server);
|
||||
const hooks = createHooks<WxtHooks>();
|
||||
const pm = await createWxtPackageManager(config.root);
|
||||
|
||||
wxt = {
|
||||
config,
|
||||
@@ -28,6 +30,7 @@ export async function registerWxt(
|
||||
async reloadConfig() {
|
||||
wxt.config = await resolveConfig(inlineConfig, command, server);
|
||||
},
|
||||
pm,
|
||||
};
|
||||
|
||||
// Initialize hooks
|
||||
|
||||
+127
-22
@@ -1,6 +1,5 @@
|
||||
import { InlineConfig } from '~/types';
|
||||
import zipdir from 'zip-dir';
|
||||
import { dirname, relative, resolve } from 'node:path';
|
||||
import path from 'node:path';
|
||||
import fs from 'fs-extra';
|
||||
import { kebabCaseAlphanumeric } from '~/core/utils/strings';
|
||||
import { getPackageJson } from '~/core/utils/package';
|
||||
@@ -9,10 +8,13 @@ import { formatDuration } from '~/core/utils/time';
|
||||
import { printFileList } from '~/core/utils/log/printFileList';
|
||||
import { internalBuild } from '~/core/utils/building';
|
||||
import { registerWxt, wxt } from './wxt';
|
||||
import JSZip from 'jszip';
|
||||
import glob from 'fast-glob';
|
||||
import { normalizePath } from './utils/paths';
|
||||
|
||||
/**
|
||||
* Build and zip the extension for distribution.
|
||||
* @param config Opitonal config that will override your `<root>/wxt.config.ts`.
|
||||
* @param config Optional config that will override your `<root>/wxt.config.ts`.
|
||||
* @returns A list of all files included in the ZIP.
|
||||
*/
|
||||
export async function zip(config?: InlineConfig): Promise<string[]> {
|
||||
@@ -26,7 +28,7 @@ export async function zip(config?: InlineConfig): Promise<string[]> {
|
||||
const projectName =
|
||||
wxt.config.zip.name ??
|
||||
kebabCaseAlphanumeric(
|
||||
(await getPackageJson())?.name || dirname(process.cwd()),
|
||||
(await getPackageJson())?.name || path.dirname(process.cwd()),
|
||||
);
|
||||
const applyTemplate = (template: string): string =>
|
||||
template
|
||||
@@ -43,31 +45,29 @@ export async function zip(config?: InlineConfig): Promise<string[]> {
|
||||
// ZIP output directory
|
||||
|
||||
const outZipFilename = applyTemplate(wxt.config.zip.artifactTemplate);
|
||||
const outZipPath = resolve(wxt.config.outBaseDir, outZipFilename);
|
||||
await zipdir(wxt.config.outDir, {
|
||||
saveTo: outZipPath,
|
||||
});
|
||||
const outZipPath = path.resolve(wxt.config.outBaseDir, outZipFilename);
|
||||
await zipDir(wxt.config.outDir, outZipPath);
|
||||
zipFiles.push(outZipPath);
|
||||
|
||||
// ZIP sources for Firefox
|
||||
|
||||
if (wxt.config.browser === 'firefox') {
|
||||
const { overrides, files: downloadedPackages } =
|
||||
await downloadPrivatePackages();
|
||||
const sourcesZipFilename = applyTemplate(wxt.config.zip.sourcesTemplate);
|
||||
const sourcesZipPath = resolve(wxt.config.outBaseDir, sourcesZipFilename);
|
||||
await zipdir(wxt.config.zip.sourcesRoot, {
|
||||
saveTo: sourcesZipPath,
|
||||
filter(path) {
|
||||
const relativePath = relative(wxt.config.zip.sourcesRoot, path);
|
||||
|
||||
return (
|
||||
wxt.config.zip.includeSources.some((pattern) =>
|
||||
minimatch(relativePath, pattern),
|
||||
) ||
|
||||
!wxt.config.zip.excludeSources.some((pattern) =>
|
||||
minimatch(relativePath, pattern),
|
||||
)
|
||||
);
|
||||
const sourcesZipPath = path.resolve(
|
||||
wxt.config.outBaseDir,
|
||||
sourcesZipFilename,
|
||||
);
|
||||
await zipDir(wxt.config.zip.sourcesRoot, sourcesZipPath, {
|
||||
include: wxt.config.zip.includeSources,
|
||||
exclude: wxt.config.zip.excludeSources,
|
||||
transform(absolutePath, zipPath, content) {
|
||||
if (zipPath.endsWith('package.json')) {
|
||||
return addOverridesToPackageJson(absolutePath, content, overrides);
|
||||
}
|
||||
},
|
||||
additionalFiles: downloadedPackages,
|
||||
});
|
||||
zipFiles.push(sourcesZipPath);
|
||||
}
|
||||
@@ -81,3 +81,108 @@ export async function zip(config?: InlineConfig): Promise<string[]> {
|
||||
|
||||
return zipFiles;
|
||||
}
|
||||
async function zipDir(
|
||||
directory: string,
|
||||
outputPath: string,
|
||||
options?: {
|
||||
include?: string[];
|
||||
exclude?: string[];
|
||||
transform?: (
|
||||
absolutePath: string,
|
||||
zipPath: string,
|
||||
content: string,
|
||||
) => Promise<string | undefined | void> | string | undefined | void;
|
||||
additionalWork?: (archive: JSZip) => Promise<void> | void;
|
||||
additionalFiles?: string[];
|
||||
},
|
||||
): Promise<void> {
|
||||
const archive = new JSZip();
|
||||
const files = (
|
||||
await glob('**/*', {
|
||||
cwd: directory,
|
||||
// Ignore node_modules, otherwise this glob step takes forever
|
||||
ignore: ['**/node_modules'],
|
||||
onlyFiles: true,
|
||||
})
|
||||
).filter((relativePath) => {
|
||||
return (
|
||||
wxt.config.zip.includeSources.some((pattern) =>
|
||||
minimatch(relativePath, pattern),
|
||||
) ||
|
||||
!wxt.config.zip.excludeSources.some((pattern) =>
|
||||
minimatch(relativePath, pattern),
|
||||
)
|
||||
);
|
||||
});
|
||||
const filesToZip = [
|
||||
...files,
|
||||
...(options?.additionalFiles ?? []).map((file) =>
|
||||
path.relative(directory, file),
|
||||
),
|
||||
];
|
||||
for (const file of filesToZip) {
|
||||
const absolutePath = path.resolve(directory, file);
|
||||
if (file.endsWith('.json')) {
|
||||
const content = await fs.readFile(absolutePath, 'utf-8');
|
||||
archive.file(
|
||||
file,
|
||||
(await options?.transform?.(absolutePath, file, content)) || content,
|
||||
);
|
||||
} else {
|
||||
const content = await fs.readFile(absolutePath);
|
||||
archive.file(file, content);
|
||||
}
|
||||
}
|
||||
await options?.additionalWork?.(archive);
|
||||
const buffer = await archive.generateAsync({ type: 'base64' });
|
||||
await fs.writeFile(outputPath, buffer, 'base64');
|
||||
}
|
||||
|
||||
async function downloadPrivatePackages() {
|
||||
const overrides: Record<string, string> = {};
|
||||
const files: string[] = [];
|
||||
|
||||
if (wxt.config.zip.downloadPackages.length > 0) {
|
||||
const _downloadPackages = new Set(wxt.config.zip.downloadPackages);
|
||||
const allPackages = await wxt.pm.listDependencies({
|
||||
all: true,
|
||||
cwd: wxt.config.root,
|
||||
});
|
||||
const downloadPackages = allPackages.filter((pkg) =>
|
||||
_downloadPackages.has(pkg.name),
|
||||
);
|
||||
|
||||
for (const pkg of downloadPackages) {
|
||||
wxt.logger.info(`Downloading package: ${pkg.name}@${pkg.version}`);
|
||||
const id = `${pkg.name}@${pkg.version}`;
|
||||
const tgzPath = await wxt.pm.downloadDependency(
|
||||
id,
|
||||
wxt.config.zip.downloadedPackagesDir,
|
||||
);
|
||||
files.push(tgzPath);
|
||||
overrides[id] = tgzPath;
|
||||
}
|
||||
}
|
||||
|
||||
return { overrides, files };
|
||||
}
|
||||
|
||||
function addOverridesToPackageJson(
|
||||
absolutePackageJsonPath: string,
|
||||
content: string,
|
||||
overrides: Record<string, string>,
|
||||
): string {
|
||||
if (Object.keys(overrides).length === 0) return content;
|
||||
|
||||
const packageJsonDir = path.dirname(absolutePackageJsonPath);
|
||||
const oldPackage = JSON.parse(content);
|
||||
const newPackage = {
|
||||
...oldPackage,
|
||||
[wxt.pm.overridesKey]: { ...oldPackage[wxt.pm.overridesKey] },
|
||||
};
|
||||
Object.entries(overrides).forEach(([key, absolutePath]) => {
|
||||
newPackage[wxt.pm.overridesKey][key] =
|
||||
'file://./' + normalizePath(path.relative(packageJsonDir, absolutePath));
|
||||
});
|
||||
return JSON.stringify(newPackage, null, 2);
|
||||
}
|
||||
|
||||
+44
-25
@@ -258,14 +258,14 @@ function createStorage(): WxtStorage {
|
||||
);
|
||||
}
|
||||
const migrate = async () => {
|
||||
const [value, meta] = await Promise.all([
|
||||
// TODO: Optimize with getItems
|
||||
getItem(driver, driverKey, undefined),
|
||||
getMeta(driver, driverKey),
|
||||
const driverMetaKey = getMetaKey(driverKey);
|
||||
const [{ value }, { value: meta }] = await driver.getItems([
|
||||
driverKey,
|
||||
driverMetaKey,
|
||||
]);
|
||||
if (value == null) return;
|
||||
|
||||
const currentVersion = meta.v ?? 1;
|
||||
const currentVersion = meta?.v ?? 1;
|
||||
if (currentVersion > targetVersion) {
|
||||
throw Error(
|
||||
`Version downgrade detected (v${currentVersion} -> v${targetVersion}) for "${key}"`,
|
||||
@@ -285,24 +285,21 @@ function createStorage(): WxtStorage {
|
||||
(await migrations?.[migrateToVersion]?.(migratedValue)) ??
|
||||
migratedValue;
|
||||
}
|
||||
await Promise.all([
|
||||
// TODO: Optimize with `setItem`
|
||||
setItem(driver, driverKey, migratedValue),
|
||||
setMeta(driver, driverKey, { v: targetVersion }),
|
||||
await driver.setItems([
|
||||
{ key: driverKey, value: migratedValue },
|
||||
{ key: driverMetaKey, value: { ...meta, v: targetVersion } },
|
||||
]);
|
||||
logger.debug(
|
||||
`Storage migration completed for ${key} v${targetVersion}`,
|
||||
{ migratedValue },
|
||||
);
|
||||
};
|
||||
browser.runtime.onInstalled.addListener(async ({ reason }) => {
|
||||
if (reason !== 'update') return;
|
||||
try {
|
||||
await migrate();
|
||||
} catch (err) {
|
||||
logger.error(`Migration failed for ${key}`, err);
|
||||
}
|
||||
});
|
||||
const migrationsDone =
|
||||
opts?.migrations == null
|
||||
? Promise.resolve()
|
||||
: migrate().catch((err) => {
|
||||
logger.error(`Migration failed for ${key}`, err);
|
||||
});
|
||||
|
||||
const getDefaultValue = () => opts?.defaultValue ?? null;
|
||||
|
||||
@@ -310,12 +307,30 @@ function createStorage(): WxtStorage {
|
||||
get defaultValue() {
|
||||
return getDefaultValue();
|
||||
},
|
||||
getValue: () => getItem(driver, driverKey, opts),
|
||||
getMeta: () => getMeta(driver, driverKey),
|
||||
setValue: (value) => setItem(driver, driverKey, value),
|
||||
setMeta: (properties) => setMeta(driver, driverKey, properties),
|
||||
removeValue: (opts) => removeItem(driver, driverKey, opts),
|
||||
removeMeta: (properties) => removeMeta(driver, driverKey, properties),
|
||||
getValue: async () => {
|
||||
await migrationsDone;
|
||||
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 ?? getDefaultValue(), oldValue ?? getDefaultValue()),
|
||||
@@ -346,7 +361,10 @@ function createDriver(
|
||||
);
|
||||
}
|
||||
|
||||
return browser.storage[storageArea];
|
||||
const area = browser.storage[storageArea];
|
||||
if (area == null)
|
||||
throw Error(`"browser.storage.${storageArea}" is undefined`);
|
||||
return area;
|
||||
};
|
||||
const watchListeners = new Set<
|
||||
(changes: Storage.StorageAreaOnChangedChangesType) => void
|
||||
@@ -421,7 +439,8 @@ export interface WxtStorage {
|
||||
*/
|
||||
getItem<T>(key: string, opts?: GetItemOptions<T>): Promise<T | null>;
|
||||
/**
|
||||
* Get multiple items from storage. There is no guarantee of order in the returned array.
|
||||
* Get multiple items from storage. The return order is guaranteed to be the same as the order
|
||||
* requested.
|
||||
*
|
||||
* @example
|
||||
* await storage.getItems(["local:installDate", "session:someCounter"]);
|
||||
|
||||
+89
-3
@@ -3,10 +3,11 @@ import type { Manifest, Scripting } from '~/browser';
|
||||
import { UnimportOptions } from 'unimport';
|
||||
import { LogLevel } from 'consola';
|
||||
import { ContentScriptContext } from '../client/content-scripts/content-script-context';
|
||||
import type { PluginVisualizerOptions } from 'rollup-plugin-visualizer';
|
||||
import type { PluginVisualizerOptions } from '@aklinker1/rollup-plugin-visualizer';
|
||||
import type { FSWatcher } from 'chokidar';
|
||||
import { ResolvedConfig as C12ResolvedConfig } from 'c12';
|
||||
import { Hookable, NestedHooks } from 'hookable';
|
||||
import type * as Nypm from 'nypm';
|
||||
|
||||
export interface InlineConfig {
|
||||
/**
|
||||
@@ -20,6 +21,9 @@ 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
|
||||
* source dir.
|
||||
*
|
||||
* @default config.root
|
||||
*/
|
||||
srcDir?: string;
|
||||
@@ -171,6 +175,31 @@ export interface InlineConfig {
|
||||
* ]
|
||||
*/
|
||||
excludeSources?: string[];
|
||||
/**
|
||||
* The Firefox review process requires the extension be buildable from source to make reviewing
|
||||
* easier. This field allows you to use private packages without exposing your auth tokens.
|
||||
*
|
||||
* Just list the name of all the packages you want to download and include in the sources zip.
|
||||
* Usually, these will be private packages behind auth tokens, but they don't have to be.
|
||||
*
|
||||
* All packages listed here will be downloaded to in `.wxt/local_modules/` and an `overrides` or
|
||||
* `resolutions` field (depending on your package manager) will be added to the `package.json`,
|
||||
* pointing to the downloaded packages.
|
||||
*
|
||||
* > ***DO NOT include versions or version filters.*** Just the package name. If multiple
|
||||
* > versions of a package are present in the project, all versions will be downloaded and
|
||||
* > referenced in the package.json correctly.
|
||||
*
|
||||
* @default []
|
||||
*
|
||||
* @example
|
||||
* // Correct:
|
||||
* ["@scope/package-name", "package-name"]
|
||||
*
|
||||
* // Incorrect, don't include versions!!!
|
||||
* ["@scope/package-name@1.1.3", "package-name@^2"]
|
||||
*/
|
||||
downloadPackages?: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -729,14 +758,23 @@ export type UserManifestFn = (
|
||||
) => UserManifest | Promise<UserManifest>;
|
||||
|
||||
export interface ConfigEnv {
|
||||
/**
|
||||
* The build mode passed into the CLI. By default, `wxt` uses `"development"` and `wxt build|zip`
|
||||
* uses `"production"`.
|
||||
*/
|
||||
mode: string;
|
||||
/**
|
||||
* The command used to run WXT. `"serve"` during development and `"build"` for any other command.
|
||||
*/
|
||||
command: 'build' | 'serve';
|
||||
/**
|
||||
* Browser passed in from the CLI
|
||||
* Browser passed in from the CLI via the `-b` or `--browser` flag. Defaults to `"chrome"` when not passed.
|
||||
*/
|
||||
browser: TargetBrowser;
|
||||
/**
|
||||
* Manifest version passed in from the CLI
|
||||
* Manifest version passed in from the CLI via the `--mv2` or `--mv3` flags. When not passed, it depends on the target browser. See
|
||||
* [the guide](https://wxt.dev/guide/multiple-browsers.html#target-manifest-version) for more
|
||||
* details.
|
||||
*/
|
||||
manifestVersion: 2 | 3;
|
||||
}
|
||||
@@ -950,6 +988,10 @@ export interface Wxt {
|
||||
* Reload config file and update the `config` field with the result.
|
||||
*/
|
||||
reloadConfig: () => Promise<void>;
|
||||
/**
|
||||
* Package manager utilities.
|
||||
*/
|
||||
pm: WxtPackageManager;
|
||||
}
|
||||
|
||||
export interface ResolvedConfig {
|
||||
@@ -963,6 +1005,10 @@ export interface ResolvedConfig {
|
||||
outBaseDir: string;
|
||||
outDir: string;
|
||||
debug: boolean;
|
||||
/**
|
||||
* Directory pointing to `node_modules/wxt`, wherever WXT is installed.
|
||||
*/
|
||||
wxtModuleDir: string;
|
||||
mode: string;
|
||||
command: 'build' | 'serve';
|
||||
browser: TargetBrowser;
|
||||
@@ -981,6 +1027,8 @@ export interface ResolvedConfig {
|
||||
includeSources: string[];
|
||||
excludeSources: string[];
|
||||
sourcesRoot: string;
|
||||
downloadedPackagesDir: string;
|
||||
downloadPackages: string[];
|
||||
};
|
||||
transformManifest: (manifest: Manifest.WebExtensionManifest) => void;
|
||||
analysis: {
|
||||
@@ -1072,3 +1120,41 @@ export type WxtUnimportOptions = Partial<UnimportOptions> & {
|
||||
export type WxtResolvedUnimportOptions = Partial<UnimportOptions> & {
|
||||
eslintrc: ResolvedEslintrc;
|
||||
};
|
||||
|
||||
/**
|
||||
* Package management utils built on top of [`nypm`](https://www.npmjs.com/package/nypm)
|
||||
*/
|
||||
export interface WxtPackageManager extends Nypm.PackageManager {
|
||||
addDependency: typeof Nypm.addDependency;
|
||||
addDevDependency: typeof Nypm.addDevDependency;
|
||||
ensureDependencyInstalled: typeof Nypm.ensureDependencyInstalled;
|
||||
installDependencies: typeof Nypm.installDependencies;
|
||||
removeDependency: typeof Nypm.removeDependency;
|
||||
/**
|
||||
* Download a package's TGZ file and move it into the `downloadDir`. Use's `npm pack <name>`, so
|
||||
* you must have setup authorization in `.npmrc` file, regardless of the package manager used.
|
||||
*
|
||||
* @param id Name of the package to download, can include a version (like `wxt@0.17.1`)
|
||||
* @param downloadDir Where to store the package.
|
||||
* @returns Absolute path to downloaded file.
|
||||
*/
|
||||
downloadDependency: (id: string, downloadDir: string) => Promise<string>;
|
||||
/**
|
||||
* Run `npm ls`, `pnpm ls`, or `bun pm ls`, or `yarn list` and return the results.
|
||||
*
|
||||
* WARNING: Yarn always returns all dependencies
|
||||
*/
|
||||
listDependencies: (options?: {
|
||||
cwd?: string;
|
||||
all?: boolean;
|
||||
}) => Promise<Dependency[]>;
|
||||
/**
|
||||
* Key used to override package versions. Sometimes called "resolutions".
|
||||
*/
|
||||
overridesKey: string;
|
||||
}
|
||||
|
||||
export interface Dependency {
|
||||
name: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
Vendored
-12
@@ -50,18 +50,6 @@ declare module 'web-ext-run/util/logger' {
|
||||
export const consoleStream: IConsoleStream;
|
||||
}
|
||||
|
||||
declare module 'manage-path' {
|
||||
export interface ManagedPath {
|
||||
push(...paths: string[]);
|
||||
push(paths: string[]);
|
||||
shift(...paths: string[]);
|
||||
shift(paths: string[]);
|
||||
get(): string;
|
||||
restore(): void;
|
||||
}
|
||||
export default function managePath(env: object): ManagedPath;
|
||||
}
|
||||
|
||||
declare module 'wxt/browser' {
|
||||
// Overridden when types are generated per project
|
||||
export type PublicPath = string;
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export default defineContentScript({
|
||||
matches: ['*://*.google.com/*'],
|
||||
main() {
|
||||
console.log('Hello content.');
|
||||
},
|
||||
});
|
||||
@@ -23,6 +23,6 @@
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"typescript": "^5.3.3",
|
||||
"wxt": "^0.16.0"
|
||||
"wxt": "^0.17.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export default defineContentScript({
|
||||
matches: ['*://*.google.com/*'],
|
||||
main() {
|
||||
console.log('Hello content.');
|
||||
},
|
||||
});
|
||||
@@ -20,6 +20,6 @@
|
||||
"devDependencies": {
|
||||
"typescript": "^5.3.3",
|
||||
"vite-plugin-solid": "^2.8.0",
|
||||
"wxt": "^0.16.0"
|
||||
"wxt": "^0.17.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,6 @@
|
||||
"svelte-check": "^3.6.2",
|
||||
"tslib": "^2.6.2",
|
||||
"typescript": "^5.3.3",
|
||||
"wxt": "^0.16.0"
|
||||
"wxt": "^0.17.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export default defineContentScript({
|
||||
matches: ['*://*.google.com/*'],
|
||||
main() {
|
||||
console.log('Hello content.');
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
export default defineContentScript({
|
||||
matches: ['*://*.google.com/*'],
|
||||
main() {
|
||||
console.log('Hello content.');
|
||||
},
|
||||
});
|
||||
@@ -16,6 +16,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.3.3",
|
||||
"wxt": "^0.16.0"
|
||||
"wxt": "^0.17.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export default defineContentScript({
|
||||
matches: ['*://*.google.com/*'],
|
||||
main() {
|
||||
console.log('Hello content.');
|
||||
},
|
||||
});
|
||||
@@ -21,6 +21,6 @@
|
||||
"@vitejs/plugin-vue": "^5.0.1",
|
||||
"typescript": "^5.3.3",
|
||||
"vue-tsc": "^1.8.27",
|
||||
"wxt": "^0.16.0"
|
||||
"wxt": "^0.17.0"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user