Compare commits
95 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b99da14799 | |||
| f02400a1fb | |||
| bbcb7f7cdd | |||
| 30cf8e6d54 | |||
| 4f772fc0f7 | |||
| 2cf2582138 | |||
| ad4de4b92c | |||
| 27ed73a6ae | |||
| a1570c8917 | |||
| 7e419ddd43 | |||
| b045d3616d | |||
| f6b50e2e77 | |||
| 9d1381cbb3 | |||
| 99b0242d86 | |||
| db27628211 | |||
| 9b9d86229a | |||
| 46f830dacf | |||
| 3dd9bc7772 | |||
| c7335add19 | |||
| c133498208 | |||
| 6a52bb22a3 | |||
| 6f970efda5 | |||
| c69350a0eb | |||
| 4add8820eb | |||
| 033dd4811e | |||
| da5cd32502 | |||
| edf33fdec9 | |||
| 448cbf16c9 | |||
| e45b77b528 | |||
| b61fd1716e | |||
| d584b5cc7e | |||
| faf9cedfbd | |||
| 3a38ac9339 | |||
| 7ea36a3cfc | |||
| 3577c0b47a | |||
| d0bef42186 | |||
| d9f10c62ad | |||
| 35d3c03e4b | |||
| fdd38a1580 | |||
| 3018801f06 | |||
| 8e96bfef06 | |||
| 31071bd11e | |||
| f5619f6be1 | |||
| adad1b5a2c | |||
| a6c4e19a5d | |||
| af6f74c344 | |||
| ce45cb8a89 | |||
| 14d4aa202d | |||
| 4c35798dba | |||
| 656a9b365f | |||
| 67fa3db921 | |||
| f2d3061e97 | |||
| 760c34e416 | |||
| 48398b315c | |||
| 45d0d9d7f1 | |||
| b0f4ac8221 | |||
| 719192b1aa | |||
| 7eb32bdac8 | |||
| 862756fd5b | |||
| 5ba16e72e0 | |||
| e43ae0504d | |||
| acb6cd180c | |||
| a3301d0413 | |||
| b9e72358ac | |||
| 61b42ef326 | |||
| da9e27c28d | |||
| 6f80fbabaa | |||
| d2308bd45c | |||
| d35972d24f | |||
| 298a264934 | |||
| ad63b595b4 | |||
| 986a9ceebf | |||
| 682b65e723 | |||
| cb661506c0 | |||
| 505aa06443 | |||
| f534a978f1 | |||
| 1293815934 | |||
| 0c0fd18bf4 | |||
| c49c31813f | |||
| 877ab11bb4 | |||
| 0d18d8d7e3 | |||
| 5f977857a1 | |||
| a63b21d417 | |||
| e1b7d6090e | |||
| 1bcc5cdd81 | |||
| 873fdbc4b3 | |||
| 79128effea | |||
| 14ef94fc09 | |||
| 759e3e3a4d | |||
| 5b7b16efba | |||
| 7fd171acb5 | |||
| 02ed3f1d44 | |||
| 4f7f3f36a3 | |||
| 73a8cf12a9 | |||
| 5f5eb16705 |
@@ -14,6 +14,7 @@ on:
|
||||
- module-solid
|
||||
- module-svelte
|
||||
- module-vue
|
||||
- runner
|
||||
- storage
|
||||
- unocss
|
||||
- webextension-polyfill
|
||||
|
||||
@@ -14,6 +14,7 @@ on:
|
||||
- module-solid
|
||||
- module-svelte
|
||||
- module-vue
|
||||
- runner
|
||||
- storage
|
||||
- webextension-polyfill
|
||||
- wxt
|
||||
|
||||
@@ -36,8 +36,7 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
commit_message: 'fix: Upgrade \`@wxt-dev/browser\` to latest \`@types/chrome\` version'
|
||||
file_pattern: 'packages/browser/**'
|
||||
commit_message: 'fix: Upgrade `@wxt-dev/browser` to latest `@types/chrome` version'
|
||||
|
||||
- name: Publish Package
|
||||
if: steps.commit.outputs.changes_detected == 'true'
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
.output
|
||||
.webextrc
|
||||
.wxt
|
||||
.wxt-runner
|
||||
*.log
|
||||
/docs/.vitepress/cache
|
||||
docs/.vitepress/.temp
|
||||
coverage
|
||||
dist
|
||||
node_modules
|
||||
|
||||
@@ -70,6 +70,21 @@ const chromeExtensionIds = [
|
||||
'hafcajcllbjnoolpfngclfmmgpikdhlm', // Monochromate
|
||||
'bmoggiinmnodjphdjnmpcnlleamkfedj', // AliasVault - Open-Source Password & (Email) Alias Manager
|
||||
'hlnhhamckimoaiekbglafiebkfimhapb', // SnapThePrice: AI-Powered Real-time Lowest Price Finder
|
||||
'gdjampjdgjmbifnhldgcnccdjkcoicmg', // radiofrance - news & broadcasts (French), music (international)
|
||||
'jlnhphlghikichhgbnkepenehbmloenb', // Blens - Time Tracker and AI Insight
|
||||
'njnammmpdodmfkodnfpammnpdcbhnlcm', // Always Light Mode - Setting website always in light mode
|
||||
'lblmfclcfniabobmamfkdogcgdagbhhb', // DesignPicker - Color Picker & Font Detector
|
||||
'pamnlaoeobcmhkliljfaofekeddpmfoh', // Web to PDF
|
||||
'jmbcbeepjfenihlocplnbmbhimcoooka', // Online CSV Viewer
|
||||
'nkjcoophmpcmmgadnljnlpbpfdfacgbo', // YouTube Video Transcript
|
||||
'lcaieahkjgeggeiihblhcjbbjlppgieh', // NetSuite Record Scripts
|
||||
'gmocfknjllodfiomnljmaehcplnekhlo', // VueTracker
|
||||
'ggcfemmoabhhelfkhknhbnkmeahloiod', // CanCopy - A web extension that allow you to copy any content from website
|
||||
'modkelfkcfjpgbfmnbnllalkiogfofhb', // Language Learning with AI
|
||||
'npfopljnjbamegincfjelhjhnonnjloo', // Bilibili Feed History Helper
|
||||
'edkhpdceeinkcacjdgebjehipmnbomce', // NZBDonkey - The ultimate NZB file download tool
|
||||
'cckggnbnimdbbpmdinkkgbbncopbloob', // WeChat Markdown Editor(微信 Markdown 编辑器)
|
||||
'jcblcjolcojmfopefcighfmkkefbaofg', // Tab Grab
|
||||
];
|
||||
|
||||
const { data, err, isLoading } = useListExtensionDetails(chromeExtensionIds);
|
||||
|
||||
+29
-14
@@ -1,4 +1,4 @@
|
||||
import { defineConfig } from 'vitepress';
|
||||
import { DefaultTheme, defineConfig } from 'vitepress';
|
||||
import typedocSidebar from '../api/reference/typedoc-sidebar.json';
|
||||
import {
|
||||
menuGroup,
|
||||
@@ -15,7 +15,8 @@ import { version as autoIconsVersion } from '../../packages/auto-icons/package.j
|
||||
import { version as unocssVersion } from '../../packages/unocss/package.json';
|
||||
import { version as storageVersion } from '../../packages/storage/package.json';
|
||||
import { version as analyticsVersion } from '../../packages/analytics/package.json';
|
||||
import knowledge from 'vitepress-knowledge';
|
||||
import { version as runnerVersion } from '../../packages/runner/package.json';
|
||||
import addKnowledge from 'vitepress-knowledge';
|
||||
import {
|
||||
groupIconMdPlugin,
|
||||
groupIconVitePlugin,
|
||||
@@ -41,20 +42,28 @@ const otherPackages = {
|
||||
i18n: i18nVersion,
|
||||
storage: storageVersion,
|
||||
unocss: unocssVersion,
|
||||
runner: runnerVersion,
|
||||
};
|
||||
|
||||
const knowledge = addKnowledge<DefaultTheme.Config>({
|
||||
serverUrl: 'https://knowledge.wxt.dev',
|
||||
paths: {
|
||||
'/': 'docs',
|
||||
'/api/': 'api-reference',
|
||||
'/blog/': 'blog',
|
||||
},
|
||||
layoutSelectors: {
|
||||
blog: '.container-content',
|
||||
},
|
||||
pageSelectors: {
|
||||
'examples.md': '#VPContent > .VPPage',
|
||||
'blog.md': '#VPContent > .VPPage',
|
||||
},
|
||||
});
|
||||
|
||||
// https://vitepress.dev/reference/site-config
|
||||
export default defineConfig({
|
||||
extends: knowledge({
|
||||
serverUrl: 'https://knowledge.wxt.dev',
|
||||
paths: {
|
||||
'/': 'docs',
|
||||
'/api/': 'api-reference',
|
||||
},
|
||||
pageSelectors: {
|
||||
'examples.md': '#VPContent > .VPPage',
|
||||
},
|
||||
}),
|
||||
extends: knowledge,
|
||||
|
||||
titleTemplate: `:title${titleSuffix}`,
|
||||
title: 'WXT',
|
||||
@@ -78,6 +87,9 @@ export default defineConfig({
|
||||
},
|
||||
|
||||
async buildEnd(site) {
|
||||
// @ts-expect-error: knowledge.buildEnd is not typed, but it exists.
|
||||
await knowledge.buildEnd(site);
|
||||
|
||||
// Only construct the RSS document for production builds
|
||||
const { default: blogDataLoader } = await import('./loaders/blog.data');
|
||||
const posts = await blogDataLoader.load();
|
||||
@@ -95,8 +107,8 @@ export default defineConfig({
|
||||
description: post.frontmatter.description,
|
||||
});
|
||||
});
|
||||
console.log('rss.xml:');
|
||||
console.log(feed.rss2());
|
||||
// console.log('rss.xml:');
|
||||
// console.log(feed.rss2());
|
||||
await writeFile(join(site.outDir, 'rss.xml'), feed.rss2(), 'utf8');
|
||||
},
|
||||
|
||||
@@ -118,6 +130,9 @@ export default defineConfig({
|
||||
md.use(footnote);
|
||||
md.use(groupIconMdPlugin);
|
||||
},
|
||||
languageAlias: {
|
||||
mjs: 'js',
|
||||
},
|
||||
},
|
||||
|
||||
themeConfig: {
|
||||
|
||||
@@ -70,3 +70,7 @@ You don't have to use `#imports` if you don't like - you can continue importing
|
||||
Happy Coding 😄
|
||||
|
||||
> P.S. Yes, this is exactly how [Nuxt's `#imports`](https://nuxt.com/docs/guide/concepts/auto-imports#explicit-imports) works! We use the exact same library, [`unimport`](https://github.com/unjs/unimport).
|
||||
|
||||
---
|
||||
|
||||
[Discuss this blog post on Github](https://github.com/wxt-dev/wxt/discussions/1543).
|
||||
|
||||
@@ -8,13 +8,23 @@ outline: deep
|
||||
|
||||
During development, WXT uses [`web-ext` by Mozilla](https://www.npmjs.com/package/web-ext) to automatically open a browser window with your extension installed.
|
||||
|
||||
:::danger
|
||||
Chrome 137 removed support for the `--load-extension` CLI flag, which WXT relied on to open the browser with an extension installed. So this feature will not work for Chrome.
|
||||
|
||||
You have two options:
|
||||
|
||||
1. Install [Chrome for Testing](https://developer.chrome.com/blog/chrome-for-testing/) (which still supports the `--load-extension` flag) and [point the `chrome` binary to it](#set-browser-binaries), or
|
||||
2. [Disable this feature](#disable-opening-browser) and manually load your extension
|
||||
|
||||
:::
|
||||
|
||||
## Config Files
|
||||
|
||||
You can configure browser startup in 3 places:
|
||||
|
||||
1. `<rootDir>/web-ext.config.ts`: Ignored from version control, this file lets you configure your own options for a specific project without affecting other developers
|
||||
|
||||
```ts
|
||||
```ts [web-ext.config.ts]
|
||||
import { defineWebExtConfig } from 'wxt';
|
||||
|
||||
export default defineWebExtConfig({
|
||||
@@ -22,7 +32,7 @@ You can configure browser startup in 3 places:
|
||||
});
|
||||
```
|
||||
|
||||
2. `<rootDir>/wxt.config.ts`: Via the [`runner` config](/api/reference/wxt/interfaces/InlineConfig#runner), included in version control
|
||||
2. `<rootDir>/wxt.config.ts`: Via the [`webExt` config](/api/reference/wxt/interfaces/InlineConfig#webext), included in version control
|
||||
3. `$HOME/web-ext.config.ts`: Provide default values for all WXT projects on your computer
|
||||
|
||||
## Recipes
|
||||
@@ -31,7 +41,7 @@ You can configure browser startup in 3 places:
|
||||
|
||||
To set or customize the browser opened during development:
|
||||
|
||||
```ts
|
||||
```ts [web-ext.config.ts]
|
||||
export default defineWebExtConfig({
|
||||
binaries: {
|
||||
chrome: '/path/to/chrome-beta', // Use Chrome Beta instead of regular Chrome
|
||||
@@ -81,7 +91,7 @@ You can use any directory you'd like for `--user-data-dir`, the examples above c
|
||||
|
||||
If you prefer to load the extension into your browser manually, you can disable the auto-open behavior:
|
||||
|
||||
```ts
|
||||
```ts [web-ext.config.ts]
|
||||
export default defineWebExtConfig({
|
||||
disabled: true,
|
||||
});
|
||||
|
||||
@@ -42,6 +42,8 @@ WXT provides some custom environment variables based on the current command:
|
||||
| `import.meta.env.EDGE` | `boolean` | Equivalent to `import.meta.env.BROWSER === "edge"` |
|
||||
| `import.meta.env.OPERA` | `boolean` | Equivalent to `import.meta.env.BROWSER === "opera"` |
|
||||
|
||||
You can set the [`targetBrowsers`](/api/reference/wxt/interfaces/InlineConfig#targetbrowsers) option to make the `BROWSER` variable a more specific type, like `"chrome" | "firefox"`.
|
||||
|
||||
You can also access all of [Vite's environment variables](https://vite.dev/guide/env-and-mode.html#env-variables):
|
||||
|
||||
| Usage | Type | Description |
|
||||
|
||||
@@ -4,50 +4,30 @@ outline: deep
|
||||
|
||||
# Entrypoints
|
||||
|
||||
WXT uses the files inside the `entrypoints/` directory as inputs when bundling your extension. They can be HTML, JS, CSS, or any variant of those file types supported by Vite (Pug, TS, JSX, SCSS, etc).
|
||||
WXT uses the files inside the `entrypoints/` directory as inputs when bundling your extension. They can be HTML, JS, CSS, or any variant of those file types supported by Vite (TS, JSX, SCSS, etc).
|
||||
|
||||
Here's an example set of entrypoints:
|
||||
## Folder Structure
|
||||
|
||||
Inside the `entrypoints/` directory, an entrypoint is defined as a single file or directory (with an `index` file) inside it.
|
||||
|
||||
:::code-group
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```html
|
||||
```html [Single File]
|
||||
📂 entrypoints/
|
||||
📂 popup/
|
||||
📄 index.html
|
||||
📄 main.ts
|
||||
📄 style.css
|
||||
📄 background.ts
|
||||
📄 content.ts
|
||||
📄 {name}.{ext}
|
||||
```
|
||||
|
||||
[[toc]]
|
||||
<!-- prettier-ignore -->
|
||||
```html [Directory]
|
||||
📂 entrypoints/
|
||||
📂 {name}/
|
||||
📄 index.{ext}
|
||||
```
|
||||
|
||||
## Listed vs Unlisted
|
||||
|
||||
For web extensions, there are two types of entrypoints:
|
||||
|
||||
- **Listed**: Referenced in the `manifest.json`
|
||||
- **Unlisted**: Not referenced in the `manifest.json`
|
||||
|
||||
Throughout the rest of WXT's documentation, listed entrypoints are referred to by name. For example:
|
||||
|
||||
- Popup
|
||||
- Options
|
||||
- Background
|
||||
- Content Scripts
|
||||
- Etc.
|
||||
|
||||
Some examples of "unlisted" entrypoints:
|
||||
|
||||
- A welcome page shown when the extension is installed
|
||||
- JS files injected by content scripts into the page's main world
|
||||
|
||||
:::tip
|
||||
Regardless of whether an entrypoint is listed or unlisted, it will still be bundled into your extension and be available at runtime.
|
||||
:::
|
||||
|
||||
## Adding Entrypoints
|
||||
|
||||
An entrypoint can be defined as a single file or directory with an `index` file inside it.
|
||||
The entrypoint's `name` dictates the type of entrypoint. For example, to add a ["Background" entrypoint](#background), either of these files would work:
|
||||
|
||||
:::code-group
|
||||
|
||||
@@ -66,10 +46,96 @@ An entrypoint can be defined as a single file or directory with an `index` file
|
||||
|
||||
:::
|
||||
|
||||
The entrypoint's name dictates the type of entrypoint, listed vs unlisted. In this example, "background" is the name of the ["Background" entrypoint](#background).
|
||||
|
||||
Refer to the [Entrypoint Types](#entrypoint-types) section for the full list of listed entrypoints and their filename patterns.
|
||||
|
||||
### Including Other Files
|
||||
|
||||
When using an entrypoint directory, `entrypoints/{name}/index.{ext}`, you can add related files next to the `index` file.
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```html
|
||||
📂 entrypoints/
|
||||
📂 popup/
|
||||
📄 index.html ← This file is the entrypoint
|
||||
📄 main.ts
|
||||
📄 style.css
|
||||
📂 background/
|
||||
📄 index.ts ← This file is the entrypoint
|
||||
📄 alarms.ts
|
||||
📄 messaging.ts
|
||||
📂 youtube.content/
|
||||
📄 index.ts ← This file is the entrypoint
|
||||
📄 style.css
|
||||
```
|
||||
|
||||
:::danger
|
||||
**DO NOT** put files related to an entrypoint directly inside the `entrypoints/` directory. WXT will treat them as entrypoints and try to build them, usually resulting in an error.
|
||||
|
||||
Instead, use a directory for that entrypoint:
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```html
|
||||
📂 entrypoints/
|
||||
📄 popup.html <!-- [!code --] -->
|
||||
📄 popup.ts <!-- [!code --] -->
|
||||
📄 popup.css <!-- [!code --] -->
|
||||
📂 popup/ <!-- [!code ++] -->
|
||||
📄 index.html <!-- [!code ++] -->
|
||||
📄 main.ts <!-- [!code ++] -->
|
||||
📄 style.css <!-- [!code ++] -->
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
### Deeply Nested Entrypoints
|
||||
|
||||
While the `entrypoints/` directory might resemble the `pages/` directory of other web frameworks, like Nuxt or Next.js, **it does not support deeply nesting entrypoints** in the same way.
|
||||
|
||||
Entrypoints must be zero or one levels deep for WXT to discover and build them:
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```html
|
||||
📂 entrypoints/
|
||||
📂 youtube/ <!-- [!code --] -->
|
||||
📂 content/ <!-- [!code --] -->
|
||||
📄 index.ts <!-- [!code --] -->
|
||||
📄 ... <!-- [!code --] -->
|
||||
📂 injected/ <!-- [!code --] -->
|
||||
📄 index.ts <!-- [!code --] -->
|
||||
📄 ... <!-- [!code --] -->
|
||||
📂 youtube.content/ <!-- [!code ++] -->
|
||||
📄 index.ts <!-- [!code ++] -->
|
||||
📄 ... <!-- [!code ++] -->
|
||||
📂 youtube-injected/ <!-- [!code ++] -->
|
||||
📄 index.ts <!-- [!code ++] -->
|
||||
📄 ... <!-- [!code ++] -->
|
||||
```
|
||||
|
||||
## Unlisted Entrypoints
|
||||
|
||||
In web extensions, there are two types of entrypoints:
|
||||
|
||||
1. **Listed**: Referenced in the `manifest.json`
|
||||
2. **Unlisted**: Not referenced in the `manifest.json`
|
||||
|
||||
Throughout the rest of WXT's documentation, listed entrypoints are referred to by name. For example:
|
||||
|
||||
- Popup
|
||||
- Options
|
||||
- Background
|
||||
- Content Script
|
||||
|
||||
However, not all entrypoints in web extensions are listed in the manifest. Some are not listed in the manifest, but are still used by extensions. For example:
|
||||
|
||||
- A welcome page shown in a new tab when the extension is installed
|
||||
- JS files injected by content scripts into the main world
|
||||
|
||||
For more details on how to add unlisted entrypoints, see:
|
||||
|
||||
- [Unlisted Pages](#unlisted-pages)
|
||||
- [Unlisted Scripts](#unlisted-scripts)
|
||||
- [Unlisted CSS](#unlisted-css)
|
||||
|
||||
## Defining Manifest Options
|
||||
|
||||
Most listed entrypoints have options that need to be added to the `manifest.json`. However with WXT, instead of defining the options in a separate file, _you define these options inside the entrypoint file itself_.
|
||||
@@ -106,8 +172,6 @@ When building your extension, WXT will look at the options defined in your entry
|
||||
|
||||
[Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/manifest/background/) • [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/background)
|
||||
|
||||
For MV2, the background is added as a script to the background page. For MV3, the background becomes a service worker.
|
||||
|
||||
<EntrypointPatterns
|
||||
:patterns="[
|
||||
['background.[jt]s', 'background.js'],
|
||||
@@ -141,6 +205,25 @@ export default defineBackground({
|
||||
|
||||
:::
|
||||
|
||||
For MV2, the background is added as a script to the background page. For MV3, the background becomes a service worker.
|
||||
|
||||
When defining your background entrypoint, keep in mind that WXT will import this file in a NodeJS environment during the build process. That means you cannot place any runtime code outside the `main` function.
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```ts
|
||||
browser.action.onClicked.addListener(() => { // [!code --]
|
||||
// ... // [!code --]
|
||||
}); // [!code --]
|
||||
|
||||
export default defineBackground(() => {
|
||||
browser.action.onClicked.addListener(() => { // [!code ++]
|
||||
// ... // [!code ++]
|
||||
}); // [!code ++]
|
||||
});
|
||||
```
|
||||
|
||||
Refer to the [Entrypoint Loaders](/guide/essentials/config/entrypoint-loaders) documentation for more details.
|
||||
|
||||
### Bookmarks
|
||||
|
||||
[Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/override/) • [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/chrome_url_overrides)
|
||||
@@ -169,18 +252,18 @@ export default defineBackground({
|
||||
</html>
|
||||
```
|
||||
|
||||
When you define a Bookmarks entrypoint, WXT will automatically update the manifest to override the browser's bookmarks page with your own HTML page.
|
||||
|
||||
### Content Scripts
|
||||
|
||||
[Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/content_scripts/) • [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Content_scripts)
|
||||
|
||||
See [Content Script UI](/guide/essentials/content-scripts) for more info on creating UIs and including CSS in content scripts.
|
||||
|
||||
<EntrypointPatterns
|
||||
:patterns="[
|
||||
['content.[jt]sx?', 'content-scripts/content.js'],
|
||||
['content/index.[jt]sx?', 'content-scripts/content.js'],
|
||||
['<name>.content.[jt]sx?', 'content-scripts/<name>.js'],
|
||||
['<name>.content/index.[jt]sx?', 'content-scripts/<name>.js'],
|
||||
['{name}.content.[jt]sx?', 'content-scripts/{name}.js'],
|
||||
['{name}.content/index.[jt]sx?', 'content-scripts/{name}.js'],
|
||||
]"
|
||||
/>
|
||||
|
||||
@@ -213,12 +296,29 @@ export default defineContentScript({
|
||||
});
|
||||
```
|
||||
|
||||
When defining content script entrypoints, keep in mind that WXT will import this file in a NodeJS environment during the build process. That means you cannot place any runtime code outside the `main` function.
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```ts
|
||||
browser.runtime.onMessage.addListener((message) => { // [!code --]
|
||||
// ... // [!code --]
|
||||
}); // [!code --]
|
||||
|
||||
export default defineBackground(() => {
|
||||
browser.runtime.onMessage.addListener((message) => { // [!code ++]
|
||||
// ... // [!code ++]
|
||||
}); // [!code ++]
|
||||
});
|
||||
```
|
||||
|
||||
Refer to the [Entrypoint Loaders](/guide/essentials/config/entrypoint-loaders) documentation for more details.
|
||||
|
||||
See [Content Script UI](/guide/essentials/content-scripts) for more info on creating UIs and including CSS in content scripts.
|
||||
|
||||
### Devtools
|
||||
|
||||
[Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/devtools/) • [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/devtools_page)
|
||||
|
||||
Follow the [Devtools Example](https://github.com/wxt-dev/examples/tree/main/examples/devtools-extension#readme) to add different panels and panes.
|
||||
|
||||
<EntrypointPatterns
|
||||
:patterns="[
|
||||
['devtools.html', 'devtools.html'],
|
||||
@@ -242,6 +342,8 @@ Follow the [Devtools Example](https://github.com/wxt-dev/examples/tree/main/exam
|
||||
</html>
|
||||
```
|
||||
|
||||
Follow the [Devtools Example](https://github.com/wxt-dev/examples/tree/main/examples/devtools-extension#readme) to add different panels and panes.
|
||||
|
||||
### History
|
||||
|
||||
[Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/override/) • [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/chrome_url_overrides)
|
||||
@@ -270,6 +372,8 @@ Follow the [Devtools Example](https://github.com/wxt-dev/examples/tree/main/exam
|
||||
</html>
|
||||
```
|
||||
|
||||
When you define a History entrypoint, WXT will automatically update the manifest to override the browser's history page with your own HTML page.
|
||||
|
||||
### Newtab
|
||||
|
||||
[Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/override/) • [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/chrome_url_overrides)
|
||||
@@ -298,6 +402,8 @@ Follow the [Devtools Example](https://github.com/wxt-dev/examples/tree/main/exam
|
||||
</html>
|
||||
```
|
||||
|
||||
When you define a Newtab entrypoint, WXT will automatically update the manifest to override the browser's new tab page with your own HTML page.
|
||||
|
||||
### Options
|
||||
|
||||
[Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/options/) • [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/options_ui)
|
||||
@@ -387,8 +493,8 @@ Firefox does not support sandboxed pages.
|
||||
:patterns="[
|
||||
['sandbox.html', 'sandbox.html'],
|
||||
['sandbox/index.html', 'sandbox.html'],
|
||||
['<name>.sandbox.html', '<name>.html'],
|
||||
['<name>.sandbox/index.html', '<name>.html'],
|
||||
['{name}.sandbox.html', '{name}.html'],
|
||||
['{name}.sandbox/index.html', '{name}.html'],
|
||||
]"
|
||||
/>
|
||||
|
||||
@@ -414,14 +520,12 @@ Firefox does not support sandboxed pages.
|
||||
|
||||
[Chrome Docs](https://developer.chrome.com/docs/extensions/reference/sidePanel/) • [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/user_interface/Sidebars)
|
||||
|
||||
In Chrome, side panels use the `side_panel` API, while Firefox uses the `sidebar_action` API.
|
||||
|
||||
<EntrypointPatterns
|
||||
:patterns="[
|
||||
['sidepanel.html', 'sidepanel.html'],
|
||||
['sidepanel/index.html', 'sidepanel.html'],
|
||||
['<name>.sidepanel.html', '<name>.html` '],
|
||||
['<name>.sidepanel/index.html', '<name>.html` '],
|
||||
['{name}.sidepanel.html', '{name}.html` '],
|
||||
['{name}.sidepanel/index.html', '{name}.html` '],
|
||||
]"
|
||||
/>
|
||||
|
||||
@@ -455,20 +559,18 @@ In Chrome, side panels use the `side_panel` API, while Firefox uses the `sidebar
|
||||
</html>
|
||||
```
|
||||
|
||||
In Chrome, side panels use the `side_panel` API, while Firefox uses the `sidebar_action` API.
|
||||
|
||||
### Unlisted CSS
|
||||
|
||||
Follow Vite's guide to setup your preprocessor of choice: https://vitejs.dev/guide/features.html#css-pre-processors
|
||||
|
||||
CSS entrypoints are always unlisted. To add CSS to a content script, see the [Content Script](/guide/essentials/content-scripts#css) docs.
|
||||
|
||||
<EntrypointPatterns
|
||||
:patterns="[
|
||||
['<name>.(css|scss|sass|less|styl|stylus)', '<name>.css'],
|
||||
['<name>/index.(css|scss|sass|less|styl|stylus)', '<name>.css'],
|
||||
['{name}.(css|scss|sass|less|styl|stylus)', '{name}.css'],
|
||||
['{name}/index.(css|scss|sass|less|styl|stylus)', '{name}.css'],
|
||||
['content.(css|scss|sass|less|styl|stylus)', 'content-scripts/content.css'],
|
||||
['content/index.(css|scss|sass|less|styl|stylus)', 'content-scripts/content.css'],
|
||||
['<name>.content.(css|scss|sass|less|styl|stylus)', 'content-scripts/<name>.css'],
|
||||
['<name>.content/index.(css|scss|sass|less|styl|stylus)', 'content-scripts/<name>.css'],
|
||||
['{name}.content.(css|scss|sass|less|styl|stylus)', 'content-scripts/{name}.css'],
|
||||
['{name}.content/index.(css|scss|sass|less|styl|stylus)', 'content-scripts/{name}.css'],
|
||||
]"
|
||||
/>
|
||||
|
||||
@@ -478,12 +580,16 @@ body {
|
||||
}
|
||||
```
|
||||
|
||||
Follow Vite's guide to setup your preprocessor of choice: https://vitejs.dev/guide/features.html#css-pre-processors
|
||||
|
||||
CSS entrypoints are always unlisted. To add CSS to a content script, see the [Content Script](/guide/essentials/content-scripts#css) docs.
|
||||
|
||||
### Unlisted Pages
|
||||
|
||||
<EntrypointPatterns
|
||||
:patterns="[
|
||||
['<name>.html', '<name>.html'],
|
||||
['<name>/index.html', '<name>.html'],
|
||||
['{name}.html', '{name}.html'],
|
||||
['{name}/index.html', '{name}.html'],
|
||||
]"
|
||||
/>
|
||||
|
||||
@@ -505,20 +611,21 @@ body {
|
||||
</html>
|
||||
```
|
||||
|
||||
Pages are accessible at `/<name>.html`:
|
||||
At runtime, unlisted pages are accessible at `/{name}.html`:
|
||||
|
||||
```ts
|
||||
const url = browser.runtime.getURL('/<name>.html');
|
||||
const url = browser.runtime.getURL('/{name}.html');
|
||||
|
||||
console.log(url); // "chrome-extension://<id>/<name>.html"
|
||||
console.log(url); // "chrome-extension://{id}/{name}.html"
|
||||
window.open(url); // Open the page in a new tab
|
||||
```
|
||||
|
||||
### Unlisted Scripts
|
||||
|
||||
<EntrypointPatterns
|
||||
:patterns="[
|
||||
['<name>.[jt]sx?', '<name>.js'],
|
||||
['<name>/index.[jt]sx?', '<name>.js'],
|
||||
['{name}.[jt]sx?', '{name}.js'],
|
||||
['{name}/index.[jt]sx?', '{name}.js'],
|
||||
]"
|
||||
/>
|
||||
|
||||
@@ -544,12 +651,29 @@ export default defineUnlistedScript({
|
||||
|
||||
:::
|
||||
|
||||
Scripts are accessible from `/<name>.js`:
|
||||
At runtime, unlisted scripts are accessible from `/{name}.js`:
|
||||
|
||||
```ts
|
||||
const url = browser.runtime.getURL('/<name>.js');
|
||||
const url = browser.runtime.getURL('/{name}.js');
|
||||
|
||||
console.log(url); // "chrome-extension://<id>/<name>.js"
|
||||
console.log(url); // "chrome-extension://{id}/{name}.js"
|
||||
```
|
||||
|
||||
You are responsible for loading/running these scripts where needed. If necessary, don't forget to add the script and/or any related assets to [`web_accessible_resources`](https://developer.chrome.com/docs/extensions/reference/manifest/web-accessible-resources).
|
||||
|
||||
When defining an unlisted script, keep in mind that WXT will import this file in a NodeJS environment during the build process. That means you cannot place any runtime code outside the `main` function.
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```ts
|
||||
document.querySelectorAll('a').forEach((anchor) => { // [!code --]
|
||||
// ... // [!code --]
|
||||
}); // [!code --]
|
||||
|
||||
export default defineUnlistedScript(() => {
|
||||
document.querySelectorAll('a').forEach((anchor) => { // [!code ++]
|
||||
// ... // [!code ++]
|
||||
}); // [!code ++]
|
||||
});
|
||||
```
|
||||
|
||||
Refer to the [Entrypoint Loaders](/guide/essentials/config/entrypoint-loaders) documentation for more details.
|
||||
|
||||
@@ -29,7 +29,7 @@ This means you can use the promise-style API for both MV2 and MV3, and it will w
|
||||
All types can be accessed via WXT's `Browser` namespace:
|
||||
|
||||
```ts
|
||||
import { Browser } from 'wxt/browser';
|
||||
import { type Browser } from 'wxt/browser';
|
||||
|
||||
function handleMessage(message: any, sender: Browser.runtime.MessageSender) {
|
||||
// ...
|
||||
|
||||
@@ -30,9 +30,10 @@ Here's a brief summary of each of these files and directories:
|
||||
- `.wxt/`: Generated by WXT, it contains TS config
|
||||
- `assets/`: Contains all CSS, images, and other assets that should be processed by WXT
|
||||
- `components/`: Auto-imported by default, contains UI components
|
||||
- `composables/`: Auto-imported by default, contains composable functions for Vue
|
||||
- `composables/`: Auto-imported by default, contains source code for your project's composable functions for Vue
|
||||
- `entrypoints/`: Contains all the entrypoints that get bundled into your extension
|
||||
- `hooks/`: Auto-imported by default, contains hooks for React and Solid
|
||||
- `hooks/`: Auto-imported by default, contains source code for your project's hooks for React and Solid
|
||||
- `modules/`: Contains [local WXT Modules](/guide/essentials/wxt-modules) for your project
|
||||
- `public/`: Contains any files you want to copy into the output folder as-is, without being processed by WXT
|
||||
- `utils/`: Auto-imported by default, contains generic utilities used throughout your project
|
||||
- `.env`: Contains [Environment Variables](/guide/essentials/config/environment-variables)
|
||||
|
||||
@@ -23,7 +23,7 @@ WXT provides two commands to help automate submitting a new version for review a
|
||||
- `wxt submit init`: Setup all the required secrets and options for the `wxt submit` command
|
||||
- `wxt submit`: Submit new versions of your extension for review (and publish them automatically once approved)
|
||||
|
||||
To get started, run `wxt submit init` and follow the prompts. Once finished, you should have a `.env.submit` file! WXT will use this file to submit your updates.
|
||||
To get started, run `wxt submit init` and follow the prompts, or run `wxt submit --help` to view all available options. Once finished, you should have a `.env.submit` file! WXT will use this file to submit your updates.
|
||||
|
||||
> In CI, make sure you add all the environment variables to the submit step.
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ import { injectScript, createShadowRootUi } from '#imports';
|
||||
But Vitest sees this:
|
||||
|
||||
```ts
|
||||
import { injectScript } from 'wxt/browser';
|
||||
import { injectScript } from 'wxt/utils/inject-script';
|
||||
import { createShadowRootUi } from 'wxt/utils/content-script-ui/shadow-root';
|
||||
```
|
||||
|
||||
|
||||
@@ -174,3 +174,16 @@ Additionally, if you want to train your own model or provide context to your edi
|
||||
https://wxt.dev/knowledge/index.json
|
||||
|
||||
You don't need to crawl the entire website, these files already contain all the relevant docs for training a LLM on WXT. But feel free to crawl it and generate your own files if you want!
|
||||
|
||||
## How do I run my WXT project with docker / [devcontainers](https://containers.dev)?
|
||||
|
||||
To run the WXT dev server in a devcontainer, but load the dev build of your extension in your browser:
|
||||
|
||||
1. **Bind-mount your project directory to your host**
|
||||
If you're using VS Code, you can open your project folder with the `Dev Containers: Open Folder in Container...` command. This keeps the folder synchronized between your host and the devcontainer, ensuring that the extension `dist` directory remains accessible from the host.
|
||||
|
||||
2. **Disable auto-opening the browser**
|
||||
WXT automatically opens your browser during development, but since you're running inside a container, it won't be able to access it. Follow the instructions [here](https://wxt.dev/guide/essentials/config/browser-startup.html#disable-opening-browser) to disable browser auto-opening in your `wxt.config.ts`.
|
||||
|
||||
3. **Tell WXT to listen on all network interfaces**
|
||||
To enable hot-reloading, your extension has to connect to the WXT dev server running inside your container. WXT will only listen on `localhost` by default, which prevents connections from outside the devcontainer. To fix this you can instruct WXT to listen on all interfaces with `wxt --host 0.0.0.0`.
|
||||
|
||||
@@ -6,12 +6,27 @@ outline: deep
|
||||
|
||||
## Overview
|
||||
|
||||
To upgrade WXT to the latest version... just install it!
|
||||
To upgrade WXT to the latest major version:
|
||||
|
||||
1. Install it, skipping scripts so `wxt prepare` doesn't run - it will probably throw an error after a major version change (we'll run it later).
|
||||
```sh
|
||||
pnpm i wxt@latest --ignore-scripts
|
||||
```
|
||||
2. Follow the upgrade steps below to fix any breaking changes.
|
||||
3. Run `wxt prepare`. It should succeed and type errors will go away afterwords.
|
||||
```sh
|
||||
pnpm wxt prepare
|
||||
```
|
||||
4. Manually test to make sure both dev mode and production builds work.
|
||||
|
||||
For minor or patch version updates, there are no special steps. Just update it with your package manager:
|
||||
|
||||
```sh
|
||||
pnpm i wxt@latest
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Listed below are all the breaking changes you should address when upgrading to a new version of WXT.
|
||||
|
||||
Currently, WXT is in pre-release. This means changes to the second digit, `v0.X`, are considered major and have breaking changes. Once v1 is released, only major version bumps will have breaking changes.
|
||||
@@ -21,7 +36,7 @@ Currently, WXT is in pre-release. This means changes to the second digit, `v0.X`
|
||||
v0.20 is a big release! There are lots of breaking changes because this version is intended to be a release candidate for v1.0. If all goes well, v1.0 will be released with no additional breaking changes.
|
||||
|
||||
:::tip
|
||||
Read through all the changes once before making any code changes.
|
||||
Read through all the changes once before updating your code.
|
||||
:::
|
||||
|
||||
### `webextension-polyfill` Removed
|
||||
@@ -92,17 +107,19 @@ The default location for the `public/` and `modules/` directories have changed t
|
||||
- If you follow the default folder structure, you don't need to make any changes.
|
||||
- If you set a custom `srcDir`, you have two options:
|
||||
1. Move the your `public/` and `modules/` directories to the project root:
|
||||
```diff
|
||||
<root>/
|
||||
+ modules/
|
||||
+ public/
|
||||
src/
|
||||
components/
|
||||
entrypoints/
|
||||
- modules/
|
||||
- public/
|
||||
utils/
|
||||
wxt.config.ts
|
||||
<!-- prettier-ignore -->
|
||||
```html
|
||||
📂 {rootDir}/
|
||||
📁 modules/ <!-- [!code ++] -->
|
||||
📁 public/ <!-- [!code ++] -->
|
||||
📂 src/
|
||||
📁 components/
|
||||
📁 entrypoints/
|
||||
📁 modules/ <!-- [!code --] -->
|
||||
📁 public/ <!-- [!code --] -->
|
||||
📁 utils/
|
||||
📄 app.config.ts
|
||||
📄 wxt.config.ts
|
||||
```
|
||||
2. Keep the folders in the same place and update your project config:
|
||||
```ts [wxt.config.ts]
|
||||
|
||||
@@ -32,6 +32,8 @@
|
||||
/guide/build-targets.html /guide/multiple-browsers.html
|
||||
/guide/installation.html /get-started/installation.html
|
||||
/guide/introduction.html /get-started/introduction.html
|
||||
/guide/upgrade-guide/wxt /guide/resources/upgrading.html
|
||||
/guide/upgrade-guide/wxt.html /guide/resources/upgrading.html
|
||||
|
||||
# 0.19.0
|
||||
/guide/go-further/entrypoint-side-effects.html /guide/go-further/entrypoint-loaders.html
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<!--@include: ../packages/runner/README.md-->
|
||||
+28
-28
@@ -4,7 +4,7 @@
|
||||
"engines": {
|
||||
"node": ">=18.20.3"
|
||||
},
|
||||
"packageManager": "pnpm@10.5.2",
|
||||
"packageManager": "pnpm@10.10.0",
|
||||
"scripts": {
|
||||
"check": "check && pnpm -r --sequential run check",
|
||||
"test": "pnpm -r --sequential run test run",
|
||||
@@ -17,33 +17,33 @@
|
||||
"docs:preview": "pnpm -s docs:gen && vitepress preview docs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/buildc": "catalog:",
|
||||
"@aklinker1/check": "catalog:",
|
||||
"@commitlint/config-conventional": "catalog:",
|
||||
"@commitlint/types": "catalog:",
|
||||
"@types/fs-extra": "catalog:",
|
||||
"@vitest/coverage-v8": "catalog:",
|
||||
"changelogen": "catalog:",
|
||||
"consola": "catalog:",
|
||||
"fast-glob": "catalog:",
|
||||
"feed": "catalog:",
|
||||
"fs-extra": "catalog:",
|
||||
"lint-staged": "catalog:",
|
||||
"markdown-it-footnote": "catalog:",
|
||||
"nano-spawn": "catalog:",
|
||||
"prettier": "catalog:",
|
||||
"simple-git-hooks": "catalog:",
|
||||
"tsx": "catalog:",
|
||||
"typedoc": "catalog:",
|
||||
"typedoc-plugin-frontmatter": "catalog:",
|
||||
"typedoc-plugin-markdown": "catalog:",
|
||||
"typedoc-vitepress-theme": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"vitepress": "catalog:",
|
||||
"vitepress-knowledge": "catalog:",
|
||||
"vitepress-plugin-group-icons": "catalog:",
|
||||
"vitest-mock-extended": "catalog:",
|
||||
"vue": "catalog:",
|
||||
"@aklinker1/buildc": "^1.1.4",
|
||||
"@aklinker1/check": "2.0.0",
|
||||
"@commitlint/config-conventional": "^19.8.0",
|
||||
"@commitlint/types": "^19.8.0",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@vitest/coverage-v8": "^3.1.2",
|
||||
"changelogen": "^0.6.1",
|
||||
"consola": "^3.4.2",
|
||||
"fast-glob": "^3.3.3",
|
||||
"feed": "^4.2.2",
|
||||
"fs-extra": "^11.3.0",
|
||||
"lint-staged": "^15.5.1",
|
||||
"markdown-it-footnote": "^4.0.0",
|
||||
"nano-spawn": "^0.2.0",
|
||||
"prettier": "^3.5.3",
|
||||
"simple-git-hooks": "^2.13.0",
|
||||
"tsx": "4.19.4",
|
||||
"typedoc": "^0.25.4",
|
||||
"typedoc-plugin-frontmatter": "^1.3.0",
|
||||
"typedoc-plugin-markdown": "4.0.0-next.23",
|
||||
"typedoc-vitepress-theme": "1.0.0-next.3",
|
||||
"typescript": "^5.8.3",
|
||||
"vitepress": "^1.6.3",
|
||||
"vitepress-knowledge": "^0.4.1",
|
||||
"vitepress-plugin-group-icons": "^1.5.2",
|
||||
"vitest-mock-extended": "^3.1.0",
|
||||
"vue": "^3.5.13",
|
||||
"wxt": "workspace:*"
|
||||
},
|
||||
"simple-git-hooks": {
|
||||
|
||||
@@ -1,27 +1,12 @@
|
||||
# Changelog
|
||||
|
||||
## v0.5.0
|
||||
|
||||
[⚠️ breaking changes](https://wxt.dev/guide/upgrade-guide/wxt) • [compare changes](https://github.com/wxt-dev/wxt/compare/analytics-v0.4.1...analytics-v0.5.0)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Analytics module ([5217279](https://github.com/wxt-dev/wxt/commit/5217279))
|
||||
- New `@wxt-dev/analytics` package ([#790](https://github.com/wxt-dev/wxt/pull/790))
|
||||
[⚠️ breaking changes](https://wxt.dev/guide/resources/upgrading.html) • [compare changes](https://github.com/wxt-dev/wxt/compare/analytics-v0.4.1...analytics-v0.5.0)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- ⚠️ Update min WXT version to 0.20 ([2e8baf0](https://github.com/wxt-dev/wxt/commit/2e8baf0))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- **release:** Analytics-v0.4.0 ([2faba91](https://github.com/wxt-dev/wxt/commit/2faba91))
|
||||
- **release:** Analytics-v0.4.1 ([1509809](https://github.com/wxt-dev/wxt/commit/1509809))
|
||||
|
||||
#### ⚠️ Breaking Changes
|
||||
|
||||
- ⚠️ Update min WXT version to 0.20 ([2e8baf0](https://github.com/wxt-dev/wxt/commit/2e8baf0))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron ([@aklinker1](https://github.com/aklinker1))
|
||||
- Aaron ([@aklinker1](https://github.com/aklinker1))
|
||||
|
||||
@@ -9,11 +9,12 @@ import type {
|
||||
AnalyticsEventMetadata,
|
||||
AnalyticsProvider,
|
||||
} from './types';
|
||||
import { browser } from '@wxt-dev/browser';
|
||||
|
||||
const ANALYTICS_PORT = '@wxt-dev/analytics';
|
||||
|
||||
export function createAnalytics(config?: AnalyticsConfig): Analytics {
|
||||
if (typeof chrome === 'undefined' || !chrome?.runtime?.id)
|
||||
if (!browser?.runtime?.id)
|
||||
throw Error(
|
||||
'Cannot use WXT analytics in contexts without access to the browser.runtime APIs',
|
||||
);
|
||||
@@ -51,13 +52,13 @@ function createBackgroundAnalytics(
|
||||
defineStorageItem<boolean>('local:wxt-analytics:enabled', false);
|
||||
|
||||
// Cached values
|
||||
const platformInfo = chrome.runtime.getPlatformInfo();
|
||||
const platformInfo = browser.runtime.getPlatformInfo();
|
||||
const userAgent = UAParser();
|
||||
let userId = Promise.resolve(userIdStorage.getValue()).then(
|
||||
(id) => id ?? globalThis.crypto.randomUUID(),
|
||||
);
|
||||
let userProperties = userPropertiesStorage.getValue();
|
||||
const manifest = chrome.runtime.getManifest();
|
||||
const manifest = browser.runtime.getManifest();
|
||||
|
||||
const getBackgroundMeta = () => ({
|
||||
timestamp: Date.now(),
|
||||
@@ -178,7 +179,7 @@ function createBackgroundAnalytics(
|
||||
config?.providers?.map((provider) => provider(analytics, config)) ?? [];
|
||||
|
||||
// Listen for messages from the rest of the extension
|
||||
chrome.runtime.onConnect.addListener((port) => {
|
||||
browser.runtime.onConnect.addListener((port) => {
|
||||
if (port.name === ANALYTICS_PORT) {
|
||||
port.onMessage.addListener(({ fn, args }) => {
|
||||
// @ts-expect-error: Untyped fn key
|
||||
@@ -194,7 +195,7 @@ function createBackgroundAnalytics(
|
||||
* Creates an analytics client for non-background contexts.
|
||||
*/
|
||||
function createFrontendAnalytics(): Analytics {
|
||||
const port = chrome.runtime.connect({ name: ANALYTICS_PORT });
|
||||
const port = browser.runtime.connect({ name: ANALYTICS_PORT });
|
||||
const sessionId = Date.now();
|
||||
const getFrontendMetadata = (): AnalyticsEventMetadata => ({
|
||||
sessionId,
|
||||
@@ -252,8 +253,8 @@ function defineStorageItem<T>(
|
||||
): AnalyticsStorageItem<T> {
|
||||
return {
|
||||
getValue: async () =>
|
||||
(await chrome.storage.local.get(key))[key] ?? defaultValue,
|
||||
setValue: (newValue) => chrome.storage.local.set({ [key]: newValue }),
|
||||
(await browser.storage.local.get(key))[key] ?? defaultValue,
|
||||
setValue: (newValue) => browser.storage.local.set({ [key]: newValue }),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -51,15 +51,15 @@
|
||||
"wxt": ">=0.20.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "catalog:",
|
||||
"@types/chrome": "catalog:",
|
||||
"@types/ua-parser-js": "catalog:",
|
||||
"publint": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"unbuild": "catalog:",
|
||||
"@aklinker1/check": "2.0.0",
|
||||
"@types/ua-parser-js": "^0.7.39",
|
||||
"publint": "^0.3.12",
|
||||
"typescript": "^5.8.3",
|
||||
"unbuild": "^3.5.0",
|
||||
"wxt": "workspace:*"
|
||||
},
|
||||
"dependencies": {
|
||||
"ua-parser-js": "catalog:"
|
||||
"@wxt-dev/browser": "workspace:^",
|
||||
"ua-parser-js": "^1.0.40"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"#analytics": ["./.wxt/analytics/index.ts"]
|
||||
},
|
||||
"types": ["chrome"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
||||
@@ -45,16 +45,16 @@
|
||||
"wxt": ">=0.19.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"defu": "catalog:",
|
||||
"fs-extra": "catalog:",
|
||||
"sharp": "catalog:"
|
||||
"defu": "^6.1.4",
|
||||
"fs-extra": "^11.3.0",
|
||||
"sharp": "^0.34.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "catalog:",
|
||||
"oxlint": "catalog:",
|
||||
"publint": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"unbuild": "catalog:",
|
||||
"@aklinker1/check": "2.0.0",
|
||||
"oxlint": "^0.16.8",
|
||||
"publint": "^0.3.12",
|
||||
"typescript": "^5.8.3",
|
||||
"unbuild": "^3.5.0",
|
||||
"wxt": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@wxt-dev/browser",
|
||||
"description": "Provides a cross-browser API for using extension APIs and types based on @types/chrome",
|
||||
"version": "0.0.310",
|
||||
"version": "0.0.326",
|
||||
"type": "module",
|
||||
"main": "src/index.mjs",
|
||||
"types": "src/index.d.ts",
|
||||
@@ -11,6 +11,7 @@
|
||||
"directory": "packages/browser"
|
||||
},
|
||||
"scripts": {
|
||||
"check": "check",
|
||||
"gen": "tsx scripts/generate.ts"
|
||||
},
|
||||
"author": {
|
||||
@@ -22,11 +23,12 @@
|
||||
"src"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/chrome": "0.0.310",
|
||||
"fs-extra": "catalog:",
|
||||
"nano-spawn": "catalog:",
|
||||
"tsx": "catalog:",
|
||||
"vitest": "catalog:"
|
||||
"@types/chrome": "0.0.326",
|
||||
"fs-extra": "^11.3.0",
|
||||
"nano-spawn": "^0.2.0",
|
||||
"tsx": "4.19.4",
|
||||
"typescript": "^5.8.3",
|
||||
"vitest": "^3.1.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/filesystem": "*",
|
||||
|
||||
@@ -7,7 +7,7 @@ import { sep as posixSep } from 'node:path/posix';
|
||||
// Fetch latest version
|
||||
|
||||
console.log('Getting latest version of \x1b[36m@types/chrome\x1b[0m');
|
||||
await spawn('pnpm', ['i', '--ignore-scripts', '-D', '@types/chrome']);
|
||||
await spawn('pnpm', ['i', '--ignore-scripts', '-D', '@types/chrome@latest']);
|
||||
|
||||
// Generate new package.json
|
||||
|
||||
@@ -76,5 +76,7 @@ function transformFile(file: string, content: string): string {
|
||||
.replaceAll('declare namespace chrome', 'export namespace Browser')
|
||||
// Update references to `chrome` namespace to `Browser`
|
||||
.replaceAll('chrome.', 'Browser.')
|
||||
// Fix links to developer.chrome.com
|
||||
.replaceAll('developer.Browser.com', 'developer.chrome.com')
|
||||
);
|
||||
}
|
||||
|
||||
+26
-25
@@ -1,5 +1,6 @@
|
||||
/* DO NOT EDIT - generated by scripts/generate.ts */
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-wrapper-object-types */
|
||||
export namespace Browser {
|
||||
////////////////////
|
||||
// Cast
|
||||
@@ -112,7 +113,7 @@ export namespace Browser {
|
||||
*/
|
||||
export function initialize(
|
||||
apiConfig: Browser.cast.ApiConfig,
|
||||
successCallback: Function,
|
||||
successCallback: () => void,
|
||||
errorCallback: (error: Browser.cast.Error) => void,
|
||||
): void;
|
||||
|
||||
@@ -160,7 +161,7 @@ export namespace Browser {
|
||||
*/
|
||||
export function setCustomReceivers(
|
||||
receivers: Browser.cast.Receiver[],
|
||||
successCallback: Function,
|
||||
successCallback: () => void,
|
||||
errorCallback: (error: Browser.cast.Error) => void,
|
||||
): void;
|
||||
|
||||
@@ -171,7 +172,7 @@ export namespace Browser {
|
||||
*/
|
||||
export function setReceiverDisplayStatus(
|
||||
receiver: Browser.cast.Receiver,
|
||||
successCallback: Function,
|
||||
successCallback: () => void,
|
||||
errorCallback: (error: Browser.cast.Error) => void,
|
||||
): void;
|
||||
|
||||
@@ -294,7 +295,7 @@ export namespace Browser {
|
||||
*/
|
||||
setReceiverVolumeLevel(
|
||||
newLevel: number,
|
||||
successCallback: Function,
|
||||
successCallback: () => void,
|
||||
errorCallback: (error: Browser.cast.Error) => void,
|
||||
): void;
|
||||
|
||||
@@ -305,7 +306,7 @@ export namespace Browser {
|
||||
*/
|
||||
setReceiverMuted(
|
||||
muted: boolean,
|
||||
successCallback: Function,
|
||||
successCallback: () => void,
|
||||
errorCallback: (error: Browser.cast.Error) => void,
|
||||
): void;
|
||||
|
||||
@@ -313,13 +314,13 @@ export namespace Browser {
|
||||
* @param successCallback
|
||||
* @param errorCallback
|
||||
*/
|
||||
leave(successCallback: Function, errorCallback: (error: Browser.cast.Error) => void): void;
|
||||
leave(successCallback: () => void, errorCallback: (error: Browser.cast.Error) => void): void;
|
||||
|
||||
/**
|
||||
* @param successCallback
|
||||
* @param errorCallback
|
||||
*/
|
||||
stop(successCallback: Function, errorCallback: (error: Browser.cast.Error) => void): void;
|
||||
stop(successCallback: () => void, errorCallback: (error: Browser.cast.Error) => void): void;
|
||||
|
||||
/**
|
||||
* @param namespace
|
||||
@@ -330,7 +331,7 @@ export namespace Browser {
|
||||
sendMessage(
|
||||
namespace: string,
|
||||
message: string | object,
|
||||
successCallback: Function,
|
||||
successCallback: () => void,
|
||||
errorCallback: (error: Browser.cast.Error) => void,
|
||||
): void;
|
||||
|
||||
@@ -887,7 +888,7 @@ export namespace Browser {
|
||||
*/
|
||||
getStatus(
|
||||
getStatusRequest: Browser.cast.media.GetStatusRequest,
|
||||
successCallback: Function,
|
||||
successCallback: () => void,
|
||||
errorCallback: (error: Browser.cast.Error) => void,
|
||||
): void;
|
||||
|
||||
@@ -898,7 +899,7 @@ export namespace Browser {
|
||||
*/
|
||||
play(
|
||||
playRequest: Browser.cast.media.PlayRequest,
|
||||
successCallback: Function,
|
||||
successCallback: () => void,
|
||||
errorCallback: (error: Browser.cast.Error) => void,
|
||||
): void;
|
||||
|
||||
@@ -909,7 +910,7 @@ export namespace Browser {
|
||||
*/
|
||||
pause(
|
||||
pauseRequest: Browser.cast.media.PauseRequest,
|
||||
successCallback: Function,
|
||||
successCallback: () => void,
|
||||
errorCallback: (error: Browser.cast.Error) => void,
|
||||
): void;
|
||||
|
||||
@@ -920,7 +921,7 @@ export namespace Browser {
|
||||
*/
|
||||
seek(
|
||||
seekRequest: Browser.cast.media.SeekRequest,
|
||||
successCallback: Function,
|
||||
successCallback: () => void,
|
||||
errorCallback: (error: Browser.cast.Error) => void,
|
||||
): void;
|
||||
|
||||
@@ -931,7 +932,7 @@ export namespace Browser {
|
||||
*/
|
||||
stop(
|
||||
stopRequest: Browser.cast.media.StopRequest,
|
||||
successCallback: Function,
|
||||
successCallback: () => void,
|
||||
errorCallback: (error: Browser.cast.Error) => void,
|
||||
): void;
|
||||
|
||||
@@ -942,7 +943,7 @@ export namespace Browser {
|
||||
*/
|
||||
setVolume(
|
||||
volumeRequest: Browser.cast.media.VolumeRequest,
|
||||
successCallback: Function,
|
||||
successCallback: () => void,
|
||||
errorCallback: (error: Browser.cast.Error) => void,
|
||||
): void;
|
||||
|
||||
@@ -953,7 +954,7 @@ export namespace Browser {
|
||||
*/
|
||||
editTracksInfo(
|
||||
editTracksInfoRequest: Browser.cast.media.EditTracksInfoRequest,
|
||||
successCallback: Function,
|
||||
successCallback: () => void,
|
||||
errorCallback: (error: Browser.cast.Error) => void,
|
||||
): void;
|
||||
|
||||
@@ -986,7 +987,7 @@ export namespace Browser {
|
||||
*/
|
||||
queueAppendItem(
|
||||
item: Browser.cast.media.QueueItem,
|
||||
successCallback: Function,
|
||||
successCallback: () => void,
|
||||
errorCallback: (error: Browser.cast.Error) => void,
|
||||
): void;
|
||||
|
||||
@@ -997,7 +998,7 @@ export namespace Browser {
|
||||
*/
|
||||
queueInsertItems(
|
||||
queueInsertItemsRequest: Browser.cast.media.QueueInsertItemsRequest,
|
||||
successCallback: Function,
|
||||
successCallback: () => void,
|
||||
errorCallback: (error: Browser.cast.Error) => void,
|
||||
): void;
|
||||
|
||||
@@ -1008,7 +1009,7 @@ export namespace Browser {
|
||||
*/
|
||||
queueJumpToItem(
|
||||
itemId: number,
|
||||
successCallback: Function,
|
||||
successCallback: () => void,
|
||||
errorCallback: (error: Browser.cast.Error) => void,
|
||||
): void;
|
||||
|
||||
@@ -1021,7 +1022,7 @@ export namespace Browser {
|
||||
queueMoveItemToNewIndex(
|
||||
itemId: number,
|
||||
newIndex: number,
|
||||
successCallback: Function,
|
||||
successCallback: () => void,
|
||||
errorCallback: (error: Browser.cast.Error) => void,
|
||||
): void;
|
||||
|
||||
@@ -1029,13 +1030,13 @@ export namespace Browser {
|
||||
* @param successCallback
|
||||
* @param errorCallback
|
||||
*/
|
||||
queueNext(successCallback: Function, errorCallback: (error: Browser.cast.Error) => void): void;
|
||||
queueNext(successCallback: () => void, errorCallback: (error: Browser.cast.Error) => void): void;
|
||||
|
||||
/**
|
||||
* @param successCallback
|
||||
* @param errorCallback
|
||||
*/
|
||||
queuePrev(successCallback: Function, errorCallback: (error: Browser.cast.Error) => void): void;
|
||||
queuePrev(successCallback: () => void, errorCallback: (error: Browser.cast.Error) => void): void;
|
||||
|
||||
/**
|
||||
* @param itemId
|
||||
@@ -1044,7 +1045,7 @@ export namespace Browser {
|
||||
*/
|
||||
queueRemoveItem(
|
||||
itemId: number,
|
||||
successCallback: Function,
|
||||
successCallback: () => void,
|
||||
errorCallback: (error: Browser.cast.Error) => void,
|
||||
): void;
|
||||
|
||||
@@ -1055,7 +1056,7 @@ export namespace Browser {
|
||||
*/
|
||||
queueReorderItems(
|
||||
queueReorderItemsRequest: Browser.cast.media.QueueReorderItemsRequest,
|
||||
successCallback: Function,
|
||||
successCallback: () => void,
|
||||
errorCallback: (error: Browser.cast.Error) => void,
|
||||
): void;
|
||||
|
||||
@@ -1066,7 +1067,7 @@ export namespace Browser {
|
||||
*/
|
||||
queueSetRepeatMode(
|
||||
repeatMode: Browser.cast.media.RepeatMode,
|
||||
successCallback: Function,
|
||||
successCallback: () => void,
|
||||
errorCallback: (error: Browser.cast.Error) => void,
|
||||
): void;
|
||||
|
||||
@@ -1077,7 +1078,7 @@ export namespace Browser {
|
||||
*/
|
||||
queueUpdateItems(
|
||||
queueUpdateItemsRequest: Browser.cast.media.QueueUpdateItemsRequest,
|
||||
successCallback: Function,
|
||||
successCallback: () => void,
|
||||
errorCallback: (error: Browser.cast.Error) => void,
|
||||
): void;
|
||||
}
|
||||
|
||||
+1476
-1108
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,7 @@
|
||||
"directory": "packages/browser"
|
||||
},
|
||||
"scripts": {
|
||||
"check": "check",
|
||||
"gen": "tsx scripts/generate.ts"
|
||||
},
|
||||
"author": {
|
||||
@@ -23,9 +24,10 @@
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/chrome": "{{chromeTypesVersion}}",
|
||||
"fs-extra": "catalog:",
|
||||
"nano-spawn": "catalog:",
|
||||
"tsx": "catalog:",
|
||||
"vitest": "catalog:"
|
||||
"fs-extra": "^11.3.0",
|
||||
"nano-spawn": "^0.2.0",
|
||||
"tsx": "4.19.4",
|
||||
"typescript": "^5.8.3",
|
||||
"vitest": "^3.1.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {}
|
||||
}
|
||||
@@ -1,5 +1,36 @@
|
||||
# Changelog
|
||||
|
||||
## v0.2.4
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/i18n-v0.2.3...i18n-v0.2.4)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Standardize locale codes and warn about unsupported ones ([#1617](https://github.com/wxt-dev/wxt/pull/1617))
|
||||
- Use `@wxt-dev/browser` instead of `@types/chrome` ([#1645](https://github.com/wxt-dev/wxt/pull/1645))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Add react language ID to README ([#1347](https://github.com/wxt-dev/wxt/pull/1347))
|
||||
- Fix public path reference ([bcb20874](https://github.com/wxt-dev/wxt/commit/bcb20874))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- **deps:** Upgrade all non-major dependencies ([#1164](https://github.com/wxt-dev/wxt/pull/1164))
|
||||
- **deps:** Bump dev and non-breaking major dependencies ([#1167](https://github.com/wxt-dev/wxt/pull/1167))
|
||||
- Add funding links to `package.json` files ([#1446](https://github.com/wxt-dev/wxt/pull/1446))
|
||||
- Use PNPM 10's new catelog feature ([#1493](https://github.com/wxt-dev/wxt/pull/1493))
|
||||
- Move production dependencies to PNPM 10 catelog ([#1494](https://github.com/wxt-dev/wxt/pull/1494))
|
||||
- Stop using PNPM catalog ([#1644](https://github.com/wxt-dev/wxt/pull/1644))
|
||||
- Upgrade `@aklinker1/check` to v2 ([#1647](https://github.com/wxt-dev/wxt/pull/1647))
|
||||
- Change browser workspace dependency to `^` ([c7335add](https://github.com/wxt-dev/wxt/commit/c7335add))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron ([@aklinker1](https://github.com/aklinker1))
|
||||
- Okinea Dev ([@okineadev](https://github.com/okineadev))
|
||||
- Redwoodlid ([@redwoodlid](https://github.com/redwoodlid))
|
||||
|
||||
## v0.2.3
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/i18n-v0.2.2...i18n-v0.2.3)
|
||||
|
||||
+13
-13
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@wxt-dev/i18n",
|
||||
"description": "Type-safe wrapper around browser.i18n.getMessage with additional features",
|
||||
"version": "0.2.3",
|
||||
"version": "0.2.4",
|
||||
"type": "module",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -26,9 +26,10 @@
|
||||
"test": "buildc --deps-only -- vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"chokidar": "catalog:",
|
||||
"confbox": "catalog:",
|
||||
"fast-glob": "catalog:"
|
||||
"@wxt-dev/browser": "workspace:^",
|
||||
"chokidar": "^4.0.3",
|
||||
"confbox": "^0.1.8 || ^0.2.2",
|
||||
"fast-glob": "^3.3.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"wxt": ">=0.19.7"
|
||||
@@ -39,15 +40,14 @@
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "catalog:",
|
||||
"@types/chrome": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"oxlint": "catalog:",
|
||||
"publint": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"unbuild": "catalog:",
|
||||
"vitest": "catalog:",
|
||||
"vitest-plugin-random-seed": "catalog:",
|
||||
"@aklinker1/check": "2.0.0",
|
||||
"@types/node": "^20.17.6",
|
||||
"oxlint": "^0.16.8",
|
||||
"publint": "^0.3.12",
|
||||
"typescript": "^5.8.3",
|
||||
"unbuild": "^3.5.0",
|
||||
"vitest": "^3.1.2",
|
||||
"vitest-plugin-random-seed": "^1.1.1",
|
||||
"wxt": "workspace:*"
|
||||
},
|
||||
"main": "./dist/index.cjs",
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { createI18n } from '../index';
|
||||
import { browser } from '@wxt-dev/browser';
|
||||
|
||||
const getMessageMock = vi.fn();
|
||||
|
||||
vi.stubGlobal('chrome', {
|
||||
i18n: {
|
||||
getMessage: getMessageMock,
|
||||
},
|
||||
vi.mock('@wxt-dev/browser', async () => {
|
||||
const { vi } = await import('vitest');
|
||||
return {
|
||||
browser: {
|
||||
i18n: {
|
||||
getMessage: vi.fn(),
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
const getMessageMock = vi.mocked(browser.i18n.getMessage);
|
||||
|
||||
describe('createI18n', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import { beforeEach, describe, it, vi } from 'vitest';
|
||||
import { createI18n } from '..';
|
||||
import { browser } from '@wxt-dev/browser';
|
||||
|
||||
const getMessageMock = vi.fn();
|
||||
|
||||
vi.stubGlobal('chrome', {
|
||||
i18n: {
|
||||
getMessage: getMessageMock,
|
||||
},
|
||||
vi.mock('@wxt-dev/browser', async () => {
|
||||
const { vi } = await import('vitest');
|
||||
return {
|
||||
browser: {
|
||||
i18n: {
|
||||
getMessage: vi.fn(),
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
const getMessageMock = vi.mocked(browser.i18n.getMessage);
|
||||
|
||||
const n: number = 1;
|
||||
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { ChromeMessage } from '../build';
|
||||
import { applyChromeMessagePlaceholders, getSubstitutionCount } from '../utils';
|
||||
import {
|
||||
applyChromeMessagePlaceholders,
|
||||
getSubstitutionCount,
|
||||
standardizeLocale,
|
||||
} from '../utils';
|
||||
|
||||
describe('Utils', () => {
|
||||
describe('applyChromeMessagePlaceholders', () => {
|
||||
it('should return the combined stirng', () => {
|
||||
it('should return the combined string', () => {
|
||||
const input = {
|
||||
message: 'Hello $username$, welcome to $appName$',
|
||||
placeholders: {
|
||||
@@ -60,4 +64,30 @@ describe('Utils', () => {
|
||||
expect(getSubstitutionCount('Hello $10')).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('standardizeLocale', () => {
|
||||
it('should convert two-letter locale codes to lowercase', () => {
|
||||
expect(standardizeLocale('en')).toEqual('en');
|
||||
expect(standardizeLocale('EN')).toEqual('en');
|
||||
});
|
||||
|
||||
it('should convert locale code extensions to uppercase', () => {
|
||||
expect(standardizeLocale('en_US')).toEqual('en_US');
|
||||
expect(standardizeLocale('en_us')).toEqual('en_US');
|
||||
expect(standardizeLocale('es_419')).toEqual('es_419');
|
||||
});
|
||||
|
||||
it('should convert dashes to underscores', () => {
|
||||
expect(standardizeLocale('en_US')).toEqual('en_US');
|
||||
expect(standardizeLocale('en-US')).toEqual('en_US');
|
||||
});
|
||||
|
||||
it('should return the input string as-is for unknown formats', () => {
|
||||
expect(standardizeLocale('en_USSS')).toEqual('en_USSS');
|
||||
expect(standardizeLocale('en-')).toEqual('en-');
|
||||
expect(standardizeLocale('------')).toEqual('------');
|
||||
expect(standardizeLocale('test')).toEqual('test');
|
||||
expect(standardizeLocale('hello-world')).toEqual('hello-world');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,6 +10,8 @@ import { parseYAML, parseJSON5, parseTOML } from 'confbox';
|
||||
import { dirname, extname } from 'node:path';
|
||||
import { applyChromeMessagePlaceholders, getSubstitutionCount } from './utils';
|
||||
|
||||
export { SUPPORTED_LOCALES } from './supported-locales';
|
||||
|
||||
//
|
||||
// TYPES
|
||||
//
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
I18n,
|
||||
Substitution,
|
||||
} from './types';
|
||||
import { browser } from '@wxt-dev/browser';
|
||||
|
||||
export function createI18n<
|
||||
T extends I18nStructure = DefaultI18nStructure,
|
||||
@@ -39,9 +40,9 @@ export function createI18n<
|
||||
if (sub?.length) {
|
||||
// Convert all substitutions to strings
|
||||
const stringSubs = sub?.map((sub) => String(sub));
|
||||
message = chrome.i18n.getMessage(key.replaceAll('.', '_'), stringSubs);
|
||||
message = browser.i18n.getMessage(key.replaceAll('.', '_'), stringSubs);
|
||||
} else {
|
||||
message = chrome.i18n.getMessage(key.replaceAll('.', '_'));
|
||||
message = browser.i18n.getMessage(key.replaceAll('.', '_'));
|
||||
}
|
||||
if (!message) {
|
||||
console.warn(`[i18n] Message not found: "${key}"`);
|
||||
|
||||
@@ -16,12 +16,14 @@ import {
|
||||
generateChromeMessagesText,
|
||||
parseMessagesFile,
|
||||
generateTypeText,
|
||||
SUPPORTED_LOCALES,
|
||||
} from './build';
|
||||
import glob from 'fast-glob';
|
||||
import { basename, extname, join, resolve } from 'node:path';
|
||||
import { watch } from 'chokidar';
|
||||
import { GeneratedPublicFile, WxtDirFileEntry } from 'wxt';
|
||||
import { writeFile } from 'node:fs/promises';
|
||||
import { standardizeLocale } from './utils';
|
||||
|
||||
export default defineWxtModule<I18nOptions>({
|
||||
name: '@wxt-dev/i18n',
|
||||
@@ -46,10 +48,22 @@ export default defineWxtModule<I18nOptions>({
|
||||
cwd: localesDir,
|
||||
absolute: true,
|
||||
});
|
||||
return files.map((file) => ({
|
||||
file,
|
||||
locale: basename(file).replace(extname(file), ''),
|
||||
}));
|
||||
|
||||
const unsupportedLocales: string[] = [];
|
||||
|
||||
const res = files.map((file) => {
|
||||
const rawLocale = basename(file).replace(extname(file), '');
|
||||
const locale = standardizeLocale(rawLocale);
|
||||
if (!SUPPORTED_LOCALES.has(locale)) unsupportedLocales.push(locale);
|
||||
return { file, locale };
|
||||
});
|
||||
|
||||
if (unsupportedLocales.length > 0)
|
||||
wxt.logger.warn(
|
||||
`Unsupported locales: [${unsupportedLocales.join(', ')}].\n\nWeb extensions only support a limited set of locales as described here: https://developer.chrome.com/docs/extensions/reference/api/i18n#locales`,
|
||||
);
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
const generateOutputJsonFiles = async (): Promise<
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
/** From https://developer.chrome.com/docs/extensions/reference/api/i18n#locales */
|
||||
export const SUPPORTED_LOCALES = new Set([
|
||||
'ar',
|
||||
'am',
|
||||
'bg',
|
||||
'bn',
|
||||
'ca',
|
||||
'cs',
|
||||
'da',
|
||||
'de',
|
||||
'el',
|
||||
'en',
|
||||
'en_AU',
|
||||
'en_GB',
|
||||
'en_US',
|
||||
'es',
|
||||
'es_419',
|
||||
'et',
|
||||
'fa',
|
||||
'fi',
|
||||
'fil',
|
||||
'fr',
|
||||
'gu',
|
||||
'he',
|
||||
'hi',
|
||||
'hr',
|
||||
'hu',
|
||||
'id',
|
||||
'it',
|
||||
'ja',
|
||||
'kn',
|
||||
'ko',
|
||||
'lt',
|
||||
'lv',
|
||||
'ml',
|
||||
'mr',
|
||||
'ms',
|
||||
'nl',
|
||||
'no',
|
||||
'pl',
|
||||
'pt_BR',
|
||||
'pt_PT',
|
||||
'ro',
|
||||
'ru',
|
||||
'sk',
|
||||
'sl',
|
||||
'sr',
|
||||
'sv',
|
||||
'sw',
|
||||
'ta',
|
||||
'te',
|
||||
'th',
|
||||
'tr',
|
||||
'uk',
|
||||
'vi',
|
||||
'zh_CN',
|
||||
'zh_TW',
|
||||
]);
|
||||
@@ -21,3 +21,16 @@ export function getSubstitutionCount(message: string): number {
|
||||
}
|
||||
|
||||
const MAX_SUBSTITUTIONS = 9;
|
||||
|
||||
/** Given a string, standardize it to the format `xx_YY`. */
|
||||
export function standardizeLocale(locale: string): string {
|
||||
if (locale.length === 2) return locale.toLowerCase();
|
||||
|
||||
const [is_match, prefix, suffix] =
|
||||
locale.match(/^([a-z]{2})[-_]([a-z]{2,3})$/i) ?? [];
|
||||
if (is_match) {
|
||||
return `${prefix.toLowerCase()}_${suffix.toUpperCase()}`;
|
||||
}
|
||||
|
||||
return locale;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"types": ["chrome", "node"]
|
||||
"types": ["node"]
|
||||
},
|
||||
"exclude": ["node_modules/**", "dist/**"]
|
||||
}
|
||||
|
||||
@@ -48,17 +48,17 @@
|
||||
"wxt": ">=0.19.16"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vitejs/plugin-react": "catalog:"
|
||||
"@vitejs/plugin-react": "^4.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "catalog:",
|
||||
"@types/react": "catalog:",
|
||||
"@types/react-dom": "catalog:",
|
||||
"publint": "catalog:",
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"unbuild": "catalog:",
|
||||
"@aklinker1/check": "2.0.0",
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-dom": "^19.1.3",
|
||||
"publint": "^0.3.12",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"typescript": "^5.8.3",
|
||||
"unbuild": "^3.5.0",
|
||||
"wxt": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,14 +48,14 @@
|
||||
"wxt": ">=0.19.16"
|
||||
},
|
||||
"dependencies": {
|
||||
"vite-plugin-solid": "catalog:"
|
||||
"vite-plugin-solid": "^2.11.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "catalog:",
|
||||
"publint": "catalog:",
|
||||
"solid-js": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"unbuild": "catalog:",
|
||||
"@aklinker1/check": "2.0.0",
|
||||
"publint": "^0.3.12",
|
||||
"solid-js": "^1.9.6",
|
||||
"typescript": "^5.8.3",
|
||||
"unbuild": "^3.5.0",
|
||||
"wxt": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,13 +47,13 @@
|
||||
"svelte": ">=5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "catalog:"
|
||||
"@sveltejs/vite-plugin-svelte": "^4.0.0 || ^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "catalog:",
|
||||
"publint": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"unbuild": "catalog:",
|
||||
"@aklinker1/check": "2.0.0",
|
||||
"publint": "^0.3.12",
|
||||
"typescript": "^5.8.3",
|
||||
"unbuild": "^3.5.0",
|
||||
"wxt": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,13 +46,13 @@
|
||||
"wxt": ">=0.19.16"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vitejs/plugin-vue": "catalog:"
|
||||
"@vitejs/plugin-vue": "^5.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "catalog:",
|
||||
"publint": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"unbuild": "catalog:",
|
||||
"@aklinker1/check": "2.0.0",
|
||||
"publint": "^0.3.12",
|
||||
"typescript": "^5.8.3",
|
||||
"unbuild": "^3.5.0",
|
||||
"wxt": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
# `@wxt-dev/runner`
|
||||
|
||||
Programmatically open a browser and install a web extension from a local directory.
|
||||
|
||||
###### With WXT
|
||||
|
||||
> [!WARNING]
|
||||
> This package is intended to replace [`web-ext`](https://github.com/mozilla/web-ext) in the future, but it is not ready at the moment. Once it's ready for testing in WXT, more details will be added here.
|
||||
|
||||
```ts
|
||||
// ~/wxt.runner.config.ts OR <project>/wxt.runner.config.ts
|
||||
import { defineRunnerConfig } from 'wxt';
|
||||
|
||||
export default defineRunnerConfig({
|
||||
// Options go here
|
||||
});
|
||||
```
|
||||
|
||||
###### JS API
|
||||
|
||||
```ts
|
||||
import { run } from '@wxt-dev/runner';
|
||||
|
||||
await run({
|
||||
extensionDir: '/path/to/extension',
|
||||
// Other options...
|
||||
});
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- Supports all Chromium and Firefox based browsers
|
||||
- Zero dependencies
|
||||
- One-line config for persisting data between launches
|
||||
|
||||
## Requirements
|
||||
|
||||
`@wxt-dev/runner` requires a JS runtime that implements the `WebSocket` standard:
|
||||
|
||||
| JS Runtime | Version |
|
||||
| ---------- | ----------- |
|
||||
| NodeJS | ≥ 22.4.0 |
|
||||
| Bun | ≥ 1.2.0 |
|
||||
|
||||
You also need to have a specific version of the browser installed that supports the latest features so extensions can be loaded:
|
||||
|
||||
| Browser | Version |
|
||||
| -------- | -------- |
|
||||
| Chromium | Unknown |
|
||||
| Firefox | ≥ 139 |
|
||||
|
||||
## TODO
|
||||
|
||||
- [x] Provide install functions to allow hooking into already running instances of Chrome/Firefox
|
||||
- [ ] Try to setup E2E tests on Firefox with Puppeteer using this approach
|
||||
- [ ] Try to setup E2E tests on Chrome with Puppeteer using this approach
|
||||
|
||||
## Options
|
||||
|
||||
### Target
|
||||
|
||||
To open a specific browser, use the `target` option:
|
||||
|
||||
```ts
|
||||
import { run } from '@wxt-dev/runner';
|
||||
|
||||
await run({
|
||||
extensionDir: 'path/to/extension',
|
||||
target: 'firefox',
|
||||
});
|
||||
```
|
||||
|
||||
Defaults to opening `chrome`. You may see type-hints for a list of popular browsers, but you can enter any string you want here.
|
||||
|
||||
### Data Persistence
|
||||
|
||||
Browsers block you from using your normal browser profiles when using the [BiDi and CDP protocols](#implementation-details) for security reasons.
|
||||
|
||||
To change how the new profile's data is saved between sessions, use the `dataPersistence` option:
|
||||
|
||||
```ts
|
||||
import { run } from '@wxt-dev/runner';
|
||||
|
||||
await run({
|
||||
dataPersistence: 'user',
|
||||
});
|
||||
```
|
||||
|
||||
- `"none"` (default): Use a brand new browser profile every time the browser is opened (stored in the system's tmp directory)
|
||||
- `"project"`: Create a new profile that is re-used for your current directory (by default stored in `.wxt-runner` or `.wxt/runner` for WXT projects)
|
||||
- `"user"`: Create a new profile that is re-used for all projects using `@wxt-dev/runner` (by default stored in `$HOME/.wxt-runner`)
|
||||
|
||||
These presets configure different flags for different operating systems when spawning the browser process.
|
||||
|
||||
If you want to customize your data persistence beyond what these presets define, [you can override the browser flags yourself](#Arguments) to configure persistence.
|
||||
|
||||
### Browser Binaries
|
||||
|
||||
`@wxt-dev/runner` will look for browser binaries/executables in [a hard-coded list of paths](https://github.com/wxt-dev/wxt/blob/main/packages/runner/src/browser-paths.ts). It does not and will not explore your filesystem/`$PATH` to find where the browser is installed. That means there are times you will need to specify the path to a browser's binary on your system:
|
||||
|
||||
- Your browser's path is non-standard or missing from the hard-coded list.
|
||||
- You want to use a specific version/release of the browser.
|
||||
- You're using a less popular browser and `@wxt-dev/runner` doesn't have hard-coded paths for it.
|
||||
|
||||
To do this, use the `browserBinaries` option and set the path to the browser's binary:
|
||||
|
||||
```ts
|
||||
import { run } from '@wxt-dev/runner';
|
||||
|
||||
await run({
|
||||
extensionDir: 'path/to/extension',
|
||||
browserBinaries: {
|
||||
chrome: '/path/to/chrome',
|
||||
firefox: '/path/to/firefox',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Arguments
|
||||
|
||||
To pass custom arguments to the browser on startup, use the `chromiumArgs` or `firefoxArgs` options:
|
||||
|
||||
```ts
|
||||
import { run } from '@wxt-dev/runner';
|
||||
|
||||
await run({
|
||||
extensionDir: 'path/to/extension',
|
||||
chromiumArgs: ['--window-size=1920,1080'],
|
||||
firefoxArgs: ['--window-size', '1920,1080'],
|
||||
});
|
||||
```
|
||||
|
||||
### Start URLs
|
||||
|
||||
To open specific URLs in tabs by default, you also use the `chromiumArgs` or `firefoxArgs` options.
|
||||
|
||||
Any URLs passed as a CLI argument will be opened in the browser when it starts.
|
||||
|
||||
```ts
|
||||
import { run } from '@wxt-dev/runner';
|
||||
|
||||
await run({
|
||||
extensionDir: 'path/to/extension',
|
||||
chromiumArgs: ['https://example.com'],
|
||||
firefoxArgs: ['https://example.com'],
|
||||
});
|
||||
```
|
||||
|
||||
### Debugging
|
||||
|
||||
To see debug logs, set the `DEBUG` env var to `"@wxt-dev/runner"`. This will print the resolved config, commands used to spawn the browser, any messages sent on the browser's communication protocol, and more for you to debug.
|
||||
|
||||
<details>
|
||||
<summary>Example debug output</summary>
|
||||
|
||||
```
|
||||
@wxt-dev/runner:options User options: { extensionDir: 'demo-extension', target: undefined }
|
||||
@wxt-dev/runner:options Resolved options: {
|
||||
browserBinary: '/usr/bin/chromium',
|
||||
chromiumArgs: [
|
||||
'--disable-features=Translate,OptimizationHints,MediaRouter,DialMediaRouteProvider,CalculateNativeWinOcclusion,InterestFeedContentSuggestions,CertificateTransparencyComponentUpdater,AutofillServerCommunication,PrivacySandboxSettings4',
|
||||
'--disable-component-extensions-with-background-pages',
|
||||
'--disable-background-networking',
|
||||
'--disable-component-update',
|
||||
'--disable-client-side-phishing-detection',
|
||||
'--disable-sync',
|
||||
'--metrics-recording-only',
|
||||
'--disable-default-apps',
|
||||
'--no-default-browser-check',
|
||||
'--no-first-run',
|
||||
'--disable-background-timer-throttling',
|
||||
'--disable-ipc-flooding-protection',
|
||||
'--password-store=basic',
|
||||
'--use-mock-keychain',
|
||||
'--force-fieldtrials=*BackgroundTracing/default/',
|
||||
'--disable-hang-monitor',
|
||||
'--disable-prompt-on-repost',
|
||||
'--disable-domain-reliability',
|
||||
'--propagate-iph-for-testing',
|
||||
'--remote-debugging-port=0',
|
||||
'--remote-debugging-pipe',
|
||||
'--user-data-dir=/tmp/wxt-runner-pWXLO1',
|
||||
'--enable-unsafe-extension-debugging'
|
||||
],
|
||||
dataDir: '/tmp/wxt-runner-pWXLO1',
|
||||
dataPersistence: 'none',
|
||||
chromiumRemoteDebuggingPort: 0,
|
||||
extensionDir: '/home/aklinker1/Development/github.com/wxt-dev/wxt/packages/runner/demo-extension',
|
||||
firefoxArgs: [
|
||||
'--new-instance',
|
||||
'--no-remote',
|
||||
'--profile',
|
||||
'/tmp/wxt-runner-pWXLO1',
|
||||
'--remote-debugging-port=0',
|
||||
'about:debugging#/runtime/this-firefox'
|
||||
],
|
||||
firefoxRemoteDebuggingPort: 0,
|
||||
target: 'chrome'
|
||||
}
|
||||
@wxt-dev/runner:chrome:stderr DevTools listening on ws://127.0.0.1:38397/devtools/browser/93dc4de5-64cb-4e0b-a9d3-7549527015f0
|
||||
@wxt-dev/runner:cdp Sending command: {
|
||||
id: 1,
|
||||
method: 'Extensions.loadUnpacked',
|
||||
params: {
|
||||
path: '/home/aklinker1/Development/github.com/wxt-dev/wxt/packages/runner/demo-extension'
|
||||
}
|
||||
}
|
||||
@wxt-dev/runner:cdp Received response: { id: 1, result: { id: 'hckhakegfgenefhikdcfkaaonnclljmf' } }
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Implementation Details
|
||||
|
||||
All this package does is spawn a child process to open the browser with some default flags before using remote protocols to install the extension.
|
||||
|
||||
### Firefox
|
||||
|
||||
We use the new [WebDriver BiDi protocol](https://www.w3.org/TR/webdriver-bidi) to install the extension. This just involves connecting to a web socket and sending a few messages.
|
||||
|
||||
### Chrome
|
||||
|
||||
We use the [CDP](https://chromedevtools.github.io/devtools-protocol/) with `--remote-debugging-pipe` and `--enable-unsafe-extension-debugging` to install the extension by sending a message via IO pipes 3 and 4.
|
||||
|
||||
We don't use Webdriver Bidi because it's not built into Chrome yet. It requires us instantiating a separate child process for `chromedriver`. This is slower and more difficult than just using the CDP built into Chrome.
|
||||
@@ -0,0 +1 @@
|
||||
console.log('Hello background!');
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "Test",
|
||||
"version": "1.0.0",
|
||||
"manifest_version": 3,
|
||||
"background": {
|
||||
"service_worker": "background.js",
|
||||
"scripts": ["background.js"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
//
|
||||
// USAGE:
|
||||
// pnpm dev
|
||||
// pnpm dev firefox-nightly
|
||||
// pnpm dev <target>
|
||||
//
|
||||
|
||||
import { run } from './src';
|
||||
|
||||
// Uncomment to enable debug logs
|
||||
process.env.DEBUG = '@wxt-dev/runner';
|
||||
|
||||
await run({
|
||||
extensionDir: 'demo-extension',
|
||||
target: process.argv[2],
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "@wxt-dev/runner",
|
||||
"description": "Launch Chrome and Firefox with a web extension installed",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/wxt-dev/wxt.git",
|
||||
"directory": "packages/runner"
|
||||
},
|
||||
"homepage": "https://github.com/wxt-dev/wxt/tree/main/packages/runner#readme",
|
||||
"keywords": [
|
||||
"web-extension",
|
||||
"chrome-extension",
|
||||
"wxt"
|
||||
],
|
||||
"author": {
|
||||
"name": "Aaron Klinker",
|
||||
"email": "aaronklinker1+wxt@gmail.com"
|
||||
},
|
||||
"license": "MIT",
|
||||
"funding": "https://github.com/sponsors/wxt-dev",
|
||||
"scripts": {
|
||||
"check": "pnpm build && check",
|
||||
"test": "buildc --deps-only -- vitest",
|
||||
"dev": "tsx --trace-warnings dev.ts",
|
||||
"build": "buildc -- unbuild",
|
||||
"prepublishOnly": "pnpm build"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "2.0.0",
|
||||
"oxlint": "^0.16.8",
|
||||
"publint": "^0.3.12",
|
||||
"tsx": "4.19.4",
|
||||
"typescript": "^5.8.3",
|
||||
"unbuild": "^3.5.0",
|
||||
"vitest": "^3.1.2"
|
||||
},
|
||||
"main": "dist/index.mjs",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.mts",
|
||||
"default": "./dist/index.mjs"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { createCdpConnection, type CDPConnection } from '../cdp';
|
||||
import { createBidiConnection, type BidiConnection } from '../bidi';
|
||||
import type { ChildProcess } from 'node:child_process';
|
||||
import { installChromium, installFirefox } from '../install';
|
||||
|
||||
vi.mock('../cdp');
|
||||
const createCdpConnectionMock = vi.mocked(createCdpConnection);
|
||||
|
||||
vi.mock('../bidi');
|
||||
const createBidiConnectionMock = vi.mocked(createBidiConnection);
|
||||
|
||||
describe('Install', () => {
|
||||
describe('Chromium', () => {
|
||||
it('Should send the install command to the process', async () => {
|
||||
const browserProcess = mock<ChildProcess>();
|
||||
const connection = mock<CDPConnection>({
|
||||
[Symbol.dispose]: vi.fn(),
|
||||
});
|
||||
const extensionDir = '/path/to/extension';
|
||||
const expectedExtensionId = 'chromium-extension-id';
|
||||
|
||||
createCdpConnectionMock.mockReturnValue(connection);
|
||||
connection.send.mockImplementation(async (method) => {
|
||||
if (method === 'Extensions.loadUnpacked')
|
||||
return { id: expectedExtensionId };
|
||||
throw Error('Unknown method');
|
||||
});
|
||||
|
||||
const res = await installChromium(browserProcess, extensionDir);
|
||||
|
||||
expect(createCdpConnectionMock).toBeCalledTimes(1);
|
||||
expect(createCdpConnectionMock).toBeCalledWith(browserProcess);
|
||||
|
||||
expect(connection.send).toBeCalledTimes(1);
|
||||
expect(connection.send).toBeCalledWith('Extensions.loadUnpacked', {
|
||||
path: extensionDir,
|
||||
});
|
||||
|
||||
expect(res).toEqual({ id: expectedExtensionId });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Firefox', () => {
|
||||
it('Should connect to the server, start a session, then install the extension', async () => {
|
||||
const debuggerUrl = 'http://127.0.0.1:9222';
|
||||
const extensionDir = '/path/to/extension';
|
||||
const expectedExtensionId = 'firefox-extension-id';
|
||||
const connection = mock<BidiConnection>({
|
||||
[Symbol.dispose]: vi.fn(),
|
||||
});
|
||||
|
||||
createBidiConnectionMock.mockResolvedValue(connection);
|
||||
connection.send.mockImplementation(async (method) => {
|
||||
if (method === 'session.new') return { sessionId: 'session-id' };
|
||||
if (method === 'webExtension.install')
|
||||
return { extension: expectedExtensionId };
|
||||
});
|
||||
|
||||
const res = await installFirefox(debuggerUrl, extensionDir);
|
||||
|
||||
expect(createBidiConnectionMock).toBeCalledTimes(1);
|
||||
expect(createBidiConnectionMock).toBeCalledWith(debuggerUrl);
|
||||
|
||||
expect(connection.send).toBeCalledTimes(2);
|
||||
expect(connection.send).toBeCalledWith('session.new', {
|
||||
capabilities: {},
|
||||
});
|
||||
expect(connection.send).toBeCalledWith('webExtension.install', {
|
||||
extensionData: {
|
||||
type: 'path',
|
||||
path: extensionDir,
|
||||
},
|
||||
});
|
||||
|
||||
expect(res).toEqual({ extension: expectedExtensionId });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,306 @@
|
||||
import { describe, it, expect, vi, beforeAll } from 'vitest';
|
||||
import { ResolvedRunOptions, resolveRunOptions } from '../options';
|
||||
import { resolve, join } from 'node:path';
|
||||
import { tmpdir, homedir } from 'node:os';
|
||||
import { mkdir } from 'node:fs/promises';
|
||||
|
||||
vi.mock('node:os', async () => {
|
||||
const { vi } = await import('vitest');
|
||||
const os: any = await vi.importActual('node:os');
|
||||
const { join } = await import('node:path');
|
||||
return {
|
||||
...os,
|
||||
tmpdir: () => join(os.tmpdir(), 'tmpdir-mock'),
|
||||
homedir: () => join(os.tmpdir(), 'homedir-mock'),
|
||||
};
|
||||
});
|
||||
|
||||
describe('Options', () => {
|
||||
beforeAll(async () => {
|
||||
// Make sure mock test directories exist
|
||||
await mkdir(tmpdir(), { recursive: true });
|
||||
await mkdir(homedir(), { recursive: true });
|
||||
});
|
||||
|
||||
describe('extensionDir', () => {
|
||||
it('should default to the current working directory', async () => {
|
||||
const actual = await resolveRunOptions({});
|
||||
expect(actual).toMatchObject<Partial<ResolvedRunOptions>>({
|
||||
extensionDir: process.cwd(),
|
||||
});
|
||||
});
|
||||
|
||||
it('should resolve relative to the current working directory', async () => {
|
||||
const actual = await resolveRunOptions({
|
||||
extensionDir: './path/to/extension',
|
||||
});
|
||||
expect(actual).toMatchObject<Partial<ResolvedRunOptions>>({
|
||||
extensionDir: resolve(process.cwd(), './path/to/extension'),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('target', () => {
|
||||
it('should be "chrome" by default', async () => {
|
||||
const actual = await resolveRunOptions({
|
||||
extensionDir: 'path/to/extension',
|
||||
});
|
||||
expect(actual).toMatchObject<Partial<ResolvedRunOptions>>({
|
||||
target: 'chrome',
|
||||
});
|
||||
});
|
||||
|
||||
it('should be what is passed in', async () => {
|
||||
const actual = await resolveRunOptions({
|
||||
extensionDir: 'path/to/extension',
|
||||
target: 'custom',
|
||||
browserBinaries: {
|
||||
custom: '/path/to/custom/browser',
|
||||
},
|
||||
});
|
||||
expect(actual).toMatchObject<Partial<ResolvedRunOptions>>({
|
||||
target: 'custom',
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if the target binary could not be found', async () => {
|
||||
const actual = resolveRunOptions({
|
||||
extensionDir: 'path/to/extension',
|
||||
target: 'custom',
|
||||
});
|
||||
await expect(actual).rejects.toThrow('Could not find "custom" binary.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('browserBinary', () => {
|
||||
it('should denormalize the browserBinary', async () => {
|
||||
const path =
|
||||
process.platform === 'win32'
|
||||
? 'C:/path/to/custom/browser.exe'
|
||||
: '/path/to/custom/browser';
|
||||
const expectedPath =
|
||||
process.platform === 'win32'
|
||||
? 'C:\\path\\to\\custom\\browser.exe'
|
||||
: path;
|
||||
const actual = await resolveRunOptions({
|
||||
extensionDir: 'path/to/extension',
|
||||
target: 'custom',
|
||||
browserBinaries: {
|
||||
custom: path,
|
||||
},
|
||||
});
|
||||
expect(actual).toMatchObject<Partial<ResolvedRunOptions>>({
|
||||
browserBinary: expectedPath,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('chromiumArgs', () => {
|
||||
it('should log a warning when --user-data-dir is passed in', async () => {
|
||||
const warnSpy = vi.spyOn(console, 'warn');
|
||||
await resolveRunOptions({
|
||||
chromiumArgs: ['--user-data-dir=some/custom/path'],
|
||||
});
|
||||
expect(warnSpy).toBeCalledTimes(1);
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'Custom Chromium --user-data-dir argument ignored',
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('should log a warning when --remote-debugging-port is passed in', async () => {
|
||||
const warnSpy = vi.spyOn(console, 'warn');
|
||||
await resolveRunOptions({
|
||||
chromiumArgs: ['--remote-debugging-port=9222'],
|
||||
});
|
||||
expect(warnSpy).toBeCalledTimes(1);
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'Custom Chromium --remote-debugging-port argument ignored',
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('should combine default args with user provided ones', async () => {
|
||||
const actual = await resolveRunOptions({
|
||||
chromiumArgs: ['--window-size=1920,1080'],
|
||||
});
|
||||
expect(actual.chromiumArgs).toEqual([
|
||||
// Defaults
|
||||
'--disable-features=Translate,OptimizationHints,MediaRouter,DialMediaRouteProvider,CalculateNativeWinOcclusion,InterestFeedContentSuggestions,CertificateTransparencyComponentUpdater,AutofillServerCommunication,PrivacySandboxSettings4',
|
||||
'--disable-component-extensions-with-background-pages',
|
||||
'--disable-background-networking',
|
||||
'--disable-component-update',
|
||||
'--disable-client-side-phishing-detection',
|
||||
'--disable-sync',
|
||||
'--metrics-recording-only',
|
||||
'--disable-default-apps',
|
||||
'--no-default-browser-check',
|
||||
'--no-first-run',
|
||||
'--disable-background-timer-throttling',
|
||||
'--disable-ipc-flooding-protection',
|
||||
'--password-store=basic',
|
||||
'--use-mock-keychain',
|
||||
'--force-fieldtrials=*BackgroundTracing/default/',
|
||||
'--disable-hang-monitor',
|
||||
'--disable-prompt-on-repost',
|
||||
'--disable-domain-reliability',
|
||||
'--propagate-iph-for-testing',
|
||||
// Debugging
|
||||
expect.stringContaining('--remote-debugging-port='),
|
||||
'--remote-debugging-pipe',
|
||||
expect.stringContaining('--user-data-dir='), // See dataPersistence tests
|
||||
'--enable-unsafe-extension-debugging',
|
||||
// User provided
|
||||
'--window-size=1920,1080',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('firefoxArgs', () => {
|
||||
it('should log a warning when --profile is passed in', async () => {
|
||||
const warnSpy = vi.spyOn(console, 'warn');
|
||||
await resolveRunOptions({
|
||||
firefoxArgs: ['--profile=some/custom/path'],
|
||||
});
|
||||
expect(warnSpy).toBeCalledTimes(1);
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Custom Firefox --profile argument ignored'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should log a warning when --remote-debugging-port is passed in', async () => {
|
||||
const warnSpy = vi.spyOn(console, 'warn');
|
||||
await resolveRunOptions({
|
||||
firefoxArgs: ['--remote-debugging-port=9222'],
|
||||
});
|
||||
expect(warnSpy).toBeCalledTimes(1);
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'Custom Firefox --remote-debugging-port argument ignored',
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('should combine default args with user provided ones', async () => {
|
||||
const actual = await resolveRunOptions({
|
||||
firefoxArgs: ['--window-size=1920,1080'],
|
||||
});
|
||||
expect(actual.firefoxArgs).toEqual([
|
||||
// Defaults
|
||||
'--new-instance',
|
||||
'--no-remote',
|
||||
'--profile',
|
||||
expect.any(String), // See dataPersistence tests
|
||||
expect.stringContaining('--remote-debugging-port='),
|
||||
'about:debugging#/runtime/this-firefox',
|
||||
// User provided
|
||||
'--window-size=1920,1080',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('chromiumRemoteDebuggingPort', () => {
|
||||
it('should default to 0', async () => {
|
||||
const actual = await resolveRunOptions({});
|
||||
expect(actual).toMatchObject<Partial<ResolvedRunOptions>>({
|
||||
chromiumRemoteDebuggingPort: 0,
|
||||
chromiumArgs: expect.arrayContaining([`--remote-debugging-port=0`]),
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect user provided port', async () => {
|
||||
const actual = await resolveRunOptions({
|
||||
chromiumRemoteDebuggingPort: 9222,
|
||||
});
|
||||
expect(actual).toMatchObject<Partial<ResolvedRunOptions>>({
|
||||
chromiumRemoteDebuggingPort: 9222,
|
||||
chromiumArgs: expect.arrayContaining([`--remote-debugging-port=9222`]),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('firefoxRemoteDebuggingPort', () => {
|
||||
it('should default to 0', async () => {
|
||||
const actual = await resolveRunOptions({});
|
||||
expect(actual).toMatchObject<Partial<ResolvedRunOptions>>({
|
||||
firefoxRemoteDebuggingPort: 0,
|
||||
firefoxArgs: expect.arrayContaining([`--remote-debugging-port=0`]),
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect user provided port', async () => {
|
||||
const actual = await resolveRunOptions({
|
||||
firefoxRemoteDebuggingPort: 9222,
|
||||
});
|
||||
expect(actual).toMatchObject<Partial<ResolvedRunOptions>>({
|
||||
firefoxRemoteDebuggingPort: 9222,
|
||||
firefoxArgs: expect.arrayContaining([`--remote-debugging-port=9222`]),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('dataPersistence', () => {
|
||||
it('should default to "none"', async () => {
|
||||
const actual = await resolveRunOptions({});
|
||||
expect(actual).toMatchObject<Partial<ResolvedRunOptions>>({
|
||||
dataPersistence: 'none',
|
||||
});
|
||||
});
|
||||
|
||||
it('should use a temporary directory when set to "none"', async () => {
|
||||
const actual = await resolveRunOptions({
|
||||
dataPersistence: 'none',
|
||||
});
|
||||
expect(actual).toMatchObject<Partial<ResolvedRunOptions>>({
|
||||
dataPersistence: 'none',
|
||||
dataDir: expect.stringContaining(join(tmpdir(), 'wxt-runner-')),
|
||||
chromiumArgs: expect.arrayContaining([
|
||||
expect.stringContaining(
|
||||
`--user-data-dir=${join(tmpdir(), 'wxt-runner-')}`,
|
||||
),
|
||||
]),
|
||||
firefoxArgs: expect.arrayContaining([
|
||||
expect.stringContaining(join(tmpdir(), 'wxt-runner-')),
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
it('should use a directory in the current working directory when set to "project"', async () => {
|
||||
const actual = await resolveRunOptions({
|
||||
dataPersistence: 'project',
|
||||
});
|
||||
expect(actual).toMatchObject<Partial<ResolvedRunOptions>>({
|
||||
dataPersistence: 'project',
|
||||
dataDir: expect.stringContaining(join(process.cwd(), '.wxt-runner')),
|
||||
chromiumArgs: expect.arrayContaining([
|
||||
expect.stringContaining(
|
||||
`--user-data-dir=${join(process.cwd(), '.wxt-runner')}`,
|
||||
),
|
||||
]),
|
||||
firefoxArgs: expect.arrayContaining([
|
||||
expect.stringContaining(join(process.cwd(), '.wxt-runner')),
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
it('should use a directory in the user\'s home directory when set to "user"', async () => {
|
||||
const actual = await resolveRunOptions({
|
||||
dataPersistence: 'user',
|
||||
});
|
||||
expect(actual).toMatchObject<Partial<ResolvedRunOptions>>({
|
||||
dataPersistence: 'user',
|
||||
dataDir: expect.stringContaining(join(homedir(), '.wxt-runner')),
|
||||
chromiumArgs: expect.arrayContaining([
|
||||
expect.stringContaining(
|
||||
`--user-data-dir=${join(homedir(), '.wxt-runner')}`,
|
||||
),
|
||||
]),
|
||||
firefoxArgs: expect.arrayContaining([
|
||||
expect.stringContaining(join(homedir(), '.wxt-runner')),
|
||||
]),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
import { openWebSocket } from './web-socket';
|
||||
import { debug } from './debug';
|
||||
|
||||
const debugBidi = debug.scoped('bidi');
|
||||
|
||||
export interface BidiConnection extends Disposable {
|
||||
send<T>(method: string, params: any, timeout?: number): Promise<T>;
|
||||
close(): void;
|
||||
}
|
||||
|
||||
export async function createBidiConnection(
|
||||
baseUrl: string,
|
||||
): Promise<BidiConnection> {
|
||||
const url = new URL('/session', baseUrl);
|
||||
debugBidi('Connecting to BiDi server @', url.href);
|
||||
|
||||
const webSocket = await openWebSocket(url.href);
|
||||
debugBidi('Connected');
|
||||
|
||||
let requestId = 0;
|
||||
|
||||
return {
|
||||
send(method, params, timeout = 10e3) {
|
||||
const id = ++requestId;
|
||||
const command = { id, method, params };
|
||||
debugBidi('Sending command:', command);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const cleanup = () => {
|
||||
webSocket.removeEventListener('message', onMessage);
|
||||
webSocket.removeEventListener('error', onError);
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
cleanup();
|
||||
reject(
|
||||
new Error(
|
||||
`Timed out after ${timeout}ms waiting for ${method} response`,
|
||||
),
|
||||
);
|
||||
}, timeout);
|
||||
|
||||
const onMessage = (event: MessageEvent) => {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.id === id) {
|
||||
debugBidi('Received response:', data);
|
||||
cleanup();
|
||||
if (data.type === 'success') resolve(data.result);
|
||||
else reject(Error(data.message, { cause: data }));
|
||||
}
|
||||
};
|
||||
const onError = (error: any) => {
|
||||
cleanup();
|
||||
reject(new Error('Error sending request', { cause: error }));
|
||||
};
|
||||
|
||||
webSocket.addEventListener('message', onMessage);
|
||||
webSocket.addEventListener('error', onError);
|
||||
|
||||
webSocket.send(JSON.stringify(command));
|
||||
});
|
||||
},
|
||||
|
||||
close() {
|
||||
debugBidi('Closing connection...');
|
||||
webSocket.close();
|
||||
debugBidi('Closed connection');
|
||||
},
|
||||
[Symbol.dispose]() {
|
||||
debugBidi('Disposing connection...');
|
||||
webSocket.close();
|
||||
debugBidi('Disposed connection');
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
export type BrowserPlatform = 'windows' | 'mac' | 'linux';
|
||||
|
||||
export type KnownTarget =
|
||||
| 'chromium'
|
||||
| 'chrome'
|
||||
| 'chrome-beta'
|
||||
| 'chrome-dev'
|
||||
| 'chrome-canary'
|
||||
| 'edge'
|
||||
| 'edge-beta'
|
||||
| 'edge-dev'
|
||||
| 'edge-canary'
|
||||
| 'firefox'
|
||||
| 'firefox-nightly'
|
||||
| 'firefox-developer-edition'
|
||||
| 'zen';
|
||||
|
||||
export const KNOWN_BROWSER_PATHS: Record<
|
||||
KnownTarget,
|
||||
Record<BrowserPlatform, string[]>
|
||||
> = {
|
||||
// Chromium based targets
|
||||
|
||||
chromium: {
|
||||
mac: [],
|
||||
linux: [
|
||||
// Arch
|
||||
'/usr/bin/chromium',
|
||||
],
|
||||
windows: [],
|
||||
},
|
||||
chrome: {
|
||||
mac: ['/Applications/Chrome.app/Contents/MacOS/Google Chrome'],
|
||||
linux: [],
|
||||
windows: ['C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'],
|
||||
},
|
||||
'chrome-beta': {
|
||||
mac: [],
|
||||
linux: [],
|
||||
windows: [],
|
||||
},
|
||||
'chrome-canary': {
|
||||
mac: [
|
||||
'/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
|
||||
],
|
||||
linux: [],
|
||||
windows: [],
|
||||
},
|
||||
'chrome-dev': {
|
||||
mac: [],
|
||||
linux: [],
|
||||
windows: [],
|
||||
},
|
||||
edge: {
|
||||
mac: [],
|
||||
linux: [],
|
||||
windows: [],
|
||||
},
|
||||
'edge-beta': {
|
||||
mac: [],
|
||||
linux: [],
|
||||
windows: [],
|
||||
},
|
||||
'edge-canary': {
|
||||
mac: [],
|
||||
linux: [],
|
||||
windows: [],
|
||||
},
|
||||
'edge-dev': {
|
||||
mac: [],
|
||||
linux: [],
|
||||
windows: [],
|
||||
},
|
||||
|
||||
// Firefox based targets
|
||||
|
||||
firefox: {
|
||||
mac: ['/Applications/Firefox.app/Contents/MacOS/firefox'],
|
||||
linux: [
|
||||
// Arch
|
||||
'/usr/bin/firefox',
|
||||
],
|
||||
windows: [],
|
||||
},
|
||||
'firefox-nightly': {
|
||||
mac: ['/Applications/Firefox Nightly.app/Contents/MacOS/firefox'],
|
||||
linux: [],
|
||||
windows: ['C:\\Program Files\\Firefox Nightly\\firefox.exe'],
|
||||
},
|
||||
'firefox-developer-edition': {
|
||||
mac: ['/Applications/Firefox Developer Edition.app/Contents/MacOS/firefox'],
|
||||
linux: [
|
||||
// Arch
|
||||
'/usr/bin/firefox-developer-edition',
|
||||
],
|
||||
windows: [],
|
||||
},
|
||||
zen: {
|
||||
mac: ['/Applications/Zen Browser.app/Contents/MacOS/zen'],
|
||||
linux: [],
|
||||
windows: [],
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* When targeting a browser, this map contains the other targets to fall back on when a binary could not be found for the primary target.
|
||||
*/
|
||||
export const FALLBACK_TARGETS: Partial<Record<KnownTarget, KnownTarget[]>> = {
|
||||
chrome: [
|
||||
'chromium',
|
||||
'chrome-canary',
|
||||
'chrome-beta',
|
||||
'chrome-dev',
|
||||
'edge',
|
||||
'edge-canary',
|
||||
'edge-beta',
|
||||
'edge-dev',
|
||||
],
|
||||
firefox: ['firefox-developer-edition', 'firefox-nightly', 'zen'],
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
import type { ChildProcess } from 'node:child_process';
|
||||
import type { Readable, Writable } from 'node:stream';
|
||||
import { debug } from './debug';
|
||||
|
||||
const debugCdp = debug.scoped('cdp');
|
||||
|
||||
export interface CDPConnection extends Disposable {
|
||||
send<T>(method: string, params: any, timeout?: number): Promise<T>;
|
||||
close(): void;
|
||||
}
|
||||
|
||||
export function createCdpConnection(
|
||||
browserProcess: ChildProcess,
|
||||
): CDPConnection {
|
||||
const inputStream = browserProcess.stdio[3] as Writable;
|
||||
const outputStream = browserProcess.stdio[4] as Readable;
|
||||
|
||||
let requestId = 0;
|
||||
|
||||
return {
|
||||
send(method, params, timeout = 10e3) {
|
||||
const id = ++requestId;
|
||||
const command = { id, method, params };
|
||||
debugCdp('Sending command:', command);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
reject(new Error(`CDP command timed out: ${method}`));
|
||||
}, timeout);
|
||||
|
||||
const onData = (data: Buffer) => {
|
||||
// Trim the trailing null character
|
||||
const text = data.toString().slice(0, -1);
|
||||
const res = JSON.parse(text);
|
||||
|
||||
if (res.id !== id) return;
|
||||
|
||||
debugCdp('Received response:', res);
|
||||
clearTimeout(timer);
|
||||
outputStream.removeListener('data', onData);
|
||||
|
||||
if ('error' in res) {
|
||||
reject(new Error(res.error.message, { cause: res.error }));
|
||||
} else {
|
||||
resolve(res.result);
|
||||
}
|
||||
};
|
||||
|
||||
outputStream.addListener('data', onData);
|
||||
|
||||
inputStream.write(JSON.stringify(command) + '\0');
|
||||
});
|
||||
},
|
||||
close() {},
|
||||
[Symbol.dispose]() {},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
export interface Debug {
|
||||
(...args: any[]): void;
|
||||
scoped: (scope: string) => Debug;
|
||||
}
|
||||
|
||||
function createDebug(scopes: string[]): Debug {
|
||||
const debug = (...args: any[]) => {
|
||||
const scope = scopes.join(':');
|
||||
if (
|
||||
process.env.DEBUG === '1' ||
|
||||
process.env.DEBUG === 'true' ||
|
||||
scope.startsWith(process.env.DEBUG ?? '@NOT')
|
||||
) {
|
||||
const params = scope ? [`\x1b[31m${scope}\x1b[0m`, ...args] : args;
|
||||
console.log(...params);
|
||||
}
|
||||
};
|
||||
|
||||
debug.scoped = (scope: string) => createDebug([...scopes, scope]);
|
||||
|
||||
return debug;
|
||||
}
|
||||
|
||||
export const debug = createDebug(['@wxt-dev/runner']);
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './run';
|
||||
export * from './options';
|
||||
export * from './install';
|
||||
@@ -0,0 +1,59 @@
|
||||
import type { ChildProcess } from 'node:child_process';
|
||||
import { createBidiConnection } from './bidi';
|
||||
import { createCdpConnection } from './cdp';
|
||||
|
||||
/**
|
||||
* Install an extension to an already running instance of Firefox.
|
||||
* @param debuggerUrl The URL of the Firefox BiDi server (ex: `ws://127.0.0.1:45912`).
|
||||
* @param extensionDir Absolute path to the directory containing the extension to be installed.
|
||||
*/
|
||||
export async function installFirefox(
|
||||
debuggerUrl: string,
|
||||
extensionDir: string,
|
||||
): Promise<BidiWebExtensionInstallResponse> {
|
||||
using bidi = await createBidiConnection(debuggerUrl);
|
||||
|
||||
// Start a session
|
||||
await bidi.send<unknown>('session.new', { capabilities: {} });
|
||||
|
||||
// Install the extension
|
||||
return await bidi.send<BidiWebExtensionInstallResponse>(
|
||||
'webExtension.install',
|
||||
{
|
||||
extensionData: {
|
||||
type: 'path',
|
||||
path: extensionDir,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export type BidiWebExtensionInstallResponse = {
|
||||
extension: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a child process of Chrome, install an extension. The process must be started with the following flags:
|
||||
*
|
||||
* - `--remote-debugging-pipe`
|
||||
* - `--user-data-dir=...`
|
||||
* - `--enable-unsafe-extension-debugging`
|
||||
*
|
||||
* Otherwise it the CDP doesn't have permission to install extensions.
|
||||
*/
|
||||
export async function installChromium(
|
||||
browserProcess: ChildProcess,
|
||||
extensionDir: string,
|
||||
): Promise<CdpExtensionsLoadUnpackedResponse> {
|
||||
using cdp = createCdpConnection(browserProcess);
|
||||
return await cdp.send<CdpExtensionsLoadUnpackedResponse>(
|
||||
'Extensions.loadUnpacked',
|
||||
{
|
||||
path: extensionDir,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export type CdpExtensionsLoadUnpackedResponse = {
|
||||
id: string;
|
||||
};
|
||||
@@ -0,0 +1,286 @@
|
||||
import {
|
||||
FALLBACK_TARGETS,
|
||||
KNOWN_BROWSER_PATHS,
|
||||
KnownTarget,
|
||||
type BrowserPlatform,
|
||||
} from './browser-paths';
|
||||
import { resolve, join } from 'node:path';
|
||||
import { homedir, tmpdir } from 'node:os';
|
||||
import { debug } from './debug';
|
||||
import { mkdtemp, open } from 'node:fs/promises';
|
||||
|
||||
const debugOptions = debug.scoped('options');
|
||||
|
||||
export type UnknownTarget = string & {};
|
||||
export type Target = KnownTarget | UnknownTarget;
|
||||
|
||||
export type RunOptions = {
|
||||
/** Paths to binaries to use for each target. */
|
||||
browserBinaries?: Record<string, string>;
|
||||
/** Customize the arguments passed to the chromium binary. Conflicting arguments with required ones to install extensions are ignored. */
|
||||
chromiumArgs?: string[];
|
||||
/** Control how data is persisted between launches. Either save data at a user level, project level, or don't persist data at all. Defaults to `project`. */
|
||||
dataPersistence?: 'user' | 'project' | 'none';
|
||||
/** Customize where your profile's data is stored when using `dataPersistence: 'project'`. Can be absolute or relative to the current working directory. */
|
||||
projectDataDir?: string;
|
||||
/** Customize the port Chrome's debugger is listening on. Defaults to a random open port. */
|
||||
chromiumRemoteDebuggingPort?: number;
|
||||
/** Directory where the extension will be installed from. Should contain a `manifest.json` file. Can be relative to the current working directory. Defaults to the current working directory. */
|
||||
extensionDir?: string;
|
||||
/** Customize the arguments passed to the firefox binary. Conflicting arguments with required ones to install extensions are ignored. */
|
||||
firefoxArgs?: string[];
|
||||
/** Customize the port Firefox's debugger is listening on. Defaults to a random open port. */
|
||||
firefoxRemoteDebuggingPort?: number;
|
||||
/** Specify the browser to open. Defaults to `"chrome"`, but you can pass any string. */
|
||||
target?: Target;
|
||||
};
|
||||
|
||||
export type ResolvedRunOptions = {
|
||||
/** Absolute path to the browser binary. */
|
||||
browserBinary: string;
|
||||
chromiumArgs: string[];
|
||||
chromiumRemoteDebuggingPort: number;
|
||||
/** Absolute path to the directory where browser data will be stored. */
|
||||
dataDir: string;
|
||||
dataPersistence: 'user' | 'project' | 'none';
|
||||
/** Absolute path to the extension directory. */
|
||||
extensionDir: string;
|
||||
firefoxArgs: string[];
|
||||
firefoxRemoteDebuggingPort: number;
|
||||
target: string;
|
||||
};
|
||||
|
||||
export async function resolveRunOptions(
|
||||
options: RunOptions | undefined,
|
||||
): Promise<ResolvedRunOptions> {
|
||||
debugOptions('User options:', options);
|
||||
|
||||
const target = options?.target || 'chrome';
|
||||
|
||||
const _browserBinary =
|
||||
options?.browserBinaries?.[target] ?? (await findBrowserBinary(target));
|
||||
if (!_browserBinary)
|
||||
throw Error(
|
||||
`Could not find "${target}" binary.\n\nIf it is installed in a custom location, you can specify the path with the browserPaths option.`,
|
||||
);
|
||||
|
||||
// Denormalize the path so it uses the correct path separator for the OS
|
||||
const browserBinary = resolve(_browserBinary);
|
||||
|
||||
const chromiumRemoteDebuggingPort = options?.chromiumRemoteDebuggingPort ?? 0;
|
||||
const firefoxRemoteDebuggingPort = options?.firefoxRemoteDebuggingPort ?? 0;
|
||||
const dataPersistence = options?.dataPersistence ?? 'none';
|
||||
const dataDir =
|
||||
dataPersistence === 'user'
|
||||
? join(homedir(), '.wxt-runner', target)
|
||||
: dataPersistence === 'project'
|
||||
? options?.projectDataDir
|
||||
? resolve(options.projectDataDir)
|
||||
: resolve('.wxt-runner', target)
|
||||
: dataPersistence === 'none'
|
||||
? await mkdtemp(join(tmpdir(), 'wxt-runner-'))
|
||||
: resolve(dataPersistence);
|
||||
|
||||
const resolved: ResolvedRunOptions = {
|
||||
browserBinary,
|
||||
chromiumArgs: resolveChromiumArgs(
|
||||
options?.chromiumArgs,
|
||||
chromiumRemoteDebuggingPort,
|
||||
dataDir,
|
||||
),
|
||||
dataDir,
|
||||
dataPersistence,
|
||||
chromiumRemoteDebuggingPort,
|
||||
extensionDir: resolve(options?.extensionDir ?? '.'),
|
||||
firefoxArgs: resolveFirefoxArgs(
|
||||
options?.firefoxArgs,
|
||||
firefoxRemoteDebuggingPort,
|
||||
dataDir,
|
||||
),
|
||||
firefoxRemoteDebuggingPort,
|
||||
target,
|
||||
};
|
||||
debugOptions('Resolved options:', resolved);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
async function findBrowserBinary(target: string): Promise<string | undefined> {
|
||||
const targets = new Set<KnownTarget>([target as KnownTarget]);
|
||||
FALLBACK_TARGETS[target as KnownTarget]?.forEach((fallback) =>
|
||||
targets.add(fallback),
|
||||
);
|
||||
const platform = getPlatform();
|
||||
|
||||
for (const target of targets) {
|
||||
const potentialPaths = KNOWN_BROWSER_PATHS[target]?.[platform] ?? [];
|
||||
for (const path of potentialPaths) {
|
||||
if (await exists(path)) return path;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getPlatform(): BrowserPlatform {
|
||||
switch (process.platform) {
|
||||
case 'win32':
|
||||
return 'windows';
|
||||
case 'darwin':
|
||||
return 'mac';
|
||||
default:
|
||||
return 'linux';
|
||||
}
|
||||
}
|
||||
|
||||
function resolveChromiumArgs(
|
||||
userArgs: string[] | undefined,
|
||||
chromiumRemoteDebuggingPort: ResolvedRunOptions['chromiumRemoteDebuggingPort'],
|
||||
dataDir: string,
|
||||
): string[] {
|
||||
return deduplicateArgs(
|
||||
[
|
||||
// Limit features to improve performance
|
||||
...CHROME_LAUNCHER_DEFAULT_FLAGS,
|
||||
// Enable debugging
|
||||
`--remote-debugging-port=${chromiumRemoteDebuggingPort}`,
|
||||
// Required for installing extensions
|
||||
`--remote-debugging-pipe`,
|
||||
`--user-data-dir=${dataDir}`,
|
||||
`--enable-unsafe-extension-debugging`,
|
||||
],
|
||||
userArgs,
|
||||
{
|
||||
'--remote-debugging-port':
|
||||
'\x1b[1m\x1b[33mCustom Chromium --remote-debugging-port argument ignored.\x1b[0m Use \x1b[36mchromiumRemoteDebuggingPort\x1b[0m option instead.',
|
||||
'--user-data-dir':
|
||||
'\x1b[1m\x1b[33mCustom Chromium --user-data-dir argument ignored.\x1b[0m Use \x1b[36mdataPersistence\x1b[0m option instead.',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function resolveFirefoxArgs(
|
||||
userArgs: string[] | undefined,
|
||||
firefoxRemoteDebuggingPort: ResolvedRunOptions['firefoxRemoteDebuggingPort'],
|
||||
dataDir: string,
|
||||
): string[] {
|
||||
return deduplicateArgs(
|
||||
[
|
||||
// Allows opening multiple instances of Firefox at the same time
|
||||
`--new-instance`,
|
||||
`--no-remote`,
|
||||
`--profile`,
|
||||
dataDir,
|
||||
// Required for installing extensions
|
||||
`--remote-debugging-port=${firefoxRemoteDebuggingPort}`,
|
||||
// Default URL to start with
|
||||
`about:debugging#/runtime/this-firefox`,
|
||||
],
|
||||
userArgs,
|
||||
{
|
||||
'--remote-debugging-port':
|
||||
'\x1b[1m\x1b[33mCustom Firefox --remote-debugging-port argument ignored.\x1b[0m Use \x1b[36mfirefoxDebuggerPort\x1b[0m option instead.',
|
||||
'--profile':
|
||||
'\x1b[1m\x1b[33mCustom Firefox --profile argument ignored.\x1b[0m Use \x1b[36mdataPersistence\x1b[0m option instead.',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function deduplicateArgs(
|
||||
requiredArgs: string[],
|
||||
userArgs: string[] | undefined,
|
||||
warnings: Record<string, string>,
|
||||
): string[] {
|
||||
const getKey = (arg: string) => {
|
||||
return arg.startsWith('--') ? arg.split('=')[0] : arg;
|
||||
};
|
||||
const alreadyAdded = new Set<string>(requiredArgs.map(getKey));
|
||||
|
||||
const args = [...requiredArgs];
|
||||
userArgs?.forEach((arg) => {
|
||||
const key = getKey(arg);
|
||||
if (alreadyAdded.has(key)) {
|
||||
if (warnings[key]) console.warn(`[@wxt-dev/runner] ${warnings[key]}`);
|
||||
} else {
|
||||
alreadyAdded.add(key);
|
||||
args.push(arg);
|
||||
}
|
||||
});
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
async function exists(path: string): Promise<boolean> {
|
||||
try {
|
||||
await open(path, 'r');
|
||||
return true;
|
||||
} catch (err) {
|
||||
// @ts-expect-error: Unknown error type
|
||||
if (err?.code === 'ENOENT') return false;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copied from https://github.com/GoogleChrome/chrome-launcher/blob/main/src/flags.ts
|
||||
* with some flags commented out. Run tests after updating to compare.
|
||||
*/
|
||||
const CHROME_LAUNCHER_DEFAULT_FLAGS = [
|
||||
'--disable-features=' +
|
||||
[
|
||||
// Disable built-in Google Translate service
|
||||
'Translate',
|
||||
// Disable the Chrome Optimization Guide background networking
|
||||
'OptimizationHints',
|
||||
// Disable the Chrome Media Router (cast target discovery) background networking
|
||||
'MediaRouter',
|
||||
/// Avoid the startup dialog for _Do you want the application “Chromium.app” to accept incoming network connections?_. This is a sub-component of the MediaRouter.
|
||||
'DialMediaRouteProvider',
|
||||
// Disable the feature of: Calculate window occlusion on Windows will be used in the future to throttle and potentially unload foreground tabs in occluded windows.
|
||||
'CalculateNativeWinOcclusion',
|
||||
// Disables the Discover feed on NTP
|
||||
'InterestFeedContentSuggestions',
|
||||
// Don't update the CT lists
|
||||
'CertificateTransparencyComponentUpdater',
|
||||
// Disables autofill server communication. This feature isn't disabled via other 'parent' flags.
|
||||
'AutofillServerCommunication',
|
||||
// Disables "Enhanced ad privacy in Chrome" dialog (though as of 2024-03-20 it shouldn't show up if the profile has no stored country).
|
||||
'PrivacySandboxSettings4',
|
||||
].join(','),
|
||||
|
||||
// Disable some extensions that aren't affected by --disable-extensions
|
||||
'--disable-component-extensions-with-background-pages',
|
||||
// Disable various background network services, including extension updating,
|
||||
// safe browsing service, upgrade detector, translate, UMA
|
||||
'--disable-background-networking',
|
||||
// Don't update the browser 'components' listed at chrome://components/
|
||||
'--disable-component-update',
|
||||
// Disables client-side phishing detection.
|
||||
'--disable-client-side-phishing-detection',
|
||||
// Disable syncing to a Google account
|
||||
'--disable-sync',
|
||||
// Disable reporting to UMA, but allows for collection
|
||||
'--metrics-recording-only',
|
||||
// Disable installation of default apps on first run
|
||||
'--disable-default-apps',
|
||||
// Disable the default browser check, do not prompt to set it as such
|
||||
'--no-default-browser-check',
|
||||
// Skip first run wizards
|
||||
'--no-first-run',
|
||||
// Disable task throttling of timer tasks from background pages.
|
||||
'--disable-background-timer-throttling',
|
||||
// Disable the default throttling of IPC between renderer & browser processes.
|
||||
'--disable-ipc-flooding-protection',
|
||||
// Avoid potential instability of using Gnome Keyring or KDE wallet. crbug.com/571003 crbug.com/991424
|
||||
'--password-store=basic',
|
||||
// Use mock keychain on Mac to prevent blocking permissions dialogs
|
||||
'--use-mock-keychain',
|
||||
// Disable background tracing (aka slow reports & deep reports) to avoid 'Tracing already started'
|
||||
'--force-fieldtrials=*BackgroundTracing/default/',
|
||||
|
||||
// Suppresses hang monitor dialogs in renderer processes. This flag may allow slow unload handlers on a page to prevent the tab from closing.
|
||||
'--disable-hang-monitor',
|
||||
// Reloading a page that came from a POST normally prompts the user.
|
||||
'--disable-prompt-on-repost',
|
||||
// Disables Domain Reliability Monitoring, which tracks whether the browser has difficulty contacting Google-owned sites and uploads reports to Google.
|
||||
'--disable-domain-reliability',
|
||||
// Disable the in-product Help (IPH) system.
|
||||
'--propagate-iph-for-testing',
|
||||
];
|
||||
@@ -0,0 +1,19 @@
|
||||
export function promiseWithResolvers<T>(): {
|
||||
promise: Promise<T>;
|
||||
resolve: (value: T) => void;
|
||||
reject: (error: unknown) => void;
|
||||
} {
|
||||
let resolve: (value: T) => void;
|
||||
let reject: (error: unknown) => void;
|
||||
|
||||
const promise = new Promise<T>((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
|
||||
return {
|
||||
promise,
|
||||
resolve: resolve!,
|
||||
reject: reject!,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import { debug } from './debug';
|
||||
import {
|
||||
resolveRunOptions,
|
||||
type ResolvedRunOptions,
|
||||
type RunOptions,
|
||||
} from './options';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { installChromium, installFirefox } from './install';
|
||||
import { promiseWithResolvers } from './promises';
|
||||
|
||||
const debugFirefox = debug.scoped('firefox');
|
||||
const debugChrome = debug.scoped('chrome');
|
||||
|
||||
export interface Runner {
|
||||
stop(): void;
|
||||
}
|
||||
|
||||
export async function run(options: RunOptions): Promise<Runner> {
|
||||
const resolvedOptions = await resolveRunOptions(options);
|
||||
|
||||
if (
|
||||
resolvedOptions.target.includes('firefox') ||
|
||||
resolvedOptions.target.includes('zen')
|
||||
) {
|
||||
return runFirefox(resolvedOptions);
|
||||
} else {
|
||||
return runChromium(resolvedOptions);
|
||||
}
|
||||
}
|
||||
|
||||
async function runFirefox(options: ResolvedRunOptions): Promise<Runner> {
|
||||
const urlRes = promiseWithResolvers<string>();
|
||||
const urlTimeout = setTimeout(() => {
|
||||
urlRes.reject(Error('Timed out after 10s waiting for the browser to open'));
|
||||
}, 10e3);
|
||||
|
||||
// Firefox notifies the user if an instance is already running, so we don't add any logs for it.
|
||||
|
||||
const browserProcess = spawn(
|
||||
`"${options.browserBinary}"`,
|
||||
options.firefoxArgs,
|
||||
{
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
shell: true,
|
||||
},
|
||||
);
|
||||
const debugFirefoxStderr = debugFirefox.scoped('stderr');
|
||||
browserProcess.stderr.on('data', (data: string) => {
|
||||
const message = data.toString().trim();
|
||||
debugFirefoxStderr(message);
|
||||
|
||||
if (message.startsWith('WebDriver BiDi listening on ws://')) {
|
||||
clearTimeout(urlTimeout);
|
||||
urlRes.resolve(message.slice(28));
|
||||
}
|
||||
});
|
||||
const debugFirefoxStdout = debugFirefox.scoped('stdout');
|
||||
browserProcess.stdout.on('data', (data: string) => {
|
||||
const message = data.toString().trim();
|
||||
debugFirefoxStdout(message);
|
||||
});
|
||||
|
||||
const baseUrl = await urlRes.promise;
|
||||
await installFirefox(baseUrl, options.extensionDir);
|
||||
|
||||
return {
|
||||
stop() {
|
||||
browserProcess.kill('SIGINT');
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function runChromium(options: ResolvedRunOptions): Promise<Runner> {
|
||||
const browserProcess = spawn(
|
||||
`"${options.browserBinary}"`,
|
||||
options.chromiumArgs,
|
||||
{
|
||||
stdio: ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'],
|
||||
shell: true,
|
||||
},
|
||||
);
|
||||
|
||||
const opened = promiseWithResolvers<void>();
|
||||
const openedTimeout = setTimeout(() => {
|
||||
opened.reject(Error('Timed out after 10s waiting for browser to open.'));
|
||||
}, 10e3);
|
||||
|
||||
const debugChromeStderr = debugChrome.scoped('stderr');
|
||||
browserProcess.stderr!.on('data', (data: string) => {
|
||||
const message = data.toString().trim();
|
||||
debugChromeStderr(message);
|
||||
|
||||
// This message signifies Chrome started up correctly.
|
||||
if (message.startsWith('DevTools listening on')) {
|
||||
clearTimeout(openedTimeout);
|
||||
opened.resolve();
|
||||
}
|
||||
});
|
||||
const debugChromeStdout = debugChrome.scoped('stdout');
|
||||
browserProcess.stdout!.on('data', (data: string) => {
|
||||
const message = data.toString().trim();
|
||||
debugChromeStdout(message);
|
||||
|
||||
// This message signifies Chrome was already open, and thus we couldn't open the required new instance.
|
||||
if (message === 'Opening in existing browser session.') {
|
||||
clearTimeout(openedTimeout);
|
||||
opened.reject(
|
||||
Error(
|
||||
'An instance of the browser is already running. Close it and try again.',
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Wait for the browser to open before proceeding.
|
||||
await opened.promise;
|
||||
|
||||
await installChromium(browserProcess, options.extensionDir);
|
||||
|
||||
return {
|
||||
stop() {
|
||||
browserProcess.kill('SIGINT');
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
export function openWebSocket(url: string): Promise<WebSocket> {
|
||||
if (typeof WebSocket === 'undefined') {
|
||||
throw new Error(
|
||||
'To open Firefox, your JS runtime must support the standard WebSocket API (NodeJS >=22.4.0, Bun, etc).',
|
||||
);
|
||||
}
|
||||
|
||||
return new Promise<WebSocket>((resolve, reject) => {
|
||||
const webSocket = new WebSocket(url);
|
||||
|
||||
const cleanup = () => {
|
||||
webSocket.removeEventListener('open', onOpen);
|
||||
webSocket.removeEventListener('error', onError);
|
||||
webSocket.removeEventListener('close', onClose);
|
||||
};
|
||||
const onOpen = async () => {
|
||||
cleanup();
|
||||
resolve(webSocket);
|
||||
};
|
||||
const onClose = (event: CloseEvent) => {
|
||||
cleanup();
|
||||
reject(
|
||||
new Error(
|
||||
`Connection closed: code=${event.code}, reason=${event.reason}`,
|
||||
),
|
||||
);
|
||||
};
|
||||
const onError = (error: any) => {
|
||||
cleanup();
|
||||
reject(new Error('Error connecting to WebSocket', { cause: error }));
|
||||
};
|
||||
|
||||
webSocket.addEventListener('open', onOpen);
|
||||
webSocket.addEventListener('error', onError);
|
||||
webSocket.addEventListener('close', onClose);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { defineProject } from 'vitest/config';
|
||||
|
||||
export default defineProject({
|
||||
test: {
|
||||
mockReset: true,
|
||||
restoreMocks: true,
|
||||
},
|
||||
});
|
||||
@@ -30,18 +30,18 @@
|
||||
"test": "buildc --deps-only -- vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"async-mutex": "catalog:",
|
||||
"dequal": "catalog:"
|
||||
"@wxt-dev/browser": "workspace:^",
|
||||
"async-mutex": "^0.5.0",
|
||||
"dequal": "^2.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "catalog:",
|
||||
"@types/chrome": "catalog:",
|
||||
"@webext-core/fake-browser": "catalog:",
|
||||
"oxlint": "catalog:",
|
||||
"publint": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"unbuild": "catalog:",
|
||||
"vitest": "catalog:"
|
||||
"@aklinker1/check": "2.0.0",
|
||||
"@webext-core/fake-browser": "^1.3.2",
|
||||
"oxlint": "^0.16.8",
|
||||
"publint": "^0.3.12",
|
||||
"typescript": "^5.8.3",
|
||||
"unbuild": "^3.5.0",
|
||||
"vitest": "^3.1.2"
|
||||
},
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { fakeBrowser } from '@webext-core/fake-browser';
|
||||
import { describe, it, expect, beforeEach, vi, expectTypeOf } from 'vitest';
|
||||
import { MigrationError, type WxtStorageItem, storage } from '../index';
|
||||
import { browser } from '@wxt-dev/browser';
|
||||
|
||||
/**
|
||||
* This works because fakeBrowser is synchronous, and is will finish any number of chained
|
||||
@@ -222,7 +223,7 @@ describe('Storage Utils', () => {
|
||||
describe('setMeta', () => {
|
||||
it('should set metadata at key+$', async () => {
|
||||
const existing = { v: 1 };
|
||||
await chrome.storage[storageArea].set({ count$: existing });
|
||||
await browser.storage[storageArea].set({ count$: existing });
|
||||
const newValues = {
|
||||
date: Date.now(),
|
||||
};
|
||||
@@ -238,7 +239,7 @@ describe('Storage Utils', () => {
|
||||
'should remove any properties set to %s',
|
||||
async (version) => {
|
||||
const existing = { v: 1 };
|
||||
await chrome.storage[storageArea].set({ count$: existing });
|
||||
await browser.storage[storageArea].set({ count$: existing });
|
||||
const expected = {};
|
||||
|
||||
await storage.setMeta(`${storageArea}:count`, { v: version });
|
||||
@@ -979,7 +980,7 @@ describe('Storage Utils', () => {
|
||||
|
||||
const actual = await item.getMeta();
|
||||
|
||||
expect(actual).toBe(expected);
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should return an empty object if missing', async () => {
|
||||
@@ -1265,7 +1266,7 @@ describe('Storage Utils', () => {
|
||||
|
||||
await item.removeValue();
|
||||
// Make sure it's actually blank before running the test
|
||||
expect(await chrome.storage.local.get()).toEqual({});
|
||||
expect(await browser.storage.local.get()).toEqual({});
|
||||
init.mockClear();
|
||||
|
||||
const [value1, value2] = await Promise.all([
|
||||
@@ -1281,6 +1282,19 @@ describe('Storage Utils', () => {
|
||||
it('should define a nullable value when options are not passed', () => {
|
||||
const item = storage.defineItem<number>(`local:test`);
|
||||
expectTypeOf(item).toEqualTypeOf<WxtStorageItem<number | null, {}>>();
|
||||
|
||||
const item2 = storage.defineItem<number>(`local:test`, {});
|
||||
expectTypeOf(item2).toEqualTypeOf<WxtStorageItem<number | null, {}>>();
|
||||
|
||||
const item3 = storage.defineItem<number>(`local:test`, {
|
||||
fallback: undefined,
|
||||
});
|
||||
expectTypeOf(item3).toEqualTypeOf<WxtStorageItem<number | null, {}>>();
|
||||
|
||||
const item4 = storage.defineItem<number>(`local:test`, {
|
||||
defaultValue: undefined,
|
||||
});
|
||||
expectTypeOf(item4).toEqualTypeOf<WxtStorageItem<number | null, {}>>();
|
||||
});
|
||||
|
||||
it('should define a non-null value when options are passed with a nullish default value', () => {
|
||||
@@ -1288,6 +1302,11 @@ describe('Storage Utils', () => {
|
||||
defaultValue: 123,
|
||||
});
|
||||
expectTypeOf(item).toEqualTypeOf<WxtStorageItem<number, {}>>();
|
||||
|
||||
const item2 = storage.defineItem(`local:test`, {
|
||||
fallback: 123,
|
||||
});
|
||||
expectTypeOf(item2).toEqualTypeOf<WxtStorageItem<number, {}>>();
|
||||
});
|
||||
|
||||
it('should define a nullable value when options are passed with null default value', () => {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/// <reference types="chrome" />
|
||||
/**
|
||||
* Simplified storage APIs with support for versioned fields, snapshots, metadata, and item definitions.
|
||||
*
|
||||
@@ -7,13 +6,7 @@
|
||||
*/
|
||||
import { dequal } from 'dequal/lite';
|
||||
import { Mutex } from 'async-mutex';
|
||||
|
||||
const browser: typeof chrome =
|
||||
// @ts-expect-error
|
||||
globalThis.browser?.runtime?.id == null
|
||||
? globalThis.chrome
|
||||
: // @ts-expect-error
|
||||
globalThis.browser;
|
||||
import { browser, type Browser } from '@wxt-dev/browser';
|
||||
|
||||
export const storage = createStorage();
|
||||
|
||||
@@ -750,8 +743,16 @@ export interface WxtStorage {
|
||||
): WxtStorageItem<TValue | null, TMetadata>;
|
||||
defineItem<TValue, TMetadata extends Record<string, unknown> = {}>(
|
||||
key: StorageItemKey,
|
||||
options: WxtStorageItemOptions<TValue>,
|
||||
options: WxtStorageItemOptions<TValue> & { fallback: TValue },
|
||||
): WxtStorageItem<TValue, TMetadata>;
|
||||
defineItem<TValue, TMetadata extends Record<string, unknown> = {}>(
|
||||
key: StorageItemKey,
|
||||
options: WxtStorageItemOptions<TValue> & { defaultValue: TValue },
|
||||
): WxtStorageItem<TValue, TMetadata>;
|
||||
defineItem<TValue, TMetadata extends Record<string, unknown> = {}>(
|
||||
key: StorageItemKey,
|
||||
options: WxtStorageItemOptions<TValue>,
|
||||
): WxtStorageItem<TValue | null, TMetadata>;
|
||||
}
|
||||
|
||||
interface WxtStorageDriver {
|
||||
@@ -889,7 +890,7 @@ export interface WxtStorageItemOptions<T> {
|
||||
}
|
||||
|
||||
export type StorageAreaChanges = {
|
||||
[key: string]: chrome.storage.StorageChange;
|
||||
[key: string]: Browser.storage.StorageChange;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"verbatimModuleSyntax": true,
|
||||
"types": ["chrome"]
|
||||
"verbatimModuleSyntax": true
|
||||
},
|
||||
"exclude": ["node_modules/**", "dist/**"]
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ export default defineConfig({
|
||||
Now in your entrypoint, import UnoCSS:
|
||||
|
||||
```ts
|
||||
import 'uno.css';
|
||||
import 'virtual:uno.css';
|
||||
```
|
||||
|
||||
> [!IMPORTANT]
|
||||
|
||||
@@ -40,16 +40,16 @@
|
||||
"wxt": ">=0.19.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "catalog:",
|
||||
"oxlint": "catalog:",
|
||||
"publint": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"unbuild": "catalog:",
|
||||
"unocss": "catalog:",
|
||||
"@aklinker1/check": "2.0.0",
|
||||
"oxlint": "^0.16.8",
|
||||
"publint": "^0.3.12",
|
||||
"typescript": "^5.8.3",
|
||||
"unbuild": "^3.5.0",
|
||||
"unocss": "^0.64.0 || ^0.65.0 || ^65.0.0 || ^66.0.0",
|
||||
"wxt": "workspace:*"
|
||||
},
|
||||
"dependencies": {
|
||||
"defu": "catalog:",
|
||||
"fast-glob": "catalog:"
|
||||
"defu": "^6.1.4",
|
||||
"fast-glob": "^3.3.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,12 +45,12 @@
|
||||
"wxt": ">=0.20.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "catalog:",
|
||||
"@types/webextension-polyfill": "catalog:",
|
||||
"publint": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"unbuild": "catalog:",
|
||||
"webextension-polyfill": "catalog:",
|
||||
"@aklinker1/check": "2.0.0",
|
||||
"@types/webextension-polyfill": "^0.12.3",
|
||||
"publint": "^0.3.12",
|
||||
"typescript": "^5.8.3",
|
||||
"unbuild": "^3.5.0",
|
||||
"webextension-polyfill": "^0.12.0",
|
||||
"wxt": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,19 +18,19 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@wxt-dev/i18n": "workspace:*",
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:"
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "catalog:",
|
||||
"@types/react-dom": "catalog:",
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-dom": "^19.1.3",
|
||||
"@wxt-dev/auto-icons": "workspace:*",
|
||||
"@wxt-dev/unocss": "workspace:*",
|
||||
"sass": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"unocss": "catalog:",
|
||||
"vitest": "catalog:",
|
||||
"vitest-plugin-random-seed": "catalog:",
|
||||
"sass": "^1.87.0",
|
||||
"typescript": "^5.8.3",
|
||||
"unocss": "^0.64.0 || ^0.65.0 || ^65.0.0 || ^66.0.0",
|
||||
"vitest": "^3.1.2",
|
||||
"vitest-plugin-random-seed": "^1.1.1",
|
||||
"wxt": "workspace:*"
|
||||
},
|
||||
"buildc": {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { presetUno } from 'unocss';
|
||||
|
||||
export default defineConfig({
|
||||
srcDir: 'src',
|
||||
targetBrowsers: ['chrome', 'firefox', 'safari'],
|
||||
manifest: {
|
||||
permissions: ['storage'],
|
||||
default_locale: 'en',
|
||||
@@ -19,7 +20,7 @@ export default defineConfig({
|
||||
analysis: {
|
||||
open: true,
|
||||
},
|
||||
runner: {
|
||||
webExt: {
|
||||
startUrls: ['https://duckduckgo.com'],
|
||||
},
|
||||
example: {
|
||||
|
||||
+111
-14
@@ -1,8 +1,118 @@
|
||||
# Changelog
|
||||
|
||||
## v0.20.6
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.5...wxt-v0.20.6)
|
||||
|
||||
## v0.20.5
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.4...wxt-v0.20.5)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Don't use crypto.randUUID for shadow root UIs ([3577c0b](https://github.com/wxt-dev/wxt/commit/3577c0b))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron ([@aklinker1](https://github.com/aklinker1))
|
||||
|
||||
## v0.20.4
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.3...wxt-v0.20.4)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Ignore popup/index.ts instead of erroring ([#1520](https://github.com/wxt-dev/wxt/pull/1520))
|
||||
- Ignore elements with a `vite-ignore` or `wxt-ignore` attribute ([#1603](https://github.com/wxt-dev/wxt/pull/1603))
|
||||
- Add `{{packageVersion}}` as template variable ([#1604](https://github.com/wxt-dev/wxt/pull/1604))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Adding missing `""` to `PublicPath` and `browser.runtime.getUrl` ([#1597](https://github.com/wxt-dev/wxt/pull/1597))
|
||||
- Fix CORS error in Firefox ([#1607](https://github.com/wxt-dev/wxt/pull/1607))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- **Content Script UI:** Add additional details about when `onRemove` is called ([656a9b3](https://github.com/wxt-dev/wxt/commit/656a9b3))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Yunsup Sim ([@SimYunSup](https://github.com/SimYunSup))
|
||||
- ТΞNSΛI ([@Tensai75](https://github.com/Tensai75))
|
||||
- Nishu ([@nishu-murmu](https://github.com/nishu-murmu))
|
||||
- Aaron ([@aklinker1](https://github.com/aklinker1))
|
||||
|
||||
## v0.20.3
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.2...wxt-v0.20.3)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Automatically place document-level CSS outside shadow root ([#1594](https://github.com/wxt-dev/wxt/pull/1594))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Fix double hashing of inline script keys ([b0f4ac8](https://github.com/wxt-dev/wxt/commit/b0f4ac8))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron ([@aklinker1](https://github.com/aklinker1))
|
||||
|
||||
## v0.20.2
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.1...wxt-v0.20.2)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Fix hashing issue with inline scripts ([#1591](https://github.com/wxt-dev/wxt/pull/1591))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Fix typo in changelog ([acb6cd1](https://github.com/wxt-dev/wxt/commit/acb6cd1))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Yunsup Sim ([@SimYunSup](https://github.com/SimYunSup))
|
||||
- Aaron ([@aklinker1](https://github.com/aklinker1))
|
||||
|
||||
## v0.20.1
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.20.0...wxt-v0.20.1)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Enable wxt usage inside of devcontainers ([#1406](https://github.com/wxt-dev/wxt/pull/1406))
|
||||
- Type-safe `import.meta.env.BROWSER` with new `targetBrowsers` config ([#1574](https://github.com/wxt-dev/wxt/pull/1574))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Don't remove top-level destructured variable definitions when importing entrypoints ([#1561](https://github.com/wxt-dev/wxt/pull/1561))
|
||||
- Add JSDoc type annotation to auto-imports for ESlint ([#1558](https://github.com/wxt-dev/wxt/pull/1558))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Fix knowledge file generation ([#1550](https://github.com/wxt-dev/wxt/pull/1550))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- **deps:** Update all dependencies ([#1568](https://github.com/wxt-dev/wxt/pull/1568))
|
||||
- Update comment ([61b42ef](https://github.com/wxt-dev/wxt/commit/61b42ef))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- 7sDream ([@7sDream](https://github.com/7sDream))
|
||||
- Aaron ([@aklinker1](https://github.com/aklinker1))
|
||||
- Nostro ([@nostrorom](https://github.com/nostrorom))
|
||||
- Khalil Yao ([@yyz945947732](https://github.com/yyz945947732))
|
||||
- Alec WM ([@alecdwm](https://github.com/alecdwm))
|
||||
|
||||
## v0.20.0
|
||||
|
||||
[⚠️ breaking changes](https://wxt.dev/guide/upgrade-guide/wxt) • [compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.29...wxt-v0.20.0)
|
||||
Excited to release the next major version of WXT! Follow the [Upgrade guide](https://wxt.dev/guide/resources/upgrading.html) to update!
|
||||
|
||||
---
|
||||
|
||||
[⚠️ breaking changes](https://wxt.dev/guide/resources/upgrading.html) • [compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.29...wxt-v0.20.0)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
@@ -33,19 +143,6 @@
|
||||
- Fix type errors ([aad17c8](https://github.com/wxt-dev/wxt/commit/aad17c8))
|
||||
- Remove duplicate test ([e54df0a](https://github.com/wxt-dev/wxt/commit/e54df0a))
|
||||
|
||||
#### ⚠️ Breaking Changes
|
||||
|
||||
- ⚠️ Remove `webextension-polyfill` ([#1084](https://github.com/wxt-dev/wxt/pull/1084))
|
||||
- ⚠️ Individual exports and introduce the `#imports` module ([#1258](https://github.com/wxt-dev/wxt/pull/1258))
|
||||
- ⚠️ Reset inherited styles inside shadow root ([#1269](https://github.com/wxt-dev/wxt/pull/1269))
|
||||
- ⚠️ Auto-import types ([#1315](https://github.com/wxt-dev/wxt/pull/1315))
|
||||
- ⚠️ Add suffix to non-production output directories ([#1086](https://github.com/wxt-dev/wxt/pull/1086))
|
||||
- ⚠️ Remove deprecated `jiti` entrypoint loader ([#1087](https://github.com/wxt-dev/wxt/pull/1087))
|
||||
- ⚠️ Rename `runner` to `webExt` ([#1180](https://github.com/wxt-dev/wxt/pull/1180))
|
||||
- ⚠️ Remove `transformManfiest` option ([#1181](https://github.com/wxt-dev/wxt/pull/1181))
|
||||
- ⚠️ Make `publicDir` and `modulesDir` relative to project root ([#1216](https://github.com/wxt-dev/wxt/pull/1216))
|
||||
- ⚠️ Move `wxt/storage` to `wxt/utils/storage` ([#1271](https://github.com/wxt-dev/wxt/pull/1271))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron ([@aklinker1](https://github.com/aklinker1))
|
||||
|
||||
@@ -110,6 +110,7 @@ export default {
|
||||
name: "wxt/auto-imports",
|
||||
languageOptions: {
|
||||
globals,
|
||||
/** @type {import('eslint').Linter.SourceType} */
|
||||
sourceType: "module",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -48,6 +48,7 @@ describe('TypeScript Project', () => {
|
||||
|
||||
declare module "wxt/browser" {
|
||||
export type PublicPath =
|
||||
| ""
|
||||
| "/"
|
||||
| "/options.html"
|
||||
| "/popup.html"
|
||||
@@ -396,6 +397,19 @@ describe('TypeScript Project', () => {
|
||||
expect(output).toContain('./example.ts');
|
||||
});
|
||||
|
||||
it('should set correct import.meta.env.BROWSER type based on targetBrowsers', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/unlisted.html', '<html></html>');
|
||||
project.setConfigFileConfig({
|
||||
targetBrowsers: ['firefox', 'chrome'],
|
||||
});
|
||||
|
||||
await project.prepare();
|
||||
|
||||
const output = await project.serializeFile('.wxt/types/globals.d.ts');
|
||||
expect(output).toContain('readonly BROWSER: "firefox" | "chrome";');
|
||||
});
|
||||
|
||||
// TODO: Once a module has been published, use it here for testing - local files are never added to the .wxt/wxt.d.ts file
|
||||
it.todo(
|
||||
'should add modules from NPM to the TS project if they have a configKey',
|
||||
|
||||
+65
-61
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "wxt",
|
||||
"type": "module",
|
||||
"version": "0.20.0",
|
||||
"version": "0.20.6",
|
||||
"description": "⚡ Next-gen Web Extension Framework",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
@@ -16,69 +16,69 @@
|
||||
"prepack": "pnpm build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@1natsu/wait-element": "catalog:",
|
||||
"@aklinker1/rollup-plugin-visualizer": "catalog:",
|
||||
"@webext-core/fake-browser": "catalog:",
|
||||
"@webext-core/isolated-element": "catalog:",
|
||||
"@webext-core/match-patterns": "catalog:",
|
||||
"@wxt-dev/browser": "workspace:*",
|
||||
"@1natsu/wait-element": "^4.1.2",
|
||||
"@aklinker1/rollup-plugin-visualizer": "5.12.0",
|
||||
"@webext-core/fake-browser": "^1.3.2",
|
||||
"@webext-core/isolated-element": "^1.1.2",
|
||||
"@webext-core/match-patterns": "^1.0.3",
|
||||
"@wxt-dev/browser": "workspace:^",
|
||||
"@wxt-dev/storage": "workspace:^1.0.0",
|
||||
"async-mutex": "catalog:",
|
||||
"c12": "catalog:",
|
||||
"cac": "catalog:",
|
||||
"chokidar": "catalog:",
|
||||
"ci-info": "catalog:",
|
||||
"consola": "catalog:",
|
||||
"defu": "catalog:",
|
||||
"dotenv": "catalog:",
|
||||
"dotenv-expand": "catalog:",
|
||||
"esbuild": "catalog:",
|
||||
"fast-glob": "catalog:",
|
||||
"filesize": "catalog:",
|
||||
"fs-extra": "catalog:",
|
||||
"get-port-please": "catalog:",
|
||||
"giget": "catalog:",
|
||||
"hookable": "catalog:",
|
||||
"import-meta-resolve": "catalog:",
|
||||
"is-wsl": "catalog:",
|
||||
"json5": "catalog:",
|
||||
"jszip": "catalog:",
|
||||
"linkedom": "catalog:",
|
||||
"magicast": "catalog:",
|
||||
"minimatch": "catalog:",
|
||||
"nano-spawn": "catalog:",
|
||||
"normalize-path": "catalog:",
|
||||
"nypm": "catalog:",
|
||||
"ohash": "catalog:",
|
||||
"open": "catalog:",
|
||||
"ora": "catalog:",
|
||||
"perfect-debounce": "catalog:",
|
||||
"picocolors": "catalog:",
|
||||
"prompts": "catalog:",
|
||||
"publish-browser-extension": "catalog:",
|
||||
"scule": "catalog:",
|
||||
"unimport": "catalog:",
|
||||
"vite": "catalog:",
|
||||
"vite-node": "catalog:",
|
||||
"web-ext-run": "catalog:"
|
||||
"async-mutex": "^0.5.0",
|
||||
"c12": "^3.0.3",
|
||||
"cac": "^6.7.14",
|
||||
"chokidar": "^4.0.3",
|
||||
"ci-info": "^4.2.0",
|
||||
"consola": "^3.4.2",
|
||||
"defu": "^6.1.4",
|
||||
"dotenv": "^16.5.0",
|
||||
"dotenv-expand": "^12.0.2",
|
||||
"esbuild": "^0.25.0",
|
||||
"fast-glob": "^3.3.3",
|
||||
"filesize": "^10.1.6",
|
||||
"fs-extra": "^11.3.0",
|
||||
"get-port-please": "^3.1.2",
|
||||
"giget": "^1.2.3 || ^2.0.0",
|
||||
"hookable": "^5.5.3",
|
||||
"import-meta-resolve": "^4.1.0",
|
||||
"is-wsl": "^3.1.0",
|
||||
"json5": "^2.2.3",
|
||||
"jszip": "^3.10.1",
|
||||
"linkedom": "^0.18.10",
|
||||
"magicast": "^0.3.5",
|
||||
"minimatch": "^10.0.1",
|
||||
"nano-spawn": "^0.2.0",
|
||||
"normalize-path": "^3.0.0",
|
||||
"nypm": "^0.6.0",
|
||||
"ohash": "^2.0.11",
|
||||
"open": "^10.1.2",
|
||||
"ora": "^8.2.0",
|
||||
"perfect-debounce": "^1.0.0",
|
||||
"picocolors": "^1.1.1",
|
||||
"prompts": "^2.4.2",
|
||||
"publish-browser-extension": "^2.3.0 || ^3.0.0",
|
||||
"scule": "^1.3.0",
|
||||
"unimport": "^3.13.1 || ^4.0.0 || ^5.0.0",
|
||||
"vite": "^5.4.19 || ^6.3.4",
|
||||
"vite-node": "^2.1.4 || ^3.1.2",
|
||||
"web-ext-run": "^0.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "catalog:",
|
||||
"@faker-js/faker": "catalog:",
|
||||
"@types/fs-extra": "catalog:",
|
||||
"@types/lodash.merge": "catalog:",
|
||||
"@types/node": "catalog:",
|
||||
"@types/normalize-path": "catalog:",
|
||||
"@types/prompts": "catalog:",
|
||||
"extract-zip": "catalog:",
|
||||
"happy-dom": "catalog:",
|
||||
"lodash.merge": "catalog:",
|
||||
"oxlint": "catalog:",
|
||||
"publint": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"unbuild": "catalog:",
|
||||
"vitest": "catalog:",
|
||||
"vitest-plugin-random-seed": "catalog:"
|
||||
"@aklinker1/check": "2.0.0",
|
||||
"@faker-js/faker": "^9.7.0",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/lodash.merge": "^4.6.9",
|
||||
"@types/node": "^20.17.6",
|
||||
"@types/normalize-path": "^3.0.2",
|
||||
"@types/prompts": "^2.4.9",
|
||||
"extract-zip": "^2.0.1",
|
||||
"happy-dom": "^17.4.6",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"oxlint": "^0.16.8",
|
||||
"publint": "^0.3.12",
|
||||
"typescript": "^5.8.3",
|
||||
"unbuild": "^3.5.0",
|
||||
"vitest": "^3.1.2",
|
||||
"vitest-plugin-random-seed": "^1.1.1"
|
||||
},
|
||||
"peerDependenciesMeta": {},
|
||||
"repository": {
|
||||
@@ -167,6 +167,10 @@
|
||||
"types": "./dist/utils/match-patterns.d.ts",
|
||||
"default": "./dist/utils/match-patterns.mjs"
|
||||
},
|
||||
"./utils/split-shadow-root-css": {
|
||||
"types": "./dist/utils/split-shadow-root-css.d.ts",
|
||||
"default": "./dist/utils/split-shadow-root-css.mjs"
|
||||
},
|
||||
"./utils/storage": {
|
||||
"types": "./dist/utils/storage.d.ts",
|
||||
"default": "./dist/utils/storage.mjs"
|
||||
|
||||
Vendored
+1
-3
@@ -1,6 +1,4 @@
|
||||
declare const __DEV_SERVER_PROTOCOL__: string;
|
||||
declare const __DEV_SERVER_HOSTNAME__: string;
|
||||
declare const __DEV_SERVER_PORT__: string;
|
||||
declare const __DEV_SERVER_ORIGIN__: string;
|
||||
|
||||
// Globals defined by the vite-plugins/devServerGlobals.ts and utils/globals.ts
|
||||
interface ImportMetaEnv {
|
||||
|
||||
@@ -141,6 +141,7 @@ export default {
|
||||
name: "wxt/auto-imports",
|
||||
languageOptions: {
|
||||
globals,
|
||||
/** @type {import('eslint').Linter.SourceType} */
|
||||
sourceType: "module",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -16,7 +16,8 @@ cli
|
||||
.option('-c, --config <file>', 'use specified config file')
|
||||
.option('-m, --mode <mode>', 'set env mode')
|
||||
.option('-b, --browser <browser>', 'specify a browser')
|
||||
.option('-p, --port <port>', 'specify a port for the dev server')
|
||||
.option('--host <host>', 'specify a host for the dev server to bind to')
|
||||
.option('-p, --port <port>', 'specify a port for the dev server to bind to')
|
||||
.option(
|
||||
'-e, --filter-entrypoint <entrypoint>',
|
||||
'only build specific entrypoints',
|
||||
@@ -28,6 +29,12 @@ cli
|
||||
.option('--mv2', 'target manifest v2')
|
||||
.action(
|
||||
wrapAction(async (root, flags) => {
|
||||
const serverOptions: NonNullable<
|
||||
NonNullable<Parameters<typeof createServer>[0]>['dev']
|
||||
>['server'] = {};
|
||||
if (flags.host) serverOptions.host = flags.host;
|
||||
if (flags.port) serverOptions.port = parseInt(flags.port);
|
||||
|
||||
const server = await createServer({
|
||||
root,
|
||||
mode: flags.mode,
|
||||
@@ -37,13 +44,9 @@ cli
|
||||
debug: flags.debug,
|
||||
filterEntrypoints: getArrayFromFlags(flags, 'filterEntrypoint'),
|
||||
dev:
|
||||
flags.port == null
|
||||
Object.keys(serverOptions).length === 0
|
||||
? undefined
|
||||
: {
|
||||
server: {
|
||||
port: parseInt(flags.port),
|
||||
},
|
||||
},
|
||||
: { server: serverOptions },
|
||||
});
|
||||
await server.start();
|
||||
return { isOngoing: true };
|
||||
|
||||
@@ -327,9 +327,9 @@ export async function createViteBuilder(
|
||||
async createServer(info) {
|
||||
const serverConfig: vite.InlineConfig = {
|
||||
server: {
|
||||
host: info.host,
|
||||
port: info.port,
|
||||
strictPort: true,
|
||||
host: info.hostname,
|
||||
origin: info.origin,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -20,7 +20,7 @@ describe('Dev HTML Prerender Plugin', () => {
|
||||
['~local/style.css', 'http://localhost:5173/style.css'],
|
||||
['~absolute/style.css', 'http://localhost:5173/assets/style.css'],
|
||||
['~file', 'http://localhost:5173/example.css'],
|
||||
// Absolute paths are loaded with the `/@fs/` base path
|
||||
// Paths outside the project root are loaded with the `/@fs/` base path
|
||||
[
|
||||
'~outside/test.css',
|
||||
`http://localhost:5173/@fs${
|
||||
@@ -48,7 +48,7 @@ describe('Dev HTML Prerender Plugin', () => {
|
||||
},
|
||||
});
|
||||
const server = fakeDevServer({
|
||||
hostname: 'localhost',
|
||||
host: 'localhost',
|
||||
port: 5173,
|
||||
origin: 'http://localhost:5173',
|
||||
});
|
||||
|
||||
@@ -4,10 +4,10 @@ import { getEntrypointName } from '../../../utils/entrypoints';
|
||||
import { parseHTML } from 'linkedom';
|
||||
import { dirname, relative, resolve } from 'node:path';
|
||||
import { normalizePath } from '../../../utils/paths';
|
||||
import { murmurHash } from 'ohash';
|
||||
import { hash } from 'ohash';
|
||||
|
||||
// Stored outside the plugin to effect all instances of the devHtmlPrerender plugin.
|
||||
const inlineScriptContents: Record<number, string> = {};
|
||||
const inlineScriptContents: Record<string, string> = {};
|
||||
|
||||
/**
|
||||
* Pre-renders the HTML entrypoints when building the extension to connect to the dev server.
|
||||
@@ -85,13 +85,13 @@ export function devHtmlPrerender(
|
||||
inlineScripts.forEach((script) => {
|
||||
// Save the text content for later
|
||||
const textContent = script.textContent ?? '';
|
||||
const hash = murmurHash(textContent);
|
||||
inlineScriptContents[hash] = textContent;
|
||||
const key = hash(textContent);
|
||||
inlineScriptContents[key] = textContent;
|
||||
|
||||
// Replace unsafe inline script
|
||||
const virtualScript = document.createElement('script');
|
||||
virtualScript.type = 'module';
|
||||
virtualScript.src = `${server.origin}/@id/${virtualInlineScript}?${hash}`;
|
||||
virtualScript.src = `${server.origin}/@id/${virtualInlineScript}?${key}`;
|
||||
script.replaceWith(virtualScript);
|
||||
});
|
||||
|
||||
@@ -111,7 +111,7 @@ export function devHtmlPrerender(
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'wxt:virtualize-react-refresh',
|
||||
name: 'wxt:virtualize-inline-scripts',
|
||||
apply: 'serve',
|
||||
resolveId(id) {
|
||||
// Resolve inline scripts
|
||||
@@ -127,9 +127,9 @@ export function devHtmlPrerender(
|
||||
load(id) {
|
||||
// Resolve virtualized inline scripts
|
||||
if (id.startsWith(resolvedVirtualInlineScript)) {
|
||||
// id="virtual:wxt-inline-script?<hash>"
|
||||
const hash = Number(id.substring(id.indexOf('?') + 1));
|
||||
return inlineScriptContents[hash];
|
||||
// id="virtual:wxt-inline-script?<key>"
|
||||
const key = id.substring(id.indexOf('?') + 1);
|
||||
return inlineScriptContents[key];
|
||||
}
|
||||
|
||||
// Ignore chunks during HTML file pre-rendering
|
||||
@@ -150,6 +150,13 @@ export function pointToDevServer(
|
||||
attr: string,
|
||||
) {
|
||||
document.querySelectorAll(querySelector).forEach((element) => {
|
||||
if (
|
||||
element.hasAttribute('vite-ignore') ||
|
||||
element.hasAttribute('wxt-ignore')
|
||||
) {
|
||||
element.removeAttribute('wxt-ignore');
|
||||
return;
|
||||
}
|
||||
const src = element.getAttribute(attr);
|
||||
if (!src || isUrl(src)) return;
|
||||
|
||||
|
||||
@@ -15,9 +15,9 @@ export function devServerGlobals(
|
||||
|
||||
return {
|
||||
define: {
|
||||
__DEV_SERVER_PROTOCOL__: JSON.stringify('ws:'),
|
||||
__DEV_SERVER_HOSTNAME__: JSON.stringify(server.hostname),
|
||||
__DEV_SERVER_PORT__: JSON.stringify(server.port),
|
||||
__DEV_SERVER_ORIGIN__: JSON.stringify(
|
||||
server.origin.replace(/^http(s):/, 'ws$1:'),
|
||||
),
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
@@ -48,7 +48,7 @@ try {
|
||||
handler(html, _ctx) {
|
||||
const src =
|
||||
config.command === 'serve'
|
||||
? `http://${config.dev.server?.hostname}:${config.dev.server?.port}/@id/${virtualHtmlModuleId}`
|
||||
? `${config.dev.server?.origin}/@id/${virtualHtmlModuleId}`
|
||||
: virtualHtmlModuleId;
|
||||
|
||||
const { document } = parseHTML(html);
|
||||
|
||||
@@ -52,12 +52,8 @@ export async function createServer(
|
||||
|
||||
async function createServerInternal(): Promise<WxtDevServer> {
|
||||
const getServerInfo = (): ServerInfo => {
|
||||
const { port, hostname } = wxt.config.dev.server!;
|
||||
return {
|
||||
port,
|
||||
hostname,
|
||||
origin: `http://${hostname}:${port}`,
|
||||
};
|
||||
const { host, port, origin } = wxt.config.dev.server!;
|
||||
return { host, port, origin };
|
||||
};
|
||||
|
||||
let [runner, builderServer] = await Promise.all([
|
||||
@@ -71,8 +67,8 @@ async function createServerInternal(): Promise<WxtDevServer> {
|
||||
// Server instance must be created first so its reference can be added to the internal config used
|
||||
// to pre-render entrypoints
|
||||
const server: WxtDevServer = {
|
||||
get hostname() {
|
||||
return getServerInfo().hostname;
|
||||
get host() {
|
||||
return getServerInfo().host;
|
||||
},
|
||||
get port() {
|
||||
return getServerInfo().port;
|
||||
@@ -96,7 +92,9 @@ async function createServerInternal(): Promise<WxtDevServer> {
|
||||
}
|
||||
|
||||
await builderServer.listen();
|
||||
wxt.logger.success(`Started dev server @ ${server.origin}`);
|
||||
const hostInfo =
|
||||
server.host === 'localhost' ? '' : ` (listening on ${server.host})`;
|
||||
wxt.logger.success(`Started dev server @ ${server.origin}${hostInfo}`);
|
||||
await wxt.hooks.callHook('server:started', wxt, server);
|
||||
|
||||
// Register content scripts for the first time after the background starts
|
||||
|
||||
@@ -73,16 +73,18 @@ async function getPathsDeclarationEntry(
|
||||
isHtmlEntrypoint(entry) ? '.html' : '.js',
|
||||
),
|
||||
)
|
||||
.concat([''])
|
||||
.concat(await getPublicFiles());
|
||||
|
||||
await wxt.hooks.callHook('prepare:publicPaths', wxt, paths);
|
||||
|
||||
const unions = paths
|
||||
.map(normalizePath)
|
||||
.sort()
|
||||
.map((path) => ` | "/${path}"`)
|
||||
.join('\n');
|
||||
const unions = [
|
||||
` | ""`,
|
||||
` | "/"`,
|
||||
...paths
|
||||
.map(normalizePath)
|
||||
.sort()
|
||||
.map((path) => ` | "/${path}"`),
|
||||
].join('\n');
|
||||
|
||||
const template = `// Generated by wxt
|
||||
import "wxt/browser";
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { WxtPackageManagerImpl } from './types';
|
||||
|
||||
export const deno: WxtPackageManagerImpl = {
|
||||
overridesKey: 'na',
|
||||
downloadDependency() {
|
||||
throw Error('Deno not supported');
|
||||
},
|
||||
listDependencies() {
|
||||
throw Error('Deno not supported');
|
||||
},
|
||||
};
|
||||
@@ -14,6 +14,7 @@ import { WxtPackageManagerImpl } from './types';
|
||||
import { yarn } from './yarn';
|
||||
import { pnpm } from './pnpm';
|
||||
import { npm } from './npm';
|
||||
import { deno } from './deno';
|
||||
|
||||
export async function createWxtPackageManager(
|
||||
root: string,
|
||||
@@ -73,4 +74,5 @@ const packageManagers: Record<PackageManagerName, WxtPackageManagerImpl> = {
|
||||
pnpm,
|
||||
bun,
|
||||
yarn,
|
||||
deno,
|
||||
};
|
||||
|
||||
@@ -70,6 +70,12 @@ export async function resolveConfig(
|
||||
if (debug) logger.level = LogLevels.debug;
|
||||
|
||||
const browser = mergedConfig.browser ?? 'chrome';
|
||||
const targetBrowsers = mergedConfig.targetBrowsers ?? [];
|
||||
if (targetBrowsers.length > 0 && !targetBrowsers.includes(browser)) {
|
||||
throw new Error(
|
||||
`Current target browser \`${browser}\` is not in your \`targetBrowsers\` list!`,
|
||||
);
|
||||
}
|
||||
const manifestVersion =
|
||||
mergedConfig.manifestVersion ??
|
||||
(browser === 'firefox' || browser === 'safari' ? 2 : 3);
|
||||
@@ -143,19 +149,37 @@ export async function resolveConfig(
|
||||
|
||||
let devServerConfig: ResolvedConfig['dev']['server'];
|
||||
if (command === 'serve') {
|
||||
const hostname = mergedConfig.dev?.server?.hostname ?? 'localhost';
|
||||
if (mergedConfig.dev?.server?.hostname)
|
||||
logger.warn(
|
||||
`The 'hostname' option is deprecated, please use 'host' or 'origin' depending on your circumstances.`,
|
||||
);
|
||||
|
||||
const host =
|
||||
mergedConfig.dev?.server?.host ??
|
||||
mergedConfig.dev?.server?.hostname ??
|
||||
'localhost';
|
||||
let port = mergedConfig.dev?.server?.port;
|
||||
const origin =
|
||||
mergedConfig.dev?.server?.origin ??
|
||||
mergedConfig.dev?.server?.hostname ??
|
||||
'localhost';
|
||||
if (port == null || !isFinite(port)) {
|
||||
port = await getPort({
|
||||
// Passing host required for Mac, unsure of Windows/Linux
|
||||
host,
|
||||
port: 3000,
|
||||
portRange: [3001, 3010],
|
||||
// Passing host required for Mac, unsure of Windows/Linux
|
||||
host: hostname,
|
||||
});
|
||||
}
|
||||
const originWithProtocolAndPort = [
|
||||
origin.match(/^https?:\/\//) ? '' : 'http://',
|
||||
origin,
|
||||
origin.match(/:[0-9]+$/) ? '' : `:${port}`,
|
||||
].join('');
|
||||
devServerConfig = {
|
||||
host,
|
||||
port,
|
||||
hostname,
|
||||
origin: originWithProtocolAndPort,
|
||||
watchDebounce: safeStringToNumber(process.env.WXT_WATCH_DEBOUNCE) ?? 800,
|
||||
};
|
||||
}
|
||||
@@ -179,6 +203,7 @@ export async function resolveConfig(
|
||||
|
||||
return {
|
||||
browser,
|
||||
targetBrowsers,
|
||||
command,
|
||||
debug,
|
||||
entrypointsDir,
|
||||
|
||||
@@ -1463,7 +1463,7 @@ describe('Manifest Utils', () => {
|
||||
command: 'build',
|
||||
},
|
||||
server: {
|
||||
hostname: 'localhost',
|
||||
host: 'localhost',
|
||||
port: 3000,
|
||||
origin: 'http://localhost:3000',
|
||||
},
|
||||
@@ -1487,8 +1487,8 @@ describe('Manifest Utils', () => {
|
||||
manifestVersion: 2,
|
||||
},
|
||||
server: fakeWxtDevServer({
|
||||
host: 'localhost',
|
||||
port: 3000,
|
||||
hostname: 'localhost',
|
||||
origin: 'http://localhost:3000',
|
||||
}),
|
||||
});
|
||||
@@ -1515,7 +1515,7 @@ describe('Manifest Utils', () => {
|
||||
browser: 'chrome',
|
||||
},
|
||||
server: fakeWxtDevServer({
|
||||
hostname: 'localhost',
|
||||
host: 'localhost',
|
||||
port: 3000,
|
||||
origin: 'http://localhost:3000',
|
||||
}),
|
||||
@@ -1561,8 +1561,8 @@ describe('Manifest Utils', () => {
|
||||
},
|
||||
},
|
||||
server: fakeWxtDevServer({
|
||||
host: 'localhost',
|
||||
port: 3000,
|
||||
hostname: 'localhost',
|
||||
origin: 'http://localhost:3000',
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -137,5 +137,38 @@ export default defineContentScript({
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should not remove any variables delcared outside the main function that are used', () => {
|
||||
const input = `
|
||||
const [ a ] = [ 123, 456 ];
|
||||
const { b } = { b: 123 };
|
||||
const { c: { d } } = { c: { d: 123 } };
|
||||
const { e, ...rest } = { e: 123, f: 456 };
|
||||
|
||||
console.log(a);
|
||||
console.log(b);
|
||||
console.log(d);
|
||||
console.log(e);
|
||||
console.log(rest);
|
||||
|
||||
export default defineBackground(() => {
|
||||
console.log('Hello background!', { id: browser.runtime.id });
|
||||
});`;
|
||||
const expected = `const [ a ] = [ 123, 456 ];
|
||||
const { b } = { b: 123 };
|
||||
const { c: { d } } = { c: { d: 123 } };
|
||||
const { e, ...rest } = { e: 123, f: 456 };
|
||||
|
||||
console.log(a);
|
||||
console.log(b);
|
||||
console.log(d);
|
||||
console.log(e);
|
||||
console.log(rest);
|
||||
|
||||
export default defineBackground();`;
|
||||
|
||||
const actual = removeMainFunctionCode(input).code;
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -668,27 +668,46 @@ describe('findEntrypoints', () => {
|
||||
expect(entrypoints[0]).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should not allow multiple entrypoints with the same name', async () => {
|
||||
it('should ignore other index files in the same directory when index.html exists', async () => {
|
||||
globMock.mockResolvedValueOnce([
|
||||
'content/index.ts',
|
||||
'popup/index.html',
|
||||
'popup/index.ts',
|
||||
'popup/index.css',
|
||||
]);
|
||||
|
||||
const entrypoints = await findEntrypoints();
|
||||
|
||||
expect(entrypoints).toHaveLength(2);
|
||||
expect(entrypoints[0]).toMatchObject({
|
||||
type: 'content-script',
|
||||
name: 'content',
|
||||
});
|
||||
expect(entrypoints[1]).toMatchObject({
|
||||
type: 'popup',
|
||||
name: 'popup',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not allow a file entrypoint and directory entrypoint to have the same name', async () => {
|
||||
globMock.mockResolvedValueOnce([
|
||||
'options/index.html',
|
||||
'options/index.jsx',
|
||||
'popup.html',
|
||||
'popup/index.html',
|
||||
'popup/index.ts',
|
||||
'ui.html',
|
||||
'other.ts',
|
||||
'other/index.ts',
|
||||
]);
|
||||
|
||||
await expect(() => findEntrypoints()).rejects.toThrowError(
|
||||
[
|
||||
'Multiple entrypoints with the same name detected, only one entrypoint for each name is allowed.',
|
||||
'',
|
||||
'- options',
|
||||
` - ${unnormalizePath('src/entrypoints/options/index.html')}`,
|
||||
` - ${unnormalizePath('src/entrypoints/options/index.jsx')}`,
|
||||
'- other',
|
||||
` - ${unnormalizePath('src/entrypoints/other.ts')}`,
|
||||
` - ${unnormalizePath('src/entrypoints/other/index.ts')}`,
|
||||
'- popup',
|
||||
` - ${unnormalizePath('src/entrypoints/popup.html')}`,
|
||||
` - ${unnormalizePath('src/entrypoints/popup/index.html')}`,
|
||||
` - ${unnormalizePath('src/entrypoints/popup/index.ts')}`,
|
||||
].join('\n'),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -53,20 +53,34 @@ export async function findEntrypoints(): Promise<Entrypoint[]> {
|
||||
relativePaths.sort();
|
||||
|
||||
const pathGlobs = Object.keys(PATH_GLOB_TO_TYPE_MAP);
|
||||
const entrypointInfos: EntrypointInfo[] = relativePaths.reduce<
|
||||
EntrypointInfo[]
|
||||
>((results, relativePath) => {
|
||||
const inputPath = resolve(wxt.config.entrypointsDir, relativePath);
|
||||
const name = getEntrypointName(wxt.config.entrypointsDir, inputPath);
|
||||
const matchingGlob = pathGlobs.find((glob) =>
|
||||
minimatch(relativePath, glob),
|
||||
);
|
||||
if (matchingGlob) {
|
||||
const type = PATH_GLOB_TO_TYPE_MAP[matchingGlob];
|
||||
results.push({ name, inputPath, type });
|
||||
}
|
||||
return results;
|
||||
}, []);
|
||||
const entrypointInfos: EntrypointInfo[] = relativePaths
|
||||
.reduce<EntrypointInfo[]>((results, relativePath) => {
|
||||
const inputPath = resolve(wxt.config.entrypointsDir, relativePath);
|
||||
const name = getEntrypointName(wxt.config.entrypointsDir, inputPath);
|
||||
const matchingGlob = pathGlobs.find((glob) =>
|
||||
minimatch(relativePath, glob),
|
||||
);
|
||||
if (matchingGlob) {
|
||||
const type = PATH_GLOB_TO_TYPE_MAP[matchingGlob];
|
||||
results.push({ name, inputPath, type });
|
||||
}
|
||||
return results;
|
||||
}, [])
|
||||
.filter(({ name, inputPath }, _, entrypointInfos) => {
|
||||
// Remove <name>/index.* if <name>/index.html exists
|
||||
|
||||
if (inputPath.endsWith('.html')) return true;
|
||||
const isIndexFile = /index\..+$/.test(inputPath);
|
||||
if (!isIndexFile) return true;
|
||||
|
||||
const hasIndexHtml = entrypointInfos.some(
|
||||
(entry) =>
|
||||
entry.name === name && entry.inputPath.endsWith('index.html'),
|
||||
);
|
||||
if (hasIndexHtml) return false;
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
await wxt.hooks.callHook('entrypoints:found', wxt, entrypointInfos);
|
||||
|
||||
|
||||
@@ -12,7 +12,10 @@ export function getGlobals(
|
||||
{
|
||||
name: 'BROWSER',
|
||||
value: config.browser,
|
||||
type: `string`,
|
||||
type:
|
||||
config.targetBrowsers.length === 0
|
||||
? 'string'
|
||||
: config.targetBrowsers.map((browser) => `"${browser}"`).join(' | '),
|
||||
},
|
||||
{
|
||||
name: 'CHROME',
|
||||
|
||||
@@ -459,7 +459,13 @@ function discoverIcons(
|
||||
}
|
||||
|
||||
function addDevModeCsp(manifest: Browser.runtime.Manifest): void {
|
||||
const permission = `http://${wxt.server?.hostname ?? ''}/*`;
|
||||
let permissonUrl = wxt.server?.origin;
|
||||
if (permissonUrl) {
|
||||
const permissionUrlInstance = new URL(permissonUrl);
|
||||
permissionUrlInstance.port = '';
|
||||
permissonUrl = permissionUrlInstance.toString();
|
||||
}
|
||||
const permission = `${permissonUrl}*`;
|
||||
const allowedCsp = wxt.server?.origin ?? 'http://localhost:*';
|
||||
|
||||
if (manifest.manifest_version === 3) {
|
||||
|
||||
@@ -235,6 +235,7 @@ export const fakeResolvedConfig = fakeObjectCreator<ResolvedConfig>(() => {
|
||||
|
||||
return {
|
||||
browser,
|
||||
targetBrowsers: [],
|
||||
command,
|
||||
entrypointsDir: fakeDir(),
|
||||
modulesDir: fakeDir(),
|
||||
@@ -320,9 +321,9 @@ export const fakeWxt = fakeObjectCreator<Wxt>(() => ({
|
||||
|
||||
export const fakeWxtDevServer = fakeObjectCreator<WxtDevServer>(() => ({
|
||||
currentOutput: fakeBuildOutput(),
|
||||
hostname: 'localhost',
|
||||
origin: 'http://localhost:3000',
|
||||
host: 'localhost',
|
||||
port: 3000,
|
||||
origin: 'http://localhost:3000',
|
||||
reloadContentScript: vi.fn(),
|
||||
reloadExtension: vi.fn(),
|
||||
reloadPage: vi.fn(),
|
||||
@@ -367,9 +368,9 @@ export const fakeManifestCommand = fakeObjectCreator<Browser.commands.Command>(
|
||||
);
|
||||
|
||||
export const fakeDevServer = fakeObjectCreator<WxtDevServer>(() => ({
|
||||
hostname: 'localhost',
|
||||
origin: 'http://localhost',
|
||||
host: 'localhost',
|
||||
port: 5173,
|
||||
origin: 'http://localhost:3000',
|
||||
reloadContentScript: vi.fn(),
|
||||
reloadExtension: vi.fn(),
|
||||
reloadPage: vi.fn(),
|
||||
|
||||
@@ -50,19 +50,83 @@ function removeUnusedTopLevelVariables(mod: ProxifiedModule): number {
|
||||
|
||||
let deletedCount = 0;
|
||||
const ast = mod.$ast as any;
|
||||
for (let i = ast.body.length - 1; i >= 0; i--) {
|
||||
if (ast.body[i].type === 'VariableDeclaration') {
|
||||
for (let j = ast.body[i].declarations.length - 1; j >= 0; j--) {
|
||||
if (!usedMap.get(ast.body[i].declarations[j].id.name)) {
|
||||
ast.body[i].declarations.splice(j, 1);
|
||||
|
||||
const isUsed = (id: any) => {
|
||||
return id?.type === 'Identifier' && usedMap.get(id.name);
|
||||
};
|
||||
|
||||
const cleanArrayPattern = (pattern: any): boolean => {
|
||||
const elements = pattern.elements;
|
||||
for (let i = elements.length - 1; i >= 0; i--) {
|
||||
const el = elements[i];
|
||||
if (el?.type === 'Identifier' && !isUsed(el)) {
|
||||
elements.splice(i, 1);
|
||||
deletedCount++;
|
||||
}
|
||||
}
|
||||
return elements.length === 0;
|
||||
};
|
||||
|
||||
const cleanObjectPattern = (pattern: any): boolean => {
|
||||
const properties = pattern.properties;
|
||||
for (let i = properties.length - 1; i >= 0; i--) {
|
||||
const prop = properties[i];
|
||||
|
||||
if (prop.type === 'Property') {
|
||||
const value = prop.value;
|
||||
// support nested object
|
||||
if (value.type === 'ObjectPattern') {
|
||||
const isEmpty = cleanObjectPattern(value);
|
||||
if (isEmpty) {
|
||||
properties.splice(i, 1);
|
||||
}
|
||||
} else if (value.type === 'ArrayPattern') {
|
||||
const isEmpty = cleanArrayPattern(value);
|
||||
if (isEmpty) {
|
||||
properties.splice(i, 1);
|
||||
}
|
||||
} else if (value.type === 'Identifier' && !isUsed(value)) {
|
||||
properties.splice(i, 1);
|
||||
deletedCount++;
|
||||
}
|
||||
} else if (prop.type === 'RestElement') {
|
||||
const arg = prop.argument;
|
||||
if (arg.type === 'Identifier' && !isUsed(arg)) {
|
||||
properties.splice(i, 1);
|
||||
deletedCount++;
|
||||
}
|
||||
}
|
||||
if (ast.body[i].declarations.length === 0) {
|
||||
ast.body.splice(i, 1);
|
||||
}
|
||||
return properties.length === 0;
|
||||
};
|
||||
|
||||
for (let i = ast.body.length - 1; i >= 0; i--) {
|
||||
if (ast.body[i].type !== 'VariableDeclaration') continue;
|
||||
|
||||
for (let j = ast.body[i].declarations.length - 1; j >= 0; j--) {
|
||||
const id = ast.body[i].declarations[j].id;
|
||||
|
||||
let shouldRemove = false;
|
||||
|
||||
if (id.type === 'Identifier') {
|
||||
shouldRemove = !isUsed(id);
|
||||
if (shouldRemove) deletedCount++;
|
||||
} else if (id.type === 'ArrayPattern') {
|
||||
shouldRemove = cleanArrayPattern(id);
|
||||
} else if (id.type === 'ObjectPattern') {
|
||||
shouldRemove = cleanObjectPattern(id);
|
||||
}
|
||||
|
||||
if (shouldRemove) {
|
||||
ast.body[i].declarations.splice(j, 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (ast.body[i].declarations.length === 0) {
|
||||
ast.body.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
return deletedCount;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,11 +26,10 @@ export async function zip(config?: InlineConfig): Promise<string[]> {
|
||||
wxt.logger.info('Zipping extension...');
|
||||
const zipFiles: string[] = [];
|
||||
|
||||
const packageJson = await getPackageJson();
|
||||
const projectName =
|
||||
wxt.config.zip.name ??
|
||||
safeFilename(
|
||||
(await getPackageJson())?.name || path.basename(process.cwd()),
|
||||
);
|
||||
safeFilename(packageJson?.name || path.basename(process.cwd()));
|
||||
const applyTemplate = (template: string): string =>
|
||||
template
|
||||
.replaceAll('{{name}}', projectName)
|
||||
@@ -39,6 +38,7 @@ export async function zip(config?: InlineConfig): Promise<string[]> {
|
||||
'{{version}}',
|
||||
output.manifest.version_name ?? output.manifest.version,
|
||||
)
|
||||
.replaceAll('{{packageVersion}}', packageJson?.version)
|
||||
.replaceAll('{{mode}}', wxt.config.mode)
|
||||
.replaceAll('{{manifestVersion}}', `mv${wxt.config.manifestVersion}`);
|
||||
|
||||
|
||||
@@ -108,6 +108,12 @@ export interface InlineConfig {
|
||||
* "chrome"
|
||||
*/
|
||||
browser?: TargetBrowser;
|
||||
/**
|
||||
* Target browsers to support. When set, `import.meta.env.BROWSER` will be narrowed to a string literal type containing only the specified browser names.
|
||||
*
|
||||
* @default []
|
||||
*/
|
||||
targetBrowsers?: TargetBrowser[];
|
||||
/**
|
||||
* Explicitly set a manifest version to target. This will override the default manifest version
|
||||
* for each command, and can be overridden by the command line `--mv2` or `--mv3` option.
|
||||
@@ -141,6 +147,7 @@ export interface InlineConfig {
|
||||
*
|
||||
* - <span v-pre>`{{name}}`</span> - The project's name converted to kebab-case
|
||||
* - <span v-pre>`{{version}}`</span> - The version_name or version from the manifest
|
||||
* - <span v-pre>`{{packageVersion}}`</span> - The version from the package.json
|
||||
* - <span v-pre>`{{browser}}`</span> - The target browser from the `--browser` CLI flag
|
||||
* - <span v-pre>`{{mode}}`</span> - The current mode
|
||||
* - <span v-pre>`{{manifestVersion}}`</span> - Either "2" or "3"
|
||||
@@ -165,6 +172,7 @@ export interface InlineConfig {
|
||||
*
|
||||
* - <span v-pre>`{{name}}`</span> - The project's name converted to kebab-case
|
||||
* - <span v-pre>`{{version}}`</span> - The version_name or version from the manifest
|
||||
* - <span v-pre>`{{packageVersion}}`</span> - The version from the package.json
|
||||
* - <span v-pre>`{{browser}}`</span> - The target browser from the `--browser` CLI flag
|
||||
* - <span v-pre>`{{mode}}`</span> - The current mode
|
||||
* - <span v-pre>`{{manifestVersion}}`</span> - Either "2" or "3"
|
||||
@@ -319,14 +327,26 @@ export interface InlineConfig {
|
||||
*/
|
||||
dev?: {
|
||||
server?: {
|
||||
/**
|
||||
* Host to bind the dev server to.
|
||||
*
|
||||
* @default "localhost"
|
||||
*/
|
||||
host?: string;
|
||||
/**
|
||||
* Port to run the dev server on. Defaults to the first open port from 3000 to 3010.
|
||||
*/
|
||||
port?: number;
|
||||
/**
|
||||
* Origin to use to connect from the extension ui runtime to the dev server.
|
||||
*
|
||||
* @default "http://localhost:3000"
|
||||
*/
|
||||
origin?: string;
|
||||
/**
|
||||
* Hostname to run the dev server on.
|
||||
*
|
||||
* @default "localhost"
|
||||
* @deprecated use `host` to specify the interface to bind to, or use `origin` to specify the dev server hostname.
|
||||
*/
|
||||
hostname?: string;
|
||||
};
|
||||
@@ -1067,14 +1087,14 @@ export interface WxtBuilderServer {
|
||||
}
|
||||
|
||||
export interface ServerInfo {
|
||||
/**
|
||||
* Ex: `"localhost"`
|
||||
*/
|
||||
host: string;
|
||||
/**
|
||||
* Ex: `3000`
|
||||
*/
|
||||
port: number;
|
||||
/**
|
||||
* Ex: `"localhost"`
|
||||
*/
|
||||
hostname: string;
|
||||
/**
|
||||
* Ex: `"http://localhost:3000"`
|
||||
*/
|
||||
@@ -1299,6 +1319,7 @@ export interface ResolvedConfig {
|
||||
mode: string;
|
||||
command: WxtCommand;
|
||||
browser: TargetBrowser;
|
||||
targetBrowsers: TargetBrowser[];
|
||||
manifestVersion: TargetManifestVersion;
|
||||
env: ConfigEnv;
|
||||
logger: Logger;
|
||||
@@ -1343,8 +1364,9 @@ export interface ResolvedConfig {
|
||||
dev: {
|
||||
/** Only defined during dev command */
|
||||
server?: {
|
||||
host: string;
|
||||
port: number;
|
||||
hostname: string;
|
||||
origin: string;
|
||||
/**
|
||||
* The milliseconds to debounce when a file is saved before reloading.
|
||||
* The only way to set this option is to set the `WXT_WATCH_DEBOUNCE`
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user