Compare commits

..

28 Commits

Author SHA1 Message Date
GitHub Actions b226d9b947 chore(release): v0.17.6 2024-03-03 16:05:15 +00:00
Aaron aabba41e86 fix(zip): List all private packages correctly in a PNPM workspace (#520) 2024-03-03 09:56:42 -06:00
Aaron f19e6908b0 feat: Automatically generate browser_action based on action for MV2 (#519) 2024-03-03 09:52:43 -06:00
Aaron 5f651cc3fe feat: Automatically remove top-level MV2-only or MV3-only keys (#518)
When targeting MV2, all MV3 keys will be removed. When targeting MV3, all MV2 keys will be removed.
2024-03-03 09:22:24 -06:00
Aaron 8a6acbaaf2 feat: Add warnings when important directories are missing (#516) 2024-03-02 08:56:20 -06:00
dependabot[bot] 13ab8e4d01 chore(deps-dev): bump @types/react from 18.2.34 to 18.2.61 (#510)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-02 08:55:51 -06:00
dependabot[bot] bd039dcbab chore(deps): bump chokidar from 3.5.3 to 3.6.0 (#511)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-02 08:55:00 -06:00
Aaron Klinker 9cd4e8342b docs: Mentions moving folders into srcDir
Apart of #515
2024-03-02 08:42:31 -06:00
dependabot[bot] c9633cefec chore(deps): bump linkedom from 0.16.1 to 0.16.8 (#513)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-01 17:04:05 -06:00
GitHub Actions 1dd1370ed7 chore(release): v0.17.5 2024-03-01 18:01:15 +00:00
Aaron 9ff90d0acf feat: Download and override private packages for Firefox code review (#507) 2024-03-01 10:17:45 -06:00
Aaron Klinker b754435589 docs: Fix CLI generation 2024-02-29 12:13:28 -06:00
Aaron Klinker 195d2cceaf docs: Add docs about configuring the manifest as a function 2024-02-29 11:39:06 -06:00
Aaron 420d78b638 feat: Expose package management utils under wxt.pm (#502) 2024-02-28 21:38:35 -06:00
btea b35e499355 docs: Fix typos (#503) 2024-02-28 21:36:43 -06:00
Aaron 41c3d6d015 chore: Use JSZip for wxt zip, enabling future features (#501) 2024-02-28 14:57:29 -06:00
GitHub Actions 64610bad41 chore(release): v0.17.4 2024-02-28 18:28:34 +00:00
Aaron 45b7d7c67f fix: Resolve the path to node_modules/wxt correctly (#498) 2024-02-28 12:25:04 -06:00
Aaron 0bb2746869 feat: Add ResolvedConfig.wxtModuleDir, resolving the directory once (#497) 2024-02-28 11:52:46 -06:00
Aaron Klinker f9b0aa45f8 chore: Fix tests after template change 2024-02-28 11:34:15 -06:00
btea 6f6ce33bbd feat: Add basic content script to templates (#495)
Co-authored-by: Aaron Klinker <aaronklinker1@gmail.com>
2024-02-28 11:30:53 -06:00
Aaron Klinker e1c6020997 docs: Add testing example for ContentScriptContext
Apart of #491
2024-02-28 11:17:20 -06:00
Aaron Klinker 012bd7e67f docs: Update context usage 2024-02-28 11:16:56 -06:00
Aaron Klinker 35778f70cb docs: Update comparison page 2024-02-28 10:39:47 -06:00
Aaron Klinker cb4f9aa752 docs: Add CRXJS to comparison page 2024-02-28 08:05:39 -06:00
Aaron Klinker f80fb42483 docs: Fix typos 2024-02-28 07:42:22 -06:00
Leo Shklovskii 02c79ca244 docs: Added DocVersionRedirector to "Using WXT" section (#492) 2024-02-27 23:47:10 -06:00
Qiwei Yang 50e8c86ce9 doc: Add markdown sticky ntoes (#488) 2024-02-25 13:37:10 -06:00
53 changed files with 1486 additions and 120 deletions
+3
View File
@@ -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
+1
View File
@@ -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
+76
View File
@@ -1,5 +1,81 @@
# Changelog
## 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)
+3
View File
@@ -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);
+2 -2
View File
@@ -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',
+8 -2
View File
@@ -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 +
+16 -7
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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: [
// ...
],
},
}),
});
```
+22
View File
@@ -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
View File
@@ -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",
+74
View File
@@ -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
View File
@@ -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(
+5 -3
View File
@@ -1,7 +1,7 @@
{
"name": "wxt",
"type": "module",
"version": "0.17.3",
"version": "0.17.6",
"description": "Next gen framework for developing web extensions",
"engines": {
"node": ">=18",
@@ -128,10 +128,12 @@
"is-wsl": "^3.0.0",
"jiti": "^1.21.0",
"json5": "^2.2.3",
"jszip": "^3.10.1",
"linkedom": "^0.16.1",
"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",
@@ -139,8 +141,7 @@
"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",
@@ -152,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",
+55 -30
View File
@@ -34,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
@@ -74,9 +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
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
@@ -110,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
@@ -141,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
@@ -1149,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:
@@ -1160,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
@@ -1177,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):
@@ -1369,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:
@@ -1712,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==}
@@ -1756,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
@@ -1842,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
@@ -1879,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'}
@@ -2260,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==}
@@ -2482,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==}
@@ -2503,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==}
@@ -2660,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==}
@@ -3388,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
@@ -3673,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==}
@@ -3848,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'}
@@ -3876,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:
@@ -4053,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==}
@@ -4098,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==}
@@ -4213,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==}
@@ -4461,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
@@ -4971,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
@@ -5072,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==}
@@ -5130,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
@@ -5691,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==}
+1 -1
View File
@@ -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,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';
@@ -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' },
]);
});
});
},
);
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,46 @@
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);
});
});
});
@@ -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' },
]);
});
});
});
+27
View File
@@ -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 })),
);
},
};
+76
View File
@@ -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,
};
+85
View File
@@ -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;
}
+28
View File
@@ -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);
},
};
+6
View File
@@ -0,0 +1,6 @@
import { WxtPackageManager } from '~/types';
export type WxtPackageManagerImpl = Pick<
WxtPackageManager,
'downloadDependency' | 'listDependencies' | 'overridesKey'
>;
+54
View File
@@ -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[];
}
+145 -4
View File
@@ -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', () => {
+2 -2
View File
@@ -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
+37
View File
@@ -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),
)}`,
);
}
+63 -1
View File
@@ -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'];
+5
View File
@@ -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>(() => ({
+3
View File
@@ -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
+124 -22
View File
@@ -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(file, content) {
if (file.endsWith('package.json')) {
return addOverridesToPackageJson(content, overrides);
}
},
additionalFiles: downloadedPackages,
});
zipFiles.push(sourcesZipPath);
}
@@ -81,3 +81,105 @@ export async function zip(config?: InlineConfig): Promise<string[]> {
return zipFiles;
}
async function zipDir(
directory: string,
outputPath: string,
options?: {
include?: string[];
exclude?: string[];
transform?: (
file: 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?.(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] =
'file://./' + normalizePath(path.relative(wxt.config.root, tgzPath));
}
}
return { overrides, files };
}
function addOverridesToPackageJson(
content: string,
overrides: Record<string, string>,
): string {
if (Object.keys(overrides).length === 0) return content;
const oldPackage = JSON.parse(content);
const newPackage = {
...oldPackage,
[wxt.pm.overridesKey]: {
...oldPackage[wxt.pm.overridesKey],
...overrides,
},
};
return JSON.stringify(newPackage, null, 2);
}
+88 -2
View File
@@ -7,6 +7,7 @@ import type { PluginVisualizerOptions } from '@aklinker1/rollup-plugin-visualize
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;
}
+6
View File
@@ -0,0 +1,6 @@
export default defineContentScript({
matches: ['*://*.google.com/*'],
main() {
console.log('Hello content.');
},
});
+6
View File
@@ -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.');
},
});
+6
View File
@@ -0,0 +1,6 @@
export default defineContentScript({
matches: ['*://*.google.com/*'],
main() {
console.log('Hello content.');
},
});
+6
View File
@@ -0,0 +1,6 @@
export default defineContentScript({
matches: ['*://*.google.com/*'],
main() {
console.log('Hello content.');
},
});