Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 98bc1f2cd1 | |||
| f6a066f166 | |||
| c20478cde2 | |||
| ebd50079a7 | |||
| b17bd87865 | |||
| e8b471e23f | |||
| 32ffca4168 | |||
| e9c573d5a1 | |||
| ae5126d726 | |||
| 24a77ca9b5 | |||
| 174cbbc5eb | |||
| 607c70dd56 | |||
| 616d012326 | |||
| 75b6ca7172 | |||
| 1497b3099c |
@@ -30,10 +30,10 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup
|
||||
- run: pnpm tsc --noEmit
|
||||
working-directory: demo
|
||||
- run: pnpm build:all
|
||||
working-directory: demo
|
||||
- run: pnpm tsc --noEmit
|
||||
working-directory: demo
|
||||
- run: pnpm wxt zip
|
||||
working-directory: demo
|
||||
- run: pnpm vitest run
|
||||
|
||||
@@ -1,90 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## v0.14.6
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.14.5...v0.14.6)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Restart dev mode when saving config ([#365](https://github.com/wxt-dev/wxt/pull/365))
|
||||
- Add basic validation for entrypoint options ([#368](https://github.com/wxt-dev/wxt/pull/368))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Add subdependency bin directory so `wxt build --analyze` works with PNPM ([#363](https://github.com/wxt-dev/wxt/pull/363))
|
||||
- Sort build output files naturally ([#364](https://github.com/wxt-dev/wxt/pull/364))
|
||||
|
||||
### 🤖 CI
|
||||
|
||||
- Check for type errors in demo before building ([4b005b4](https://github.com/wxt-dev/wxt/commit/4b005b4))
|
||||
|
||||
## v0.14.5
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.14.4...v0.14.5)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Add `dev.reloadCommand` config ([#362](https://github.com/wxt-dev/wxt/pull/362))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Disable reload dev command when 4 commands are already registered ([#361](https://github.com/wxt-dev/wxt/pull/361))
|
||||
|
||||
## v0.14.4
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.14.3...v0.14.4)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Allow requiring built-in node modules from ESM CLI ([#356](https://github.com/wxt-dev/wxt/pull/356))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Add unit tests for passing flags via the CLI ([#354](https://github.com/wxt-dev/wxt/pull/354))
|
||||
|
||||
## v0.14.3
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.14.2...v0.14.3)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Make `getArrayFromFlags` result can be undefined ([#352](https://github.com/wxt-dev/wxt/pull/352))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Yuns ([@yunsii](http://github.com/yunsii))
|
||||
|
||||
## v0.14.2
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.14.1...v0.14.2)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Add `filterEntrypoints` option to speed up development ([#344](https://github.com/wxt-dev/wxt/pull/344))
|
||||
|
||||
### 🔥 Performance
|
||||
|
||||
- Only call `findEntrypoint` once per build ([#342](https://github.com/wxt-dev/wxt/pull/342))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Improve error message and document use of imported variables outside an entrypoint's `main` function ([#346](https://github.com/wxt-dev/wxt/pull/346))
|
||||
- Allow `browser.runtime.getURL` to include hashes and query params for HTML paths ([#350](https://github.com/wxt-dev/wxt/pull/350))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Fix typos and outdated ui function usage ([#347](https://github.com/wxt-dev/wxt/pull/347))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Update templates to `^0.14.0` ([70a4961](https://github.com/wxt-dev/wxt/commit/70a4961))
|
||||
- Fix typo in function name ([a329e24](https://github.com/wxt-dev/wxt/commit/a329e24))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Yuns ([@yunsii](http://github.com/yunsii))
|
||||
- Armin
|
||||
|
||||
## v0.14.1
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.14.0...v0.14.1)
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import messages from 'public/_locales/en/messages.json';
|
||||
|
||||
export default defineBackground(() => {
|
||||
console.log(browser.runtime.id);
|
||||
logId();
|
||||
@@ -8,24 +6,32 @@ export default defineBackground(() => {
|
||||
chrome: __IS_CHROME__,
|
||||
firefox: __IS_FIREFOX__,
|
||||
manifestVersion: __MANIFEST_VERSION__,
|
||||
messages,
|
||||
});
|
||||
|
||||
// @ts-expect-error: should only accept entrypoints or public assets
|
||||
browser.runtime.getURL('/');
|
||||
browser.runtime.getURL('/background.js');
|
||||
browser.runtime.getURL('/icon/128.png');
|
||||
browser.runtime.getURL('/example.html#hash');
|
||||
browser.runtime.getURL('/example.html?query=param');
|
||||
// @ts-expect-error: should only allow hashes/query params on HTML files
|
||||
browser.runtime.getURL('/icon-128.png?query=param');
|
||||
browser.runtime.getURL('/icon-128.png');
|
||||
|
||||
// @ts-expect-error: should only accept known message names
|
||||
browser.i18n.getMessage('test');
|
||||
browser.i18n.getMessage('prompt_for_name');
|
||||
browser.i18n.getMessage('hello', 'Aaron');
|
||||
browser.i18n.getMessage('bye', ['Aaron']);
|
||||
browser.i18n.getMessage('@@extension_id');
|
||||
console.log([
|
||||
// @ts-expect-error: browser.i18n should only accept known message names
|
||||
browser.i18n.getMessage('test'),
|
||||
browser.i18n.getMessage('promptForName'),
|
||||
browser.i18n.getMessage('hello', ['Aaron']),
|
||||
browser.i18n.getMessage('bye', ['Aaron']),
|
||||
browser.i18n.getMessage('@@extension_id'),
|
||||
browser.i18n.getMessage('nItems'),
|
||||
]);
|
||||
|
||||
console.log([
|
||||
// @ts-expect-error: i18n should only accept known message names
|
||||
i18n.t('test'),
|
||||
i18n.t('promptForName'),
|
||||
i18n.t('hello', ['Aaron']),
|
||||
i18n.t('bye', ['Aaron']),
|
||||
i18n.t('@@extension_id'),
|
||||
i18n.tp('nItems', 0, ['0']),
|
||||
]);
|
||||
|
||||
console.log('WXT MODE:', {
|
||||
MODE: import.meta.env.MODE,
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
promptForName: What's your name?
|
||||
hello:
|
||||
message: Hello, $USER$
|
||||
description: Greet the user
|
||||
placeholders:
|
||||
user:
|
||||
content: $1
|
||||
example: Paul
|
||||
bye:
|
||||
message: Goodbye, $USER$. Come back to $OUR_SITE$ soon!
|
||||
description: Say goodbye to the user
|
||||
placeholders:
|
||||
our_site:
|
||||
content: Example.com
|
||||
user:
|
||||
content: $1
|
||||
example: Paul
|
||||
nItems:
|
||||
0: 0 items
|
||||
1: 1 item
|
||||
n: $1 items
|
||||
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"prompt_for_name": {
|
||||
"message": "What's your name?",
|
||||
"description": "Ask for the user's name"
|
||||
},
|
||||
"hello": {
|
||||
"message": "Hello, $USER$",
|
||||
"description": "Greet the user",
|
||||
"placeholders": {
|
||||
"user": {
|
||||
"content": "$1",
|
||||
"example": "Cira"
|
||||
}
|
||||
}
|
||||
},
|
||||
"bye": {
|
||||
"message": "Goodbye, $USER$. Come back to $OUR_SITE$ soon!",
|
||||
"description": "Say goodbye to the user",
|
||||
"placeholders": {
|
||||
"our_site": {
|
||||
"content": "Example.com"
|
||||
},
|
||||
"user": {
|
||||
"content": "$1",
|
||||
"example": "Cira"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 504 B After Width: | Height: | Size: 504 B |
|
Before Width: | Height: | Size: 936 B After Width: | Height: | Size: 936 B |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
@@ -89,6 +89,7 @@ export default defineConfig({
|
||||
{ text: 'Storage', link: '/guide/storage.md' },
|
||||
{ text: 'Assets', link: '/guide/assets.md' },
|
||||
{ text: 'Content Script UI', link: '/guide/content-script-ui.md' },
|
||||
{ text: 'Localization', link: '/guide/localization.md' },
|
||||
{ text: 'Multiple Browsers', link: '/guide/multiple-browsers.md' },
|
||||
{ text: 'Auto-imports', link: '/guide/auto-imports.md' },
|
||||
{ text: 'Vite', link: '/guide/vite.md' },
|
||||
|
||||
@@ -95,7 +95,7 @@ export default defineContentScript({
|
||||
});
|
||||
```
|
||||
|
||||
```ts [Svelte]
|
||||
```ts [Svelete]
|
||||
// entrypoints/example-ui.content/index.ts
|
||||
import App from './App.svelte';
|
||||
|
||||
@@ -243,8 +243,7 @@ export default defineContentScript({
|
||||
|
||||
main(ctx) {
|
||||
// 3. Define your UI
|
||||
const ui = createShadowRootUi(ctx, {
|
||||
name: 'example-ui',
|
||||
const ui = createIntegratedUi(ctx, {
|
||||
position: 'inline',
|
||||
anchor: '#anchor',
|
||||
onMount: (container) => {
|
||||
@@ -265,7 +264,7 @@ export default defineContentScript({
|
||||
});
|
||||
```
|
||||
|
||||
```ts [Svelte]
|
||||
```ts [Svelete]
|
||||
// 1. Import the style
|
||||
import './style.css';
|
||||
import App from './App.svelte';
|
||||
@@ -276,8 +275,7 @@ export default defineContentScript({
|
||||
|
||||
main(ctx) {
|
||||
// 3. Define your UI
|
||||
const ui = createShadowRootUi(ctx, {
|
||||
name: 'example-ui',
|
||||
const ui = createIntegratedUi(ctx, {
|
||||
position: 'inline',
|
||||
anchor: '#anchor',
|
||||
onMount: (container) => {
|
||||
@@ -310,8 +308,7 @@ export default defineContentScript({
|
||||
|
||||
main(ctx) {
|
||||
// 3. Define your UI
|
||||
const ui = createShadowRootUi(ctx, {
|
||||
name: 'example-ui',
|
||||
const ui = createIntegratedUi(ctx, {
|
||||
position: 'inline',
|
||||
anchor: '#anchor',
|
||||
onMount: (container) => {
|
||||
|
||||
@@ -86,56 +86,3 @@ export default defineContentScript({
|
||||
:::info
|
||||
For a full list of entrypoints and each of their options, see the [`/entrypoints` folder](/entrypoints/background) documentation.
|
||||
:::
|
||||
|
||||
### Side Effects
|
||||
|
||||
You cannot use imported variables outside the `main` function is JS entrypoints. This includes options, as shown below:
|
||||
|
||||
```ts
|
||||
// entrypoints/content.ts
|
||||
import { GOOGLE_MATCHES } from '~/utils/match-patterns';
|
||||
|
||||
export default defineContentScript({
|
||||
matches: GOOGLE_MATCHES,
|
||||
main() {
|
||||
// ...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```
|
||||
$ wxt build
|
||||
wxt build
|
||||
|
||||
WXT 0.14.1
|
||||
ℹ Building chrome-mv3 for production with Vite 5.0.5
|
||||
✖ Command failed after 360 ms
|
||||
|
||||
[8:55:54 AM] ERROR entrypoints/content.ts: Cannot use imported variable "GOOGLE_MATCHES" before main function. See https://wxt.dev/guide/entrypoints.html#side-effects
|
||||
```
|
||||
|
||||
This throws an error because WXT needs to import each entrypoint during the build process to extract its definition (containing the `match`, `run_at`, `include`/`exclude`, etc.) to render the `manifest.json` correctly. Before loading an entrypoint, a transformation is applied to remove all imports. This prevents imported modules (local or NPM) with side-effects from running during the build process, potentially throwing an error.
|
||||
|
||||
:::details Why?
|
||||
|
||||
When importing your entrypoint to get its definition, the file is imported in a **_node environement_**, and doesn't have access to the `window`, `chrome`, or `browser` globals a web extension ususally has access to. If WXT doesn't remove all the imports from the file, the imported modules could try and access one of these variables, throwing an error.
|
||||
|
||||
:::
|
||||
|
||||
:::warning
|
||||
See [`wxt-dev/wxt#336`](https://github.com/wxt-dev/wxt/issues/336) to track the status of this bug.
|
||||
:::
|
||||
|
||||
Usually, this error occurs when you try to extract options into a shared file or try to run code outside the `main` function. To fix the example from above, use litteral values when defining an entrypoint instead of importing them:
|
||||
|
||||
```ts
|
||||
import { GOOGLE_MATCHES } from '~/utils/match-patterns'; // [!code --]
|
||||
|
||||
export default defineContentScript({
|
||||
matches: GOOGLE_MATCHES, // [!code --]
|
||||
matches: ['*//*.google.com/*'], // [!code ++]
|
||||
main() {
|
||||
// ...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
---
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# Localization
|
||||
|
||||
WXT includes a util, [`i18n`](/api/wxt/i18n/), that provides a type-safe feature-rich alternative to `browser.i18n.getMessage`.
|
||||
|
||||
It is available automatically when you create a messages file under the `locales/` directory, and add a `default_locale` to the manifest:
|
||||
|
||||
```
|
||||
<srcDir>
|
||||
└─ locales/
|
||||
├─ en.json
|
||||
├─ es.json5
|
||||
├─ fr.yml
|
||||
├─ de.yaml
|
||||
└─ ...
|
||||
```
|
||||
|
||||
> You can use JSON, JSON5, or YAML formats.
|
||||
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
export default defineConfig({
|
||||
manifest: {
|
||||
default_locale: 'en',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Message File Format
|
||||
|
||||
```yml
|
||||
# Use plain strings
|
||||
simpleMessage: Hello world!
|
||||
|
||||
# Nest strings in objects
|
||||
popup:
|
||||
overview:
|
||||
title: Nested text
|
||||
|
||||
# Plural form support
|
||||
items:
|
||||
1: 1 item
|
||||
n: $1 items
|
||||
# Optionally include a custom string for 0
|
||||
cartSize:
|
||||
0: Empty
|
||||
1: 1 item
|
||||
n: $1 items
|
||||
|
||||
# Or stick with the standard web extension format (with a message, description, and placeholders)
|
||||
manifestMessage:
|
||||
message: $THIS$ is translated
|
||||
description: This is not-translated, helps translators
|
||||
placeholder:
|
||||
this:
|
||||
content: This
|
||||
```
|
||||
|
||||
:::tip
|
||||
`locales/<code>.json` is 100% compatible with the standard web extension localization format (`_locales/<code>/messages.json`). If you have existing messages files, just move them into the `locales/` directory.
|
||||
:::
|
||||
|
||||
## Usage
|
||||
|
||||
`i18n` is auto-imported, but can be manually imported from `wxt/i18n`.
|
||||
|
||||
```ts
|
||||
import { i18n } from 'wxt/i18n';
|
||||
```
|
||||
|
||||
### Basic Usage
|
||||
|
||||
You can access messages by their name:
|
||||
|
||||
```yml
|
||||
helloWorld: Hello world!
|
||||
```
|
||||
|
||||
```ts
|
||||
i18n.t('helloWorld'); // "Hello world!"
|
||||
```
|
||||
|
||||
Nested messages are combined into one string using an `_`.
|
||||
|
||||
```yml
|
||||
popup:
|
||||
overview:
|
||||
title: Hello world!
|
||||
```
|
||||
|
||||
```ts
|
||||
i18n.t('popup_overview_title'); // "Hello world!"
|
||||
```
|
||||
|
||||
If a message is in the standard web extension format, don't include a `_message`, even if it's nested.
|
||||
|
||||
```yml
|
||||
helloWorld:
|
||||
message: Hello world!
|
||||
description: Some description
|
||||
popup:
|
||||
overview:
|
||||
title:
|
||||
message: Nested Title
|
||||
```
|
||||
|
||||
```ts
|
||||
i18n.t('helloWorld'); // "Hello world!"
|
||||
i18n.t('popup_overview_title'); // "Nested Title"
|
||||
```
|
||||
|
||||
### Substitutions
|
||||
|
||||
To insert a custom string into a translation, pass an array of values as the second parameter of the `i18n.t` function:
|
||||
|
||||
```yml
|
||||
hello: Hello, $1, my name is $2.
|
||||
```
|
||||
|
||||
```ts
|
||||
i18n.t('hello', ['Aaron', 'Mark']); // "Hello Aaron, my name is Mark."
|
||||
```
|
||||
|
||||
### Plural Form
|
||||
|
||||
When getting the translation for text with a plural form, use the `i18n.tp` function.
|
||||
|
||||
```yml
|
||||
friends:
|
||||
0: I have no friends.
|
||||
1: I have a friend.
|
||||
n: I have many friends.
|
||||
```
|
||||
|
||||
```ts
|
||||
i18n.tp('friends', 0); // "I have no friends."
|
||||
i18n.tp('friends', 1); // "I have one friend."
|
||||
i18n.tp('friends', 2); // "I have many friends."
|
||||
```
|
||||
|
||||
The first number is the `count`. It is what decides which form will be used.
|
||||
|
||||
Substitutions are not required. But usually, a plural form will look something like this:
|
||||
|
||||
```yml
|
||||
items:
|
||||
1: 1 item
|
||||
n: $1 items
|
||||
```
|
||||
|
||||
```ts
|
||||
i18n.tp('items', 0, ['0']); // "0 items"
|
||||
i18n.tp('items', 1, ['1']); // "1 item"
|
||||
i18n.tp('items', 2, ['2']); // "2 items"
|
||||
i18n.tp('items', 3, ['3']); // "3 items"
|
||||
```
|
||||
@@ -5,7 +5,8 @@
|
||||
"../src/browser.ts",
|
||||
"../src/sandbox",
|
||||
"../src/storage.ts",
|
||||
"../src/testing"
|
||||
"../src/testing",
|
||||
"../src/i18n"
|
||||
],
|
||||
"plugin": ["typedoc-plugin-markdown", "typedoc-vitepress-theme"],
|
||||
"out": "./api",
|
||||
|
||||
@@ -28,6 +28,7 @@ describe('Auto Imports', () => {
|
||||
const defineContentScript: typeof import('wxt/sandbox')['defineContentScript']
|
||||
const defineUnlistedScript: typeof import('wxt/sandbox')['defineUnlistedScript']
|
||||
const fakeBrowser: typeof import('wxt/testing')['fakeBrowser']
|
||||
const i18n: typeof import('wxt/i18n')['i18n']
|
||||
const storage: typeof import('wxt/storage')['storage']
|
||||
}
|
||||
"
|
||||
|
||||
@@ -49,10 +49,8 @@ describe('TypeScript Project', () => {
|
||||
| "/options.html"
|
||||
| "/popup.html"
|
||||
| "/sandbox.html"
|
||||
type HtmlPublicPath = Extract<PublicPath, \`\${string}.html\`>
|
||||
export interface WxtRuntime extends Runtime.Static {
|
||||
getURL(path: PublicPath): string;
|
||||
getURL(path: \`\${HtmlPublicPath}\${string}\`): string;
|
||||
}
|
||||
}
|
||||
"
|
||||
@@ -63,7 +61,7 @@ describe('TypeScript Project', () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/unlisted.html');
|
||||
project.addFile(
|
||||
'public/_locales/en/messages.json',
|
||||
'locales/en.json',
|
||||
JSON.stringify({
|
||||
prompt_for_name: {
|
||||
message: "What's your name?",
|
||||
@@ -121,6 +119,36 @@ describe('TypeScript Project', () => {
|
||||
}
|
||||
|
||||
export interface WxtI18n extends I18n.Static {
|
||||
/**
|
||||
* Ask for the user's name
|
||||
*
|
||||
* "What's your name?"
|
||||
*/
|
||||
getMessage(
|
||||
messageName: "prompt_for_name",
|
||||
substitutions?: string | string[],
|
||||
options?: GetMessageOptions,
|
||||
): string;
|
||||
/**
|
||||
* Greet the user
|
||||
*
|
||||
* "Hello, $USER$"
|
||||
*/
|
||||
getMessage(
|
||||
messageName: "hello",
|
||||
substitutions?: string | string[],
|
||||
options?: GetMessageOptions,
|
||||
): string;
|
||||
/**
|
||||
* Say goodbye to the user
|
||||
*
|
||||
* "Goodbye, $USER$. Come back to $OUR_SITE$ soon!"
|
||||
*/
|
||||
getMessage(
|
||||
messageName: "bye",
|
||||
substitutions?: string | string[],
|
||||
options?: GetMessageOptions,
|
||||
): string;
|
||||
/**
|
||||
* The extension or app ID; you might use this string to construct URLs for resources inside the extension. Even unlocalized extensions can use this message.
|
||||
Note: You can't use this message in a manifest file.
|
||||
@@ -133,7 +161,7 @@ describe('TypeScript Project', () => {
|
||||
options?: GetMessageOptions,
|
||||
): string;
|
||||
/**
|
||||
*
|
||||
* No message description.
|
||||
*
|
||||
* "<browser.i18n.getUiLocale()>"
|
||||
*/
|
||||
@@ -182,36 +210,25 @@ describe('TypeScript Project', () => {
|
||||
substitutions?: string | string[],
|
||||
options?: GetMessageOptions,
|
||||
): string;
|
||||
/**
|
||||
* Ask for the user's name
|
||||
*
|
||||
* "What's your name?"
|
||||
*/
|
||||
getMessage(
|
||||
messageName: "prompt_for_name",
|
||||
substitutions?: string | string[],
|
||||
options?: GetMessageOptions,
|
||||
): string;
|
||||
/**
|
||||
* Greet the user
|
||||
*
|
||||
* "Hello, $USER$"
|
||||
*/
|
||||
getMessage(
|
||||
messageName: "hello",
|
||||
substitutions?: string | string[],
|
||||
options?: GetMessageOptions,
|
||||
): string;
|
||||
/**
|
||||
* Say goodbye to the user
|
||||
*
|
||||
* "Goodbye, $USER$. Come back to $OUR_SITE$ soon!"
|
||||
*/
|
||||
getMessage(
|
||||
messageName: "bye",
|
||||
substitutions?: string | string[],
|
||||
options?: GetMessageOptions,
|
||||
): string;
|
||||
}
|
||||
}
|
||||
|
||||
declare module "wxt/i18n" {
|
||||
export interface WxtMessageSchema {
|
||||
t: {
|
||||
"prompt_for_name": any;
|
||||
"hello": any;
|
||||
"bye": any;
|
||||
"@@extension_id": any;
|
||||
"@@ui_locale": any;
|
||||
"@@bidi_dir": any;
|
||||
"@@bidi_reversed_dir": any;
|
||||
"@@bidi_start_edge": any;
|
||||
"@@bidi_end_edge": any;
|
||||
};
|
||||
tp: {
|
||||
|
||||
};
|
||||
}
|
||||
}
|
||||
"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "wxt",
|
||||
"type": "module",
|
||||
"version": "0.14.6",
|
||||
"version": "0.14.2-alpha2",
|
||||
"description": "Next gen framework for developing web extensions",
|
||||
"engines": {
|
||||
"node": ">=18",
|
||||
@@ -77,6 +77,16 @@
|
||||
"default": "./dist/storage.cjs"
|
||||
}
|
||||
},
|
||||
"./i18n": {
|
||||
"import": {
|
||||
"types": "./dist/i18n.d.ts",
|
||||
"default": "./dist/i18n.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/i18n.d.cts",
|
||||
"default": "./dist/i18n.cjs"
|
||||
}
|
||||
},
|
||||
"./vite-builder-env": {
|
||||
"types": "./dist/vite-builder-env.d.ts"
|
||||
}
|
||||
@@ -125,9 +135,7 @@
|
||||
"jiti": "^1.21.0",
|
||||
"json5": "^2.2.3",
|
||||
"linkedom": "^0.16.1",
|
||||
"manage-path": "^2.0.0",
|
||||
"minimatch": "^9.0.3",
|
||||
"natural-compare": "^1.4.0",
|
||||
"normalize-path": "^3.0.0",
|
||||
"ora": "^7.0.1",
|
||||
"picocolors": "^1.0.0",
|
||||
@@ -137,13 +145,13 @@
|
||||
"vite": "^5.0.0",
|
||||
"web-ext-run": "^0.1.0",
|
||||
"webextension-polyfill": "^0.10.0",
|
||||
"yaml": "^2.3.4",
|
||||
"zip-dir": "^2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^8.3.1",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/lodash.merge": "^4.6.9",
|
||||
"@types/natural-compare": "^1.4.3",
|
||||
"@types/node": "^20.10.3",
|
||||
"@types/normalize-path": "^3.0.2",
|
||||
"@types/prompts": "^2.4.9",
|
||||
|
||||
@@ -74,15 +74,9 @@ importers:
|
||||
linkedom:
|
||||
specifier: ^0.16.1
|
||||
version: 0.16.1
|
||||
manage-path:
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0
|
||||
minimatch:
|
||||
specifier: ^9.0.3
|
||||
version: 9.0.3
|
||||
natural-compare:
|
||||
specifier: ^1.4.0
|
||||
version: 1.4.0
|
||||
normalize-path:
|
||||
specifier: ^3.0.0
|
||||
version: 3.0.0
|
||||
@@ -110,6 +104,9 @@ importers:
|
||||
webextension-polyfill:
|
||||
specifier: ^0.10.0
|
||||
version: 0.10.0
|
||||
yaml:
|
||||
specifier: ^2.3.4
|
||||
version: 2.3.4
|
||||
zip-dir:
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0
|
||||
@@ -123,9 +120,6 @@ importers:
|
||||
'@types/lodash.merge':
|
||||
specifier: ^4.6.9
|
||||
version: 4.6.9
|
||||
'@types/natural-compare':
|
||||
specifier: ^1.4.3
|
||||
version: 1.4.3
|
||||
'@types/node':
|
||||
specifier: ^20.10.3
|
||||
version: 20.10.3
|
||||
@@ -1115,10 +1109,6 @@ packages:
|
||||
resolution: {integrity: sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==}
|
||||
dev: false
|
||||
|
||||
/@types/natural-compare@1.4.3:
|
||||
resolution: {integrity: sha512-XCAxy+Gg6+S6VagwzcknnvCKujj/bVv1q+GFuCrFEelqaZPqJoC+FeXLwc2dp+oLP7qDZQ4ZfQiTJQ9sIUmlLw==}
|
||||
dev: true
|
||||
|
||||
/@types/node@20.10.3:
|
||||
resolution: {integrity: sha512-XJavIpZqiXID5Yxnxv3RUDKTN5b81ddNC3ecsA0SoFXz/QU8OGBwZGMomiq0zw+uuqbL/krztv/DINAQ/EV4gg==}
|
||||
dependencies:
|
||||
@@ -3365,10 +3355,6 @@ packages:
|
||||
resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
|
||||
dev: false
|
||||
|
||||
/manage-path@2.0.0:
|
||||
resolution: {integrity: sha512-NJhyB+PJYTpxhxZJ3lecIGgh4kwIY2RAh44XvAz9UlqthlQwtPBf62uBVR8XaD8CRuSjQ6TnZH2lNJkbLPZM2A==}
|
||||
dev: false
|
||||
|
||||
/mark.js@8.11.1:
|
||||
resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==}
|
||||
dev: true
|
||||
@@ -3550,10 +3536,6 @@ packages:
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
|
||||
/natural-compare@1.4.0:
|
||||
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||
dev: false
|
||||
|
||||
/ncp@2.0.0:
|
||||
resolution: {integrity: sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==}
|
||||
hasBin: true
|
||||
@@ -5406,7 +5388,6 @@ packages:
|
||||
/yaml@2.3.4:
|
||||
resolution: {integrity: sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==}
|
||||
engines: {node: '>= 14'}
|
||||
dev: true
|
||||
|
||||
/yargs-parser@21.1.1:
|
||||
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
|
||||
|
||||
@@ -58,6 +58,7 @@ const config: tsup.Options[] = [
|
||||
index: 'src/index.ts',
|
||||
testing: 'src/testing/index.ts',
|
||||
storage: 'src/storage.ts',
|
||||
i18n: 'src/i18n/index.ts',
|
||||
},
|
||||
format: ['cjs', 'esm'],
|
||||
clean: true,
|
||||
@@ -97,11 +98,6 @@ const config: tsup.Options[] = [
|
||||
cli: 'src/cli.ts',
|
||||
},
|
||||
format: ['esm'],
|
||||
banner: {
|
||||
// Fixes dynamic require of nodejs modules. See https://github.com/wxt-dev/wxt/issues/355
|
||||
// https://github.com/evanw/esbuild/issues/1921#issuecomment-1152991694
|
||||
js: "import { createRequire } from 'module';const require = createRequire(import.meta.url);",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -1,376 +0,0 @@
|
||||
import { describe, it, vi, beforeEach, expect } from 'vitest';
|
||||
import { build } from '../core/build';
|
||||
import { createServer } from '../core/create-server';
|
||||
import { zip } from '../core/zip';
|
||||
import { prepare } from '../core/prepare';
|
||||
import { clean } from '../core/clean';
|
||||
import { initialize } from '../core/initialize';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import consola from 'consola';
|
||||
|
||||
vi.mock('../core/build');
|
||||
const buildMock = vi.mocked(build);
|
||||
|
||||
vi.mock('../core/create-server');
|
||||
const createServerMock = vi.mocked(createServer);
|
||||
|
||||
vi.mock('../core/zip');
|
||||
const zipMock = vi.mocked(zip);
|
||||
|
||||
vi.mock('../core/prepare');
|
||||
const prepareMock = vi.mocked(prepare);
|
||||
|
||||
vi.mock('../core/clean');
|
||||
const cleanMock = vi.mocked(clean);
|
||||
|
||||
vi.mock('../core/initialize');
|
||||
const initializeMock = vi.mocked(initialize);
|
||||
|
||||
consola.wrapConsole();
|
||||
|
||||
const ogArgv = process.argv;
|
||||
|
||||
function mockArgv(...args: string[]) {
|
||||
process.argv = ['/bin/node', 'bin/wxt.mjs', ...args];
|
||||
}
|
||||
|
||||
describe('CLI', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
process.argv = ogArgv;
|
||||
createServerMock.mockResolvedValue(mock());
|
||||
});
|
||||
|
||||
describe('dev', () => {
|
||||
it('should not pass any config when no flags are passed', async () => {
|
||||
mockArgv();
|
||||
await import('../cli');
|
||||
|
||||
expect(createServerMock).toBeCalledWith({});
|
||||
});
|
||||
|
||||
it('should respect passing a custom root', async () => {
|
||||
mockArgv('path/to/root');
|
||||
await import('../cli');
|
||||
|
||||
expect(createServerMock).toBeCalledWith({
|
||||
root: 'path/to/root',
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect a custom config file', async () => {
|
||||
mockArgv('-c', './path/to/config.ts');
|
||||
await import('../cli');
|
||||
|
||||
expect(createServerMock).toBeCalledWith({
|
||||
configFile: './path/to/config.ts',
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect passing a custom mode', async () => {
|
||||
mockArgv('-m', 'development');
|
||||
await import('../cli');
|
||||
|
||||
expect(createServerMock).toBeCalledWith({
|
||||
mode: 'development',
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect passing a custom browser', async () => {
|
||||
mockArgv('-b', 'firefox');
|
||||
await import('../cli');
|
||||
|
||||
expect(createServerMock).toBeCalledWith({
|
||||
browser: 'firefox',
|
||||
});
|
||||
});
|
||||
|
||||
it('should pass correct filtered entrypoints', async () => {
|
||||
mockArgv('-e', 'popup', '-e', 'options');
|
||||
await import('../cli');
|
||||
|
||||
expect(createServerMock).toBeCalledWith({
|
||||
filterEntrypoints: ['popup', 'options'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect passing --mv2', async () => {
|
||||
mockArgv('--mv2');
|
||||
await import('../cli');
|
||||
|
||||
expect(createServerMock).toBeCalledWith({
|
||||
manifestVersion: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect passing --mv3', async () => {
|
||||
mockArgv('--mv3');
|
||||
await import('../cli');
|
||||
|
||||
expect(createServerMock).toBeCalledWith({
|
||||
manifestVersion: 3,
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect passing --debug', async () => {
|
||||
mockArgv('--debug');
|
||||
await import('../cli');
|
||||
|
||||
expect(createServerMock).toBeCalledWith({
|
||||
debug: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('build', () => {
|
||||
it('should not pass any config when no flags are passed', async () => {
|
||||
mockArgv('build');
|
||||
await import('../cli');
|
||||
|
||||
expect(buildMock).toBeCalledWith({});
|
||||
});
|
||||
|
||||
it('should respect passing a custom root', async () => {
|
||||
mockArgv('build', 'path/to/root');
|
||||
await import('../cli');
|
||||
|
||||
expect(buildMock).toBeCalledWith({
|
||||
root: 'path/to/root',
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect a custom config file', async () => {
|
||||
mockArgv('build', '-c', './path/to/config.ts');
|
||||
await import('../cli');
|
||||
|
||||
expect(buildMock).toBeCalledWith({
|
||||
configFile: './path/to/config.ts',
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect passing a custom mode', async () => {
|
||||
mockArgv('build', '-m', 'development');
|
||||
await import('../cli');
|
||||
|
||||
expect(buildMock).toBeCalledWith({
|
||||
mode: 'development',
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect passing a custom browser', async () => {
|
||||
mockArgv('build', '-b', 'firefox');
|
||||
await import('../cli');
|
||||
|
||||
expect(buildMock).toBeCalledWith({
|
||||
browser: 'firefox',
|
||||
});
|
||||
});
|
||||
|
||||
it('should pass correct filtered entrypoints', async () => {
|
||||
mockArgv('build', '-e', 'popup', '-e', 'options');
|
||||
await import('../cli');
|
||||
|
||||
expect(buildMock).toBeCalledWith({
|
||||
filterEntrypoints: ['popup', 'options'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect passing --mv2', async () => {
|
||||
mockArgv('build', '--mv2');
|
||||
await import('../cli');
|
||||
|
||||
expect(buildMock).toBeCalledWith({
|
||||
manifestVersion: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect passing --mv3', async () => {
|
||||
mockArgv('build', '--mv3');
|
||||
await import('../cli');
|
||||
|
||||
expect(buildMock).toBeCalledWith({
|
||||
manifestVersion: 3,
|
||||
});
|
||||
});
|
||||
|
||||
it('should include analysis in the build', async () => {
|
||||
mockArgv('build', '--analyze');
|
||||
await import('../cli');
|
||||
|
||||
expect(buildMock).toBeCalledWith({
|
||||
analysis: {
|
||||
enabled: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect passing --debug', async () => {
|
||||
mockArgv('build', '--debug');
|
||||
await import('../cli');
|
||||
|
||||
expect(buildMock).toBeCalledWith({
|
||||
debug: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('zip', () => {
|
||||
it('should not pass any config when no flags are passed', async () => {
|
||||
mockArgv('zip');
|
||||
await import('../cli');
|
||||
|
||||
expect(zipMock).toBeCalledWith({});
|
||||
});
|
||||
|
||||
it('should respect passing a custom root', async () => {
|
||||
mockArgv('zip', 'path/to/root');
|
||||
await import('../cli');
|
||||
|
||||
expect(zipMock).toBeCalledWith({
|
||||
root: 'path/to/root',
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect a custom config file', async () => {
|
||||
mockArgv('zip', '-c', './path/to/config.ts');
|
||||
await import('../cli');
|
||||
|
||||
expect(zipMock).toBeCalledWith({
|
||||
configFile: './path/to/config.ts',
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect passing a custom mode', async () => {
|
||||
mockArgv('zip', '-m', 'development');
|
||||
await import('../cli');
|
||||
|
||||
expect(zipMock).toBeCalledWith({
|
||||
mode: 'development',
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect passing a custom browser', async () => {
|
||||
mockArgv('zip', '-b', 'firefox');
|
||||
await import('../cli');
|
||||
|
||||
expect(zipMock).toBeCalledWith({
|
||||
browser: 'firefox',
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect passing --mv2', async () => {
|
||||
mockArgv('zip', '--mv2');
|
||||
await import('../cli');
|
||||
|
||||
expect(zipMock).toBeCalledWith({
|
||||
manifestVersion: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect passing --mv3', async () => {
|
||||
mockArgv('zip', '--mv3');
|
||||
await import('../cli');
|
||||
|
||||
expect(zipMock).toBeCalledWith({
|
||||
manifestVersion: 3,
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect passing --debug', async () => {
|
||||
mockArgv('zip', '--debug');
|
||||
await import('../cli');
|
||||
|
||||
expect(zipMock).toBeCalledWith({
|
||||
debug: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('prepare', () => {
|
||||
it('should not pass any config when no flags are passed', async () => {
|
||||
mockArgv('prepare');
|
||||
await import('../cli');
|
||||
|
||||
expect(prepareMock).toBeCalledWith({});
|
||||
});
|
||||
|
||||
it('should respect passing a custom root', async () => {
|
||||
mockArgv('prepare', 'path/to/root');
|
||||
await import('../cli');
|
||||
|
||||
expect(prepareMock).toBeCalledWith({
|
||||
root: 'path/to/root',
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect a custom config file', async () => {
|
||||
mockArgv('prepare', '-c', './path/to/config.ts');
|
||||
await import('../cli');
|
||||
|
||||
expect(prepareMock).toBeCalledWith({
|
||||
configFile: './path/to/config.ts',
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect passing --debug', async () => {
|
||||
mockArgv('prepare', '--debug');
|
||||
await import('../cli');
|
||||
|
||||
expect(prepareMock).toBeCalledWith({
|
||||
debug: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('clean', () => {
|
||||
it('should not pass any config when no flags are passed', async () => {
|
||||
mockArgv('clean');
|
||||
await import('../cli');
|
||||
|
||||
expect(cleanMock).toBeCalledWith(undefined);
|
||||
});
|
||||
|
||||
it('should respect passing a custom root', async () => {
|
||||
mockArgv('clean', 'path/to/root');
|
||||
await import('../cli');
|
||||
|
||||
expect(cleanMock).toBeCalledWith('path/to/root');
|
||||
});
|
||||
});
|
||||
|
||||
describe('init', () => {
|
||||
it('should not pass any options when no flags are passed', async () => {
|
||||
mockArgv('init');
|
||||
await import('../cli');
|
||||
|
||||
expect(initializeMock).toBeCalledWith({});
|
||||
});
|
||||
|
||||
it('should respect the provided folder', async () => {
|
||||
mockArgv('init', 'path/to/folder');
|
||||
await import('../cli');
|
||||
|
||||
expect(initializeMock).toBeCalledWith({
|
||||
directory: 'path/to/folder',
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect passing --template', async () => {
|
||||
mockArgv('init', '-t', 'vue');
|
||||
await import('../cli');
|
||||
|
||||
expect(initializeMock).toBeCalledWith({
|
||||
template: 'vue',
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect passing --pm', async () => {
|
||||
mockArgv('init', '--pm', 'pnpm');
|
||||
await import('../cli');
|
||||
|
||||
expect(initializeMock).toBeCalledWith({
|
||||
packageManager: 'pnpm',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4,7 +4,6 @@ import { build, clean, createServer, initialize, prepare, zip } from '~/core';
|
||||
import consola, { LogLevels } from 'consola';
|
||||
import { printHeader } from '~/core/utils/log';
|
||||
import { formatDuration } from '~/core/utils/time';
|
||||
import { ValidationError } from './core/utils/validation';
|
||||
|
||||
// TODO: Remove. See https://github.com/wxt-dev/wxt/issues/277
|
||||
process.env.VITE_CJS_IGNORE_WARNING = 'true';
|
||||
@@ -21,13 +20,6 @@ cli
|
||||
.option('-c, --config <file>', 'use specified config file')
|
||||
.option('-m, --mode <mode>', 'set env mode')
|
||||
.option('-b, --browser <browser>', 'specify a browser')
|
||||
.option(
|
||||
'-e, --filter-entrypoint <entrypoint>',
|
||||
'only build specific entrypoints',
|
||||
{
|
||||
type: [],
|
||||
},
|
||||
)
|
||||
.option('--mv3', 'target manifest v3')
|
||||
.option('--mv2', 'target manifest v2')
|
||||
.action(
|
||||
@@ -39,7 +31,6 @@ cli
|
||||
manifestVersion: flags.mv3 ? 3 : flags.mv2 ? 2 : undefined,
|
||||
configFile: flags.config,
|
||||
debug: flags.debug,
|
||||
filterEntrypoints: getArrayFromFlags(flags, 'filterEntrypoint'),
|
||||
});
|
||||
await server.start();
|
||||
return { isOngoing: true };
|
||||
@@ -52,13 +43,6 @@ cli
|
||||
.option('-c, --config <file>', 'use specified config file')
|
||||
.option('-m, --mode <mode>', 'set env mode')
|
||||
.option('-b, --browser <browser>', 'specify a browser')
|
||||
.option(
|
||||
'-e, --filter-entrypoint <entrypoint>',
|
||||
'only build specific entrypoints',
|
||||
{
|
||||
type: [],
|
||||
},
|
||||
)
|
||||
.option('--mv3', 'target manifest v3')
|
||||
.option('--mv2', 'target manifest v2')
|
||||
.option('--analyze', 'visualize extension bundle')
|
||||
@@ -71,8 +55,9 @@ cli
|
||||
manifestVersion: flags.mv3 ? 3 : flags.mv2 ? 2 : undefined,
|
||||
configFile: flags.config,
|
||||
debug: flags.debug,
|
||||
analysis: flags.analyze ? { enabled: true } : undefined,
|
||||
filterEntrypoints: getArrayFromFlags(flags, 'filterEntrypoint'),
|
||||
analysis: {
|
||||
enabled: flags.analyze,
|
||||
},
|
||||
});
|
||||
}),
|
||||
);
|
||||
@@ -140,7 +125,7 @@ cli
|
||||
),
|
||||
);
|
||||
|
||||
cli.parse(process.argv);
|
||||
cli.parse();
|
||||
|
||||
/**
|
||||
* Wrap an action handler to add a timer, error handling, and maybe enable debug mode.
|
||||
@@ -175,22 +160,8 @@ function wrapAction(
|
||||
consola.fail(
|
||||
`Command failed after ${formatDuration(Date.now() - startTime)}`,
|
||||
);
|
||||
if (err instanceof ValidationError) {
|
||||
// Don't log these errors, they've already been logged
|
||||
} else {
|
||||
consola.error(err);
|
||||
}
|
||||
consola.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Array flags, when not passed, are either `undefined` or `[undefined]`. This function filters out
|
||||
* the
|
||||
*/
|
||||
function getArrayFromFlags<T>(flags: any, name: string): T[] | undefined {
|
||||
const array = [flags[name]].flat() as Array<T | undefined>;
|
||||
const result = array.filter((item) => item != null) as T[];
|
||||
return result.length ? result : undefined;
|
||||
}
|
||||
|
||||
@@ -230,9 +230,6 @@ export async function createViteBuilder(
|
||||
async listen() {
|
||||
await viteServer.listen(info.port);
|
||||
},
|
||||
async close() {
|
||||
await viteServer.close();
|
||||
},
|
||||
transformHtml(...args) {
|
||||
return viteServer.transformIndexHtml(...args);
|
||||
},
|
||||
|
||||
@@ -49,46 +49,22 @@ export async function createServer(
|
||||
origin,
|
||||
};
|
||||
|
||||
const buildAndOpenBrowser = async () => {
|
||||
// Build after starting the dev server so it can be used to transform HTML files
|
||||
server.currentOutput = await internalBuild(config);
|
||||
|
||||
// Open browser after everything is ready to go.
|
||||
await runner.openBrowser(config);
|
||||
};
|
||||
|
||||
/**
|
||||
* Stops the previous runner, grabs the latest config, and recreates the runner.
|
||||
*/
|
||||
const closeAndRecreateRunner = async () => {
|
||||
await runner.closeBrowser();
|
||||
config = await getLatestConfig();
|
||||
runner = await createExtensionRunner(config);
|
||||
};
|
||||
|
||||
// Server instance must be created first so its reference can be added to the internal config used
|
||||
// to pre-render entrypoints
|
||||
const server: WxtDevServer = {
|
||||
...serverInfo,
|
||||
get watcher() {
|
||||
return builderServer.watcher;
|
||||
},
|
||||
get ws() {
|
||||
return builderServer.ws;
|
||||
},
|
||||
currentOutput: undefined,
|
||||
watcher: undefined as any, // Filled out later down below
|
||||
ws: undefined as any, // Filled out later down below
|
||||
currentOutput: undefined as any, // Filled out later down below
|
||||
async start() {
|
||||
await builderServer.listen();
|
||||
config.logger.success(`Started dev server @ ${serverInfo.origin}`);
|
||||
await buildAndOpenBrowser();
|
||||
},
|
||||
async stop() {
|
||||
await runner.closeBrowser();
|
||||
await builderServer.close();
|
||||
},
|
||||
async restart() {
|
||||
await closeAndRecreateRunner();
|
||||
await buildAndOpenBrowser();
|
||||
|
||||
// Build after starting the dev server so it can be used to transform HTML files
|
||||
server.currentOutput = await internalBuild(config);
|
||||
|
||||
// Open browser after everything is ready to go.
|
||||
await runner.openBrowser(config);
|
||||
},
|
||||
transformHtml(url, html, originalUrl) {
|
||||
return builderServer.transformHtml(url, html, originalUrl);
|
||||
@@ -102,25 +78,23 @@ export async function createServer(
|
||||
reloadExtension() {
|
||||
server.ws.send('wxt:reload-extension');
|
||||
},
|
||||
async restartBrowser() {
|
||||
await closeAndRecreateRunner();
|
||||
await runner.openBrowser(config);
|
||||
},
|
||||
};
|
||||
|
||||
const getLatestConfig = () =>
|
||||
getInternalConfig(inlineConfig ?? {}, 'serve', server);
|
||||
let config = await getLatestConfig();
|
||||
|
||||
let [runner, builderServer] = await Promise.all([
|
||||
const [runner, builderServer] = await Promise.all([
|
||||
createExtensionRunner(config),
|
||||
config.builder.createServer(server),
|
||||
]);
|
||||
|
||||
server.watcher = builderServer.watcher;
|
||||
server.ws = builderServer.ws;
|
||||
|
||||
// Register content scripts for the first time after the background starts up since they're not
|
||||
// listed in the manifest
|
||||
server.ws.on('wxt:background-initialized', () => {
|
||||
if (server.currentOutput == null) return;
|
||||
reloadContentScripts(server.currentOutput.steps, config, server);
|
||||
});
|
||||
|
||||
@@ -164,39 +138,18 @@ function createFileReloader(options: {
|
||||
changeQueue.push([event, path]);
|
||||
|
||||
await fileChangedMutex.runExclusive(async () => {
|
||||
if (server.currentOutput == null) return;
|
||||
|
||||
const fileChanges = changeQueue
|
||||
.splice(0, changeQueue.length)
|
||||
.map(([_, file]) => file);
|
||||
const fileChanges = changeQueue.splice(0, changeQueue.length);
|
||||
if (fileChanges.length === 0) return;
|
||||
|
||||
const changes = detectDevChanges(
|
||||
config,
|
||||
fileChanges,
|
||||
server.currentOutput,
|
||||
);
|
||||
const changes = detectDevChanges(fileChanges, server.currentOutput);
|
||||
if (changes.type === 'no-change') return;
|
||||
|
||||
if (changes.type === 'full-restart') {
|
||||
config.logger.info('Config changed, restarting server...');
|
||||
server.restart();
|
||||
return;
|
||||
}
|
||||
|
||||
if (changes.type === 'browser-restart') {
|
||||
config.logger.info('Runner config changed, restarting browser...');
|
||||
server.restartBrowser();
|
||||
return;
|
||||
}
|
||||
|
||||
// Log the entrypoints that were effected
|
||||
config.logger.info(
|
||||
`Changed: ${Array.from(new Set(fileChanges))
|
||||
`Changed: ${Array.from(new Set(fileChanges.map((change) => change[1])))
|
||||
.map((file) => pc.dim(relative(config.root, file)))
|
||||
.join(', ')}`,
|
||||
);
|
||||
|
||||
const rebuiltNames = changes.rebuildGroups
|
||||
.flat()
|
||||
.map((entry) => {
|
||||
@@ -244,8 +197,6 @@ function reloadContentScripts(
|
||||
) {
|
||||
if (config.manifestVersion === 3) {
|
||||
steps.forEach((step) => {
|
||||
if (server.currentOutput == null) return;
|
||||
|
||||
const entry = step.entrypoints;
|
||||
if (Array.isArray(entry) || entry.type !== 'content-script') return;
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { WebExtRunInstance } from 'web-ext-run';
|
||||
import { ExtensionRunner } from '~/types';
|
||||
import { formatDuration } from '../utils/time';
|
||||
|
||||
/**
|
||||
* Create an `ExtensionRunner` backed by `web-ext`.
|
||||
@@ -10,7 +9,7 @@ export function createWebExtRunner(): ExtensionRunner {
|
||||
|
||||
return {
|
||||
async openBrowser(config) {
|
||||
const startTime = Date.now();
|
||||
config.logger.info('Opening browser...');
|
||||
|
||||
if (config.browser === 'firefox' && config.manifestVersion === 3) {
|
||||
throw Error(
|
||||
@@ -18,7 +17,7 @@ export function createWebExtRunner(): ExtensionRunner {
|
||||
);
|
||||
}
|
||||
|
||||
// Use WXT's logger instead of web-ext's built-in one.
|
||||
// Use the plugin's logger instead of web-ext's built-in one.
|
||||
const webExtLogger = await import('web-ext-run/util/logger');
|
||||
webExtLogger.consoleStream.write = ({ level, msg, name }) => {
|
||||
if (level >= ERROR_LOG_LEVEL) config.logger.error(name, msg);
|
||||
@@ -62,8 +61,7 @@ export function createWebExtRunner(): ExtensionRunner {
|
||||
const webExt = await import('web-ext-run');
|
||||
runner = await webExt.default.cmd.run(finalConfig, options);
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
config.logger.success(`Opened browser in ${formatDuration(duration)}`);
|
||||
config.logger.success('Opened!');
|
||||
},
|
||||
|
||||
async closeBrowser() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { every, some } from '~/core/utils/arrays';
|
||||
import { every } from '~/core/utils/arrays';
|
||||
|
||||
describe('Array Utils', () => {
|
||||
describe('every', () => {
|
||||
@@ -15,20 +15,4 @@ describe('Array Utils', () => {
|
||||
expect(every([1, 2, 1], (item) => item === 1)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('some', () => {
|
||||
it('should return true if one value returns true', () => {
|
||||
const array = [1, 2, 3];
|
||||
const predicate = (item: number) => item === 2;
|
||||
|
||||
expect(some(array, predicate)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if no values match', () => {
|
||||
const array = [1, 2, 3];
|
||||
const predicate = (item: number) => item === 4;
|
||||
|
||||
expect(some(array, predicate)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -47,7 +47,6 @@ describe('Entrypoint Utils', () => {
|
||||
name,
|
||||
outputDir,
|
||||
options: {},
|
||||
skipped: false,
|
||||
};
|
||||
|
||||
const actual = getEntrypointOutputFile(entrypoint, ext);
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
fakeBuildOutput,
|
||||
fakeEntrypoint,
|
||||
fakeInternalConfig,
|
||||
fakeManifestCommand,
|
||||
fakeOptionsEntrypoint,
|
||||
fakePopupEntrypoint,
|
||||
} from '../testing/fake-objects';
|
||||
@@ -53,11 +52,7 @@ describe('Manifest Utils', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
[popup],
|
||||
buildOutput,
|
||||
config,
|
||||
);
|
||||
const actual = await generateManifest([popup], buildOutput, config);
|
||||
|
||||
expect(actual).toMatchObject(expected);
|
||||
});
|
||||
@@ -84,11 +79,7 @@ describe('Manifest Utils', () => {
|
||||
default_popup: 'popup.html',
|
||||
};
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
[popup],
|
||||
buildOutput,
|
||||
config,
|
||||
);
|
||||
const actual = await generateManifest([popup], buildOutput, config);
|
||||
|
||||
expect(actual[expectedType]).toEqual(expected);
|
||||
},
|
||||
@@ -111,11 +102,7 @@ describe('Manifest Utils', () => {
|
||||
action: config.manifest.action,
|
||||
};
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
[],
|
||||
buildOutput,
|
||||
config,
|
||||
);
|
||||
const actual = await generateManifest([], buildOutput, config);
|
||||
|
||||
expect(actual).toMatchObject(expected);
|
||||
});
|
||||
@@ -144,11 +131,7 @@ describe('Manifest Utils', () => {
|
||||
page: 'options.html',
|
||||
};
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
[options],
|
||||
buildOutput,
|
||||
config,
|
||||
);
|
||||
const actual = await generateManifest([options], buildOutput, config);
|
||||
|
||||
expect(actual.options_ui).toEqual(expected);
|
||||
});
|
||||
@@ -166,11 +149,7 @@ describe('Manifest Utils', () => {
|
||||
page: 'options.html',
|
||||
};
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
[options],
|
||||
buildOutput,
|
||||
config,
|
||||
);
|
||||
const actual = await generateManifest([options], buildOutput, config);
|
||||
|
||||
expect(actual.options_ui).toEqual(expected);
|
||||
});
|
||||
@@ -200,7 +179,7 @@ describe('Manifest Utils', () => {
|
||||
service_worker: 'background.js',
|
||||
};
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
const actual = await generateManifest(
|
||||
[background],
|
||||
buildOutput,
|
||||
config,
|
||||
@@ -222,7 +201,7 @@ describe('Manifest Utils', () => {
|
||||
scripts: ['background.js'],
|
||||
};
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
const actual = await generateManifest(
|
||||
[background],
|
||||
buildOutput,
|
||||
config,
|
||||
@@ -247,7 +226,7 @@ describe('Manifest Utils', () => {
|
||||
scripts: ['background.js'],
|
||||
};
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
const actual = await generateManifest(
|
||||
[background],
|
||||
buildOutput,
|
||||
config,
|
||||
@@ -269,7 +248,7 @@ describe('Manifest Utils', () => {
|
||||
scripts: ['background.js'],
|
||||
};
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
const actual = await generateManifest(
|
||||
[background],
|
||||
buildOutput,
|
||||
config,
|
||||
@@ -295,11 +274,7 @@ describe('Manifest Utils', () => {
|
||||
});
|
||||
const config = fakeInternalConfig();
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
entrypoints,
|
||||
buildOutput,
|
||||
config,
|
||||
);
|
||||
const actual = await generateManifest(entrypoints, buildOutput, config);
|
||||
|
||||
expect(actual.icons).toEqual({
|
||||
16: 'icon-16.png',
|
||||
@@ -321,11 +296,7 @@ describe('Manifest Utils', () => {
|
||||
});
|
||||
const config = fakeInternalConfig();
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
entrypoints,
|
||||
buildOutput,
|
||||
config,
|
||||
);
|
||||
const actual = await generateManifest(entrypoints, buildOutput, config);
|
||||
|
||||
expect(actual.icons).toBeUndefined();
|
||||
});
|
||||
@@ -352,11 +323,7 @@ describe('Manifest Utils', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
entrypoints,
|
||||
buildOutput,
|
||||
config,
|
||||
);
|
||||
const actual = await generateManifest(entrypoints, buildOutput, config);
|
||||
|
||||
expect(actual.icons).toEqual(expected);
|
||||
});
|
||||
@@ -372,7 +339,6 @@ describe('Manifest Utils', () => {
|
||||
options: {
|
||||
matches: ['*://google.com/*'],
|
||||
},
|
||||
skipped: false,
|
||||
};
|
||||
const cs1Styles: OutputAsset = {
|
||||
type: 'asset',
|
||||
@@ -387,7 +353,6 @@ describe('Manifest Utils', () => {
|
||||
matches: ['*://google.com/*'],
|
||||
runAt: 'document_end',
|
||||
},
|
||||
skipped: false,
|
||||
};
|
||||
const cs2Styles: OutputAsset = {
|
||||
type: 'asset',
|
||||
@@ -402,7 +367,6 @@ describe('Manifest Utils', () => {
|
||||
matches: ['*://google.com/*'],
|
||||
runAt: 'document_end',
|
||||
},
|
||||
skipped: false,
|
||||
};
|
||||
const cs3Styles: OutputAsset = {
|
||||
type: 'asset',
|
||||
@@ -417,7 +381,6 @@ describe('Manifest Utils', () => {
|
||||
matches: ['*://duckduckgo.com/*'],
|
||||
runAt: 'document_end',
|
||||
},
|
||||
skipped: false,
|
||||
};
|
||||
const cs4Styles: OutputAsset = {
|
||||
type: 'asset',
|
||||
@@ -432,7 +395,6 @@ describe('Manifest Utils', () => {
|
||||
matches: ['*://google.com/*'],
|
||||
world: 'MAIN',
|
||||
},
|
||||
skipped: false,
|
||||
};
|
||||
const cs5Styles: OutputAsset = {
|
||||
type: 'asset',
|
||||
@@ -456,11 +418,7 @@ describe('Manifest Utils', () => {
|
||||
],
|
||||
};
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
entrypoints,
|
||||
buildOutput,
|
||||
config,
|
||||
);
|
||||
const actual = await generateManifest(entrypoints, buildOutput, config);
|
||||
|
||||
expect(actual.content_scripts).toContainEqual({
|
||||
matches: ['*://google.com/*'],
|
||||
@@ -496,7 +454,6 @@ describe('Manifest Utils', () => {
|
||||
options: {
|
||||
matches: ['*://google.com/*'],
|
||||
},
|
||||
skipped: false,
|
||||
};
|
||||
const generatedContentScript = {
|
||||
matches: ['*://google.com/*'],
|
||||
@@ -517,11 +474,7 @@ describe('Manifest Utils', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
entrypoints,
|
||||
buildOutput,
|
||||
config,
|
||||
);
|
||||
const actual = await generateManifest(entrypoints, buildOutput, config);
|
||||
|
||||
expect(actual.content_scripts).toContainEqual(userContentScript);
|
||||
expect(actual.content_scripts).toContainEqual(generatedContentScript);
|
||||
@@ -540,7 +493,6 @@ describe('Manifest Utils', () => {
|
||||
matches: ['*://google.com/*'],
|
||||
cssInjectionMode,
|
||||
},
|
||||
skipped: false,
|
||||
};
|
||||
const styles: OutputAsset = {
|
||||
type: 'asset',
|
||||
@@ -557,7 +509,7 @@ describe('Manifest Utils', () => {
|
||||
command: 'build',
|
||||
});
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
const actual = await generateManifest(
|
||||
entrypoints,
|
||||
buildOutput,
|
||||
config,
|
||||
@@ -585,7 +537,6 @@ describe('Manifest Utils', () => {
|
||||
matches: ['*://google.com/*'],
|
||||
cssInjectionMode,
|
||||
},
|
||||
skipped: false,
|
||||
};
|
||||
const styles: OutputAsset = {
|
||||
type: 'asset',
|
||||
@@ -602,7 +553,7 @@ describe('Manifest Utils', () => {
|
||||
command: 'build',
|
||||
});
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
const actual = await generateManifest(
|
||||
entrypoints,
|
||||
buildOutput,
|
||||
config,
|
||||
@@ -627,7 +578,6 @@ describe('Manifest Utils', () => {
|
||||
matches: ['*://google.com/*'],
|
||||
cssInjectionMode: 'ui',
|
||||
},
|
||||
skipped: false,
|
||||
};
|
||||
const styles: OutputAsset = {
|
||||
type: 'asset',
|
||||
@@ -645,7 +595,7 @@ describe('Manifest Utils', () => {
|
||||
manifestVersion: 3,
|
||||
});
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
const actual = await generateManifest(
|
||||
entrypoints,
|
||||
buildOutput,
|
||||
config,
|
||||
@@ -669,7 +619,6 @@ describe('Manifest Utils', () => {
|
||||
matches: ['*://google.com/*'],
|
||||
cssInjectionMode: 'ui',
|
||||
},
|
||||
skipped: false,
|
||||
};
|
||||
const styles: OutputAsset = {
|
||||
type: 'asset',
|
||||
@@ -687,7 +636,7 @@ describe('Manifest Utils', () => {
|
||||
manifestVersion: 2,
|
||||
});
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
const actual = await generateManifest(
|
||||
entrypoints,
|
||||
buildOutput,
|
||||
config,
|
||||
@@ -708,7 +657,6 @@ describe('Manifest Utils', () => {
|
||||
matches: ['*://play.google.com/books/*'],
|
||||
cssInjectionMode: 'ui',
|
||||
},
|
||||
skipped: false,
|
||||
};
|
||||
const styles: OutputAsset = {
|
||||
type: 'asset',
|
||||
@@ -726,7 +674,7 @@ describe('Manifest Utils', () => {
|
||||
manifestVersion: 3,
|
||||
});
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
const actual = await generateManifest(
|
||||
entrypoints,
|
||||
buildOutput,
|
||||
config,
|
||||
@@ -753,7 +701,6 @@ describe('Manifest Utils', () => {
|
||||
matches: ['*://google.com/*'],
|
||||
cssInjectionMode: 'ui',
|
||||
},
|
||||
skipped: false,
|
||||
};
|
||||
const styles: OutputAsset = {
|
||||
type: 'asset',
|
||||
@@ -776,11 +723,7 @@ describe('Manifest Utils', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
entrypoints,
|
||||
buildOutput,
|
||||
config,
|
||||
);
|
||||
const actual = await generateManifest(entrypoints, buildOutput, config);
|
||||
|
||||
expect(actual.web_accessible_resources).toEqual([
|
||||
{ resources: ['one.png'], matches: ['*://one.com/*'] },
|
||||
@@ -801,7 +744,6 @@ describe('Manifest Utils', () => {
|
||||
matches: ['*://google.com/*'],
|
||||
cssInjectionMode: 'ui',
|
||||
},
|
||||
skipped: false,
|
||||
};
|
||||
const styles: OutputAsset = {
|
||||
type: 'asset',
|
||||
@@ -822,11 +764,7 @@ describe('Manifest Utils', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
entrypoints,
|
||||
buildOutput,
|
||||
config,
|
||||
);
|
||||
const actual = await generateManifest(entrypoints, buildOutput, config);
|
||||
|
||||
expect(actual.web_accessible_resources).toEqual([
|
||||
'one.png',
|
||||
@@ -849,11 +787,7 @@ describe('Manifest Utils', () => {
|
||||
author: newAuthor,
|
||||
};
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
entrypoints,
|
||||
buildOutput,
|
||||
config,
|
||||
);
|
||||
const actual = await generateManifest(entrypoints, buildOutput, config);
|
||||
|
||||
expect(actual).toMatchObject(expected);
|
||||
});
|
||||
@@ -875,7 +809,7 @@ describe('Manifest Utils', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
const actual = await generateManifest(
|
||||
entrypoints,
|
||||
buildOutput,
|
||||
config,
|
||||
@@ -901,7 +835,7 @@ describe('Manifest Utils', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
const actual = await generateManifest(
|
||||
entrypoints,
|
||||
buildOutput,
|
||||
config,
|
||||
@@ -926,7 +860,7 @@ describe('Manifest Utils', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
const actual = await generateManifest(
|
||||
entrypoints,
|
||||
buildOutput,
|
||||
config,
|
||||
@@ -947,11 +881,7 @@ describe('Manifest Utils', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
entrypoints,
|
||||
buildOutput,
|
||||
config,
|
||||
);
|
||||
const actual = await generateManifest(entrypoints, buildOutput, config);
|
||||
|
||||
expect(actual.version).toBe('0.0.0');
|
||||
expect(actual.version_name).toBeUndefined();
|
||||
@@ -965,7 +895,6 @@ describe('Manifest Utils', () => {
|
||||
describe('commands', () => {
|
||||
const reloadCommandName = 'wxt:reload-extension';
|
||||
const reloadCommand = {
|
||||
description: expect.any(String),
|
||||
suggested_key: {
|
||||
default: 'Alt+R',
|
||||
},
|
||||
@@ -976,65 +905,21 @@ describe('Manifest Utils', () => {
|
||||
const output = fakeBuildOutput();
|
||||
const entrypoints = fakeArray(fakeEntrypoint);
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
entrypoints,
|
||||
output,
|
||||
config,
|
||||
);
|
||||
const actual = await generateManifest(entrypoints, output, config);
|
||||
|
||||
expect(actual.commands).toEqual({
|
||||
expect(actual.commands).toMatchObject({
|
||||
[reloadCommandName]: reloadCommand,
|
||||
});
|
||||
});
|
||||
|
||||
it('should customize the reload commands key binding if passing a custom command', async () => {
|
||||
const config = fakeInternalConfig({
|
||||
command: 'serve',
|
||||
dev: {
|
||||
reloadCommand: 'Ctrl+E',
|
||||
},
|
||||
});
|
||||
const output = fakeBuildOutput();
|
||||
const entrypoints = fakeArray(fakeEntrypoint);
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
entrypoints,
|
||||
output,
|
||||
config,
|
||||
);
|
||||
|
||||
expect(actual.commands).toEqual({
|
||||
[reloadCommandName]: {
|
||||
...reloadCommand,
|
||||
suggested_key: {
|
||||
default: 'Ctrl+E',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should not include the reload command when it's been disabled", async () => {
|
||||
const config = fakeInternalConfig({
|
||||
command: 'serve',
|
||||
dev: {
|
||||
reloadCommand: false,
|
||||
},
|
||||
});
|
||||
const output = fakeBuildOutput();
|
||||
const entrypoints = fakeArray(fakeEntrypoint);
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
entrypoints,
|
||||
output,
|
||||
config,
|
||||
);
|
||||
|
||||
expect(actual.commands).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not override any existing commands when adding the one to reload the extension', async () => {
|
||||
const customCommandName = 'custom-command';
|
||||
const customCommand = fakeManifestCommand();
|
||||
const customCommand = {
|
||||
description: 'Some other command',
|
||||
suggested_key: {
|
||||
default: 'Ctrl+H',
|
||||
},
|
||||
};
|
||||
const config = fakeInternalConfig({
|
||||
command: 'serve',
|
||||
manifest: {
|
||||
@@ -1046,52 +931,20 @@ describe('Manifest Utils', () => {
|
||||
const output = fakeBuildOutput();
|
||||
const entrypoints = fakeArray(fakeEntrypoint);
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
entrypoints,
|
||||
output,
|
||||
config,
|
||||
);
|
||||
const actual = await generateManifest(entrypoints, output, config);
|
||||
|
||||
expect(actual.commands).toEqual({
|
||||
expect(actual.commands).toMatchObject({
|
||||
[reloadCommandName]: reloadCommand,
|
||||
[customCommandName]: customCommand,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not include the command if there are already 4 others (the max)', async () => {
|
||||
const commands = {
|
||||
command1: fakeManifestCommand(),
|
||||
command2: fakeManifestCommand(),
|
||||
command3: fakeManifestCommand(),
|
||||
command4: fakeManifestCommand(),
|
||||
};
|
||||
const config = fakeInternalConfig({
|
||||
command: 'serve',
|
||||
manifest: { commands },
|
||||
});
|
||||
const output = fakeBuildOutput();
|
||||
const entrypoints = fakeArray(fakeEntrypoint);
|
||||
|
||||
const { manifest: actual, warnings } = await generateManifest(
|
||||
entrypoints,
|
||||
output,
|
||||
config,
|
||||
);
|
||||
|
||||
expect(actual.commands).toEqual(commands);
|
||||
expect(warnings).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should not include the command when building an extension', async () => {
|
||||
const config = fakeInternalConfig({ command: 'build' });
|
||||
const output = fakeBuildOutput();
|
||||
const entrypoints = fakeArray(fakeEntrypoint);
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
entrypoints,
|
||||
output,
|
||||
config,
|
||||
);
|
||||
const actual = await generateManifest(entrypoints, output, config);
|
||||
|
||||
expect(actual.commands).toBeUndefined();
|
||||
});
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { defineContentScript } from '~/sandbox';
|
||||
import { faker } from '@faker-js/faker';
|
||||
|
||||
export default defineContentScript({
|
||||
matches: [faker.string.nanoid()],
|
||||
main() {},
|
||||
});
|
||||
@@ -1,100 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
fakeArray,
|
||||
fakeContentScriptEntrypoint,
|
||||
fakeEntrypoint,
|
||||
fakeGenericEntrypoint,
|
||||
} from '../testing/fake-objects';
|
||||
import { validateEntrypoints } from '../validation';
|
||||
|
||||
describe('Validation Utils', () => {
|
||||
describe('validateEntrypoints', () => {
|
||||
it('should return no errors when there are no errors', () => {
|
||||
const entrypoints = fakeArray(fakeEntrypoint);
|
||||
const expected = {
|
||||
errors: [],
|
||||
errorCount: 0,
|
||||
warningCount: 0,
|
||||
};
|
||||
|
||||
const actual = validateEntrypoints(entrypoints);
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should return an error when exclude is not an array', () => {
|
||||
const entrypoint = fakeGenericEntrypoint({
|
||||
options: {
|
||||
// @ts-expect-error
|
||||
exclude: 0,
|
||||
},
|
||||
});
|
||||
const expected = {
|
||||
errors: [
|
||||
{
|
||||
type: 'error',
|
||||
message: '`exclude` must be an array of browser names',
|
||||
value: 0,
|
||||
entrypoint,
|
||||
},
|
||||
],
|
||||
errorCount: 1,
|
||||
warningCount: 0,
|
||||
};
|
||||
|
||||
const actual = validateEntrypoints([entrypoint]);
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should return an error when include is not an array', () => {
|
||||
const entrypoint = fakeGenericEntrypoint({
|
||||
options: {
|
||||
// @ts-expect-error
|
||||
include: 0,
|
||||
},
|
||||
});
|
||||
const expected = {
|
||||
errors: [
|
||||
{
|
||||
type: 'error',
|
||||
message: '`include` must be an array of browser names',
|
||||
value: 0,
|
||||
entrypoint,
|
||||
},
|
||||
],
|
||||
errorCount: 1,
|
||||
warningCount: 0,
|
||||
};
|
||||
|
||||
const actual = validateEntrypoints([entrypoint]);
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
it("should return an error when content scripts don't have a matches", () => {
|
||||
const entrypoint = fakeContentScriptEntrypoint({
|
||||
options: {
|
||||
// @ts-expect-error
|
||||
matches: null,
|
||||
},
|
||||
});
|
||||
const expected = {
|
||||
errors: [
|
||||
{
|
||||
type: 'error',
|
||||
message: '`matches` is required',
|
||||
value: null,
|
||||
entrypoint,
|
||||
},
|
||||
],
|
||||
errorCount: 1,
|
||||
warningCount: 0,
|
||||
};
|
||||
|
||||
const actual = validateEntrypoints([entrypoint]);
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -9,15 +9,3 @@ export function every<T>(
|
||||
if (!predicate(array[i], i)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true when any of the predicates return true;
|
||||
*/
|
||||
export function some<T>(
|
||||
array: T[],
|
||||
predicate: (item: T, index: number) => boolean,
|
||||
): boolean {
|
||||
for (let i = 0; i < array.length; i++)
|
||||
if (predicate(array[i], i)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -10,15 +10,22 @@ import {
|
||||
fakePopupEntrypoint,
|
||||
fakeOutputAsset,
|
||||
fakeOutputChunk,
|
||||
fakeInternalConfig,
|
||||
} from '~/core/utils/testing/fake-objects';
|
||||
import { BuildOutput, BuildStepOutput } from '~/types';
|
||||
|
||||
describe('Detect Dev Changes', () => {
|
||||
describe('No changes', () => {
|
||||
it("should return 'no-change' when a build hasn't finished", () => {
|
||||
const actual = detectDevChanges(
|
||||
[['unknown', '/path/to/file.ts']],
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(actual).toEqual({ type: 'no-change' });
|
||||
});
|
||||
|
||||
it("should return 'no-change' when the changed file isn't used by any of the entrypoints", () => {
|
||||
const config = fakeInternalConfig();
|
||||
const changes = ['/some/path.ts'];
|
||||
const change: [string, string] = ['unknown', '/some/path.ts'];
|
||||
const currentOutput: BuildOutput = {
|
||||
manifest: fakeManifest(),
|
||||
publicAssets: [],
|
||||
@@ -34,64 +41,18 @@ describe('Detect Dev Changes', () => {
|
||||
],
|
||||
};
|
||||
|
||||
const actual = detectDevChanges(config, changes, currentOutput);
|
||||
const actual = detectDevChanges([change], currentOutput);
|
||||
|
||||
expect(actual).toEqual({ type: 'no-change' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('wxt.config.ts', () => {
|
||||
it("should return 'full-restart' when one of the changed files is the config file", () => {
|
||||
const configFile = '/root/wxt.config.ts';
|
||||
const config = fakeInternalConfig({
|
||||
userConfigMetadata: {
|
||||
configFile,
|
||||
},
|
||||
});
|
||||
const changes = ['/root/src/public/image.svg', configFile];
|
||||
const currentOutput: BuildOutput = {
|
||||
manifest: fakeManifest(),
|
||||
publicAssets: [],
|
||||
steps: [],
|
||||
};
|
||||
const expected: DevModeChange = {
|
||||
type: 'full-restart',
|
||||
};
|
||||
|
||||
const actual = detectDevChanges(config, changes, currentOutput);
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('web-ext.config.ts', () => {
|
||||
it("should return 'browser-restart' when one of the changed files is the config file", () => {
|
||||
const runnerFile = '/root/web-ext.config.ts';
|
||||
const config = fakeInternalConfig({
|
||||
runnerConfig: {
|
||||
configFile: runnerFile,
|
||||
},
|
||||
});
|
||||
const changes = ['/root/src/public/image.svg', runnerFile];
|
||||
const currentOutput: BuildOutput = {
|
||||
manifest: fakeManifest(),
|
||||
publicAssets: [],
|
||||
steps: [],
|
||||
};
|
||||
const expected: DevModeChange = {
|
||||
type: 'browser-restart',
|
||||
};
|
||||
|
||||
const actual = detectDevChanges(config, changes, currentOutput);
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Public Assets', () => {
|
||||
it("should return 'extension-reload' without any groups to rebuild when the changed file is a public asset", () => {
|
||||
const config = fakeInternalConfig();
|
||||
const changes = ['/root/src/public/image.svg'];
|
||||
const change: [string, string] = [
|
||||
'unknown',
|
||||
'/root/src/public/image.svg',
|
||||
];
|
||||
const asset1 = fakeOutputAsset({
|
||||
fileName: 'image.svg',
|
||||
});
|
||||
@@ -112,7 +73,7 @@ describe('Detect Dev Changes', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const actual = detectDevChanges(config, changes, currentOutput);
|
||||
const actual = detectDevChanges([change], currentOutput);
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
@@ -120,7 +81,6 @@ describe('Detect Dev Changes', () => {
|
||||
|
||||
describe('Background', () => {
|
||||
it("should rebuild the background and reload the extension when the changed file in it's chunks' `moduleIds` field", () => {
|
||||
const config = fakeInternalConfig();
|
||||
const changedPath = '/root/utils/shared.ts';
|
||||
const contentScript = fakeContentScriptEntrypoint({
|
||||
inputPath: '/root/overlay.content.ts',
|
||||
@@ -160,7 +120,10 @@ describe('Detect Dev Changes', () => {
|
||||
rebuildGroups: [background],
|
||||
};
|
||||
|
||||
const actual = detectDevChanges(config, [changedPath], currentOutput);
|
||||
const actual = detectDevChanges(
|
||||
[['unknown', changedPath]],
|
||||
currentOutput,
|
||||
);
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
@@ -168,7 +131,6 @@ describe('Detect Dev Changes', () => {
|
||||
|
||||
describe('HTML Pages', () => {
|
||||
it('should rebuild then reload only the effected pages', async () => {
|
||||
const config = fakeInternalConfig();
|
||||
const changedPath = '/root/page1/index.html';
|
||||
const htmlPage1 = fakePopupEntrypoint({
|
||||
inputPath: changedPath,
|
||||
@@ -212,7 +174,10 @@ describe('Detect Dev Changes', () => {
|
||||
rebuildGroups: [[htmlPage1, htmlPage2]],
|
||||
};
|
||||
|
||||
const actual = detectDevChanges(config, [changedPath], currentOutput);
|
||||
const actual = detectDevChanges(
|
||||
[['unknown', changedPath]],
|
||||
currentOutput,
|
||||
);
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
@@ -220,7 +185,6 @@ describe('Detect Dev Changes', () => {
|
||||
|
||||
describe('Content Scripts', () => {
|
||||
it('should rebuild then reload only the effected content scripts', async () => {
|
||||
const config = fakeInternalConfig();
|
||||
const changedPath = '/root/utils/shared.ts';
|
||||
const script1 = fakeContentScriptEntrypoint({
|
||||
inputPath: '/root/overlay1.content/index.ts',
|
||||
@@ -272,7 +236,10 @@ describe('Detect Dev Changes', () => {
|
||||
rebuildGroups: [script1, script3],
|
||||
};
|
||||
|
||||
const actual = detectDevChanges(config, [changedPath], currentOutput);
|
||||
const actual = detectDevChanges(
|
||||
[['unknown', changedPath]],
|
||||
currentOutput,
|
||||
);
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
@@ -53,7 +53,6 @@ describe('findEntrypoints', () => {
|
||||
defaultIcon: { '16': '/icon/16.png' },
|
||||
defaultTitle: 'Default Title',
|
||||
},
|
||||
skipped: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -73,7 +72,6 @@ describe('findEntrypoints', () => {
|
||||
options: {
|
||||
defaultTitle: 'Title',
|
||||
},
|
||||
skipped: false,
|
||||
},
|
||||
],
|
||||
])(
|
||||
@@ -105,7 +103,6 @@ describe('findEntrypoints', () => {
|
||||
inputPath: resolve(config.entrypointsDir, 'options.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
skipped: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -126,7 +123,6 @@ describe('findEntrypoints', () => {
|
||||
options: {
|
||||
openInTab: true,
|
||||
},
|
||||
skipped: false,
|
||||
},
|
||||
],
|
||||
])(
|
||||
@@ -150,7 +146,6 @@ describe('findEntrypoints', () => {
|
||||
name: 'content',
|
||||
inputPath: resolve(config.entrypointsDir, 'content.ts'),
|
||||
outputDir: resolve(config.outDir, 'content-scripts'),
|
||||
skipped: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -160,7 +155,6 @@ describe('findEntrypoints', () => {
|
||||
name: 'overlay',
|
||||
inputPath: resolve(config.entrypointsDir, 'overlay.content.ts'),
|
||||
outputDir: resolve(config.outDir, 'content-scripts'),
|
||||
skipped: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -170,7 +164,6 @@ describe('findEntrypoints', () => {
|
||||
name: 'content',
|
||||
inputPath: resolve(config.entrypointsDir, 'content/index.ts'),
|
||||
outputDir: resolve(config.outDir, 'content-scripts'),
|
||||
skipped: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -180,7 +173,6 @@ describe('findEntrypoints', () => {
|
||||
name: 'overlay',
|
||||
inputPath: resolve(config.entrypointsDir, 'overlay.content/index.ts'),
|
||||
outputDir: resolve(config.outDir, 'content-scripts'),
|
||||
skipped: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -190,7 +182,6 @@ describe('findEntrypoints', () => {
|
||||
name: 'overlay',
|
||||
inputPath: resolve(config.entrypointsDir, 'overlay.content.tsx'),
|
||||
outputDir: resolve(config.outDir, 'content-scripts'),
|
||||
skipped: false,
|
||||
},
|
||||
],
|
||||
])(
|
||||
@@ -221,7 +212,6 @@ describe('findEntrypoints', () => {
|
||||
name: 'background',
|
||||
inputPath: resolve(config.entrypointsDir, 'background.ts'),
|
||||
outputDir: config.outDir,
|
||||
skipped: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -231,7 +221,6 @@ describe('findEntrypoints', () => {
|
||||
name: 'background',
|
||||
inputPath: resolve(config.entrypointsDir, 'background/index.ts'),
|
||||
outputDir: config.outDir,
|
||||
skipped: false,
|
||||
},
|
||||
],
|
||||
])(
|
||||
@@ -269,7 +258,6 @@ describe('findEntrypoints', () => {
|
||||
name: 'background',
|
||||
options: {},
|
||||
outputDir: config.outDir,
|
||||
skipped: false,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -290,7 +278,6 @@ describe('findEntrypoints', () => {
|
||||
name: 'injected',
|
||||
inputPath: resolve(config.entrypointsDir, path),
|
||||
outputDir: config.outDir,
|
||||
skipped: false,
|
||||
};
|
||||
const options: GenericEntrypoint['options'] = {};
|
||||
globMock.mockResolvedValueOnce([path]);
|
||||
@@ -317,7 +304,6 @@ describe('findEntrypoints', () => {
|
||||
inputPath: resolve(config.entrypointsDir, 'sandbox.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
skipped: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -328,7 +314,6 @@ describe('findEntrypoints', () => {
|
||||
inputPath: resolve(config.entrypointsDir, 'sandbox/index.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
skipped: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -339,7 +324,6 @@ describe('findEntrypoints', () => {
|
||||
inputPath: resolve(config.entrypointsDir, 'named.sandbox.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
skipped: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -350,7 +334,6 @@ describe('findEntrypoints', () => {
|
||||
inputPath: resolve(config.entrypointsDir, 'named.sandbox/index.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
skipped: false,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -363,7 +346,6 @@ describe('findEntrypoints', () => {
|
||||
inputPath: resolve(config.entrypointsDir, 'bookmarks.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
skipped: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -374,7 +356,6 @@ describe('findEntrypoints', () => {
|
||||
inputPath: resolve(config.entrypointsDir, 'bookmarks/index.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
skipped: false,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -387,7 +368,6 @@ describe('findEntrypoints', () => {
|
||||
inputPath: resolve(config.entrypointsDir, 'history.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
skipped: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -398,7 +378,6 @@ describe('findEntrypoints', () => {
|
||||
inputPath: resolve(config.entrypointsDir, 'history/index.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
skipped: false,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -411,7 +390,6 @@ describe('findEntrypoints', () => {
|
||||
inputPath: resolve(config.entrypointsDir, 'newtab.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
skipped: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -422,7 +400,6 @@ describe('findEntrypoints', () => {
|
||||
inputPath: resolve(config.entrypointsDir, 'newtab/index.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
skipped: false,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -435,7 +412,6 @@ describe('findEntrypoints', () => {
|
||||
inputPath: resolve(config.entrypointsDir, 'sidepanel.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
skipped: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -446,7 +422,6 @@ describe('findEntrypoints', () => {
|
||||
inputPath: resolve(config.entrypointsDir, 'sidepanel/index.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
skipped: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -457,7 +432,6 @@ describe('findEntrypoints', () => {
|
||||
inputPath: resolve(config.entrypointsDir, 'named.sidepanel.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
skipped: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -468,7 +442,6 @@ describe('findEntrypoints', () => {
|
||||
inputPath: resolve(config.entrypointsDir, 'named.sidepanel/index.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
skipped: false,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -481,7 +454,6 @@ describe('findEntrypoints', () => {
|
||||
inputPath: resolve(config.entrypointsDir, 'devtools.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
skipped: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -492,7 +464,6 @@ describe('findEntrypoints', () => {
|
||||
inputPath: resolve(config.entrypointsDir, 'devtools/index.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
skipped: false,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -505,7 +476,6 @@ describe('findEntrypoints', () => {
|
||||
inputPath: resolve(config.entrypointsDir, 'onboarding.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
skipped: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -516,7 +486,6 @@ describe('findEntrypoints', () => {
|
||||
inputPath: resolve(config.entrypointsDir, 'onboarding/index.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
skipped: false,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -529,7 +498,6 @@ describe('findEntrypoints', () => {
|
||||
inputPath: resolve(config.entrypointsDir, 'iframe.scss'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
skipped: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -540,7 +508,6 @@ describe('findEntrypoints', () => {
|
||||
inputPath: resolve(config.entrypointsDir, 'iframe.css'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
skipped: false,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -553,7 +520,6 @@ describe('findEntrypoints', () => {
|
||||
inputPath: resolve(config.entrypointsDir, 'content.css'),
|
||||
outputDir: resolve(config.outDir, 'content-scripts'),
|
||||
options: {},
|
||||
skipped: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -564,7 +530,6 @@ describe('findEntrypoints', () => {
|
||||
inputPath: resolve(config.entrypointsDir, 'overlay.content.css'),
|
||||
outputDir: resolve(config.outDir, 'content-scripts'),
|
||||
options: {},
|
||||
skipped: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -575,7 +540,6 @@ describe('findEntrypoints', () => {
|
||||
inputPath: resolve(config.entrypointsDir, 'content/index.css'),
|
||||
outputDir: resolve(config.outDir, 'content-scripts'),
|
||||
options: {},
|
||||
skipped: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -586,7 +550,6 @@ describe('findEntrypoints', () => {
|
||||
inputPath: resolve(config.entrypointsDir, 'overlay.content/index.css'),
|
||||
outputDir: resolve(config.outDir, 'content-scripts'),
|
||||
options: {},
|
||||
skipped: false,
|
||||
},
|
||||
],
|
||||
])('should find entrypoint for %s', async (path, expected) => {
|
||||
@@ -774,29 +737,4 @@ describe('findEntrypoints', () => {
|
||||
expect(entrypoints).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('filterEntrypoints option', () => {
|
||||
it('should control entrypoints accessible', async () => {
|
||||
globMock.mockResolvedValue([
|
||||
'options/index.html',
|
||||
'popup/index.html',
|
||||
'ui.content/index.ts',
|
||||
'injected.content/index.ts',
|
||||
]);
|
||||
importEntrypointFileMock.mockResolvedValue({});
|
||||
const filterEntrypoints = ['popup', 'ui'];
|
||||
const config = fakeInternalConfig({
|
||||
root: '/',
|
||||
entrypointsDir: resolve('/src/entrypoints'),
|
||||
outDir: resolve('.output'),
|
||||
command: 'build',
|
||||
filterEntrypoints: new Set(filterEntrypoints),
|
||||
});
|
||||
|
||||
const entrypoints = await findEntrypoints(config);
|
||||
const names = entrypoints.map((item) => item.name);
|
||||
expect(names).toHaveLength(2);
|
||||
expect(names).toEqual(filterEntrypoints);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,7 +8,6 @@ const background: Entrypoint = {
|
||||
inputPath: '/background.ts',
|
||||
outputDir: '/.output/background',
|
||||
options: {},
|
||||
skipped: false,
|
||||
};
|
||||
const contentScript: Entrypoint = {
|
||||
type: 'content-script',
|
||||
@@ -18,7 +17,6 @@ const contentScript: Entrypoint = {
|
||||
options: {
|
||||
matches: ['<all_urls>'],
|
||||
},
|
||||
skipped: false,
|
||||
};
|
||||
const unlistedScript: Entrypoint = {
|
||||
type: 'unlisted-script',
|
||||
@@ -26,7 +24,6 @@ const unlistedScript: Entrypoint = {
|
||||
inputPath: '/injected.ts',
|
||||
outputDir: '/.output/injected',
|
||||
options: {},
|
||||
skipped: false,
|
||||
};
|
||||
const popup: Entrypoint = {
|
||||
type: 'popup',
|
||||
@@ -34,7 +31,6 @@ const popup: Entrypoint = {
|
||||
inputPath: '/popup.html',
|
||||
outputDir: '/.output/popup',
|
||||
options: {},
|
||||
skipped: false,
|
||||
};
|
||||
const unlistedPage: Entrypoint = {
|
||||
type: 'unlisted-page',
|
||||
@@ -42,7 +38,6 @@ const unlistedPage: Entrypoint = {
|
||||
inputPath: '/onboarding.html',
|
||||
outputDir: '/.output/onboarding',
|
||||
options: {},
|
||||
skipped: false,
|
||||
};
|
||||
const options: Entrypoint = {
|
||||
type: 'options',
|
||||
@@ -50,7 +45,6 @@ const options: Entrypoint = {
|
||||
inputPath: '/options.html',
|
||||
outputDir: '/.output/options',
|
||||
options: {},
|
||||
skipped: false,
|
||||
};
|
||||
const sandbox1: Entrypoint = {
|
||||
type: 'sandbox',
|
||||
@@ -58,7 +52,6 @@ const sandbox1: Entrypoint = {
|
||||
inputPath: '/sandbox1.html',
|
||||
outputDir: '/.output/sandbox1',
|
||||
options: {},
|
||||
skipped: false,
|
||||
};
|
||||
const sandbox2: Entrypoint = {
|
||||
type: 'sandbox',
|
||||
@@ -66,7 +59,6 @@ const sandbox2: Entrypoint = {
|
||||
inputPath: '/sandbox2.html',
|
||||
outputDir: '/.output/sandbox2',
|
||||
options: {},
|
||||
skipped: false,
|
||||
};
|
||||
const unlistedStyle: Entrypoint = {
|
||||
type: 'unlisted-style',
|
||||
@@ -74,7 +66,6 @@ const unlistedStyle: Entrypoint = {
|
||||
inputPath: '/injected.scss',
|
||||
outputDir: '/.output',
|
||||
options: {},
|
||||
skipped: false,
|
||||
};
|
||||
const contentScriptStyle: Entrypoint = {
|
||||
type: 'content-script-style',
|
||||
@@ -82,7 +73,6 @@ const contentScriptStyle: Entrypoint = {
|
||||
inputPath: '/overlay.content.scss',
|
||||
outputDir: '/.output/content-scripts',
|
||||
options: {},
|
||||
skipped: false,
|
||||
};
|
||||
|
||||
describe('groupEntrypoints', () => {
|
||||
|
||||
@@ -2,7 +2,6 @@ import { describe, expect, it } from 'vitest';
|
||||
import { importEntrypointFile } from '~/core/utils/building';
|
||||
import { fakeInternalConfig } from '~/core/utils/testing/fake-objects';
|
||||
import { resolve } from 'node:path';
|
||||
import { unnormalizePath } from '../../paths';
|
||||
|
||||
const entrypointPath = (filename: string) =>
|
||||
resolve('src/core/utils/__tests__/test-entrypoints', filename);
|
||||
@@ -39,15 +38,4 @@ describe('importEntrypointFile', () => {
|
||||
|
||||
expect(actual).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should throw a custom error message when an imported variable is used before main', async () => {
|
||||
const filePath = unnormalizePath(
|
||||
'../src/core/utils/__tests__/test-entrypoints/imported-option.ts',
|
||||
);
|
||||
await expect(() =>
|
||||
importEntrypointFile(entrypointPath('imported-option.ts'), config),
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`[Error: ${filePath}: Cannot use imported variable "faker" outside the main function. See https://wxt.dev/guide/entrypoints.html#side-effects]`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,9 +6,11 @@ import {
|
||||
} from '~/types';
|
||||
import { getPublicFiles } from '~/core/utils/fs';
|
||||
import fs from 'fs-extra';
|
||||
import { dirname, resolve } from 'path';
|
||||
import { dirname, resolve, extname } from 'node:path';
|
||||
import type { Ora } from 'ora';
|
||||
import pc from 'picocolors';
|
||||
import { unnormalizePath } from '~/core/utils/paths';
|
||||
import { convertMessagesToManifest, readMessagesFile } from '~/i18n/node';
|
||||
|
||||
export async function buildEntrypoints(
|
||||
groups: EntrypointGroup[],
|
||||
@@ -25,7 +27,13 @@ export async function buildEntrypoints(
|
||||
spinner.text = pc.dim(`[${i + 1}/${groups.length}]`) + ` ${groupNames}`;
|
||||
steps.push(await config.builder.build(group));
|
||||
}
|
||||
const publicAssets = await copyPublicDirectory(config);
|
||||
|
||||
const publicAssets = (
|
||||
await Promise.all([
|
||||
copyPublicDirectory(config),
|
||||
copyLocalesDirectory(config),
|
||||
])
|
||||
).flat();
|
||||
|
||||
return { publicAssets, steps };
|
||||
}
|
||||
@@ -51,3 +59,31 @@ async function copyPublicDirectory(
|
||||
|
||||
return publicAssets;
|
||||
}
|
||||
|
||||
async function copyLocalesDirectory(
|
||||
config: InternalConfig,
|
||||
): Promise<BuildOutput['publicAssets']> {
|
||||
const localesExist = await fs.exists(config.localesDir);
|
||||
if (!localesExist || config.manifest.default_locale == null) return [];
|
||||
|
||||
const files = await fs.readdir(config.localesDir);
|
||||
|
||||
return await Promise.all(
|
||||
files.map(async (file) => {
|
||||
const locale = file.replace(extname(file), '');
|
||||
const fileName = unnormalizePath(`_locales/${locale}/messages.json`);
|
||||
const srcPath = resolve(config.localesDir, file);
|
||||
const outPath = resolve(config.outDir, fileName);
|
||||
|
||||
const messages = await readMessagesFile(srcPath);
|
||||
const json = convertMessagesToManifest(messages);
|
||||
|
||||
await fs.ensureDir(dirname(outPath));
|
||||
await fs.writeJson(outPath, json);
|
||||
return {
|
||||
fileName,
|
||||
type: 'asset',
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,11 +2,10 @@ import {
|
||||
BuildOutput,
|
||||
BuildStepOutput,
|
||||
EntrypointGroup,
|
||||
InternalConfig,
|
||||
OutputAsset,
|
||||
OutputFile,
|
||||
} from '~/types';
|
||||
import { every, some } from '~/core/utils/arrays';
|
||||
import { every } from '~/core/utils/arrays';
|
||||
import { normalizePath } from '~/core/utils/paths';
|
||||
|
||||
/**
|
||||
@@ -30,21 +29,10 @@ import { normalizePath } from '~/core/utils/paths';
|
||||
* - Config file changed (wxt.config.ts, .env, web-ext.config.ts, etc)
|
||||
*/
|
||||
export function detectDevChanges(
|
||||
config: InternalConfig,
|
||||
changedFiles: string[],
|
||||
currentOutput: BuildOutput,
|
||||
changedFiles: [event: string, path: string][],
|
||||
currentOutput: BuildOutput | undefined,
|
||||
): DevModeChange {
|
||||
const isConfigChange = some(
|
||||
changedFiles,
|
||||
(file) => file === config.userConfigMetadata.configFile,
|
||||
);
|
||||
if (isConfigChange) return { type: 'full-restart' };
|
||||
|
||||
const isRunnerChange = some(
|
||||
changedFiles,
|
||||
(file) => file === config.runnerConfig.configFile,
|
||||
);
|
||||
if (isRunnerChange) return { type: 'browser-restart' };
|
||||
if (currentOutput == null) return { type: 'no-change' };
|
||||
|
||||
const changedSteps = new Set(
|
||||
changedFiles.flatMap((changedFile) =>
|
||||
@@ -81,7 +69,7 @@ export function detectDevChanges(
|
||||
|
||||
const isOnlyHtmlChanges =
|
||||
changedFiles.length > 0 &&
|
||||
every(changedFiles, (file) => file.endsWith('.html'));
|
||||
every(changedFiles, ([_, file]) => file.endsWith('.html'));
|
||||
if (isOnlyHtmlChanges) {
|
||||
return {
|
||||
type: 'html-reload',
|
||||
@@ -116,11 +104,11 @@ export function detectDevChanges(
|
||||
* For a single change, return all the step of the build output that were effected by it.
|
||||
*/
|
||||
function findEffectedSteps(
|
||||
changedFile: string,
|
||||
changedFile: [event: string, path: string],
|
||||
currentOutput: BuildOutput,
|
||||
): DetectedChange[] {
|
||||
const changes: DetectedChange[] = [];
|
||||
const changedPath = normalizePath(changedFile);
|
||||
const changedPath = normalizePath(changedFile[1]);
|
||||
|
||||
const isChunkEffected = (chunk: OutputFile): boolean =>
|
||||
// If it's an HTML file with the same path, is is effected because HTML files need to be pre-rendered
|
||||
@@ -151,9 +139,8 @@ export type DevModeChange =
|
||||
| NoChange
|
||||
| HtmlReload
|
||||
| ExtensionReload
|
||||
| ContentScriptReload
|
||||
| FullRestart
|
||||
| BrowserRestart;
|
||||
| ContentScriptReload;
|
||||
// | BrowserRestart
|
||||
|
||||
interface NoChange {
|
||||
type: 'no-change';
|
||||
@@ -170,14 +157,6 @@ interface RebuildChange {
|
||||
cachedOutput: BuildOutput;
|
||||
}
|
||||
|
||||
interface FullRestart {
|
||||
type: 'full-restart';
|
||||
}
|
||||
|
||||
interface BrowserRestart {
|
||||
type: 'browser-restart';
|
||||
}
|
||||
|
||||
interface HtmlReload extends RebuildChange {
|
||||
type: 'html-reload';
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
} from '~/core/utils/entrypoints';
|
||||
import { VIRTUAL_NOOP_BACKGROUND_MODULE_ID } from '~/core/utils/constants';
|
||||
import { CSS_EXTENSIONS_PATTERN } from '~/core/utils/paths';
|
||||
import pc from 'picocolors';
|
||||
|
||||
/**
|
||||
* Return entrypoints and their configuration by looking through the project's files.
|
||||
@@ -49,14 +48,7 @@ export async function findEntrypoints(
|
||||
);
|
||||
if (matchingGlob) {
|
||||
const type = PATH_GLOB_TO_TYPE_MAP[matchingGlob];
|
||||
results.push({
|
||||
name,
|
||||
inputPath,
|
||||
type,
|
||||
skipped:
|
||||
config.filterEntrypoints != null &&
|
||||
!config.filterEntrypoints.has(name),
|
||||
});
|
||||
results.push({ name, inputPath, type });
|
||||
}
|
||||
return results;
|
||||
}, []);
|
||||
@@ -114,22 +106,11 @@ export async function findEntrypoints(
|
||||
inputPath: VIRTUAL_NOOP_BACKGROUND_MODULE_ID,
|
||||
name: 'background',
|
||||
type: 'background',
|
||||
skipped: false,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
config.logger.debug('All entrypoints:', entrypoints);
|
||||
const skippedEntrypointNames = entrypointInfos
|
||||
.filter((item) => item.skipped)
|
||||
.map((item) => item.name);
|
||||
if (skippedEntrypointNames.length) {
|
||||
config.logger.warn(
|
||||
`Filter excluded the following entrypoints:\n${skippedEntrypointNames
|
||||
.map((item) => `${pc.dim('-')} ${pc.cyan(item)}`)
|
||||
.join('\n')}`,
|
||||
);
|
||||
}
|
||||
const targetEntrypoints = entrypoints.filter((entry) => {
|
||||
const { include, exclude } = entry.options;
|
||||
if (include?.length && exclude?.length) {
|
||||
@@ -144,9 +125,6 @@ export async function findEntrypoints(
|
||||
if (include?.length && !exclude?.length) {
|
||||
return include.includes(config.browser);
|
||||
}
|
||||
if (skippedEntrypointNames.includes(entry.name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
@@ -158,10 +136,6 @@ interface EntrypointInfo {
|
||||
name: string;
|
||||
inputPath: string;
|
||||
type: Entrypoint['type'];
|
||||
/**
|
||||
* @default false
|
||||
*/
|
||||
skipped: boolean;
|
||||
}
|
||||
|
||||
function preventDuplicateEntrypointNames(
|
||||
@@ -228,7 +202,7 @@ function getHtmlBaseOptions(document: Document): BaseEntrypointOptions {
|
||||
*/
|
||||
async function getPopupEntrypoint(
|
||||
config: InternalConfig,
|
||||
{ inputPath, name, skipped }: EntrypointInfo,
|
||||
{ inputPath, name }: EntrypointInfo,
|
||||
): Promise<PopupEntrypoint> {
|
||||
const content = await fs.readFile(inputPath, 'utf-8');
|
||||
const { document } = parseHTML(content);
|
||||
@@ -273,7 +247,6 @@ async function getPopupEntrypoint(
|
||||
options,
|
||||
inputPath,
|
||||
outputDir: config.outDir,
|
||||
skipped,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -283,7 +256,7 @@ async function getPopupEntrypoint(
|
||||
*/
|
||||
async function getOptionsEntrypoint(
|
||||
config: InternalConfig,
|
||||
{ inputPath, name, skipped }: EntrypointInfo,
|
||||
{ inputPath, name }: EntrypointInfo,
|
||||
): Promise<OptionsEntrypoint> {
|
||||
const content = await fs.readFile(inputPath, 'utf-8');
|
||||
const { document } = parseHTML(content);
|
||||
@@ -317,7 +290,6 @@ async function getOptionsEntrypoint(
|
||||
options,
|
||||
inputPath,
|
||||
outputDir: config.outDir,
|
||||
skipped,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -327,7 +299,7 @@ async function getOptionsEntrypoint(
|
||||
*/
|
||||
async function getUnlistedPageEntrypoint(
|
||||
config: InternalConfig,
|
||||
{ inputPath, name, skipped }: EntrypointInfo,
|
||||
{ inputPath, name }: EntrypointInfo,
|
||||
): Promise<GenericEntrypoint> {
|
||||
const content = await fs.readFile(inputPath, 'utf-8');
|
||||
const { document } = parseHTML(content);
|
||||
@@ -338,7 +310,6 @@ async function getUnlistedPageEntrypoint(
|
||||
inputPath,
|
||||
outputDir: config.outDir,
|
||||
options: getHtmlBaseOptions(document),
|
||||
skipped,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -348,7 +319,7 @@ async function getUnlistedPageEntrypoint(
|
||||
*/
|
||||
async function getUnlistedScriptEntrypoint(
|
||||
config: InternalConfig,
|
||||
{ inputPath, name, skipped }: EntrypointInfo,
|
||||
{ inputPath, name }: EntrypointInfo,
|
||||
): Promise<GenericEntrypoint> {
|
||||
const defaultExport = await importEntrypointFile<UnlistedScriptDefinition>(
|
||||
inputPath,
|
||||
@@ -367,7 +338,6 @@ async function getUnlistedScriptEntrypoint(
|
||||
inputPath,
|
||||
outputDir: config.outDir,
|
||||
options,
|
||||
skipped,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -376,7 +346,7 @@ async function getUnlistedScriptEntrypoint(
|
||||
*/
|
||||
async function getBackgroundEntrypoint(
|
||||
config: InternalConfig,
|
||||
{ inputPath, name, skipped }: EntrypointInfo,
|
||||
{ inputPath, name }: EntrypointInfo,
|
||||
): Promise<BackgroundEntrypoint> {
|
||||
let options: Omit<BackgroundDefinition, 'main'> = {};
|
||||
if (inputPath !== VIRTUAL_NOOP_BACKGROUND_MODULE_ID) {
|
||||
@@ -402,7 +372,6 @@ async function getBackgroundEntrypoint(
|
||||
type: resolvePerBrowserOption(options.type, config.browser),
|
||||
persistent: resolvePerBrowserOption(options.persistent, config.browser),
|
||||
},
|
||||
skipped,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -411,7 +380,7 @@ async function getBackgroundEntrypoint(
|
||||
*/
|
||||
async function getContentScriptEntrypoint(
|
||||
config: InternalConfig,
|
||||
{ inputPath, name, skipped }: EntrypointInfo,
|
||||
{ inputPath, name }: EntrypointInfo,
|
||||
): Promise<ContentScriptEntrypoint> {
|
||||
const { main: _, ...options } =
|
||||
await importEntrypointFile<ContentScriptDefinition>(inputPath, config);
|
||||
@@ -426,7 +395,6 @@ async function getContentScriptEntrypoint(
|
||||
inputPath,
|
||||
outputDir: resolve(config.outDir, CONTENT_SCRIPT_OUT_DIR),
|
||||
options,
|
||||
skipped,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@ import { getEntrypointBundlePath } from '~/core/utils/entrypoints';
|
||||
import { getUnimportOptions } from '~/core/utils/unimport';
|
||||
import { getEntrypointGlobals, getGlobals } from '~/core/utils/globals';
|
||||
import { normalizePath } from '~/core/utils/paths';
|
||||
import path from 'node:path';
|
||||
import { Message, parseI18nMessages } from '~/core/utils/i18n';
|
||||
import { writeFileIfDifferent, getPublicFiles } from '~/core/utils/fs';
|
||||
import glob from 'fast-glob';
|
||||
import { Message, PREDEFINED_MESSAGES, readMessagesFile } from '~/i18n/node';
|
||||
|
||||
/**
|
||||
* Generate and write all the files inside the `InternalConfig.typesDir` directory.
|
||||
@@ -79,10 +79,8 @@ import "wxt/browser";
|
||||
declare module "wxt/browser" {
|
||||
export type PublicPath =
|
||||
{{ union }}
|
||||
type HtmlPublicPath = Extract<PublicPath, \`\${string}.html\`>
|
||||
export interface WxtRuntime extends Runtime.Static {
|
||||
getURL(path: PublicPath): string;
|
||||
getURL(path: \`\${HtmlPublicPath}\${string}\`): string;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -115,30 +113,38 @@ declare module "wxt/browser" {
|
||||
}
|
||||
|
||||
export interface WxtI18n extends I18n.Static {
|
||||
{{ overrides }}
|
||||
{{ browserOverrides }}
|
||||
}
|
||||
}
|
||||
|
||||
declare module "wxt/i18n" {
|
||||
export interface WxtMessageSchema {
|
||||
t: {
|
||||
{{ translationTOverrides }}
|
||||
};
|
||||
tp: {
|
||||
{{ translationTpOverrides }}
|
||||
};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
let messages: Message[];
|
||||
if (defaultLocale) {
|
||||
const defaultLocalePath = path.resolve(
|
||||
config.publicDir,
|
||||
'_locales',
|
||||
defaultLocale,
|
||||
'messages.json',
|
||||
);
|
||||
const content = JSON.parse(await fs.readFile(defaultLocalePath, 'utf-8'));
|
||||
messages = parseI18nMessages(content);
|
||||
const [defaultLocalePath] = await glob(`${defaultLocale}.*`, {
|
||||
cwd: config.localesDir,
|
||||
absolute: true,
|
||||
});
|
||||
messages = await readMessagesFile(defaultLocalePath);
|
||||
} else {
|
||||
messages = parseI18nMessages({});
|
||||
messages = PREDEFINED_MESSAGES;
|
||||
}
|
||||
|
||||
const overrides = messages.map((message) => {
|
||||
return ` /**
|
||||
* ${message.description ?? 'No message description.'}
|
||||
* ${message.entry.description ?? 'No message description.'}
|
||||
*
|
||||
* "${message.message}"
|
||||
* "${message.entry.message}"
|
||||
*/
|
||||
getMessage(
|
||||
messageName: "${message.name}",
|
||||
@@ -148,7 +154,22 @@ declare module "wxt/browser" {
|
||||
});
|
||||
await writeFileIfDifferent(
|
||||
filePath,
|
||||
template.replace('{{ overrides }}', overrides.join('\n')),
|
||||
template
|
||||
.replace('{{ browserOverrides }}', overrides.join('\n'))
|
||||
.replace(
|
||||
'{{ translationTOverrides }}',
|
||||
messages
|
||||
.filter((message) => !message.isPlural)
|
||||
.map((message) => ` "${message.name}": any;`)
|
||||
.join('\n'),
|
||||
)
|
||||
.replace(
|
||||
'{{ translationTpOverrides }}',
|
||||
messages
|
||||
.filter((message) => message.isPlural)
|
||||
.map((message) => ` "${message.name}": any;`)
|
||||
.join('\n'),
|
||||
),
|
||||
);
|
||||
|
||||
return filePath;
|
||||
|
||||
@@ -72,14 +72,11 @@ export async function getInternalConfig(
|
||||
srcDir,
|
||||
mergedConfig.entrypointsDir ?? 'entrypoints',
|
||||
);
|
||||
const filterEntrypoints = !!mergedConfig.filterEntrypoints?.length
|
||||
? new Set(mergedConfig.filterEntrypoints)
|
||||
: undefined;
|
||||
const publicDir = path.resolve(srcDir, mergedConfig.publicDir ?? 'public');
|
||||
const localesDir = path.resolve(srcDir, mergedConfig.localesDir ?? 'locales');
|
||||
const typesDir = path.resolve(wxtDir, 'types');
|
||||
const outBaseDir = path.resolve(root, mergedConfig.outDir ?? '.output');
|
||||
const outDir = path.resolve(outBaseDir, `${browser}-mv${manifestVersion}`);
|
||||
const reloadCommand = mergedConfig.dev?.reloadCommand ?? 'Alt+R';
|
||||
|
||||
const runnerConfig = await loadConfig<ExtensionRunnerConfig>({
|
||||
name: 'web-ext',
|
||||
@@ -105,7 +102,6 @@ export async function getInternalConfig(
|
||||
command,
|
||||
debug,
|
||||
entrypointsDir,
|
||||
filterEntrypoints,
|
||||
env,
|
||||
fsCache: createFsCache(wxtDir),
|
||||
imports: mergedConfig.imports ?? {},
|
||||
@@ -116,6 +112,7 @@ export async function getInternalConfig(
|
||||
outBaseDir,
|
||||
outDir,
|
||||
publicDir,
|
||||
localesDir,
|
||||
root,
|
||||
runnerConfig,
|
||||
srcDir,
|
||||
@@ -137,9 +134,6 @@ export async function getInternalConfig(
|
||||
mergedConfig.experimental?.includeBrowserPolyfill ?? true,
|
||||
},
|
||||
server,
|
||||
dev: {
|
||||
reloadCommand,
|
||||
},
|
||||
};
|
||||
|
||||
const builder = await createViteBuilder(
|
||||
@@ -199,13 +193,12 @@ function mergeInlineConfig(
|
||||
configFile: inlineConfig.configFile,
|
||||
debug: inlineConfig.debug ?? userConfig.debug,
|
||||
entrypointsDir: inlineConfig.entrypointsDir ?? userConfig.entrypointsDir,
|
||||
filterEntrypoints:
|
||||
inlineConfig.filterEntrypoints ?? userConfig.filterEntrypoints,
|
||||
imports,
|
||||
logger: inlineConfig.logger ?? userConfig.logger,
|
||||
manifest,
|
||||
mode: inlineConfig.mode ?? userConfig.mode,
|
||||
publicDir: inlineConfig.publicDir ?? userConfig.publicDir,
|
||||
localesDir: inlineConfig.localesDir ?? userConfig.localesDir,
|
||||
runner,
|
||||
srcDir: inlineConfig.srcDir ?? userConfig.srcDir,
|
||||
outDir: inlineConfig.outDir ?? userConfig.outDir,
|
||||
@@ -225,10 +218,6 @@ function mergeInlineConfig(
|
||||
},
|
||||
vite: undefined,
|
||||
transformManifest: undefined,
|
||||
dev: {
|
||||
...userConfig.dev,
|
||||
...inlineConfig.dev,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import createJITI, { TransformOptions as JitiTransformOptions } from 'jiti';
|
||||
import { InternalConfig } from '~/types';
|
||||
import { createUnimport } from 'unimport';
|
||||
import fs from 'fs-extra';
|
||||
import { relative, resolve } from 'node:path';
|
||||
import { resolve } from 'node:path';
|
||||
import { getUnimportOptions } from '~/core/utils/unimport';
|
||||
import { removeProjectImportStatements } from '~/core/utils/strings';
|
||||
import { normalizePath } from '~/core/utils/paths';
|
||||
@@ -89,17 +89,8 @@ export async function importEntrypointFile<T>(
|
||||
const res = await jiti(path);
|
||||
return res.default;
|
||||
} catch (err) {
|
||||
if (err instanceof ReferenceError) {
|
||||
// "XXX is not defined" - usually due to WXT removing imports
|
||||
const variableName = err.message.replace(' is not defined', '');
|
||||
const filePath = relative(config.root, path);
|
||||
throw Error(
|
||||
`${filePath}: Cannot use imported variable "${variableName}" outside the main function. See https://wxt.dev/guide/entrypoints.html#side-effects`,
|
||||
{ cause: err },
|
||||
);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
config.logger.error(err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { findEntrypoints } from './find-entrypoints';
|
||||
import { InternalConfig, BuildOutput, Entrypoint } from '~/types';
|
||||
import { InternalConfig, BuildOutput } from '~/types';
|
||||
import pc from 'picocolors';
|
||||
import fs from 'fs-extra';
|
||||
import { groupEntrypoints } from './group-entrypoints';
|
||||
@@ -8,15 +8,6 @@ import { printBuildSummary } from '~/core/utils/log';
|
||||
import glob from 'fast-glob';
|
||||
import { unnormalizePath } from '~/core/utils/paths';
|
||||
import { rebuild } from './rebuild';
|
||||
import managePath from 'manage-path';
|
||||
import { resolve, relative } from 'node:path';
|
||||
import {
|
||||
ValidationError,
|
||||
ValidationResult,
|
||||
ValidationResults,
|
||||
validateEntrypoints,
|
||||
} from '../validation';
|
||||
import consola from 'consola';
|
||||
|
||||
/**
|
||||
* Builds the extension based on an internal config. No more config discovery is performed, the
|
||||
@@ -46,24 +37,8 @@ export async function internalBuild(
|
||||
|
||||
const entrypoints = await findEntrypoints(config);
|
||||
config.logger.debug('Detected entrypoints:', entrypoints);
|
||||
|
||||
const validationResults = validateEntrypoints(entrypoints);
|
||||
if (validationResults.errorCount + validationResults.warningCount > 0) {
|
||||
printValidationResults(config, validationResults);
|
||||
}
|
||||
if (validationResults.errorCount > 0) {
|
||||
throw new ValidationError(`Entrypoint validation failed`, {
|
||||
cause: validationResults,
|
||||
});
|
||||
}
|
||||
|
||||
const groups = groupEntrypoints(entrypoints);
|
||||
const { output, warnings } = await rebuild(
|
||||
config,
|
||||
entrypoints,
|
||||
groups,
|
||||
undefined,
|
||||
);
|
||||
const { output } = await rebuild(config, entrypoints, groups, undefined);
|
||||
|
||||
// Post-build
|
||||
await printBuildSummary(
|
||||
@@ -73,10 +48,6 @@ export async function internalBuild(
|
||||
config,
|
||||
);
|
||||
|
||||
for (const warning of warnings) {
|
||||
config.logger.warn(...warning);
|
||||
}
|
||||
|
||||
if (config.analysis.enabled) {
|
||||
await combineAnalysisStats(config);
|
||||
config.logger.info(
|
||||
@@ -95,10 +66,6 @@ async function combineAnalysisStats(config: InternalConfig): Promise<void> {
|
||||
});
|
||||
const absolutePaths = unixFiles.map(unnormalizePath);
|
||||
|
||||
const alterPath = managePath(process.env);
|
||||
// Add subdependency path for PNPM shamefully-hoist=false
|
||||
alterPath.push(resolve(config.root, 'node_modules/wxt/node_modules/.bin'));
|
||||
|
||||
await execaCommand(
|
||||
`rollup-plugin-visualizer ${absolutePaths.join(' ')} --template ${
|
||||
config.analysis.template
|
||||
@@ -106,33 +73,3 @@ async function combineAnalysisStats(config: InternalConfig): Promise<void> {
|
||||
{ cwd: config.root, stdio: 'inherit' },
|
||||
);
|
||||
}
|
||||
|
||||
function printValidationResults(
|
||||
config: InternalConfig,
|
||||
{ errorCount, errors, warningCount }: ValidationResults,
|
||||
) {
|
||||
(errorCount > 0 ? config.logger.error : config.logger.warn)(
|
||||
`Entrypoint validation failed: ${errorCount} error${
|
||||
errorCount === 1 ? '' : 's'
|
||||
}, ${warningCount} warning${warningCount === 1 ? '' : 's'}`,
|
||||
);
|
||||
|
||||
const cwd = process.cwd();
|
||||
const entrypointErrors = errors.reduce((map, error) => {
|
||||
const entryErrors = map.get(error.entrypoint) ?? [];
|
||||
entryErrors.push(error);
|
||||
map.set(error.entrypoint, entryErrors);
|
||||
return map;
|
||||
}, new Map<Entrypoint, ValidationResult[]>());
|
||||
|
||||
Array.from(entrypointErrors.entries()).forEach(([entrypoint, errors]) => {
|
||||
consola.log(relative(cwd, entrypoint.inputPath));
|
||||
console.log();
|
||||
errors.forEach((err) => {
|
||||
const type = err.type === 'error' ? pc.red('ERROR') : pc.yellow('WARN');
|
||||
const recieved = pc.dim(`(recieved: ${JSON.stringify(err.value)})`);
|
||||
consola.log(` - ${type} ${err.message} ${recieved}`);
|
||||
});
|
||||
console.log();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -33,11 +33,7 @@ export async function rebuild(
|
||||
steps: [],
|
||||
publicAssets: [],
|
||||
},
|
||||
): Promise<{
|
||||
output: BuildOutput;
|
||||
manifest: Manifest.WebExtensionManifest;
|
||||
warnings: any[][];
|
||||
}> {
|
||||
): Promise<{ output: BuildOutput; manifest: Manifest.WebExtensionManifest }> {
|
||||
const { default: ora } = await import('ora');
|
||||
const spinner = ora(`Preparing...`).start();
|
||||
|
||||
@@ -55,8 +51,11 @@ export async function rebuild(
|
||||
publicAssets: [...existingOutput.publicAssets, ...newOutput.publicAssets],
|
||||
};
|
||||
|
||||
const { manifest: newManifest, warnings: manifestWarnings } =
|
||||
await generateManifest(allEntrypoints, mergedOutput, config);
|
||||
const newManifest = await generateManifest(
|
||||
allEntrypoints,
|
||||
mergedOutput,
|
||||
config,
|
||||
);
|
||||
const finalOutput: BuildOutput = {
|
||||
manifest: newManifest,
|
||||
...newOutput,
|
||||
@@ -78,6 +77,5 @@ export async function rebuild(
|
||||
],
|
||||
},
|
||||
manifest: newManifest,
|
||||
warnings: manifestWarnings,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
export interface Message {
|
||||
name: string;
|
||||
message: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
const predefinedMessages = {
|
||||
'@@extension_id': {
|
||||
message: '<browser.runtime.id>',
|
||||
description:
|
||||
"The extension or app ID; you might use this string to construct URLs for resources inside the extension. Even unlocalized extensions can use this message.\nNote: You can't use this message in a manifest file.",
|
||||
},
|
||||
'@@ui_locale': {
|
||||
message: '<browser.i18n.getUiLocale()>',
|
||||
description: '',
|
||||
},
|
||||
'@@bidi_dir': {
|
||||
message: '<ltr|rtl>',
|
||||
description:
|
||||
'The text direction for the current locale, either "ltr" for left-to-right languages such as English or "rtl" for right-to-left languages such as Japanese.',
|
||||
},
|
||||
'@@bidi_reversed_dir': {
|
||||
message: '<rtl|ltr>',
|
||||
description:
|
||||
'If the @@bidi_dir is "ltr", then this is "rtl"; otherwise, it\'s "ltr".',
|
||||
},
|
||||
'@@bidi_start_edge': {
|
||||
message: '<left|right>',
|
||||
description:
|
||||
'If the @@bidi_dir is "ltr", then this is "left"; otherwise, it\'s "right".',
|
||||
},
|
||||
'@@bidi_end_edge': {
|
||||
message: '<right|left>',
|
||||
description:
|
||||
'If the @@bidi_dir is "ltr", then this is "right"; otherwise, it\'s "left".',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a list of all messages and their metadata from JSON file contents.
|
||||
*
|
||||
* @param messagesJson The contents of a `_locales/en/messages.json` file.
|
||||
*/
|
||||
export function parseI18nMessages(messagesJson: object): Message[] {
|
||||
return Object.entries({
|
||||
...predefinedMessages,
|
||||
...messagesJson,
|
||||
}).map<Message>(([name, details]) => ({
|
||||
name,
|
||||
...details,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -54,8 +54,7 @@ export async function generateManifest(
|
||||
entrypoints: Entrypoint[],
|
||||
buildOutput: Omit<BuildOutput, 'manifest'>,
|
||||
config: InternalConfig,
|
||||
): Promise<{ manifest: Manifest.WebExtensionManifest; warnings: any[][] }> {
|
||||
const warnings: any[][] = [];
|
||||
): Promise<Manifest.WebExtensionManifest> {
|
||||
const pkg = await getPackageJson(config);
|
||||
|
||||
let versionName =
|
||||
@@ -76,6 +75,16 @@ export async function generateManifest(
|
||||
short_name: pkg?.shortName,
|
||||
icons: discoverIcons(buildOutput),
|
||||
};
|
||||
if (config.command === 'serve') {
|
||||
baseManifest.commands = {
|
||||
'wxt:reload-extension': {
|
||||
description: 'Reload the extension during development',
|
||||
suggested_key: {
|
||||
default: 'Alt+R',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
const userManifest = config.manifest;
|
||||
|
||||
const manifest = defu(
|
||||
@@ -83,23 +92,6 @@ export async function generateManifest(
|
||||
baseManifest,
|
||||
) as Manifest.WebExtensionManifest;
|
||||
|
||||
// Add reload command in dev mode
|
||||
if (config.command === 'serve' && config.dev.reloadCommand) {
|
||||
if (manifest.commands && Object.keys(manifest.commands).length >= 4) {
|
||||
warnings.push([
|
||||
"Extension already has 4 registered commands, WXT's reload command is disabled",
|
||||
]);
|
||||
} else {
|
||||
manifest.commands ??= {};
|
||||
manifest.commands['wxt:reload-extension'] = {
|
||||
description: 'Reload the extension during development',
|
||||
suggested_key: {
|
||||
default: config.dev.reloadCommand,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Apply the final version fields after merging the user manifest
|
||||
manifest.version = version;
|
||||
manifest.version_name =
|
||||
@@ -125,10 +117,7 @@ export async function generateManifest(
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
manifest: finalManifest,
|
||||
warnings,
|
||||
};
|
||||
return finalManifest;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { resolve } from 'path';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import merge from 'lodash.merge';
|
||||
import { Commands, type Manifest } from '~/browser';
|
||||
import type { Manifest } from '~/browser';
|
||||
import {
|
||||
FsCache,
|
||||
InternalConfig,
|
||||
@@ -59,7 +59,7 @@ export const fakeContentScriptEntrypoint =
|
||||
name: faker.string.alpha(),
|
||||
options: {
|
||||
matches: [],
|
||||
matchAboutBlank: faker.datatype.boolean(),
|
||||
matchAboutBlank: faker.helpers.arrayElement([true, false, undefined]),
|
||||
matchOriginAsFallback: faker.helpers.arrayElement([
|
||||
true,
|
||||
false,
|
||||
@@ -73,7 +73,6 @@ export const fakeContentScriptEntrypoint =
|
||||
]),
|
||||
},
|
||||
outputDir: fakeDir('.output'),
|
||||
skipped: faker.datatype.boolean(),
|
||||
}));
|
||||
|
||||
export const fakeBackgroundEntrypoint = fakeObjectCreator<BackgroundEntrypoint>(
|
||||
@@ -82,11 +81,10 @@ export const fakeBackgroundEntrypoint = fakeObjectCreator<BackgroundEntrypoint>(
|
||||
inputPath: 'entrypoints/background.ts',
|
||||
name: 'background',
|
||||
options: {
|
||||
persistent: faker.datatype.boolean(),
|
||||
persistent: faker.helpers.arrayElement([true, false, undefined]),
|
||||
type: faker.helpers.maybe(() => 'module'),
|
||||
},
|
||||
outputDir: fakeDir('.output'),
|
||||
skipped: faker.datatype.boolean(),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -97,7 +95,6 @@ export const fakeUnlistedScriptEntrypoint =
|
||||
name: faker.string.alpha(),
|
||||
outputDir: fakeDir('.output'),
|
||||
options: {},
|
||||
skipped: faker.datatype.boolean(),
|
||||
}));
|
||||
|
||||
export const fakeOptionsEntrypoint = fakeObjectCreator<OptionsEntrypoint>(
|
||||
@@ -107,11 +104,10 @@ export const fakeOptionsEntrypoint = fakeObjectCreator<OptionsEntrypoint>(
|
||||
name: 'options',
|
||||
outputDir: fakeDir('.output'),
|
||||
options: {
|
||||
browserStyle: faker.datatype.boolean(),
|
||||
chromeStyle: faker.datatype.boolean(),
|
||||
openInTab: faker.datatype.boolean(),
|
||||
browserStyle: faker.helpers.arrayElement([true, false, undefined]),
|
||||
chromeStyle: faker.helpers.arrayElement([true, false, undefined]),
|
||||
openInTab: faker.helpers.arrayElement([true, false, undefined]),
|
||||
},
|
||||
skipped: faker.datatype.boolean(),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -138,7 +134,6 @@ export const fakePopupEntrypoint = fakeObjectCreator<PopupEntrypoint>(() => ({
|
||||
undefined,
|
||||
]),
|
||||
},
|
||||
skipped: faker.datatype.boolean(),
|
||||
}));
|
||||
|
||||
export const fakeGenericEntrypoint = fakeObjectCreator<GenericEntrypoint>(
|
||||
@@ -157,7 +152,6 @@ export const fakeGenericEntrypoint = fakeObjectCreator<GenericEntrypoint>(
|
||||
name: faker.string.alpha(),
|
||||
outputDir: fakeDir('.output'),
|
||||
options: {},
|
||||
skipped: faker.datatype.boolean(),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -217,6 +211,7 @@ export const fakeInternalConfig = fakeObjectCreator<InternalConfig>(() => {
|
||||
outBaseDir: fakeDir(),
|
||||
outDir: fakeDir(),
|
||||
publicDir: fakeDir(),
|
||||
localesDir: fakeDir(),
|
||||
root: fakeDir(),
|
||||
runnerConfig: {
|
||||
config: {},
|
||||
@@ -244,9 +239,6 @@ export const fakeInternalConfig = fakeObjectCreator<InternalConfig>(() => {
|
||||
includeBrowserPolyfill: true,
|
||||
},
|
||||
builder: mock(),
|
||||
dev: {
|
||||
reloadCommand: 'Alt+R',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -260,11 +252,3 @@ export const fakeBuildStepOutput = fakeObjectCreator<BuildStepOutput>(() => ({
|
||||
chunks: fakeArray(fakeOutputChunk),
|
||||
entrypoints: fakeArray(fakeEntrypoint),
|
||||
}));
|
||||
|
||||
export const fakeManifestCommand = fakeObjectCreator<Commands.Command>(() => ({
|
||||
description: faker.string.sample(),
|
||||
shortcut: `${faker.helpers.arrayElement(['ctrl', 'alt'])}+${faker.number.int({
|
||||
min: 0,
|
||||
max: 9,
|
||||
})}`,
|
||||
}));
|
||||
|
||||
@@ -12,6 +12,7 @@ export function getUnimportOptions(
|
||||
imports: [
|
||||
{ name: 'defineConfig', from: 'wxt' },
|
||||
{ name: 'fakeBrowser', from: 'wxt/testing' },
|
||||
{ name: 'i18n', from: 'wxt/i18n' },
|
||||
],
|
||||
presets: [
|
||||
{ package: 'wxt/client' },
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
import { ContentScriptEntrypoint, Entrypoint } from '~/types';
|
||||
|
||||
export function validateEntrypoints(
|
||||
entrypoints: Entrypoint[],
|
||||
): ValidationResults {
|
||||
const errors = entrypoints.flatMap((entrypoint) => {
|
||||
switch (entrypoint.type) {
|
||||
case 'content-script':
|
||||
return validateContentScriptEntrypoint(entrypoint);
|
||||
default:
|
||||
return validateBaseEntrypoint(entrypoint);
|
||||
}
|
||||
});
|
||||
|
||||
let errorCount = 0;
|
||||
let warningCount = 0;
|
||||
for (const err of errors) {
|
||||
if (err.type === 'warning') warningCount++;
|
||||
else errorCount++;
|
||||
}
|
||||
|
||||
return {
|
||||
errors,
|
||||
errorCount,
|
||||
warningCount,
|
||||
};
|
||||
}
|
||||
|
||||
function validateContentScriptEntrypoint(
|
||||
definition: ContentScriptEntrypoint,
|
||||
): ValidationResult[] {
|
||||
const errors = validateBaseEntrypoint(definition);
|
||||
if (definition.options.matches == null) {
|
||||
errors.push({
|
||||
type: 'error',
|
||||
message: '`matches` is required',
|
||||
value: definition.options.matches,
|
||||
entrypoint: definition,
|
||||
});
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
function validateBaseEntrypoint(definition: Entrypoint): ValidationResult[] {
|
||||
const errors: ValidationResult[] = [];
|
||||
|
||||
if (
|
||||
definition.options.exclude != null &&
|
||||
!Array.isArray(definition.options.exclude)
|
||||
) {
|
||||
errors.push({
|
||||
type: 'error',
|
||||
message: '`exclude` must be an array of browser names',
|
||||
value: definition.options.exclude,
|
||||
entrypoint: definition,
|
||||
});
|
||||
}
|
||||
if (
|
||||
definition.options.include != null &&
|
||||
!Array.isArray(definition.options.include)
|
||||
) {
|
||||
errors.push({
|
||||
type: 'error',
|
||||
message: '`include` must be an array of browser names',
|
||||
value: definition.options.include,
|
||||
entrypoint: definition,
|
||||
});
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
export interface ValidationResult {
|
||||
type: 'warning' | 'error';
|
||||
message: string;
|
||||
entrypoint: Entrypoint;
|
||||
value: any;
|
||||
}
|
||||
|
||||
export interface ValidationResults {
|
||||
errors: ValidationResult[];
|
||||
errorCount: number;
|
||||
warningCount: number;
|
||||
}
|
||||
|
||||
export class ValidationError extends Error {}
|
||||
@@ -0,0 +1,165 @@
|
||||
import { describe, it, vi, expect } from 'vitest';
|
||||
import { createExtensionI18n } from '../client';
|
||||
import { fakeBrowser } from '@webext-core/fake-browser';
|
||||
|
||||
let getMessageMock = vi.fn();
|
||||
fakeBrowser.i18n.getMessage = getMessageMock;
|
||||
|
||||
describe('createExtensionI18n', () => {
|
||||
describe('t', () => {
|
||||
it('should return the text from browsesr.i18n.getMessage', () => {
|
||||
const expected = 'Hello world';
|
||||
const i18n = createExtensionI18n();
|
||||
getMessageMock.mockReturnValue(expected);
|
||||
|
||||
const actual = i18n.t('key', ['']);
|
||||
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
|
||||
it("should return a blank string when the message doesn't exist", () => {
|
||||
const i18n = createExtensionI18n();
|
||||
// browser.i18n.getMessage returns a blank string when the message doesn't exist
|
||||
getMessageMock.mockReturnValue('');
|
||||
|
||||
const actual = i18n.t('unknown-key');
|
||||
|
||||
expect(actual).toBe('');
|
||||
});
|
||||
|
||||
it('should pass substitutions in correctly', () => {
|
||||
const i18n = createExtensionI18n();
|
||||
const key = 'key';
|
||||
const subs = ['1', 'two'];
|
||||
|
||||
i18n.t(key, subs);
|
||||
|
||||
expect(getMessageMock).toBeCalledWith(key, subs);
|
||||
});
|
||||
});
|
||||
|
||||
describe('tp', () => {
|
||||
it.each([
|
||||
[0, '0 items'],
|
||||
[1, '1 items'],
|
||||
[2, '2 items'],
|
||||
[3, '3 items'],
|
||||
])(
|
||||
'should return the correct string for format "{n}" and count=%s',
|
||||
(count, expected) => {
|
||||
const i18n = createExtensionI18n();
|
||||
getMessageMock.mockReturnValue(`${count} items`);
|
||||
|
||||
const actual = i18n.tp('key', count, [String(count)]);
|
||||
|
||||
expect(actual).toBe(expected);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
[0, '0 items'],
|
||||
[1, '1 item'],
|
||||
[2, '2 items'],
|
||||
[3, '3 items'],
|
||||
])(
|
||||
'should return the correct string for format "{1} | {n}" and count=%s',
|
||||
(count, expected) => {
|
||||
const i18n = createExtensionI18n();
|
||||
getMessageMock.mockReturnValue(`1 item | ${count} items`);
|
||||
|
||||
const actual = i18n.tp('key', count, [String(count)]);
|
||||
|
||||
expect(actual).toBe(expected);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
[0, '0 items'],
|
||||
[1, '1 item'],
|
||||
[2, '2 items'],
|
||||
[3, '3 items'],
|
||||
])(
|
||||
'should return the correct string for format "{0} | {1} | {n}" and count=%s',
|
||||
(count, expected) => {
|
||||
const i18n = createExtensionI18n();
|
||||
getMessageMock.mockReturnValue(`0 items | 1 item | ${count} items`);
|
||||
|
||||
const actual = i18n.tp('key', count, [String(count)]);
|
||||
|
||||
expect(actual).toBe(expected);
|
||||
},
|
||||
);
|
||||
|
||||
it('should pass substitutions in correctly', () => {
|
||||
const i18n = createExtensionI18n();
|
||||
const key = 'key';
|
||||
const count = 4;
|
||||
const subs = ['1', 'two'];
|
||||
getMessageMock.mockReturnValue(`0 items | 1 item | ${count} items`);
|
||||
|
||||
i18n.tp(key, count, subs);
|
||||
|
||||
expect(getMessageMock).toBeCalledWith(key, subs);
|
||||
});
|
||||
});
|
||||
|
||||
it('should support custom types', () => {
|
||||
const i18n = createExtensionI18n<{
|
||||
t: {
|
||||
singularNoSubs: undefined;
|
||||
singularOneSub: [name: string];
|
||||
singularTwoSubs: [string, string];
|
||||
};
|
||||
tp: {
|
||||
pluralNoSubs: undefined;
|
||||
pluralOneSub: [name: string];
|
||||
pluralTwoSubs: [string, string];
|
||||
};
|
||||
}>();
|
||||
getMessageMock.mockReturnValue('');
|
||||
|
||||
i18n.t('singularNoSubs');
|
||||
// @ts-expect-error
|
||||
i18n.t('singularNoSubs', ['1', '2']);
|
||||
|
||||
i18n.t('singularOneSub', ['one']);
|
||||
// @ts-expect-error
|
||||
i18n.t('singularOneSub');
|
||||
|
||||
i18n.t('singularTwoSubs', ['one', 'two']);
|
||||
// @ts-expect-error
|
||||
i18n.t('singularTwoSubs', ['one']);
|
||||
// @ts-expect-error
|
||||
i18n.t('singularTwoSubs');
|
||||
|
||||
// @ts-expect-error
|
||||
i18n.t('pluralNoSubs');
|
||||
// @ts-expect-error
|
||||
i18n.t('pluralOneSub');
|
||||
// @ts-expect-error
|
||||
i18n.t('pluralTwoSubs');
|
||||
|
||||
i18n.tp('pluralNoSubs', 0);
|
||||
// @ts-expect-error
|
||||
i18n.tp('pluralNoSubs', 0, ['1', '2']);
|
||||
// @ts-expect-error
|
||||
i18n.tp('pluralNoSubs');
|
||||
|
||||
i18n.tp('pluralOneSub', 0, ['one']);
|
||||
// @ts-expect-error
|
||||
i18n.tp('pluralOneSub', 0);
|
||||
|
||||
i18n.tp('pluralTwoSubs', 0, ['one', 'two']);
|
||||
// @ts-expect-error
|
||||
i18n.tp('pluralTwoSubs', 0, ['one']);
|
||||
// @ts-expect-error
|
||||
i18n.tp('pluralTwoSubs', 0);
|
||||
|
||||
// @ts-expect-error
|
||||
i18n.tp('singularNoSubs');
|
||||
// @ts-expect-error
|
||||
i18n.tp('singularOneSub');
|
||||
// @ts-expect-error
|
||||
i18n.tp('singularTwoSubs');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
simple: Hello world!
|
||||
manifestStyle:
|
||||
message: This is the translated text
|
||||
description: This is a description to give translators more context (it's never translated)
|
||||
interpolation: Hello $1, my name is $2
|
||||
some:
|
||||
nested:
|
||||
translation: You can nest translations inside deep objects
|
||||
escapeTheDollarSign: You owe me $$100
|
||||
pluralForm:
|
||||
n: $1 items
|
||||
1: 1 item
|
||||
0: Zero items
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"simple": {
|
||||
"message": "Hello world!"
|
||||
},
|
||||
"manifestStyle": {
|
||||
"message": "This is the translated text",
|
||||
"description": "This is a description to give translators more context (it's never translated)"
|
||||
},
|
||||
"interpolation": {
|
||||
"message": "Hello $1, my name is $2"
|
||||
},
|
||||
"some_nested_translation": {
|
||||
"message": "You can nest translations inside deep objects"
|
||||
},
|
||||
"escapeTheDollarSign": {
|
||||
"message": "You owe me $$100"
|
||||
},
|
||||
"pluralForm": {
|
||||
"message": "Zero items | 1 item | $1 items"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
[
|
||||
{
|
||||
"entry": {
|
||||
"message": "Hello world!"
|
||||
},
|
||||
"name": "simple"
|
||||
},
|
||||
{
|
||||
"entry": {
|
||||
"description": "This is a description to give translators more context (it's never translated)",
|
||||
"message": "This is the translated text"
|
||||
},
|
||||
"name": "manifestStyle"
|
||||
},
|
||||
{
|
||||
"entry": {
|
||||
"message": "Hello $1, my name is $2"
|
||||
},
|
||||
"name": "interpolation"
|
||||
},
|
||||
{
|
||||
"entry": {
|
||||
"message": "You can nest translations inside deep objects"
|
||||
},
|
||||
"name": "some_nested_translation"
|
||||
},
|
||||
{
|
||||
"entry": {
|
||||
"message": "You owe me $$100"
|
||||
},
|
||||
"name": "escapeTheDollarSign"
|
||||
},
|
||||
{
|
||||
"entry": {
|
||||
"message": "Zero items | 1 item | $1 items"
|
||||
},
|
||||
"isPlural": true,
|
||||
"name": "pluralForm"
|
||||
},
|
||||
{
|
||||
"entry": {
|
||||
"description": "The extension or app ID; you might use this string to construct URLs for resources inside the extension. Even unlocalized extensions can use this message.\nNote: You can't use this message in a manifest file.",
|
||||
"message": "<browser.runtime.id>"
|
||||
},
|
||||
"isBuiltin": true,
|
||||
"name": "@@extension_id"
|
||||
},
|
||||
{
|
||||
"entry": {
|
||||
"message": "<browser.i18n.getUiLocale()>"
|
||||
},
|
||||
"isBuiltin": true,
|
||||
"name": "@@ui_locale"
|
||||
},
|
||||
{
|
||||
"entry": {
|
||||
"description": "The text direction for the current locale, either \"ltr\" for left-to-right languages such as English or \"rtl\" for right-to-left languages such as Japanese.",
|
||||
"message": "<ltr|rtl>"
|
||||
},
|
||||
"isBuiltin": true,
|
||||
"name": "@@bidi_dir"
|
||||
},
|
||||
{
|
||||
"entry": {
|
||||
"description": "If the @@bidi_dir is \"ltr\", then this is \"rtl\"; otherwise, it's \"ltr\".",
|
||||
"message": "<rtl|ltr>"
|
||||
},
|
||||
"isBuiltin": true,
|
||||
"name": "@@bidi_reversed_dir"
|
||||
},
|
||||
{
|
||||
"entry": {
|
||||
"description": "If the @@bidi_dir is \"ltr\", then this is \"left\"; otherwise, it's \"right\".",
|
||||
"message": "<left|right>"
|
||||
},
|
||||
"isBuiltin": true,
|
||||
"name": "@@bidi_start_edge"
|
||||
},
|
||||
{
|
||||
"entry": {
|
||||
"description": "If the @@bidi_dir is \"ltr\", then this is \"right\"; otherwise, it's \"left\".",
|
||||
"message": "<right|left>"
|
||||
},
|
||||
"isBuiltin": true,
|
||||
"name": "@@bidi_end_edge"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,29 @@
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { resolve } from 'node:path';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { convertMessagesToManifest, readMessagesFile } from '../node';
|
||||
|
||||
const inputFile = resolve(__dirname, 'fixtures/input.yml');
|
||||
const messagesFile = resolve(__dirname, 'fixtures/messages.json');
|
||||
const manifestFile = resolve(__dirname, 'fixtures/manifest.json');
|
||||
|
||||
describe('I18n Node Utils', () => {
|
||||
describe('readMessagesFile', () => {
|
||||
it('should return all available messages and metadata', async () => {
|
||||
const expected = JSON.parse(await readFile(messagesFile, 'utf-8'));
|
||||
const actual = await readMessagesFile(inputFile);
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertMessagesToManifest', () => {
|
||||
it('should return all available messages and metadata', async () => {
|
||||
const input = JSON.parse(await readFile(messagesFile, 'utf-8'));
|
||||
const expected = JSON.parse(await readFile(manifestFile, 'utf-8'));
|
||||
|
||||
const actual = convertMessagesToManifest(input);
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
import { browser } from '~/browser';
|
||||
|
||||
export interface DefaultMessageSchema {
|
||||
t: {
|
||||
[key: string]: string[] | undefined;
|
||||
};
|
||||
tp: {
|
||||
[key: string]: string[] | undefined;
|
||||
};
|
||||
}
|
||||
|
||||
export function createExtensionI18n<
|
||||
TMessageSchema = unknown,
|
||||
>(): ExtensionI18n<TMessageSchema> {
|
||||
const untyped: UntypedExtensionI18n = {
|
||||
t(key, substitutions) {
|
||||
return browser.i18n.getMessage(key, substitutions);
|
||||
},
|
||||
tp(key, count, substitutions) {
|
||||
const plural = browser.i18n
|
||||
.getMessage(key as string, substitutions)
|
||||
.split(' | ');
|
||||
|
||||
// "n items"
|
||||
if (plural.length === 1) return plural[0];
|
||||
|
||||
// "1 item | n items"
|
||||
if (plural.length === 2) {
|
||||
if (count === 1) return plural[0];
|
||||
return plural[1];
|
||||
}
|
||||
|
||||
// "0 items | 1 item | n items"
|
||||
if (count === 0 || count === 1) return plural[count];
|
||||
return plural[2];
|
||||
},
|
||||
};
|
||||
return untyped as ExtensionI18n<TMessageSchema>;
|
||||
}
|
||||
|
||||
export type ExtensionI18n<TMessageSchema> =
|
||||
TMessageSchema extends DefaultMessageSchema
|
||||
? TypedExtensionI18n<TMessageSchema>
|
||||
: UntypedExtensionI18n;
|
||||
|
||||
export interface TypedExtensionI18n<
|
||||
TMessageSchema extends DefaultMessageSchema,
|
||||
> {
|
||||
t<TKey extends KeysWithoutSub<TMessageSchema['t']>>(key: TKey): string;
|
||||
t<TKey extends KeysWithSub<TMessageSchema['t']>>(
|
||||
key: TKey,
|
||||
substitutions: TMessageSchema['t'][TKey],
|
||||
): string;
|
||||
|
||||
tp<TKey extends KeysWithoutSub<TMessageSchema['tp']>>(
|
||||
key: TKey,
|
||||
count: number,
|
||||
): string;
|
||||
tp<TKey extends KeysWithSub<TMessageSchema['tp']>>(
|
||||
key: TKey,
|
||||
count: number,
|
||||
substitutions: TMessageSchema['tp'][TKey],
|
||||
): string;
|
||||
}
|
||||
|
||||
export interface UntypedExtensionI18n {
|
||||
t(key: string, substitutions?: string[]): string;
|
||||
tp(key: string, count: number, substitutions?: string[]): string;
|
||||
}
|
||||
|
||||
type FilterKeys<TObject, TFilterType> = {
|
||||
[K in keyof TObject]-?: TObject[K] extends TFilterType ? K : never;
|
||||
}[keyof TObject];
|
||||
type KeysWithSub<TObject> = FilterKeys<TObject, string[]>;
|
||||
type KeysWithoutSub<TObject> = FilterKeys<TObject, undefined>;
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Simple, type-safe alternative to `browser.i18n.getMessage` with support for placeholders and plurals.
|
||||
*
|
||||
* See [the guide](https://wxt.dev/guide/localization.html) for more information.
|
||||
*
|
||||
* @module wxt/i18n
|
||||
*/
|
||||
|
||||
import { DefaultMessageSchema, createExtensionI18n } from './client';
|
||||
|
||||
export interface WxtMessageSchema extends DefaultMessageSchema {
|
||||
// Overriden per-project
|
||||
}
|
||||
|
||||
export const i18n = createExtensionI18n<WxtMessageSchema>();
|
||||
export {
|
||||
DefaultMessageSchema,
|
||||
ExtensionI18n,
|
||||
TypedExtensionI18n,
|
||||
UntypedExtensionI18n,
|
||||
} from './client';
|
||||
@@ -0,0 +1,210 @@
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import JSON5 from 'json5';
|
||||
import YAML from 'yaml';
|
||||
|
||||
/**
|
||||
* Convert a file containing localized text in WXT's custom format into valid extension manifest
|
||||
* format.
|
||||
*/
|
||||
export async function readMessagesFile(file: string): Promise<Message[]> {
|
||||
const text = await readFile(file, 'utf-8');
|
||||
return readMessagesText(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a string containing localized text in WXT's custom format into valid extension manifest
|
||||
* format.
|
||||
*/
|
||||
export function readMessagesText(text: string): Message[] {
|
||||
const parsers: Array<(text: string) => any> = [
|
||||
JSON.parse,
|
||||
JSON5.parse,
|
||||
YAML.parse,
|
||||
];
|
||||
|
||||
for (const parse of parsers) {
|
||||
try {
|
||||
const result = parse(text);
|
||||
if (typeof result === 'object') {
|
||||
return readMessagesObject(result);
|
||||
}
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
throw Error('I18n messages text is not valid JSON, JSON5, or YAML');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an object containing localized text in WXT's custom format into valid extension manifest
|
||||
* format.
|
||||
*/
|
||||
export function readMessagesObject(input: ExtensionI18nSchema): Message[] {
|
||||
const messagesFromInput = findEntries([], input);
|
||||
return [...messagesFromInput, ...PREDEFINED_MESSAGES];
|
||||
}
|
||||
|
||||
export function convertMessagesToManifest(
|
||||
messages: Message[],
|
||||
): RawExtensionI18nSchema {
|
||||
return messages
|
||||
.filter((message) => !message.isBuiltin)
|
||||
.reduce<RawExtensionI18nSchema>((schema, { name, entry }) => {
|
||||
schema[name] = entry;
|
||||
return schema;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function findEntries(
|
||||
keyPath: string[],
|
||||
input: ExtensionI18nSchema | ExtensionI18nEntry,
|
||||
): Message[] {
|
||||
const name = keyPath.join('_');
|
||||
if (isBasicEntry(input))
|
||||
return [
|
||||
{
|
||||
name,
|
||||
entry: { message: input },
|
||||
},
|
||||
];
|
||||
if (isManifestEntry(input))
|
||||
return [
|
||||
{
|
||||
name,
|
||||
entry: input,
|
||||
},
|
||||
];
|
||||
if (isPluralEntry(input))
|
||||
return [
|
||||
{
|
||||
name,
|
||||
entry: {
|
||||
message: Object.values(input).join(' | '),
|
||||
},
|
||||
isPlural: true,
|
||||
},
|
||||
];
|
||||
|
||||
return Object.entries(input).reduce<Message[]>((items, [key, child]) => {
|
||||
const nestedEntries = findEntries(keyPath.concat(key), child);
|
||||
return [...items, ...nestedEntries];
|
||||
}, []);
|
||||
}
|
||||
|
||||
function isBasicEntry(
|
||||
entry: ExtensionI18nSchema | ExtensionI18nEntry,
|
||||
): entry is ExtensionI18nBasicEntry {
|
||||
return typeof entry === 'string';
|
||||
}
|
||||
|
||||
function isManifestEntry(
|
||||
entry: ExtensionI18nSchema | ExtensionI18nEntry,
|
||||
): entry is ExtensionI18nManifestEntry {
|
||||
const keys = Object.keys(entry);
|
||||
if (keys.length < 1 || keys.length > 3) return false;
|
||||
|
||||
const knownKeys = new Set(['message', 'placeholders', 'description']);
|
||||
const unknownKeys = keys.filter((key) => !knownKeys.has(key));
|
||||
return unknownKeys.length === 0;
|
||||
}
|
||||
|
||||
function isPluralEntry(
|
||||
entry: ExtensionI18nSchema | ExtensionI18nEntry,
|
||||
): entry is ExtensionI18nPluralEntry {
|
||||
const keys = Object.keys(entry);
|
||||
if (keys.length === 0) return false;
|
||||
|
||||
const invalidKeys = keys.filter((key) => key !== 'n' && isNaN(Number(key)));
|
||||
return invalidKeys.length === 0;
|
||||
}
|
||||
|
||||
export const PREDEFINED_MESSAGES: Message[] = [
|
||||
{
|
||||
name: '@@extension_id',
|
||||
isBuiltin: true,
|
||||
entry: {
|
||||
message: '<browser.runtime.id>',
|
||||
description:
|
||||
"The extension or app ID; you might use this string to construct URLs for resources inside the extension. Even unlocalized extensions can use this message.\nNote: You can't use this message in a manifest file.",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '@@ui_locale',
|
||||
isBuiltin: true,
|
||||
entry: {
|
||||
message: '<browser.i18n.getUiLocale()>',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '@@bidi_dir',
|
||||
isBuiltin: true,
|
||||
entry: {
|
||||
message: '<ltr|rtl>',
|
||||
description:
|
||||
'The text direction for the current locale, either "ltr" for left-to-right languages such as English or "rtl" for right-to-left languages such as Japanese.',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '@@bidi_reversed_dir',
|
||||
isBuiltin: true,
|
||||
entry: {
|
||||
message: '<rtl|ltr>',
|
||||
description:
|
||||
'If the @@bidi_dir is "ltr", then this is "rtl"; otherwise, it\'s "ltr".',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '@@bidi_start_edge',
|
||||
isBuiltin: true,
|
||||
entry: {
|
||||
message: '<left|right>',
|
||||
description:
|
||||
'If the @@bidi_dir is "ltr", then this is "left"; otherwise, it\'s "right".',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '@@bidi_end_edge',
|
||||
isBuiltin: true,
|
||||
entry: {
|
||||
message: '<right|left>',
|
||||
description:
|
||||
'If the @@bidi_dir is "ltr", then this is "right"; otherwise, it\'s "left".',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export type RawExtensionI18nSchema = Record<string, RawExtensionI18nEntry>;
|
||||
export interface RawExtensionI18nEntry {
|
||||
message: string;
|
||||
description?: string;
|
||||
placeholders?: Record<
|
||||
string,
|
||||
{
|
||||
content: string;
|
||||
example?: string;
|
||||
}
|
||||
>;
|
||||
}
|
||||
|
||||
export type ExtensionI18nBasicEntry = string;
|
||||
export type ExtensionI18nManifestEntry = RawExtensionI18nEntry;
|
||||
export type ExtensionI18nPluralEntry = {
|
||||
n: string;
|
||||
[i: number]: string;
|
||||
};
|
||||
export type ExtensionI18nEntry =
|
||||
| ExtensionI18nBasicEntry
|
||||
| ExtensionI18nManifestEntry
|
||||
| ExtensionI18nPluralEntry;
|
||||
|
||||
export interface ExtensionI18nSchema {
|
||||
[name: string]: ExtensionI18nSchema | ExtensionI18nEntry;
|
||||
}
|
||||
|
||||
export interface Message {
|
||||
name: string;
|
||||
entry: RawExtensionI18nEntry;
|
||||
isPlural?: boolean;
|
||||
isBuiltin?: boolean;
|
||||
}
|
||||
@@ -27,16 +27,16 @@ export interface InlineConfig {
|
||||
* @default "${config.root}/public"
|
||||
*/
|
||||
publicDir?: string;
|
||||
/**
|
||||
* Directory containing localization files used for translation.
|
||||
*
|
||||
* @default "${config.srcDir}/locales"
|
||||
*/
|
||||
localesDir?: string;
|
||||
/**
|
||||
* @default "${config.srcDir}/entrypoints"
|
||||
*/
|
||||
entrypointsDir?: string;
|
||||
/**
|
||||
* A list of entrypoint names (`"popup"`, `"options"`, etc.) to build. Will speed up the build if
|
||||
* your extension has lots of entrypoints, and you don't need to build all of them to develop a
|
||||
* feature.
|
||||
*/
|
||||
filterEntrypoints?: string[];
|
||||
/**
|
||||
* Output directory that stored build folders and ZIPs.
|
||||
*
|
||||
@@ -229,24 +229,6 @@ export interface InlineConfig {
|
||||
*/
|
||||
includeBrowserPolyfill?: boolean;
|
||||
};
|
||||
/**
|
||||
* Config effecting dev mode only.
|
||||
*/
|
||||
dev?: {
|
||||
/**
|
||||
* Controls whether a custom keyboard shortcut command, `Alt+R`, is added during dev mode to
|
||||
* quickly reload the extension.
|
||||
*
|
||||
* If false, the shortcut is not added during development.
|
||||
*
|
||||
* If set to a custom string, you can override the key combo used. See
|
||||
* [Chrome's command docs](https://developer.chrome.com/docs/extensions/reference/api/commands)
|
||||
* for available options.
|
||||
*
|
||||
* @default "Alt+R"
|
||||
*/
|
||||
reloadCommand?: string | false;
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: Extract to @wxt/vite-builder and use module augmentation to include the vite field
|
||||
@@ -308,24 +290,16 @@ export interface BuildStepOutput {
|
||||
}
|
||||
|
||||
export interface WxtDevServer
|
||||
extends Omit<WxtBuilderServer, 'listen' | 'close'>,
|
||||
extends Omit<WxtBuilderServer, 'listen'>,
|
||||
ServerInfo {
|
||||
/**
|
||||
* Stores the current build output of the server.
|
||||
*/
|
||||
currentOutput: BuildOutput | undefined;
|
||||
currentOutput: BuildOutput;
|
||||
/**
|
||||
* Start the server.
|
||||
*/
|
||||
start(): Promise<void>;
|
||||
/**
|
||||
* Stop the server.
|
||||
*/
|
||||
stop(): Promise<void>;
|
||||
/**
|
||||
* Close the browser, stop the server, rebuild the entire extension, and start the server again.
|
||||
*/
|
||||
restart(): Promise<void>;
|
||||
/**
|
||||
* Transform the HTML for dev mode.
|
||||
*/
|
||||
@@ -358,10 +332,6 @@ export interface WxtDevServer
|
||||
reloadContentScript: (
|
||||
contentScript: Omit<Scripting.RegisteredContentScript, 'id'>,
|
||||
) => void;
|
||||
/**
|
||||
* Grab the latest runner config and restart the browser.
|
||||
*/
|
||||
restartBrowser: () => void;
|
||||
}
|
||||
|
||||
export type TargetBrowser = string;
|
||||
@@ -413,7 +383,6 @@ export interface BaseEntrypoint {
|
||||
*/
|
||||
outputDir: string;
|
||||
options: BaseEntrypointOptions;
|
||||
skipped: boolean;
|
||||
}
|
||||
|
||||
export interface GenericEntrypoint extends BaseEntrypoint {
|
||||
@@ -704,10 +673,6 @@ export interface WxtBuilderServer {
|
||||
* Start the server.
|
||||
*/
|
||||
listen(): Promise<void>;
|
||||
/**
|
||||
* Stop the server.
|
||||
*/
|
||||
close(): Promise<void>;
|
||||
/**
|
||||
* Transform the HTML for dev mode.
|
||||
*/
|
||||
|
||||
@@ -18,10 +18,10 @@ export interface InternalConfig {
|
||||
root: string;
|
||||
srcDir: string;
|
||||
publicDir: string;
|
||||
localesDir: string;
|
||||
wxtDir: string;
|
||||
typesDir: string;
|
||||
entrypointsDir: string;
|
||||
filterEntrypoints?: Set<string>;
|
||||
outBaseDir: string;
|
||||
outDir: string;
|
||||
debug: boolean;
|
||||
@@ -57,9 +57,6 @@ export interface InternalConfig {
|
||||
includeBrowserPolyfill: boolean;
|
||||
};
|
||||
builder: WxtBuilder;
|
||||
dev: {
|
||||
reloadCommand: string | false;
|
||||
};
|
||||
}
|
||||
|
||||
export interface FsCache {
|
||||
|
||||
@@ -50,10 +50,6 @@ declare module 'web-ext-run/util/logger' {
|
||||
export const consoleStream: IConsoleStream;
|
||||
}
|
||||
|
||||
declare module 'manage-path' {
|
||||
export default function managePath(env: object): string[];
|
||||
}
|
||||
|
||||
declare module 'wxt/browser' {
|
||||
// Overridden when types are generated per project
|
||||
export type PublicPath = string;
|
||||
|
||||