Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8771676e69 | |||
| aebbbb0685 | |||
| 68183b648e | |||
| 45809c0198 | |||
| 13163c9ba1 | |||
| aedbba3f3d | |||
| 763dbd5ee8 | |||
| 5821ae0e8c | |||
| 751706d2ae | |||
| cf8554718d | |||
| 5f653b3ff9 | |||
| 5f2e1c38a7 | |||
| 8a9d8be8a4 | |||
| 067b2eccf3 | |||
| 2e1bd6ec93 | |||
| b463cef1c2 | |||
| adf9d39d69 | |||
| b1699592e4 | |||
| 8675abfced | |||
| a1fc19e47c | |||
| 8235de04d2 | |||
| 46a324adc2 | |||
| c0af62316d | |||
| 2fd3503694 | |||
| e6529e6e41 | |||
| 72673ca366 | |||
| cbda49ba7b | |||
| 0ab32e5afa | |||
| 36cfcd0acb | |||
| f746d46247 | |||
| a2de6cbe44 |
@@ -7,12 +7,13 @@ on:
|
||||
default: wxt
|
||||
type: choice
|
||||
options:
|
||||
- wxt
|
||||
- module-react
|
||||
- module-vue
|
||||
- module-svelte
|
||||
- module-solid
|
||||
- auto-icons
|
||||
- i18n
|
||||
- module-react
|
||||
- module-solid
|
||||
- module-svelte
|
||||
- module-vue
|
||||
- wxt
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
|
||||
@@ -21,3 +21,4 @@ docs/api/reference
|
||||
stats.html
|
||||
.tool-versions
|
||||
.cache
|
||||
*-stats.txt
|
||||
|
||||
@@ -57,6 +57,21 @@ pnpm test
|
||||
pnpm docs:dev
|
||||
```
|
||||
|
||||
## Profiling
|
||||
|
||||
```sh
|
||||
# Build the latest version
|
||||
pnpm --filter wxt build
|
||||
|
||||
# CD to the demo directory
|
||||
cd packages/wxt-demo
|
||||
|
||||
# 1. Generate a flamechart with 0x
|
||||
pnpm dlx 0x node_modules/wxt/bin/wxt.mjs build
|
||||
# 2. Inspect the process with chrome @ chrome://inspect
|
||||
pnpm node --inspect node_modules/wxt/bin/wxt.mjs build
|
||||
```
|
||||
|
||||
## Updating Docs
|
||||
|
||||
Documentation is written with VitePress, and is located in the `docs/` directory.
|
||||
|
||||
@@ -130,6 +130,7 @@ export default defineConfig({
|
||||
menuGroup('Extension APIs', '/guide/extension-apis/', [
|
||||
menuItem('Storage', 'storage'),
|
||||
menuItem('Messaging', 'messaging'),
|
||||
menuItem('I18n', 'i18n'),
|
||||
menuItem('Scripting', 'scripting'),
|
||||
menuItem('Others', 'others'),
|
||||
]),
|
||||
@@ -148,6 +149,13 @@ export default defineConfig({
|
||||
menuGroup('Upgrade Guide', '/guide/upgrade-guide/', [
|
||||
menuItem('wxt', 'wxt'),
|
||||
]),
|
||||
menuGroup('@wxt-dev/i18n', '/guide/i18n/', [
|
||||
menuItem('Introduction', 'introduction.md'),
|
||||
menuItem('Installation', 'installation.md'),
|
||||
menuItem('Messages File Format', 'messages-file-format.md'),
|
||||
menuItem('Build Integrations', 'build-integrations.md'),
|
||||
menuItem('Editor Support', 'editor-support.md'),
|
||||
]),
|
||||
]),
|
||||
'/api/': menuRoot([
|
||||
menuGroup('CLI', '/api/cli/', [
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
# Internationalization
|
||||
|
||||
This guide is for using the vanilla, `browser.i18n` APIs. There are two other alternatives:
|
||||
|
||||
1. [`@wxt-dev/i18n`](/guide/i18n/installation) - a wrapper around `browser.i18n` APIs with additional features, a simplified localization file format, and editor support
|
||||
2. Third party packages - You can use any i18n package on NPM, most of which are more feature rich than `browser.i18n` and `@wxt-dev/i18n`
|
||||
|
||||
:::info
|
||||
Currently, using the `browser.i18n` APIs are the recommended approach. WXT has some built-in support for them and they work well enough. `@wxt-dev/i18n` was recently released, and it will become the recommended approach after some of the bugs have been worked out. Head over to [it's docs](/guide/i18n/introduction.md) to learn more.
|
||||
:::
|
||||
|
||||
## Setup
|
||||
|
||||
First familiarize yourself with [Chrome's docs](https://developer.chrome.com/docs/extensions/reference/api/i18n). The only difference when using these APIs with WXT is where you put the localization files - in the [`public` directory](/guide/directory-structure/public/).
|
||||
|
||||
```
|
||||
<srcDir>/
|
||||
└─ public/
|
||||
└─ _locales/
|
||||
├─ en/
|
||||
│ └─ messages.json
|
||||
├─ de/
|
||||
│ └─ messages.json
|
||||
└─ ko/
|
||||
└─ messages.json
|
||||
```
|
||||
|
||||
Next, to set a `default_locale` on your manifest, add it to your `wxt.config.ts` file:
|
||||
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
export default defineConfig({
|
||||
manifest: {
|
||||
default_locale: 'en',
|
||||
name: '__MSG_extName__',
|
||||
description: '__MSG_extDescription__',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
> You can localize the `name` and `description` of your extension from the `manifest` config as well.
|
||||
|
||||
Finally, to get a translation, call `browser.i18n.getMessage`:
|
||||
|
||||
```ts
|
||||
browser.i18n.getMessage('extName');
|
||||
browser.i18n.getMessage('extDescription');
|
||||
browser.i18n.getMessage(/* etc */);
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
See the official localization examples for more details:
|
||||
|
||||
- [I18n](https://github.com/wxt-dev/wxt-examples/tree/main/examples/vanilla-i18n#readme)
|
||||
- [Vue I18n](https://github.com/wxt-dev/wxt-examples/tree/main/examples/vue-i18n#readme)
|
||||
@@ -35,4 +35,6 @@ Only MV3 support ESM background scripts/service workers. When targeting MV2, the
|
||||
|
||||
## Content Scripts
|
||||
|
||||
Coming soon. Follow [Content Script ESM Support #357](https://github.com/wxt-dev/wxt/issues/357) for updates.
|
||||
WXT does not include built-in support for ESM content scripts. There are several technical issues that make implementing a generic solution impossible. See [Content Script ESM Support #357](https://github.com/wxt-dev/wxt/issues/357) for details.
|
||||
|
||||
Instead, depending on your requirements, you can implement ESM support manually. See the [ESM Content Script UI](https://github.com/wxt-dev/examples/tree/main/examples/esm-content-script-ui) example to get started.
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
# Build Integrations
|
||||
|
||||
To use the custom messages file format, you'll need to use `@wxt-dev/i18n/build` to transform the custom format to the one expected by browsers.
|
||||
|
||||
Here's a list of build tools that already have an integration:
|
||||
|
||||
[[toc]]
|
||||
|
||||
:::info
|
||||
If you want to contribute, please do! In particular, an `unplugin` integration would be awesome!
|
||||
:::
|
||||
|
||||
## WXT
|
||||
|
||||
See [Installation with WXT](./installation#with-wxt).
|
||||
|
||||
But TLDR, all you need is:
|
||||
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
export default defineConfig({
|
||||
modules: ['@wxt-dev/i18n/module'],
|
||||
});
|
||||
```
|
||||
|
||||
Types are generated whenever you run `wxt prepare` or another build command:
|
||||
|
||||
```sh
|
||||
wxt prepare
|
||||
wxt
|
||||
wxt build
|
||||
wxt zip
|
||||
# etc
|
||||
```
|
||||
|
||||
## Custom
|
||||
|
||||
If you're not using WXT, you'll have to pre-process the localization files yourself. Here's a basic script to generate the `_locales/.../messages.json` and `wxt-i18n-structure.d.ts` files:
|
||||
|
||||
```ts
|
||||
// build-i18n.js
|
||||
import {
|
||||
parseMessagesFile,
|
||||
generateChromeMessagesFile,
|
||||
generateTypeFile,
|
||||
} from '@wxt-dev/i18n/build';
|
||||
|
||||
// Read your localization files
|
||||
const messages = {
|
||||
en: await parseMessagesFile('path/locales/en.yml'),
|
||||
de: await parseMessagesFile('path/locales/de.yml'),
|
||||
// ...
|
||||
};
|
||||
|
||||
// Generate JSON files for the browser
|
||||
await generateChromeMessagesFile('dist/_locales/en/messages.json', messages.en);
|
||||
await generateChromeMessagesFile('dist/_locales/de/messages.json', messages.de);
|
||||
// ...
|
||||
|
||||
// Generate a types file based on your default_locale
|
||||
await generateTypeFile('wxt-i18n-structure.d.ts', messages.en);
|
||||
```
|
||||
|
||||
Then run the script:
|
||||
|
||||
```sh
|
||||
node generate-i18n.js
|
||||
```
|
||||
|
||||
If your build tool has a dev mode, you'll also want to rerun the script whenever you change a localization file.
|
||||
|
||||
### Type Safety
|
||||
|
||||
Once you've generated `wxt-i18n-structure.d.ts` (see the [Custom](#custom) section), you can use it to pass the generated type into `createI18n`:
|
||||
|
||||
```ts
|
||||
import type { WxtI18nStructure } from './wxt-i18n-structure';
|
||||
|
||||
export const i18n = createI18n<WxtI18nStructure>();
|
||||
```
|
||||
|
||||
And just like that, you have type safety!
|
||||
@@ -0,0 +1,48 @@
|
||||
# Editor Support
|
||||
|
||||
For better DX, you can configure your editor with plugins and extensions.
|
||||
|
||||
[[toc]]
|
||||
|
||||
## VS Code
|
||||
|
||||
Install the [I18n Ally Extension](https://marketplace.visualstudio.com/items?itemName=lokalise.i18n-ally) to:
|
||||
|
||||
- Go to translation definition
|
||||
- Inline previews of text
|
||||
- Hover to see other translations
|
||||
|
||||
You'll need to configure it the extension so it knows where your localization files are and what function represents getting a translation:
|
||||
|
||||
`.vscode/i18n-ally-custom-framework.yml`:
|
||||
|
||||
```yml
|
||||
# An array of strings which contain Language Ids defined by VS Code
|
||||
# You can check available language ids here: https://code.visualstudio.com/docs/languages/identifiers
|
||||
languageIds:
|
||||
- typescript
|
||||
|
||||
# Look for t("...")
|
||||
usageMatchRegex:
|
||||
- "[^\\w\\d]t\\(['\"`]({key})['\"`]"
|
||||
|
||||
# Disable other built-in i18n ally frameworks
|
||||
monopoly: true
|
||||
```
|
||||
|
||||
`.vscode/settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"i18n-ally.localesPaths": ["src/locales"],
|
||||
"i18n-ally.keystyle": "nested"
|
||||
}
|
||||
```
|
||||
|
||||
## Zed
|
||||
|
||||
As of time of writing, Aug 18, 2024, no extensions exist for Zed to add I18n support.
|
||||
|
||||
## IntelliJ
|
||||
|
||||
Unknown - Someone who uses IntelliJ will have to open a PR for this!
|
||||
@@ -0,0 +1,81 @@
|
||||
# Installation
|
||||
|
||||
[[toc]]
|
||||
|
||||
### With WXT
|
||||
|
||||
1. Install `@wxt-dev/i18n` via your package manager:
|
||||
|
||||
```sh
|
||||
pnpm i @wxt-dev/i18n
|
||||
```
|
||||
|
||||
2. Add the WXT module to your `wxt.config.ts` file and setup a default locale:
|
||||
|
||||
```ts
|
||||
export default defineConfig({
|
||||
modules: ['@wxt-dev/i18n/module'],
|
||||
manifest: {
|
||||
default_locale: 'en',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
3. Create a localization file at `<srcDir>/locales/<default_locale>.yml` or move your existing localization files there.
|
||||
|
||||
```yml
|
||||
# <srcDir>/locales/en.yml
|
||||
helloWorld: Hello world!
|
||||
```
|
||||
|
||||
:::tip
|
||||
`@wxt-dev/i18n` supports the standard messages format, so if you already have localization files at `<srcDir>/public/_locale/<lang>/messages.json`, you don't need to convert them to YAML or refactor them - just move them to `<srcDir>/locales/<lang>.json` and they'll just work out of the box!
|
||||
:::
|
||||
|
||||
4. To get a translation, use the auto-imported `i18n` object or import it manually:
|
||||
|
||||
```ts
|
||||
import { i18n } from '#i18n';
|
||||
|
||||
i18n.t('helloWorld'); // "Hello world!"
|
||||
```
|
||||
|
||||
And you're done! Using WXT, you get type-safety out of the box.
|
||||
|
||||
### Without WXT
|
||||
|
||||
1. Install `@wxt-dev/i18n` via your package manager:
|
||||
|
||||
```sh
|
||||
pnpm i @wxt-dev/i18n
|
||||
```
|
||||
|
||||
2. Create a messages file at `_locales/<lang>/messages.json` or move your existing translations there:
|
||||
|
||||
```json
|
||||
{
|
||||
"helloWorld": {
|
||||
"message": "Hello world!"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
:::info
|
||||
For the best DX, you should to integrate `@wxt-dev/i18n` into your build process. This enables:
|
||||
|
||||
1. Plural forms
|
||||
2. Simple messages file format
|
||||
3. Type safety
|
||||
|
||||
See [Build Integrations](./build-integrations) to set it up.
|
||||
:::
|
||||
|
||||
3. Create the `i18n` object, export it, and use it wherever you want!
|
||||
|
||||
```ts
|
||||
import { createI18n } from '@wxt-dev/i18n';
|
||||
|
||||
export const i18n = createI18n();
|
||||
|
||||
i18n.t('helloWorld'); // "Hello world!";
|
||||
```
|
||||
@@ -0,0 +1,22 @@
|
||||
# Introduction
|
||||
|
||||
:::info
|
||||
You don't have to use `wxt` to use this package - it will work in any bundler setup. See [Installation without WXT](./installation#without-wxt) for more details.
|
||||
:::
|
||||
|
||||
`@wxt-dev/i18n` is a simple, type-safe wrapper around the `browser.i18n` APIs. It provides several benefits over the standard API:
|
||||
|
||||
1. Simple messages file format
|
||||
2. Plural form support
|
||||
3. Integrates with the [I18n Ally VS Code extension](./editor-support#vscode)
|
||||
|
||||
It also provides several benefits over other non-web extension specific i18n packages:
|
||||
|
||||
1. Does not bundle localization files into every entrypoint
|
||||
2. Don't need to fetch the localization files asynchronously
|
||||
3. Can localize extension name in manifest
|
||||
4. Can access localized strings inside CSS files
|
||||
|
||||
However, it does have one major downside:
|
||||
|
||||
1. Like the `browser.i18n` API, to change the language, users must change the browser's language
|
||||
@@ -0,0 +1,170 @@
|
||||
# Messages File Format
|
||||
|
||||
You can only use the file format discussed on this page if you have [integrated `@wxt-dev/i18n` into your build process](./build-integrations). If you have not integrated it into your build process, you must use JSON files in the `_locales` directory, like a normal web extension.
|
||||
|
||||
[[toc]]
|
||||
|
||||
## File Extensions
|
||||
|
||||
You can define your messages in several different file types:
|
||||
|
||||
- `.yml`
|
||||
- `.yaml`
|
||||
- `.json`
|
||||
- `.jsonc`
|
||||
- `.json5`
|
||||
- `.toml`
|
||||
|
||||
## Nested Keys
|
||||
|
||||
You can have translations at the top level or nest them into groups:
|
||||
|
||||
```yml
|
||||
ok: OK
|
||||
cancel: Cancel
|
||||
welcome:
|
||||
title: Welcome to XYZ
|
||||
dialogs:
|
||||
confirmation:
|
||||
title: 'Are you sure?'
|
||||
```
|
||||
|
||||
To access a nested key, use `.`:
|
||||
|
||||
```ts
|
||||
i18n.t('ok'); // "OK"
|
||||
i18n.t('cancel'); // "Cancel"
|
||||
i18n.t('welcome.title'); // "Welcome to XYZ"
|
||||
i18n.t('dialogs.confirmation.title'); // "Are you sure?"
|
||||
```
|
||||
|
||||
## Substitutions
|
||||
|
||||
Because `@wxt-dev/i18n` is based on `browser.i18n`, you define substitutions the same way, with `$1`-`$9`:
|
||||
|
||||
```yml
|
||||
hello: Hello $1!
|
||||
order: Thanks for ordering your $1
|
||||
```
|
||||
|
||||
### Escapting `$`
|
||||
|
||||
To escape the dollar sign, put another `$` in front of it:
|
||||
|
||||
```yml
|
||||
dollars: $$$1
|
||||
```
|
||||
|
||||
```ts
|
||||
i18n.t('dollars', ['1.00']); // "$1.00"
|
||||
```
|
||||
|
||||
## Plural Forms
|
||||
|
||||
:::warning
|
||||
Plural support languages like Arabic, that have different forms for "few" or "many", is not supported right now. Feel free to open a PR if you are interested in this!
|
||||
:::
|
||||
|
||||
To get a different translation based on a count:
|
||||
|
||||
```yml
|
||||
items:
|
||||
1: 1 item
|
||||
n: $1 items
|
||||
```
|
||||
|
||||
Then you pass in the count as the second argument to `i18n.t`:
|
||||
|
||||
```ts
|
||||
i18n.t('items', 0); // "0 items"
|
||||
i18n.t('items', 1); // "1 item"
|
||||
i18n.t('items', 2); // "2 items"
|
||||
```
|
||||
|
||||
To add a custom translation for 0 items:
|
||||
|
||||
```yml
|
||||
items:
|
||||
0: No items
|
||||
1: 1 item
|
||||
n: $1 items
|
||||
```
|
||||
|
||||
```ts
|
||||
i18n.t('items', 0); // "No items"
|
||||
i18n.t('items', 1); // "1 item"
|
||||
i18n.t('items', 2); // "2 items"
|
||||
```
|
||||
|
||||
If you need to pass a custom substitution for `$1` instead of the count, just add the substitution:
|
||||
|
||||
```yml
|
||||
items:
|
||||
0: No items
|
||||
1: $1 item
|
||||
n: $1 items
|
||||
```
|
||||
|
||||
```ts
|
||||
i18n.t('items', 0, ['Zero']); // "No items"
|
||||
i18n.t('items', 1, ['One']); // "One item"
|
||||
i18n.t('items', 2, ['Multiple']); // "Multiple items"
|
||||
```
|
||||
|
||||
## Verbose Keys
|
||||
|
||||
`@wxt-dev/i18n` is compatible with the message format used by [`browser.i18n`](https://developer.chrome.com/docs/extensions/reference/api/i18n).
|
||||
|
||||
:::info
|
||||
This means if you're migrating to `@wxt-dev/i18n` and you're already using the verbose format, you don't have to change anything!
|
||||
:::
|
||||
|
||||
A key is treated as "verbose" when it is:
|
||||
|
||||
1. At the top level (not nested)
|
||||
2. Only contains the following properties: `message`, `description` and/or `placeholder`
|
||||
|
||||
:::code-group
|
||||
|
||||
```json [JSON]
|
||||
{
|
||||
"appName": {
|
||||
"message": "GitHub - Better Line Counts",
|
||||
"description": "The app's name, should not be translated",
|
||||
},
|
||||
"ok": "OK",
|
||||
"deleteConfirmation": {
|
||||
"title": "Delete XYZ?"
|
||||
"message": "You cannot undo this action once taken."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```yml [YAML]
|
||||
appName:
|
||||
message: GitHub - Better Line Counts
|
||||
description: The app's name, should not be translated
|
||||
ok: OK
|
||||
deleteConfirmation:
|
||||
title: Delete XYZ?
|
||||
message: You cannot undo this action once taken.
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
In this example, only `appName` is considered verbose. `deleteConfirmation` is not verbose because it contains the additional property `title`.
|
||||
|
||||
```ts
|
||||
i18n.t('appName'); // ✅ "GitHub - Better Line Counts"
|
||||
i18n.t('appName.message'); // ❌
|
||||
i18n.t('ok'); // ✅ "OK"
|
||||
i18n.t('deleteConfirmation'); // ❌
|
||||
i18n.t('deleteConfirmation.title'); // ✅ "Delete XYZ?"
|
||||
i18n.t('deleteConfirmation.message'); // ✅ "You cannot undo this action once taken."
|
||||
```
|
||||
|
||||
If this is confusing, don't worry! With type-safety, you'll get a type error if you do it wrong. If type-safety is disabled, you'll get a runtime warning in the devtools console.
|
||||
|
||||
:::warning
|
||||
Using the verbose format is not recommended. I have yet to see a translation service and software that supports this format out of the box. Stick with the simple format when you can.
|
||||
:::
|
||||
@@ -172,37 +172,9 @@ export default defineConfig({
|
||||
|
||||
:::
|
||||
|
||||
## Localization
|
||||
## Default Locale
|
||||
|
||||
Similar to the icon, the [`_locales` directory](https://developer.chrome.com/docs/extensions/reference/i18n/) should be placed inside the the WXT's [`public` directory](/guide/directory-structure/public/).
|
||||
|
||||
```
|
||||
public/
|
||||
└─ _locales/
|
||||
├─ en/
|
||||
│ └─ messages.json
|
||||
├─ es/
|
||||
│ └─ messages.json
|
||||
└─ ko/
|
||||
└─ messages.json
|
||||
```
|
||||
|
||||
Then you'll need to explicitly override the `name` and `description` properties in your config for them to be localized.
|
||||
|
||||
```ts
|
||||
export default defineConfig({
|
||||
manifest: {
|
||||
name: '__MSG_extName__',
|
||||
description: '__MSG_extDescription__',
|
||||
default_locale: 'en',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
See the official localization examples for more details:
|
||||
|
||||
- [I18n](https://github.com/wxt-dev/wxt-examples/tree/main/examples/vanilla-i18n#readme)
|
||||
- [Vue I18n](https://github.com/wxt-dev/wxt-examples/tree/main/examples/vue-i18n#readme)
|
||||
See the dedicated [I18n docs](/guide/extension-apis/i18n) for setting up localization and a `default_locale`.
|
||||
|
||||
## Actions
|
||||
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@
|
||||
"docs:preview": "pnpm -s docs:gen && vitepress preview docs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/buildc": "^1.0.11",
|
||||
"@aklinker1/buildc": "^1.1.0",
|
||||
"@aklinker1/check": "^1.3.1",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@vitest/coverage-v8": "^2.0.4",
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# WXT I18n
|
||||
|
||||
[Read the docs](https://wxt.dev/guide/i18n/installation) to get started.
|
||||
@@ -0,0 +1,89 @@
|
||||
{
|
||||
"name": "@wxt-dev/i18n",
|
||||
"description": "Type-safe wrapper around browser.i18n.getMessage with additional features",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/wxt-dev/wxt.git",
|
||||
"directory": "packages/i18n"
|
||||
},
|
||||
"homepage": "http://wxt.dev/guide/i18n/installation.html",
|
||||
"keywords": [
|
||||
"wxt",
|
||||
"module",
|
||||
"i18n"
|
||||
],
|
||||
"author": {
|
||||
"name": "Aaron Klinker",
|
||||
"email": "aaronklinker1+wxt@gmail.com"
|
||||
},
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "buildc -- unbuild",
|
||||
"check": "buildc --deps-only -- check",
|
||||
"test": "buildc --deps-only -- vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"chokidar": "^3.6.0",
|
||||
"confbox": "^0.1.7",
|
||||
"fast-glob": "^3.3.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"wxt": ">=0.19.7"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"wxt": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "^1.3.1",
|
||||
"@types/chrome": "^0.0.268",
|
||||
"@types/node": "^20.14.2",
|
||||
"publint": "^0.2.8",
|
||||
"typescript": "^5.4.5",
|
||||
"unbuild": "^2.0.0",
|
||||
"vitest": "^1.6.0",
|
||||
"vitest-plugin-random-seed": "^1.1.0",
|
||||
"wxt": "workspace:*"
|
||||
},
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": {
|
||||
"types": "./dist/index.d.mts",
|
||||
"default": "./dist/index.mjs"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/index.d.cts",
|
||||
"default": "./dist/index.cjs"
|
||||
}
|
||||
},
|
||||
"./build": {
|
||||
"import": {
|
||||
"types": "./dist/build.d.mts",
|
||||
"default": "./dist/build.mjs"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/build.d.cts",
|
||||
"default": "./dist/build.cjs"
|
||||
}
|
||||
},
|
||||
"./module": {
|
||||
"import": {
|
||||
"types": "./dist/module.d.mts",
|
||||
"default": "./dist/module.mjs"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/module.d.cts",
|
||||
"default": "./dist/module.cjs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import {
|
||||
parseMessagesFile,
|
||||
generateChromeMessagesFile,
|
||||
generateTypeFile,
|
||||
} from '../build';
|
||||
import {
|
||||
stringifyTOML,
|
||||
stringifyYAML,
|
||||
stringifyJSON5,
|
||||
stringifyJSON,
|
||||
stringifyJSONC,
|
||||
} from 'confbox';
|
||||
import { writeFile, readFile } from 'node:fs/promises';
|
||||
|
||||
vi.mock('node:fs/promises');
|
||||
const mockWriteFile = vi.mocked(writeFile);
|
||||
const mockReadFile = vi.mocked(readFile);
|
||||
|
||||
describe('Built Tools', () => {
|
||||
it('should correctly convert all types of message formats', async () => {
|
||||
const fileText = stringifyYAML({
|
||||
simple: 'example',
|
||||
sub: 'Hello $1',
|
||||
nested: {
|
||||
example: 'This is nested',
|
||||
array: ['One', 'Two'],
|
||||
notChrome: {
|
||||
message: 'test 1',
|
||||
},
|
||||
},
|
||||
chrome: {
|
||||
message: 'test 2',
|
||||
description: 'test',
|
||||
},
|
||||
plural0: {
|
||||
0: 'Zero items',
|
||||
1: 'One item',
|
||||
n: '$1 items',
|
||||
},
|
||||
plural1: {
|
||||
1: 'One item',
|
||||
n: '$1 items',
|
||||
},
|
||||
pluralN: {
|
||||
n: '$1 items',
|
||||
},
|
||||
pluralSub: {
|
||||
1: 'Hello $2, I have one problem',
|
||||
n: 'Hello $2, I have $1 problems',
|
||||
},
|
||||
});
|
||||
|
||||
mockReadFile.mockResolvedValue(fileText);
|
||||
|
||||
const messages = await parseMessagesFile(`file.yml`);
|
||||
await generateChromeMessagesFile('output.json', messages);
|
||||
await generateTypeFile('output.d.ts', messages);
|
||||
const actualChromeMessagesFile = mockWriteFile.mock.calls[0][1];
|
||||
const actualDtsFile = mockWriteFile.mock.calls[1][1];
|
||||
|
||||
expect(mockWriteFile).toBeCalledTimes(2);
|
||||
expect(actualChromeMessagesFile).toMatchInlineSnapshot(`
|
||||
"{
|
||||
"simple": {
|
||||
"message": "example"
|
||||
},
|
||||
"sub": {
|
||||
"message": "Hello $1"
|
||||
},
|
||||
"nested_example": {
|
||||
"message": "This is nested"
|
||||
},
|
||||
"nested_array_0": {
|
||||
"message": "One"
|
||||
},
|
||||
"nested_array_1": {
|
||||
"message": "Two"
|
||||
},
|
||||
"nested_notChrome_message": {
|
||||
"message": "test 1"
|
||||
},
|
||||
"chrome": {
|
||||
"message": "test 2",
|
||||
"description": "test"
|
||||
},
|
||||
"plural0": {
|
||||
"message": "Zero items | One item | $1 items"
|
||||
},
|
||||
"plural1": {
|
||||
"message": "One item | $1 items"
|
||||
},
|
||||
"pluralN": {
|
||||
"message": "$1 items"
|
||||
},
|
||||
"pluralSub": {
|
||||
"message": "Hello $2, I have one problem | Hello $2, I have $1 problems"
|
||||
}
|
||||
}
|
||||
"
|
||||
`);
|
||||
expect(actualDtsFile).toMatchInlineSnapshot(`
|
||||
"export type GeneratedI18nStructure = {
|
||||
"simple": { substitutions: 0, plural: false };
|
||||
"sub": { substitutions: 1, plural: false };
|
||||
"nested.example": { substitutions: 0, plural: false };
|
||||
"nested.array.0": { substitutions: 0, plural: false };
|
||||
"nested.array.1": { substitutions: 0, plural: false };
|
||||
"nested.notChrome.message": { substitutions: 0, plural: false };
|
||||
"chrome": { substitutions: 0, plural: false };
|
||||
"plural0": { substitutions: 1, plural: true };
|
||||
"plural1": { substitutions: 1, plural: true };
|
||||
"pluralN": { substitutions: 1, plural: true };
|
||||
"pluralSub": { substitutions: 2, plural: true };
|
||||
"@@extension_id": { substitutions: 0, plural: false };
|
||||
"@@ui_locale": { substitutions: 0, plural: false };
|
||||
"@@bidi_dir": { substitutions: 0, plural: false };
|
||||
"@@bidi_reversed_dir": { substitutions: 0, plural: false };
|
||||
"@@bidi_start_edge": { substitutions: 0, plural: false };
|
||||
"@@bidi_end_edge": { substitutions: 0, plural: false };
|
||||
}
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['yaml', stringifyYAML],
|
||||
['yml', stringifyYAML],
|
||||
['toml', stringifyTOML],
|
||||
['json', stringifyJSON],
|
||||
['jsonc', stringifyJSONC],
|
||||
['json5', stringifyJSON5],
|
||||
])('Parse and generate: %s', async (extension, stringify) => {
|
||||
const fileText = stringify({
|
||||
simple: 'example',
|
||||
});
|
||||
const expectedDts = `export type GeneratedI18nStructure = {
|
||||
"simple": { substitutions: 0, plural: false };
|
||||
"@@extension_id": { substitutions: 0, plural: false };
|
||||
"@@ui_locale": { substitutions: 0, plural: false };
|
||||
"@@bidi_dir": { substitutions: 0, plural: false };
|
||||
"@@bidi_reversed_dir": { substitutions: 0, plural: false };
|
||||
"@@bidi_start_edge": { substitutions: 0, plural: false };
|
||||
"@@bidi_end_edge": { substitutions: 0, plural: false };
|
||||
}
|
||||
`;
|
||||
const expectedChromeMessages =
|
||||
JSON.stringify({ simple: { message: 'example' } }, null, 2) + '\n';
|
||||
|
||||
mockReadFile.mockResolvedValue(fileText);
|
||||
|
||||
const messages = await parseMessagesFile(`file.${extension}`);
|
||||
await generateChromeMessagesFile('output.json', messages);
|
||||
await generateTypeFile('output.d.ts', messages);
|
||||
|
||||
expect(mockWriteFile).toBeCalledTimes(2);
|
||||
expect(mockWriteFile).toBeCalledWith(
|
||||
'output.json',
|
||||
expectedChromeMessages,
|
||||
'utf8',
|
||||
);
|
||||
expect(mockWriteFile).toBeCalledWith('output.d.ts', expectedDts, 'utf8');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { createI18n } from '../index';
|
||||
import { GetMessageOptions } from '../types';
|
||||
|
||||
const getMessageMock = vi.fn();
|
||||
|
||||
vi.stubGlobal('chrome', {
|
||||
i18n: {
|
||||
getMessage: getMessageMock,
|
||||
},
|
||||
});
|
||||
|
||||
describe('createI18n', () => {
|
||||
beforeEach(() => {
|
||||
getMessageMock.mockReturnValue('Some text.');
|
||||
});
|
||||
|
||||
it.each([
|
||||
['key', 'key'],
|
||||
['some_key', 'some_key'],
|
||||
['some.nested.key', 'some_nested_key'],
|
||||
])('should retrieve "%s" as "%s"', (input, expectedKey) => {
|
||||
const i18n = createI18n();
|
||||
const expectedValue = String(Math.random());
|
||||
getMessageMock.mockReturnValue(expectedValue);
|
||||
|
||||
const actual = i18n.t(input);
|
||||
|
||||
expect(actual).toBe(expectedValue);
|
||||
expect(getMessageMock).toBeCalledTimes(1);
|
||||
expect(getMessageMock).toBeCalledWith(expectedKey, undefined);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['n items', 0, 'n items'],
|
||||
['n items', 1, 'n items'],
|
||||
['n items', 2, 'n items'],
|
||||
['n items', 3, 'n items'],
|
||||
['1 item | n items', 0, 'n items'],
|
||||
['1 item | n items', 1, '1 item'],
|
||||
['1 item | n items', 2, 'n items'],
|
||||
['1 item | n items', 3, 'n items'],
|
||||
['0 items | 1 item | n items', 0, '0 items'],
|
||||
['0 items | 1 item | n items', 1, '1 item'],
|
||||
['0 items | 1 item | n items', 2, 'n items'],
|
||||
['0 items | 1 item | n items', 3, 'n items'],
|
||||
])(
|
||||
'should retrieve plural forms correctly',
|
||||
(rawMessage, count, expected) => {
|
||||
const i18n = createI18n();
|
||||
getMessageMock.mockReturnValue(rawMessage);
|
||||
const key = 'items';
|
||||
|
||||
const actual = i18n.t(key, count);
|
||||
|
||||
expect(actual).toBe(expected);
|
||||
expect(getMessageMock).toBeCalledTimes(1);
|
||||
expect(getMessageMock).toBeCalledWith(key, [String(count)], undefined);
|
||||
},
|
||||
);
|
||||
|
||||
it('should allow overriding the plural substitutions', () => {
|
||||
const i18n = createI18n();
|
||||
i18n.t('key', 3, ['custom']);
|
||||
expect(getMessageMock).toBeCalledWith('key', ['custom'], undefined);
|
||||
});
|
||||
|
||||
it('should pass options into browser.i18n.getMessage', () => {
|
||||
const i18n = createI18n();
|
||||
const options: GetMessageOptions = {
|
||||
escapeLt: true,
|
||||
};
|
||||
|
||||
i18n.t('key', options);
|
||||
i18n.t('key', [''], options);
|
||||
i18n.t('key', 1, options);
|
||||
i18n.t('key', 1, [''], options);
|
||||
getMessageMock.mock.calls.forEach((call) => {
|
||||
expect(call.pop()).toEqual(options);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,119 @@
|
||||
import { beforeEach, describe, it, vi } from 'vitest';
|
||||
import { createI18n } from '..';
|
||||
|
||||
const getMessageMock = vi.fn();
|
||||
|
||||
vi.stubGlobal('chrome', {
|
||||
i18n: {
|
||||
getMessage: getMessageMock,
|
||||
},
|
||||
});
|
||||
|
||||
const n: number = 1;
|
||||
|
||||
describe('I18n Types', () => {
|
||||
beforeEach(() => {
|
||||
getMessageMock.mockReturnValue('Some text.');
|
||||
});
|
||||
|
||||
describe('No type-safety', () => {
|
||||
const i18n = createI18n();
|
||||
|
||||
describe('t', () => {
|
||||
it('should allow passing any combination of arguments', () => {
|
||||
i18n.t('any');
|
||||
i18n.t('any', { escapeLt: true });
|
||||
i18n.t('any', ['one']);
|
||||
i18n.t('any', ['one'], { escapeLt: true });
|
||||
i18n.t('any', ['one', 'two']);
|
||||
i18n.t('any', ['one', 'two'], { escapeLt: true });
|
||||
i18n.t('any', n, ['one', 'two']);
|
||||
i18n.t('any', n, ['one', 'two'], { escapeLt: true });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('With type-safety', () => {
|
||||
const i18n = createI18n<{
|
||||
simple: { plural: false; substitutions: 0 };
|
||||
simpleSub1: { plural: false; substitutions: 1 };
|
||||
simpleSub2: { plural: false; substitutions: 2 };
|
||||
plural: { plural: true; substitutions: 0 };
|
||||
pluralSub1: { plural: true; substitutions: 1 };
|
||||
pluralSub2: { plural: true; substitutions: 2 };
|
||||
}>();
|
||||
|
||||
describe('t', () => {
|
||||
it('should only allow passing valid combinations of arguments', () => {
|
||||
i18n.t('simple');
|
||||
i18n.t('simple', { escapeLt: true });
|
||||
// @ts-expect-error
|
||||
i18n.t('simple', []);
|
||||
// @ts-expect-error
|
||||
i18n.t('simple', ['one']);
|
||||
// @ts-expect-error
|
||||
i18n.t('simple', n);
|
||||
|
||||
i18n.t('simpleSub1', ['one']);
|
||||
i18n.t('simpleSub1', ['one'], { escapeLt: true });
|
||||
// @ts-expect-error
|
||||
i18n.t('simpleSub1');
|
||||
// @ts-expect-error
|
||||
i18n.t('simpleSub1', []);
|
||||
// @ts-expect-error
|
||||
i18n.t('simpleSub1', ['one', 'two']);
|
||||
// @ts-expect-error
|
||||
i18n.t('simpleSub1', n);
|
||||
|
||||
i18n.t('simpleSub2', ['one', 'two']);
|
||||
i18n.t('simpleSub2', ['one', 'two'], { escapeLt: true });
|
||||
// @ts-expect-error
|
||||
i18n.t('simpleSub2');
|
||||
// @ts-expect-error
|
||||
i18n.t('simpleSub2', ['one']);
|
||||
// @ts-expect-error
|
||||
i18n.t('simpleSub2', ['one', 'two', 'three']);
|
||||
// @ts-expect-error
|
||||
i18n.t('simpleSub2', n);
|
||||
|
||||
i18n.t('plural', n);
|
||||
i18n.t('plural', n, { escapeLt: true });
|
||||
// @ts-expect-error
|
||||
i18n.t('plural');
|
||||
// @ts-expect-error
|
||||
i18n.t('plural', []);
|
||||
// @ts-expect-error
|
||||
i18n.t('plural', ['one']);
|
||||
// @ts-expect-error
|
||||
i18n.t('plural', n, ['sub']);
|
||||
|
||||
i18n.t('pluralSub1', n);
|
||||
i18n.t('pluralSub1', n, { escapeLt: true });
|
||||
i18n.t('pluralSub1', n, undefined, { escapeLt: true });
|
||||
i18n.t('pluralSub1', n, ['one']);
|
||||
i18n.t('pluralSub1', n, ['one'], { escapeLt: true });
|
||||
// @ts-expect-error
|
||||
i18n.t('pluralSub1');
|
||||
// @ts-expect-error
|
||||
i18n.t('pluralSub1', ['one']);
|
||||
// @ts-expect-error
|
||||
i18n.t('pluralSub1', n, []);
|
||||
// @ts-expect-error
|
||||
i18n.t('pluralSub1', n, ['one', 'two']);
|
||||
|
||||
i18n.t('pluralSub2', n, ['one', 'two']);
|
||||
i18n.t('pluralSub2', n, ['one', 'two'], { escapeLt: true });
|
||||
// @ts-expect-error
|
||||
i18n.t('pluralSub2');
|
||||
// @ts-expect-error
|
||||
i18n.t('pluralSub2', ['one', 'two']);
|
||||
// @ts-expect-error
|
||||
i18n.t('pluralSub2', n, ['one']);
|
||||
// @ts-expect-error
|
||||
i18n.t('pluralSub2', n, ['one', 'two', 'three']);
|
||||
// @ts-expect-error
|
||||
i18n.t('pluralSub2', n);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { ChromeMessage } from '../build';
|
||||
import { applyChromeMessagePlaceholders, getSubstitionCount } from '../utils';
|
||||
|
||||
describe('Utils', () => {
|
||||
describe('applyChromeMessagePlaceholders', () => {
|
||||
it('should return the combined stirng', () => {
|
||||
const input = {
|
||||
message: 'Hello $username$, welcome to $appName$',
|
||||
placeholders: {
|
||||
username: { content: '$1' },
|
||||
appName: { content: 'Example' },
|
||||
},
|
||||
} satisfies ChromeMessage;
|
||||
const expected = input.message
|
||||
.replace('$username$', input.placeholders.username.content)
|
||||
.replace('$appName$', input.placeholders.appName.content);
|
||||
|
||||
const actual = applyChromeMessagePlaceholders(input);
|
||||
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
|
||||
it('should ignore the case', () => {
|
||||
const input = {
|
||||
message: 'Hello $USERNAME$, welcome $username$',
|
||||
placeholders: {
|
||||
username: { content: '$1' },
|
||||
},
|
||||
} satisfies ChromeMessage;
|
||||
const expected = input.message
|
||||
.replace('$USERNAME$', input.placeholders.username.content)
|
||||
.replace('$username$', input.placeholders.username.content);
|
||||
|
||||
const actual = applyChromeMessagePlaceholders(input);
|
||||
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSubstitionCount', () => {
|
||||
it('should return the last substution present in the message', () => {
|
||||
expect(getSubstitionCount('I like $1, but I like $2 better')).toBe(2);
|
||||
});
|
||||
|
||||
it('should return 0 when no substitutions are present', () => {
|
||||
expect(getSubstitionCount('test')).toBe(0);
|
||||
});
|
||||
|
||||
it('should ignore escaped dollar signs', () => {
|
||||
expect(getSubstitionCount('buy $1 now for $$2 dollars')).toBe(1);
|
||||
});
|
||||
|
||||
it('should return the highest substitution when skipping numbers', () => {
|
||||
expect(getSubstitionCount('I like $1, but I like $8 better')).toBe(8);
|
||||
});
|
||||
|
||||
it('should only allow up to 9 substitutions', () => {
|
||||
expect(getSubstitionCount('Hello $9')).toBe(9);
|
||||
expect(getSubstitionCount('Hello $10')).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,291 @@
|
||||
/**
|
||||
* This module contains utils for generating types and `messages.json` files
|
||||
* based on the custom messages file format.
|
||||
*
|
||||
* @module @wxt-dev/i18n/build
|
||||
*/
|
||||
|
||||
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
||||
import { parseYAML, parseJSON5, parseTOML } from 'confbox';
|
||||
import { dirname, extname } from 'node:path';
|
||||
import { applyChromeMessagePlaceholders, getSubstitionCount } from './utils';
|
||||
|
||||
//
|
||||
// TYPES
|
||||
//
|
||||
|
||||
export interface ChromeMessage {
|
||||
message: string;
|
||||
description?: string;
|
||||
placeholders?: Record<string, { content: string; example?: string }>;
|
||||
}
|
||||
|
||||
export interface ParsedBaseMessage {
|
||||
key: string[];
|
||||
substitutions: number;
|
||||
}
|
||||
|
||||
export interface ParsedChromeMessage extends ParsedBaseMessage, ChromeMessage {
|
||||
type: 'chrome';
|
||||
}
|
||||
export interface ParsedSimpleMessage extends ParsedBaseMessage {
|
||||
type: 'simple';
|
||||
message: string;
|
||||
}
|
||||
export interface ParsedPluralMessage extends ParsedBaseMessage {
|
||||
type: 'plural';
|
||||
plurals: { [count: string]: string };
|
||||
}
|
||||
|
||||
export type ParsedMessage =
|
||||
| ParsedChromeMessage
|
||||
| ParsedSimpleMessage
|
||||
| ParsedPluralMessage;
|
||||
|
||||
export type MessageFormat = 'JSON5' | 'YAML' | 'TOML';
|
||||
|
||||
//
|
||||
// CONSTANTS
|
||||
//
|
||||
|
||||
/**
|
||||
* See https://developer.chrome.com/docs/extensions/reference/api/i18n#overview-predefined
|
||||
*/
|
||||
const PREDEFINED_MESSAGES: Record<string, ChromeMessage> = {
|
||||
'@@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".',
|
||||
},
|
||||
};
|
||||
|
||||
const EXT_FORMATS_MAP: Record<string, MessageFormat> = {
|
||||
'.json': 'JSON5',
|
||||
'.jsonc': 'JSON5',
|
||||
'.json5': 'JSON5',
|
||||
'.yaml': 'YAML',
|
||||
'.yml': 'YAML',
|
||||
'.toml': 'TOML',
|
||||
};
|
||||
|
||||
const PARSERS: Record<MessageFormat, (text: string) => any> = {
|
||||
YAML: parseYAML,
|
||||
JSON5: parseJSON5,
|
||||
TOML: parseTOML,
|
||||
};
|
||||
|
||||
const ALLOWED_CHROME_MESSAGE_KEYS: Set<string> = new Set<keyof ChromeMessage>([
|
||||
'description',
|
||||
'message',
|
||||
'placeholders',
|
||||
]);
|
||||
|
||||
//
|
||||
// PARSING
|
||||
//
|
||||
|
||||
/**
|
||||
* Parse a messages file, extract the messages. Supports JSON, JSON5, and YAML.
|
||||
*/
|
||||
export async function parseMessagesFile(
|
||||
file: string,
|
||||
): Promise<ParsedMessage[]> {
|
||||
const text = await readFile(file, 'utf8');
|
||||
const ext = extname(file).toLowerCase();
|
||||
return parseMessagesText(text, EXT_FORMATS_MAP[ext] ?? 'JSON5');
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a string, extracting the messages. Supports JSON, JSON5, and YAML.
|
||||
*/
|
||||
export function parseMessagesText(
|
||||
text: string,
|
||||
format: 'JSON5' | 'YAML' | 'TOML',
|
||||
): ParsedMessage[] {
|
||||
return parseMessagesObject(PARSERS[format](text));
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the JS object form of a raw messages file, extract the messages.
|
||||
*/
|
||||
export function parseMessagesObject(object: any): ParsedMessage[] {
|
||||
return _parseMessagesObject(
|
||||
[],
|
||||
{
|
||||
...object,
|
||||
...PREDEFINED_MESSAGES,
|
||||
},
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
function _parseMessagesObject(
|
||||
path: string[],
|
||||
object: any,
|
||||
depth: number,
|
||||
): ParsedMessage[] {
|
||||
switch (typeof object) {
|
||||
case 'string':
|
||||
case 'bigint':
|
||||
case 'boolean':
|
||||
case 'number':
|
||||
case 'symbol': {
|
||||
const message = String(object);
|
||||
const substitutions = getSubstitionCount(message);
|
||||
return [
|
||||
{
|
||||
type: 'simple',
|
||||
key: path,
|
||||
substitutions,
|
||||
message,
|
||||
},
|
||||
];
|
||||
}
|
||||
case 'object':
|
||||
if (Array.isArray(object))
|
||||
return object.flatMap((item, i) =>
|
||||
_parseMessagesObject(path.concat(String(i)), item, depth + 1),
|
||||
);
|
||||
if (isPluralMessage(object)) {
|
||||
const message = Object.values(object).join('|');
|
||||
const substitutions = getSubstitionCount(message);
|
||||
return [
|
||||
{
|
||||
type: 'plural',
|
||||
key: path,
|
||||
substitutions,
|
||||
plurals: object,
|
||||
},
|
||||
];
|
||||
}
|
||||
if (depth === 1 && isChromeMessage(object)) {
|
||||
const message = applyChromeMessagePlaceholders(object);
|
||||
const substitutions = getSubstitionCount(message);
|
||||
return [
|
||||
{
|
||||
type: 'chrome',
|
||||
key: path,
|
||||
substitutions,
|
||||
...object,
|
||||
},
|
||||
];
|
||||
}
|
||||
return Object.entries(object).flatMap(([key, value]) =>
|
||||
_parseMessagesObject(path.concat(key), value, depth + 1),
|
||||
);
|
||||
default:
|
||||
throw Error(`"Could not parse object of type "${typeof object}"`);
|
||||
}
|
||||
}
|
||||
|
||||
function isPluralMessage(object: any): object is Record<number | 'n', string> {
|
||||
return Object.keys(object).every(
|
||||
(key) => key === 'n' || isFinite(Number(key)),
|
||||
);
|
||||
}
|
||||
|
||||
function isChromeMessage(object: any): object is ChromeMessage {
|
||||
return Object.keys(object).every((key) =>
|
||||
ALLOWED_CHROME_MESSAGE_KEYS.has(key),
|
||||
);
|
||||
}
|
||||
|
||||
//
|
||||
// OUTPUT
|
||||
//
|
||||
|
||||
export function generateTypeText(messages: ParsedMessage[]): string {
|
||||
const renderMessageEntry = (message: ParsedMessage): string => {
|
||||
// Use . for deep keys at runtime and types
|
||||
const key = message.key.join('.');
|
||||
|
||||
const features = [
|
||||
`substitutions: ${message.substitutions}`,
|
||||
`plural: ${message.type === 'plural'}`,
|
||||
];
|
||||
return ` "${key}": { ${features.join(', ')} };`;
|
||||
};
|
||||
|
||||
return `export type GeneratedI18nStructure = {
|
||||
${messages.map(renderMessageEntry).join('\n')}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
export async function generateTypeFile(
|
||||
outFile: string,
|
||||
messages: ParsedMessage[],
|
||||
): Promise<void> {
|
||||
const text = generateTypeText(messages);
|
||||
await mkdir(dirname(outFile), { recursive: true });
|
||||
await writeFile(outFile, text, 'utf8');
|
||||
}
|
||||
|
||||
export function generateChromeMessages(
|
||||
messages: ParsedMessage[],
|
||||
): Record<string, ChromeMessage> {
|
||||
return messages.reduce<Record<string, ChromeMessage>>((acc, message) => {
|
||||
// Use _ for deep keys in _locales/.../messages.json
|
||||
const key = message.key.join('_');
|
||||
// Don't output predefined messages
|
||||
if (PREDEFINED_MESSAGES[key]) return acc;
|
||||
|
||||
switch (message.type) {
|
||||
case 'chrome':
|
||||
acc[key] = {
|
||||
message: message.message,
|
||||
description: message.description,
|
||||
placeholders: message.placeholders,
|
||||
};
|
||||
break;
|
||||
case 'plural':
|
||||
acc[key] = {
|
||||
message: Object.values(message.plurals).join(' | '),
|
||||
};
|
||||
break;
|
||||
case 'simple':
|
||||
acc[key] = {
|
||||
message: message.message,
|
||||
};
|
||||
break;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
export function generateChromeMessagesText(messages: ParsedMessage[]): string {
|
||||
const raw = generateChromeMessages(messages);
|
||||
return JSON.stringify(raw, null, 2);
|
||||
}
|
||||
|
||||
export async function generateChromeMessagesFile(
|
||||
file: string,
|
||||
messages: ParsedMessage[],
|
||||
): Promise<void> {
|
||||
const text = generateChromeMessagesText(messages);
|
||||
await writeFile(file, text + '\n', 'utf8');
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* @module @wxt-dev/i18n
|
||||
*/
|
||||
import {
|
||||
I18nStructure,
|
||||
DefaultI18nStructure,
|
||||
I18n,
|
||||
Substitution,
|
||||
GetMessageOptions,
|
||||
} from './types';
|
||||
|
||||
export function createI18n<
|
||||
T extends I18nStructure = DefaultI18nStructure,
|
||||
>(): I18n<T> {
|
||||
const t = (key: string, ...args: any[]) => {
|
||||
// Resolve args
|
||||
let sub: Substitution[] | undefined;
|
||||
let count: number | undefined;
|
||||
let options: GetMessageOptions | undefined;
|
||||
args.forEach((arg, i) => {
|
||||
if (arg == null) {
|
||||
// ignore nullish args
|
||||
} else if (typeof arg === 'number') {
|
||||
count = arg;
|
||||
} else if (Array.isArray(arg)) {
|
||||
sub = arg;
|
||||
} else if (typeof arg === 'object') {
|
||||
options = arg;
|
||||
} else {
|
||||
throw Error(
|
||||
`Unknown argument at index ${i}. Must be a number for pluralization, substitution array, or options object.`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Default substitutions to [n]
|
||||
if (count != null && sub == null) {
|
||||
sub = [String(count)];
|
||||
}
|
||||
|
||||
// Load the localization
|
||||
let message: string;
|
||||
if (sub?.length) {
|
||||
// Convert all substitutions to strings
|
||||
const stringSubs = sub?.map((sub) => String(sub));
|
||||
message = chrome.i18n.getMessage(
|
||||
key.replaceAll('.', '_'),
|
||||
stringSubs,
|
||||
// @ts-ignore - @types/chrome doesn't type the options object, but it's there
|
||||
options,
|
||||
);
|
||||
} else {
|
||||
message = chrome.i18n.getMessage(
|
||||
key.replaceAll('.', '_'),
|
||||
// @ts-ignore - @types/chrome doesn't type the options object, but it's there
|
||||
options,
|
||||
);
|
||||
}
|
||||
if (!message) {
|
||||
console.warn(`[i18n] Message not found: "${key}"`);
|
||||
}
|
||||
if (count == null) return message;
|
||||
|
||||
// Apply pluralization
|
||||
const plural = message.split(' | ');
|
||||
switch (plural.length) {
|
||||
// "n items"
|
||||
case 1:
|
||||
return plural[0];
|
||||
// "1 item | n items"
|
||||
case 2:
|
||||
return plural[count === 1 ? 0 : 1];
|
||||
// "0 items | 1 item | n items"
|
||||
case 3:
|
||||
return plural[count === 0 || count === 1 ? count : 2];
|
||||
default:
|
||||
throw Error('Unknown plural formatting');
|
||||
}
|
||||
};
|
||||
|
||||
return { t } as I18n<T>;
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* The WXT Module to integrate `@wxt-dev/i18n` into your project.
|
||||
*
|
||||
* ```ts
|
||||
* export default defineConfig({
|
||||
* modules: ["@wxt-dev/i18n/module"],
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @module @wxt-dev/i18n/module
|
||||
*/
|
||||
|
||||
import 'wxt';
|
||||
import { addAlias, defineWxtModule } from 'wxt/modules';
|
||||
import {
|
||||
generateChromeMessagesText,
|
||||
parseMessagesFile,
|
||||
generateTypeText,
|
||||
} from './build';
|
||||
import glob from 'fast-glob';
|
||||
import { basename, extname, join, resolve } from 'node:path';
|
||||
import { watch } from 'chokidar';
|
||||
import { GeneratedPublicFile, WxtDirFileEntry } from 'wxt';
|
||||
import { writeFile } from 'node:fs/promises';
|
||||
|
||||
export default defineWxtModule({
|
||||
name: '@wxt-dev/i18n',
|
||||
imports: [{ from: '#i18n', name: 'i18n' }],
|
||||
|
||||
setup(wxt) {
|
||||
if (wxt.config.manifest.default_locale == null) {
|
||||
wxt.logger.warn(
|
||||
`\`[i18n]\` manifest.default_locale not set, \`@wxt-dev/i18n\` disabled.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
wxt.logger.info(
|
||||
'`[i18n]` Default locale: ' + wxt.config.manifest.default_locale,
|
||||
);
|
||||
|
||||
const getLocalizationFiles = async () => {
|
||||
const files = await glob('locales/*', {
|
||||
cwd: wxt.config.srcDir,
|
||||
absolute: true,
|
||||
});
|
||||
return files.map((file) => ({
|
||||
file,
|
||||
locale: basename(file).replace(extname(file), ''),
|
||||
}));
|
||||
};
|
||||
|
||||
const generateOutputJsonFiles = async (): Promise<
|
||||
GeneratedPublicFile[]
|
||||
> => {
|
||||
const files = await getLocalizationFiles();
|
||||
return await Promise.all(
|
||||
files.map(async ({ file, locale }) => {
|
||||
const messages = await parseMessagesFile(file);
|
||||
return {
|
||||
contents: generateChromeMessagesText(messages),
|
||||
relativeDest: join('_locales', locale, 'messages.json'),
|
||||
};
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const generateTypes = async (): Promise<WxtDirFileEntry> => {
|
||||
const files = await getLocalizationFiles();
|
||||
const defaultLocaleFile = files.find(
|
||||
({ locale }) => locale === wxt.config.manifest.default_locale,
|
||||
)!;
|
||||
if (defaultLocaleFile == null) {
|
||||
throw Error(
|
||||
`\`[i18n]\` Required localization file does not exist: \`<srcDir>/locales/${wxt.config.manifest.default_locale}.{json|json5|yml|yaml|toml}\``,
|
||||
);
|
||||
}
|
||||
|
||||
const messages = await parseMessagesFile(defaultLocaleFile.file);
|
||||
return {
|
||||
path: typesPath,
|
||||
text: generateTypeText(messages),
|
||||
};
|
||||
};
|
||||
|
||||
const updateLocalizations = async (file: string): Promise<void> => {
|
||||
wxt.logger.info(
|
||||
`\`[i18n]\` Localization file changed: \`${basename(file)}\``,
|
||||
);
|
||||
|
||||
// Regenerate files
|
||||
const [typesFile, jsonFiles] = await Promise.all([
|
||||
generateTypes(),
|
||||
generateOutputJsonFiles(),
|
||||
]);
|
||||
|
||||
// Write files to disk
|
||||
await Promise.all([
|
||||
writeFile(
|
||||
resolve(wxt.config.wxtDir, typesFile.path),
|
||||
typesFile.text,
|
||||
'utf8',
|
||||
),
|
||||
...jsonFiles.map((file) =>
|
||||
writeFile(
|
||||
resolve(wxt.config.outDir, file.relativeDest),
|
||||
file.contents,
|
||||
'utf8',
|
||||
),
|
||||
),
|
||||
]);
|
||||
|
||||
// TODO: Implement HMR instead of reloading extension. The reload is
|
||||
// fast, but it causes the popup to close, which I'd like to prevent.
|
||||
wxt.server?.reloadExtension();
|
||||
wxt.logger.success(`\`[i18n]\` Extension reloaded`);
|
||||
};
|
||||
|
||||
// Create .wxt/i18n.ts
|
||||
|
||||
const sourcePath = resolve(wxt.config.wxtDir, 'i18n/index.ts');
|
||||
const typesPath = resolve(wxt.config.wxtDir, 'i18n/structure.d.ts');
|
||||
|
||||
wxt.hooks.hook('prepare:types', async (_, entries) => {
|
||||
entries.push({
|
||||
path: sourcePath,
|
||||
text: `import { createI18n } from '@wxt-dev/i18n';
|
||||
import type { GeneratedI18nStructure } from './structure';
|
||||
|
||||
export const i18n = createI18n<GeneratedI18nStructure>();
|
||||
|
||||
export { GeneratedI18nStructure }
|
||||
`,
|
||||
});
|
||||
});
|
||||
|
||||
addAlias(wxt, '#i18n', sourcePath);
|
||||
|
||||
// Generate separate declaration file containing types - this prevents
|
||||
// firing the dev server's default file watcher when updating the types,
|
||||
// which would cause a full rebuild and reload of the extension.
|
||||
|
||||
wxt.hooks.hook('prepare:types', async (_, entries) => {
|
||||
entries.push(await generateTypes());
|
||||
});
|
||||
|
||||
// Generate _locales/.../messages.json files
|
||||
|
||||
wxt.hooks.hook('build:publicAssets', async (_, assets) => {
|
||||
const outFiles = await generateOutputJsonFiles();
|
||||
assets.push(...outFiles);
|
||||
});
|
||||
|
||||
// Reload extension during development
|
||||
|
||||
if (wxt.config.command === 'serve') {
|
||||
wxt.hooks.hookOnce('build:done', () => {
|
||||
const watcher = watch(resolve(wxt.config.srcDir, 'locales'));
|
||||
watcher.on('change', updateLocalizations);
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
export interface I18nFeatures {
|
||||
plural: boolean;
|
||||
substitutions: SubstitutionCount;
|
||||
}
|
||||
|
||||
export type I18nStructure = {
|
||||
[K: string]: I18nFeatures;
|
||||
};
|
||||
|
||||
export type DefaultI18nStructure = {
|
||||
[K: string]: any;
|
||||
};
|
||||
|
||||
// prettier-ignore
|
||||
export type SubstitutionTuple<T extends SubstitutionCount> =
|
||||
T extends 1 ? [$1: Substitution]
|
||||
: T extends 2 ? [$1: Substitution, $2: Substitution]
|
||||
: T extends 3 ? [$1: Substitution, $2: Substitution, $3: Substitution]
|
||||
: T extends 4 ? [$1: Substitution, $2: Substitution, $3: Substitution, $4: Substitution]
|
||||
: T extends 5 ? [$1: Substitution, $2: Substitution, $3: Substitution, $4: Substitution, $5: Substitution]
|
||||
: T extends 6 ? [$1: Substitution, $2: Substitution, $3: Substitution, $4: Substitution, $5: Substitution, $6: Substitution]
|
||||
: T extends 7 ? [$1: Substitution, $2: Substitution, $3: Substitution, $4: Substitution, $5: Substitution, $6: Substitution, $7: Substitution]
|
||||
: T extends 8 ? [$1: Substitution, $2: Substitution, $3: Substitution, $4: Substitution, $5: Substitution, $6: Substitution, $7: Substitution, $8: Substitution]
|
||||
: T extends 9 ? [$1: Substitution, $2: Substitution, $3: Substitution, $4: Substitution, $5: Substitution, $6: Substitution, $7: Substitution, $8: Substitution, $9: Substitution]
|
||||
: never
|
||||
|
||||
export type TFunction<T extends I18nStructure> = {
|
||||
// Non-plural, no substitutions
|
||||
<K extends keyof T>(
|
||||
// prettier-ignore
|
||||
key: K & { [P in keyof T]: T[P] extends { plural: false; substitutions: 0 } ? P : never; }[keyof T],
|
||||
options?: GetMessageOptions,
|
||||
): string;
|
||||
|
||||
// Non-plural with substitutions
|
||||
<K extends keyof T>(
|
||||
// prettier-ignore
|
||||
key: K & { [P in keyof T]: T[P] extends { plural: false; substitutions: SubstitutionCount } ? P : never; }[keyof T],
|
||||
substitutions: T[K] extends I18nFeatures
|
||||
? SubstitutionTuple<T[K]['substitutions']>
|
||||
: never,
|
||||
options?: GetMessageOptions,
|
||||
): string;
|
||||
|
||||
// Plural with 1 substitution
|
||||
<K extends keyof T>(
|
||||
// prettier-ignore
|
||||
key: K & { [P in keyof T]: T[P] extends { plural: true; substitutions: 1 } ? P : never; }[keyof T],
|
||||
n: number,
|
||||
substitutions?: SubstitutionTuple<1>,
|
||||
options?: GetMessageOptions,
|
||||
): string;
|
||||
|
||||
// Plural without substitutions
|
||||
<K extends keyof T>(
|
||||
// prettier-ignore
|
||||
key: K & { [P in keyof T]: T[P] extends { plural: true; substitutions: 0 | 1 } ? P : never; }[keyof T],
|
||||
n: number,
|
||||
options?: GetMessageOptions,
|
||||
): string;
|
||||
|
||||
// Plural with substitutions
|
||||
<K extends keyof T>(
|
||||
// prettier-ignore
|
||||
key: K & { [P in keyof T]: T[P] extends { plural: true; substitutions: SubstitutionCount } ? P : never; }[keyof T],
|
||||
n: number,
|
||||
substitutions: T[K] extends I18nFeatures
|
||||
? SubstitutionTuple<T[K]['substitutions']>
|
||||
: never,
|
||||
options?: GetMessageOptions,
|
||||
): string;
|
||||
};
|
||||
|
||||
export interface I18n<T extends DefaultI18nStructure> {
|
||||
t: TFunction<T>;
|
||||
}
|
||||
|
||||
export type Substitution = string | number;
|
||||
|
||||
export interface GetMessageOptions {
|
||||
/**
|
||||
* Escape `<` in translation to `<`. This applies only to the message itself, not to the placeholders. Developers might want to use this if the translation is used in an HTML context. Closure Templates used with Closure Compiler generate this automatically.
|
||||
*
|
||||
* See https://developer.chrome.com/docs/extensions/reference/api/i18n#type-getMessage-options
|
||||
*/
|
||||
escapeLt?: boolean;
|
||||
}
|
||||
|
||||
type SubstitutionCount = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ChromeMessage } from './build';
|
||||
|
||||
export function applyChromeMessagePlaceholders(message: ChromeMessage): string {
|
||||
if (message.placeholders == null) return message.message;
|
||||
|
||||
return Object.entries(message.placeholders ?? {}).reduce(
|
||||
(text, [name, value]) => {
|
||||
return text.replaceAll(new RegExp(`\\$${name}\\$`, 'gi'), value.content);
|
||||
},
|
||||
message.message,
|
||||
);
|
||||
}
|
||||
|
||||
export function getSubstitionCount(message: string): number {
|
||||
return (
|
||||
1 +
|
||||
new Array(MAX_SUBSTITUTIONS).findLastIndex((_, i) =>
|
||||
message.match(new RegExp(`(?<!\\$)\\$${i + 1}`)),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const MAX_SUBSTITUTIONS = 9;
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"types": ["chrome", "node"]
|
||||
},
|
||||
"exclude": ["node_modules/**", "dist/**"]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { defineProject } from 'vitest/config';
|
||||
import RandomSeed from 'vitest-plugin-random-seed';
|
||||
|
||||
export default defineProject({
|
||||
test: {
|
||||
mockReset: true,
|
||||
restoreMocks: true,
|
||||
},
|
||||
plugins: [RandomSeed()],
|
||||
});
|
||||
@@ -1,5 +1,19 @@
|
||||
# Changelog
|
||||
|
||||
## v1.1.2
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/module-solid-v1.1.1...module-solid-v1.1.2)
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- **deps:** Bump all non-major dependencies ([#834](https://github.com/wxt-dev/wxt/pull/834))
|
||||
- **deps:** Upgrade all dependencies ([#869](https://github.com/wxt-dev/wxt/pull/869))
|
||||
- Add more metadata for npm ([#885](https://github.com/wxt-dev/wxt/pull/885))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Florian Metz ([@Timeraa](http://github.com/Timeraa))
|
||||
|
||||
## v1.1.1
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/module-solid-v1.1.0...module-solid-v1.1.1)
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"email": "aaronklinker1+wxt@gmail.com"
|
||||
},
|
||||
"license": "MIT",
|
||||
"version": "1.1.1",
|
||||
"version": "1.1.2",
|
||||
"type": "module",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.mjs",
|
||||
|
||||
@@ -1,5 +1,31 @@
|
||||
# Changelog
|
||||
|
||||
## v1.0.1
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/module-svelte-v1.0.0...module-svelte-v1.0.1)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Upgrade wxt peer to >= 0.18.6 ([7edf1c8](https://github.com/wxt-dev/wxt/commit/7edf1c8))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Fix link to unimport ([#826](https://github.com/wxt-dev/wxt/pull/826))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Add changelog ([21e8ca0](https://github.com/wxt-dev/wxt/commit/21e8ca0))
|
||||
- Extract build cache script to NPM package ([#737](https://github.com/wxt-dev/wxt/pull/737))
|
||||
- **deps:** Upgrade non-major deps ([#778](https://github.com/wxt-dev/wxt/pull/778))
|
||||
- **deps:** Bump all non-major dependencies ([#834](https://github.com/wxt-dev/wxt/pull/834))
|
||||
- **deps:** Upgrade all dependencies ([#869](https://github.com/wxt-dev/wxt/pull/869))
|
||||
- Add more metadata for npm ([#885](https://github.com/wxt-dev/wxt/pull/885))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Florian Metz ([@Timeraa](http://github.com/Timeraa))
|
||||
- Eetann ([@eetann](http://github.com/eetann))
|
||||
|
||||
## v1.0.0
|
||||
|
||||
Initial release 🎉
|
||||
Initial release 🎉
|
||||
@@ -17,7 +17,7 @@
|
||||
"email": "aaronklinker1+wxt@gmail.com"
|
||||
},
|
||||
"license": "MIT",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"type": "module",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.mjs",
|
||||
|
||||
@@ -1,5 +1,26 @@
|
||||
# Changelog
|
||||
|
||||
## v1.0.1
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/module-vue-v1.0.0...module-vue-v1.0.1)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Upgrade wxt peer to >= 0.18.6 ([7edf1c8](https://github.com/wxt-dev/wxt/commit/7edf1c8))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Add changelog ([21e8ca0](https://github.com/wxt-dev/wxt/commit/21e8ca0))
|
||||
- Extract build cache script to NPM package ([#737](https://github.com/wxt-dev/wxt/pull/737))
|
||||
- **deps:** Upgrade non-major deps ([#778](https://github.com/wxt-dev/wxt/pull/778))
|
||||
- **deps:** Bump all non-major dependencies ([#834](https://github.com/wxt-dev/wxt/pull/834))
|
||||
- **deps:** Upgrade all dependencies ([#869](https://github.com/wxt-dev/wxt/pull/869))
|
||||
- Add more metadata for npm ([#885](https://github.com/wxt-dev/wxt/pull/885))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Florian Metz ([@Timeraa](http://github.com/Timeraa))
|
||||
|
||||
## v1.0.0
|
||||
|
||||
Initial release 🎉
|
||||
Initial release 🎉
|
||||
@@ -17,7 +17,7 @@
|
||||
"email": "aaronklinker1+wxt@gmail.com"
|
||||
},
|
||||
"license": "MIT",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"type": "module",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.mjs",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"postinstall": "buildc --deps-only -- wxt prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"@wxt-dev/i18n": "workspace:*",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
@@ -32,6 +33,6 @@
|
||||
"wxt": "workspace:*"
|
||||
},
|
||||
"buildc": {
|
||||
"outDir": ".output/chrome-mv3"
|
||||
"cachable": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import background from '../background';
|
||||
|
||||
browser.i18n.getMessage = () => 'fake-message';
|
||||
chrome.i18n.getMessage = () => 'fake-message';
|
||||
|
||||
const logMock = vi.fn();
|
||||
console.log = logMock;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import messages from '~/public/_locales/en/messages.json';
|
||||
|
||||
export default defineBackground({
|
||||
// type: 'module',
|
||||
|
||||
@@ -12,7 +10,6 @@ export default defineBackground({
|
||||
chrome: import.meta.env.CHROME,
|
||||
firefox: import.meta.env.FIREFOX,
|
||||
manifestVersion: import.meta.env.MANIFEST_VERSION,
|
||||
messages,
|
||||
});
|
||||
|
||||
console.log(useAppConfig());
|
||||
@@ -27,11 +24,14 @@ export default defineBackground({
|
||||
browser.runtime.getURL('/icon-128.png?query=param');
|
||||
|
||||
// @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');
|
||||
i18n.t('test');
|
||||
i18n.t('prompt_for_name');
|
||||
i18n.t('hello', ['test']);
|
||||
i18n.t('bye', ['Aaron']);
|
||||
i18n.t('@@extension_id');
|
||||
i18n.t('deep.example');
|
||||
i18n.t('items', 0);
|
||||
i18n.t('items', 0, ['one']);
|
||||
|
||||
console.log('WXT MODE:', {
|
||||
MODE: import.meta.env.MODE,
|
||||
|
||||
@@ -13,7 +13,7 @@ export default defineContentScript({
|
||||
anchor: 'form[role=search]',
|
||||
onMount: (container) => {
|
||||
const app = document.createElement('div');
|
||||
app.textContent = browser.i18n.getMessage('prompt_for_name');
|
||||
app.textContent = i18n.t('prompt_for_name');
|
||||
container.append(app);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
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
|
||||
deep:
|
||||
example: 'this is deep'
|
||||
items:
|
||||
0: Zero items
|
||||
1: 1 item
|
||||
n: $1 items
|
||||
@@ -0,0 +1,3 @@
|
||||
import module from '@wxt-dev/i18n/module';
|
||||
|
||||
export default module;
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,84 @@
|
||||
# Changelog
|
||||
|
||||
## v0.19.8
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.7...wxt-v0.19.8)
|
||||
|
||||
### 🔥 Performance
|
||||
|
||||
- Ignore output directories for all `vite.watcher` ([#924](https://github.com/wxt-dev/wxt/pull/924))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Fallback to GitHub API for listing templates when ungh is down ([#944](https://github.com/wxt-dev/wxt/pull/944))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- **types:** Cleanup types in wxt/browser/chrome ([#932](https://github.com/wxt-dev/wxt/pull/932))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- 1natsu
|
||||
|
||||
## v0.19.7
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.6...wxt-v0.19.7)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- **testing:** Run WXT modules when setting up test environment ([#926](https://github.com/wxt-dev/wxt/pull/926))
|
||||
- **modules:** Add `addAlias` helper ([#928](https://github.com/wxt-dev/wxt/pull/928))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- **testing:** Stub `chrome` and `browser` globals with `fakeBrowser` automatically ([#925](https://github.com/wxt-dev/wxt/pull/925))
|
||||
- Ensure TSConfig paths start with `../` or `./` so they are valid ([#927](https://github.com/wxt-dev/wxt/pull/927))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Fix module name for `wxt/browser/chrome` ([751706d](https://github.com/wxt-dev/wxt/commit/751706d))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Remove warning log for missing public directory ([5f2e1c3](https://github.com/wxt-dev/wxt/commit/5f2e1c3))
|
||||
|
||||
## v0.19.6
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.5...wxt-v0.19.6)
|
||||
|
||||
### 🔥 Performance
|
||||
|
||||
- Ignore non-source code from the file watcher ([#919](https://github.com/wxt-dev/wxt/pull/919))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Typo in sidepanel options (`browse_style` → `browser_style`) ([#914](https://github.com/wxt-dev/wxt/pull/914))
|
||||
- **types:** Don't report type errors when using string templates with `browser.i18n.getMessage` ([#916](https://github.com/wxt-dev/wxt/pull/916))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- 1natsu ([@1natsu](https://github.com/1natsu172))
|
||||
|
||||
## v0.19.5
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.4...wxt-v0.19.5)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Allow zipping hidden files by listing them explicitly in `includeSources` ([#902](https://github.com/wxt-dev/wxt/pull/902))
|
||||
- Allow excluding files when zipping ([#906](https://github.com/wxt-dev/wxt/pull/906))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- #907 move wxt devDeps execa to dependencies ([#908](https://github.com/wxt-dev/wxt/pull/908), [#907](https://github.com/wxt-dev/wxt/issues/907))
|
||||
- Update chromium setting for enabling content script sourcemaps ([e6529e6](https://github.com/wxt-dev/wxt/commit/e6529e6))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- 1natsu ([@1natsu](https://github.com/1natsu172))
|
||||
- Florian Metz ([@Timeraa](http://github.com/Timeraa))
|
||||
- Hikiko4ern ([@hikiko4ern](http://github.com/hikiko4ern))
|
||||
|
||||
## v0.19.4
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.3...wxt-v0.19.4)
|
||||
@@ -2353,4 +2432,4 @@ Initial release of WXT. Full support for production builds and initial toolkit f
|
||||
### 🤖 CI
|
||||
|
||||
- Create validation workflow ([#12](https://github.com/wxt-dev/wxt/pull/12))
|
||||
- Create release workflow ([#13](https://github.com/wxt-dev/wxt/pull/13))
|
||||
- Create release workflow ([#13](https://github.com/wxt-dev/wxt/pull/13))
|
||||
@@ -126,7 +126,7 @@ describe('TypeScript Project', () => {
|
||||
export interface WxtI18n extends I18n.Static {
|
||||
/**
|
||||
* 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.
|
||||
* Note: You can't use this message in a manifest file.
|
||||
*
|
||||
* "<browser.runtime.id>"
|
||||
*/
|
||||
@@ -136,8 +136,6 @@ describe('TypeScript Project', () => {
|
||||
options?: GetMessageOptions,
|
||||
): string;
|
||||
/**
|
||||
* No message description.
|
||||
*
|
||||
* "<browser.i18n.getUiLocale()>"
|
||||
*/
|
||||
getMessage(
|
||||
@@ -215,6 +213,11 @@ describe('TypeScript Project', () => {
|
||||
substitutions?: string | string[],
|
||||
options?: GetMessageOptions,
|
||||
): string;
|
||||
getMessage(
|
||||
messageName: "@@extension_id" | "@@ui_locale" | "@@bidi_dir" | "@@bidi_reversed_dir" | "@@bidi_start_edge" | "@@bidi_end_edge" | "prompt_for_name" | "hello" | "bye",
|
||||
substitutions?: string | string[],
|
||||
options?: GetMessageOptions,
|
||||
): string;
|
||||
}
|
||||
}
|
||||
"
|
||||
@@ -376,6 +379,22 @@ describe('TypeScript Project', () => {
|
||||
`);
|
||||
});
|
||||
|
||||
it('should start path aliases with "./" for paths inside the .wxt dir', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('src/entrypoints/unlisted.html', '<html></html>');
|
||||
project.setConfigFileConfig({
|
||||
srcDir: 'src',
|
||||
alias: {
|
||||
example: '.wxt/example.ts',
|
||||
},
|
||||
});
|
||||
|
||||
await project.prepare();
|
||||
|
||||
const output = await project.serializeFile('.wxt/tsconfig.json');
|
||||
expect(output).toContain('./example.ts');
|
||||
});
|
||||
|
||||
// TODO: Once a module has been published, use it here for testing - local files are never added to the .wxt/wxt.d.ts file
|
||||
it.todo(
|
||||
'should add modules from NPM to the TS project if they have a configKey',
|
||||
|
||||
@@ -97,8 +97,7 @@ describe('Zipping', () => {
|
||||
expect(await project.fileExists(sourcesZip)).toBe(true);
|
||||
});
|
||||
|
||||
// TODO: Fix #738 and re-enable
|
||||
it.skip('should not zip hidden files into sources by default', async () => {
|
||||
it('should not zip hidden files into sources by default', async () => {
|
||||
const project = new TestProject({
|
||||
name: 'test',
|
||||
version: '1.0.0',
|
||||
@@ -108,6 +107,7 @@ describe('Zipping', () => {
|
||||
'export default defineBackground(() => {});',
|
||||
);
|
||||
project.addFile('.env');
|
||||
project.addFile('.hidden-dir/file');
|
||||
const unzipDir = project.resolvePath('.output/test-1.0.0-sources');
|
||||
const sourcesZip = project.resolvePath('.output/test-1.0.0-sources.zip');
|
||||
|
||||
@@ -116,10 +116,37 @@ describe('Zipping', () => {
|
||||
});
|
||||
await extract(sourcesZip, { dir: unzipDir });
|
||||
expect(await project.fileExists(unzipDir, '.env')).toBe(false);
|
||||
expect(await project.fileExists(unzipDir, '.hidden-dir/file')).toBe(false);
|
||||
});
|
||||
|
||||
// TODO: Fix #738 and re-enable
|
||||
it.skip('should allow zipping hidden files into sources when explicitly listed', async () => {
|
||||
it('should not zip files inside hidden directories if only the directory is specified', async () => {
|
||||
const project = new TestProject({
|
||||
name: 'test',
|
||||
version: '1.0.0',
|
||||
});
|
||||
project.addFile(
|
||||
'entrypoints/background.ts',
|
||||
'export default defineBackground(() => {});',
|
||||
);
|
||||
project.addFile('.hidden-dir/file');
|
||||
project.addFile('.hidden-dir/nested/file');
|
||||
const unzipDir = project.resolvePath('.output/test-1.0.0-sources');
|
||||
const sourcesZip = project.resolvePath('.output/test-1.0.0-sources.zip');
|
||||
|
||||
await project.zip({
|
||||
browser: 'firefox',
|
||||
zip: {
|
||||
includeSources: ['.hidden-dir'],
|
||||
},
|
||||
});
|
||||
await extract(sourcesZip, { dir: unzipDir });
|
||||
expect(await project.fileExists(unzipDir, '.hidden-dir/file')).toBe(false);
|
||||
expect(await project.fileExists(unzipDir, '.hidden-dir/nested/file')).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow zipping hidden files into sources when explicitly listed', async () => {
|
||||
const project = new TestProject({
|
||||
name: 'test',
|
||||
version: '1.0.0',
|
||||
@@ -129,16 +156,26 @@ describe('Zipping', () => {
|
||||
'export default defineBackground(() => {});',
|
||||
);
|
||||
project.addFile('.env');
|
||||
project.addFile('.hidden-dir/file');
|
||||
project.addFile('.hidden-dir/nested/file1');
|
||||
project.addFile('.hidden-dir/nested/file2');
|
||||
const unzipDir = project.resolvePath('.output/test-1.0.0-sources');
|
||||
const sourcesZip = project.resolvePath('.output/test-1.0.0-sources.zip');
|
||||
|
||||
await project.zip({
|
||||
browser: 'firefox',
|
||||
zip: {
|
||||
includeSources: ['.env'],
|
||||
includeSources: ['.env', '.hidden-dir/file', '.hidden-dir/nested/**'],
|
||||
},
|
||||
});
|
||||
await extract(sourcesZip, { dir: unzipDir });
|
||||
expect(await project.fileExists(unzipDir, '.env')).toBe(true);
|
||||
expect(await project.fileExists(unzipDir, '.hidden-dir/file')).toBe(true);
|
||||
expect(await project.fileExists(unzipDir, '.hidden-dir/nested/file1')).toBe(
|
||||
true,
|
||||
);
|
||||
expect(await project.fileExists(unzipDir, '.hidden-dir/nested/file2')).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "wxt",
|
||||
"type": "module",
|
||||
"version": "0.19.4",
|
||||
"version": "0.19.8",
|
||||
"description": "Next gen framework for developing web extensions",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -95,6 +95,7 @@
|
||||
"defu": "^6.1.4",
|
||||
"dequal": "^2.0.3",
|
||||
"esbuild": "^0.23.0",
|
||||
"execa": "^9.3.0",
|
||||
"fast-glob": "^3.3.2",
|
||||
"filesize": "^10.1.4",
|
||||
"fs-extra": "^11.2.0",
|
||||
@@ -133,7 +134,6 @@
|
||||
"@types/node": "^20.14.12",
|
||||
"@types/normalize-path": "^3.0.2",
|
||||
"@types/prompts": "^2.4.9",
|
||||
"execa": "^9.3.0",
|
||||
"extract-zip": "^2.0.1",
|
||||
"happy-dom": "^14.12.3",
|
||||
"lodash.merge": "^4.6.2",
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
/// <reference types="chrome" />
|
||||
import type { WxtRuntime, WxtI18n } from './index';
|
||||
|
||||
/**
|
||||
* EXPERIMENTAL
|
||||
*
|
||||
@@ -8,11 +6,11 @@ import type { WxtRuntime, WxtI18n } from './index';
|
||||
*
|
||||
* @module wxt/browser/chrome
|
||||
*/
|
||||
import type { WxtRuntime, WxtI18n } from './index';
|
||||
|
||||
export type Chrome = typeof chrome;
|
||||
export type WxtBrowser = Omit<Chrome, 'runtime' | 'i18n'> & {
|
||||
runtime: WxtRuntime & Omit<Chrome['runtime'], 'getURL'>;
|
||||
i18n: WxtI18n & Omit<Chrome['i18n'], 'getMessage'>;
|
||||
export type WxtBrowser = Omit<typeof chrome, 'runtime' | 'i18n'> & {
|
||||
runtime: WxtRuntime & Omit<(typeof chrome)['runtime'], 'getURL'>;
|
||||
i18n: WxtI18n & Omit<(typeof chrome)['i18n'], 'getMessage'>;
|
||||
};
|
||||
|
||||
export const browser: WxtBrowser =
|
||||
|
||||
@@ -59,6 +59,11 @@ export async function createViteBuilder(
|
||||
config.build.sourcemap = 'inline';
|
||||
}
|
||||
|
||||
config.server ??= {};
|
||||
config.server.watch = {
|
||||
ignored: [`${wxtConfig.outBaseDir}/**`, `${wxtConfig.wxtDir}/**`],
|
||||
};
|
||||
|
||||
config.plugins ??= [];
|
||||
config.plugins.push(
|
||||
wxtPlugins.download(wxtConfig),
|
||||
|
||||
@@ -6,7 +6,6 @@ import path from 'node:path';
|
||||
let increment = 0;
|
||||
|
||||
export function bundleAnalysis(config: ResolvedConfig): vite.Plugin {
|
||||
// @ts-expect-error: Vite version mismatch
|
||||
return visualizer({
|
||||
template: 'raw-data',
|
||||
filename: path.resolve(
|
||||
|
||||
@@ -11,6 +11,9 @@ import { ResolvedConfig } from '../../../../types';
|
||||
* `npm list` and inline them automatically.
|
||||
*/
|
||||
export function extensionApiMock(config: ResolvedConfig): vite.PluginOption {
|
||||
const virtualSetupModule = 'virtual:wxt-setup';
|
||||
const resolvedVirtualSetupModule = '\0' + virtualSetupModule;
|
||||
|
||||
return {
|
||||
name: 'wxt:extension-api-mock',
|
||||
config() {
|
||||
@@ -19,6 +22,9 @@ export function extensionApiMock(config: ResolvedConfig): vite.PluginOption {
|
||||
'dist/virtual/mock-browser',
|
||||
);
|
||||
return {
|
||||
test: {
|
||||
setupFiles: [virtualSetupModule],
|
||||
},
|
||||
resolve: {
|
||||
alias: [
|
||||
{ find: 'webextension-polyfill', replacement },
|
||||
@@ -33,5 +39,19 @@ export function extensionApiMock(config: ResolvedConfig): vite.PluginOption {
|
||||
},
|
||||
};
|
||||
},
|
||||
resolveId(id) {
|
||||
if (id.endsWith(virtualSetupModule)) return resolvedVirtualSetupModule;
|
||||
},
|
||||
load(id) {
|
||||
if (id === resolvedVirtualSetupModule) return setupTemplate;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const setupTemplate = `
|
||||
import { vi } from 'vitest';
|
||||
import { fakeBrowser } from 'wxt/testing';
|
||||
|
||||
vi.stubGlobal("chrome", fakeBrowser);
|
||||
vi.stubGlobal("browser", fakeBrowser);
|
||||
`;
|
||||
|
||||
@@ -145,11 +145,6 @@ function createFileReloader(server: WxtDevServer) {
|
||||
const changeQueue: Array<[string, string]> = [];
|
||||
|
||||
return async (event: string, path: string) => {
|
||||
await wxt.reloadConfig();
|
||||
|
||||
// Here, "path" is a non-normalized path (ie: C:\\users\\... instead of C:/users/...)
|
||||
if (path.startsWith(wxt.config.outBaseDir)) return;
|
||||
if (path.startsWith(wxt.config.wxtDir)) return;
|
||||
changeQueue.push([event, path]);
|
||||
|
||||
await fileChangedMutex.runExclusive(async () => {
|
||||
@@ -160,6 +155,8 @@ function createFileReloader(server: WxtDevServer) {
|
||||
.map(([_, file]) => file);
|
||||
if (fileChanges.length === 0) return;
|
||||
|
||||
await wxt.reloadConfig();
|
||||
|
||||
const changes = detectDevChanges(fileChanges, server.currentOutput);
|
||||
if (changes.type === 'no-change') return;
|
||||
|
||||
|
||||
@@ -99,53 +99,74 @@ interface Template {
|
||||
}
|
||||
|
||||
async function listTemplates(): Promise<Template[]> {
|
||||
try {
|
||||
const res = await fetch('https://ungh.cc/repos/wxt-dev/wxt/files/main');
|
||||
if (res.status >= 300)
|
||||
throw Error(`Request failed with status ${res.status} ${res.statusText}`);
|
||||
const templates = await listTemplatesUngh().catch((err) => {
|
||||
consola.debug('Failed to load templates via ungh:', err);
|
||||
return listTemplatesGithub();
|
||||
});
|
||||
return templates.sort((l, r) => {
|
||||
const lWeight = TEMPLATE_SORT_WEIGHT[l.name] ?? Number.MAX_SAFE_INTEGER;
|
||||
const rWeight = TEMPLATE_SORT_WEIGHT[r.name] ?? Number.MAX_SAFE_INTEGER;
|
||||
const diff = lWeight - rWeight;
|
||||
if (diff !== 0) return diff;
|
||||
return l.name.localeCompare(r.name);
|
||||
});
|
||||
}
|
||||
|
||||
const data = (await res.json()) as {
|
||||
meta: {
|
||||
sha: string;
|
||||
};
|
||||
files: Array<{
|
||||
path: string;
|
||||
mode: string;
|
||||
sha: string;
|
||||
size: number;
|
||||
}>;
|
||||
async function listTemplatesUngh(): Promise<Template[]> {
|
||||
const res = await fetch('https://ungh.cc/repos/wxt-dev/wxt/files/main');
|
||||
if (res.status !== 200)
|
||||
throw Error(
|
||||
`Request failed with status ${res.status} ${res.statusText}: ${await res.text()}`,
|
||||
);
|
||||
|
||||
const data = (await res.json()) as {
|
||||
meta: {
|
||||
sha: string;
|
||||
};
|
||||
return data.files
|
||||
.map((item) => item.path.match(/templates\/(.+)\/package\.json/)?.[1])
|
||||
.filter((name) => name != null)
|
||||
.map((name) => ({ name: name!, path: `templates/${name}` }))
|
||||
.sort((l, r) => {
|
||||
const lWeight = TEMPLATE_SORT_WEIGHT[l.name] ?? Number.MAX_SAFE_INTEGER;
|
||||
const rWeight = TEMPLATE_SORT_WEIGHT[r.name] ?? Number.MAX_SAFE_INTEGER;
|
||||
const diff = lWeight - rWeight;
|
||||
if (diff !== 0) return diff;
|
||||
return l.name.localeCompare(r.name);
|
||||
});
|
||||
} catch (err) {
|
||||
consola.error(err);
|
||||
throw Error(`Failed to load templates`);
|
||||
}
|
||||
files: Array<{
|
||||
path: string;
|
||||
mode: string;
|
||||
sha: string;
|
||||
size: number;
|
||||
}>;
|
||||
};
|
||||
return data.files
|
||||
.map((item) => item.path.match(/templates\/(.+)\/package\.json/)?.[1])
|
||||
.filter((name) => name != null)
|
||||
.map((name) => ({ name: name!, path: `templates/${name}` }));
|
||||
}
|
||||
|
||||
async function listTemplatesGithub(): Promise<Template[]> {
|
||||
const res = await fetch(
|
||||
`https://api.github.com/repos/${REPO}/contents/templates`,
|
||||
{ headers: { Accept: 'application/vnd.github+json' } },
|
||||
);
|
||||
if (res.status !== 200)
|
||||
throw Error(
|
||||
`Request failed with status ${res.status} ${res.statusText}: ${await res.text()}`,
|
||||
);
|
||||
|
||||
// Schema is Example4 of https://docs.github.com/en/rest/repos/contents?apiVersion=2022-11-28#get-repository-content
|
||||
return (await res.json()) as Array<{
|
||||
name: string;
|
||||
path: string;
|
||||
sha: string;
|
||||
size: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
async function cloneProject({
|
||||
directory,
|
||||
template,
|
||||
packageManager,
|
||||
}: {
|
||||
directory: string;
|
||||
template: Template;
|
||||
packageManager: string;
|
||||
}) {
|
||||
const { default: ora } = await import('ora');
|
||||
const spinner = ora('Downloading template').start();
|
||||
try {
|
||||
// 1. Clone repo
|
||||
await downloadTemplate(`gh:wxt-dev/wxt/${template.path}`, {
|
||||
await downloadTemplate(`gh:${REPO}/${template.path}`, {
|
||||
dir: directory,
|
||||
force: true,
|
||||
});
|
||||
@@ -180,3 +201,5 @@ const TEMPLATE_SORT_WEIGHT: Record<string, number> = {
|
||||
vue: 1,
|
||||
react: 2,
|
||||
};
|
||||
|
||||
const REPO = 'wxt-dev/wxt';
|
||||
|
||||
@@ -97,6 +97,9 @@ const DEFAULT_CHROMIUM_PREFS = {
|
||||
// and log locations show up properly, see:
|
||||
// https://github.com/wxt-dev/wxt/issues/236#issuecomment-1915364520
|
||||
skipContentScripts: false,
|
||||
// Was renamed at some point, see:
|
||||
// https://github.com/wxt-dev/wxt/issues/912#issuecomment-2284288171
|
||||
'skip-content-scripts': false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -387,7 +387,7 @@ async function getSidepanelEntrypoint(
|
||||
const options = await getHtmlEntrypointOptions<SidepanelEntrypointOptions>(
|
||||
info,
|
||||
{
|
||||
browserStyle: 'browse_style',
|
||||
browserStyle: 'browser_style',
|
||||
exclude: 'exclude',
|
||||
include: 'include',
|
||||
defaultIcon: 'default_icon',
|
||||
|
||||
@@ -150,18 +150,43 @@ declare module "wxt/browser" {
|
||||
messages = parseI18nMessages({});
|
||||
}
|
||||
|
||||
const overrides = messages.map((message) => {
|
||||
return ` /**
|
||||
* ${message.description || 'No message description.'}
|
||||
*
|
||||
* "${message.message}"
|
||||
*/
|
||||
getMessage(
|
||||
messageName: "${message.name}",
|
||||
const renderGetMessageOverload = (
|
||||
keyType: string,
|
||||
description?: string,
|
||||
translation?: string,
|
||||
) => {
|
||||
const commentLines: string[] = [];
|
||||
if (description) commentLines.push(...description.split('\n'));
|
||||
if (translation) {
|
||||
if (commentLines.length > 0) commentLines.push('');
|
||||
commentLines.push(`"${translation}"`);
|
||||
}
|
||||
const comment =
|
||||
commentLines.length > 0
|
||||
? `/**\n${commentLines.map((line) => ` * ${line}`.trimEnd()).join('\n')}\n */\n `
|
||||
: '';
|
||||
return ` ${comment}getMessage(
|
||||
messageName: ${keyType},
|
||||
substitutions?: string | string[],
|
||||
options?: GetMessageOptions,
|
||||
): string;`;
|
||||
});
|
||||
};
|
||||
|
||||
const overrides = [
|
||||
// Generate individual overloads for each message so JSDoc contains description and base translation.
|
||||
...messages.map((message) =>
|
||||
renderGetMessageOverload(
|
||||
`"${message.name}"`,
|
||||
message.description,
|
||||
message.message,
|
||||
),
|
||||
),
|
||||
// Include a final union-based override so TS accepts valid string templates or concatinations
|
||||
// ie: browser.i18n.getMessage(`some_enum_${enumValue}`)
|
||||
renderGetMessageOverload(
|
||||
messages.map((message) => `"${message.name}"`).join(' | '),
|
||||
),
|
||||
];
|
||||
|
||||
return {
|
||||
path: 'types/i18n.d.ts',
|
||||
@@ -208,7 +233,11 @@ function getMainDeclarationEntry(references: WxtDirEntry[]): WxtDirFileEntry {
|
||||
|
||||
async function getTsConfigEntry(): Promise<WxtDirFileEntry> {
|
||||
const dir = wxt.config.wxtDir;
|
||||
const getTsconfigPath = (path: string) => normalizePath(relative(dir, path));
|
||||
const getTsconfigPath = (path: string) => {
|
||||
const res = normalizePath(relative(dir, path));
|
||||
if (res.startsWith('.') || res.startsWith('/')) return res;
|
||||
return './' + res;
|
||||
};
|
||||
const paths = Object.entries(wxt.config.alias)
|
||||
.flatMap(([alias, absolutePath]) => {
|
||||
const aliasPath = getTsconfigPath(absolutePath);
|
||||
|
||||
@@ -90,9 +90,6 @@ export async function resolveConfig(
|
||||
? new Set(mergedConfig.filterEntrypoints)
|
||||
: undefined;
|
||||
const publicDir = path.resolve(srcDir, mergedConfig.publicDir ?? 'public');
|
||||
if (await isDirMissing(publicDir)) {
|
||||
logMissingDir(logger, 'Public', publicDir);
|
||||
}
|
||||
const typesDir = path.resolve(wxtDir, 'types');
|
||||
const outBaseDir = path.resolve(root, mergedConfig.outDir ?? '.output');
|
||||
const outDir = path.resolve(outBaseDir, `${browser}-mv${manifestVersion}`);
|
||||
@@ -268,6 +265,7 @@ function resolveZipConfig(
|
||||
includeSources: [],
|
||||
compressionLevel: 9,
|
||||
...mergedConfig.zip,
|
||||
exclude: mergedConfig.zip?.exclude ?? [],
|
||||
excludeSources: [
|
||||
'**/node_modules',
|
||||
// WXT files
|
||||
|
||||
@@ -286,6 +286,7 @@ export const fakeResolvedConfig = fakeObjectCreator<ResolvedConfig>(() => {
|
||||
artifactTemplate: '{{name}}-{{version}}.zip',
|
||||
includeSources: [],
|
||||
excludeSources: [],
|
||||
exclude: [],
|
||||
sourcesRoot: fakeDir(),
|
||||
sourcesTemplate: '{{name}}-sources.zip',
|
||||
name: faker.person.firstName().toLowerCase(),
|
||||
|
||||
@@ -47,7 +47,9 @@ export async function zip(config?: InlineConfig): Promise<string[]> {
|
||||
|
||||
const outZipFilename = applyTemplate(wxt.config.zip.artifactTemplate);
|
||||
const outZipPath = path.resolve(wxt.config.outBaseDir, outZipFilename);
|
||||
await zipDir(wxt.config.outDir, outZipPath);
|
||||
await zipDir(wxt.config.outDir, outZipPath, {
|
||||
exclude: wxt.config.zip.exclude,
|
||||
});
|
||||
zipFiles.push(outZipPath);
|
||||
|
||||
// ZIP sources for Firefox
|
||||
@@ -99,13 +101,11 @@ async function zipDir(
|
||||
): Promise<void> {
|
||||
const archive = new JSZip();
|
||||
const files = (
|
||||
await glob('**/*', {
|
||||
await glob(['**/*', ...(options?.include || [])], {
|
||||
cwd: directory,
|
||||
// Ignore node_modules, otherwise this glob step takes forever
|
||||
ignore: ['**/node_modules'],
|
||||
onlyFiles: true,
|
||||
// TODO: Fix #738
|
||||
// dot: true,
|
||||
})
|
||||
).filter((relativePath) => {
|
||||
return (
|
||||
|
||||
@@ -172,3 +172,42 @@ export function addImportPreset(
|
||||
wxt.config.imports.presets.push(preset);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an import alias to the project's TSConfig paths and bundler. Path can
|
||||
* be absolute or relative to the project's root directory.
|
||||
*
|
||||
* Usually, this is used to provide access to some code generated by your
|
||||
* module. In the example below, a `i18n` plugin generates a variable that it
|
||||
* wants to provide access to, so it creates the file and adds an import alias
|
||||
* to it.
|
||||
*
|
||||
* @example
|
||||
* import path from 'node:path';
|
||||
*
|
||||
* export default defineWxtModule((wxt) => {
|
||||
* const i18nPath = path.resolve(wxt.config.wxtDir, "i18n.ts");
|
||||
*
|
||||
* // Generate the file
|
||||
* wxt.hooks.hook("prepare:types", (_, entries) => {
|
||||
* entries.push({
|
||||
* path: i18nPath,
|
||||
* text: `export const i18n = ...`,
|
||||
* });
|
||||
* });
|
||||
*
|
||||
* // Add alias
|
||||
* addAlias(wxt, "#i18n", i18nPath);
|
||||
* });
|
||||
*/
|
||||
export function addAlias(wxt: Wxt, alias: string, path: string) {
|
||||
wxt.hooks.hook('ready', (wxt) => {
|
||||
const target = resolve(wxt.config.root, path);
|
||||
if (wxt.config.alias[alias] != null) {
|
||||
wxt.logger.warn(
|
||||
`Skipped adding alias (${alias} => ${target}) because an alias with the same name already exists: ${alias} => ${wxt.config.alias[alias]}`,
|
||||
);
|
||||
}
|
||||
wxt.config.alias[alias] = target;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,10 +6,10 @@ import {
|
||||
extensionApiMock,
|
||||
resolveAppConfig,
|
||||
} from '../core/builders/vite/plugins';
|
||||
import { resolveConfig } from '../core/utils/building';
|
||||
import { InlineConfig } from '../types';
|
||||
import { vitePlugin as unimportPlugin } from '../builtin-modules/unimport';
|
||||
import { createUnimport } from 'unimport';
|
||||
import { registerWxt, wxt } from '../core/wxt';
|
||||
|
||||
/**
|
||||
* Vite plugin that configures Vitest with everything required to test a WXT extension, based on the `<root>/wxt.config.ts`
|
||||
@@ -29,17 +29,17 @@ import { createUnimport } from 'unimport';
|
||||
export async function WxtVitest(
|
||||
inlineConfig?: InlineConfig,
|
||||
): Promise<vite.PluginOption[]> {
|
||||
const config = await resolveConfig(inlineConfig ?? {}, 'serve');
|
||||
await registerWxt('serve', inlineConfig ?? {});
|
||||
|
||||
const plugins: vite.PluginOption[] = [
|
||||
globals(config),
|
||||
download(config),
|
||||
tsconfigPaths(config),
|
||||
resolveAppConfig(config),
|
||||
extensionApiMock(config),
|
||||
globals(wxt.config),
|
||||
download(wxt.config),
|
||||
tsconfigPaths(wxt.config),
|
||||
resolveAppConfig(wxt.config),
|
||||
extensionApiMock(wxt.config),
|
||||
];
|
||||
if (config.imports !== false) {
|
||||
const unimport = createUnimport(config.imports);
|
||||
if (wxt.config.imports !== false) {
|
||||
const unimport = createUnimport(wxt.config.imports);
|
||||
await unimport.init();
|
||||
plugins.push(unimportPlugin(unimport));
|
||||
}
|
||||
|
||||
@@ -181,6 +181,16 @@ export interface InlineConfig {
|
||||
* ]
|
||||
*/
|
||||
excludeSources?: string[];
|
||||
/**
|
||||
* [Minimatch](https://www.npmjs.com/package/minimatch) patterns of files to exclude when
|
||||
* zipping the extension.
|
||||
*
|
||||
* @example
|
||||
* [
|
||||
* "**\/*.map", // Exclude all sourcemaps
|
||||
* ]
|
||||
*/
|
||||
exclude?: string[];
|
||||
/**
|
||||
* The Firefox review process requires the extension be buildable from source to make reviewing
|
||||
* easier. This field allows you to use private packages without exposing your auth tokens.
|
||||
@@ -1192,6 +1202,7 @@ export interface ResolvedConfig {
|
||||
downloadedPackagesDir: string;
|
||||
downloadPackages: string[];
|
||||
compressionLevel: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
|
||||
exclude: string[];
|
||||
};
|
||||
/**
|
||||
* @deprecated Use `build:manifestGenerated` hook instead.
|
||||
|
||||
Generated
+327
-26
@@ -9,8 +9,8 @@ importers:
|
||||
.:
|
||||
devDependencies:
|
||||
'@aklinker1/buildc':
|
||||
specifier: ^1.0.11
|
||||
version: 1.0.11(typescript@5.5.4)
|
||||
specifier: ^1.1.0
|
||||
version: 1.1.0(typescript@5.5.4)
|
||||
'@aklinker1/check':
|
||||
specifier: ^1.3.1
|
||||
version: 1.3.1(typescript@5.5.4)
|
||||
@@ -112,6 +112,46 @@ importers:
|
||||
specifier: workspace:*
|
||||
version: link:../wxt
|
||||
|
||||
packages/i18n:
|
||||
dependencies:
|
||||
chokidar:
|
||||
specifier: ^3.6.0
|
||||
version: 3.6.0
|
||||
confbox:
|
||||
specifier: ^0.1.7
|
||||
version: 0.1.7
|
||||
fast-glob:
|
||||
specifier: ^3.3.2
|
||||
version: 3.3.2
|
||||
devDependencies:
|
||||
'@aklinker1/check':
|
||||
specifier: ^1.3.1
|
||||
version: 1.3.1(typescript@5.5.4)
|
||||
'@types/chrome':
|
||||
specifier: ^0.0.268
|
||||
version: 0.0.268
|
||||
'@types/node':
|
||||
specifier: ^20.14.2
|
||||
version: 20.14.12
|
||||
publint:
|
||||
specifier: ^0.2.8
|
||||
version: 0.2.9
|
||||
typescript:
|
||||
specifier: ^5.4.5
|
||||
version: 5.5.4
|
||||
unbuild:
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0(sass@1.77.8)(typescript@5.5.4)
|
||||
vitest:
|
||||
specifier: ^1.6.0
|
||||
version: 1.6.0(@types/node@20.14.12)(happy-dom@14.12.3)(sass@1.77.8)
|
||||
vitest-plugin-random-seed:
|
||||
specifier: ^1.1.0
|
||||
version: 1.1.0(vite@5.3.5(@types/node@20.14.12)(sass@1.77.8))
|
||||
wxt:
|
||||
specifier: workspace:*
|
||||
version: link:../wxt
|
||||
|
||||
packages/module-react:
|
||||
dependencies:
|
||||
'@vitejs/plugin-react':
|
||||
@@ -219,7 +259,7 @@ importers:
|
||||
dependencies:
|
||||
'@aklinker1/rollup-plugin-visualizer':
|
||||
specifier: 5.12.0
|
||||
version: 5.12.0(rollup@3.29.4)
|
||||
version: 5.12.0(rollup@4.19.0)
|
||||
'@types/webextension-polyfill':
|
||||
specifier: ^0.10.7
|
||||
version: 0.10.7
|
||||
@@ -259,6 +299,9 @@ importers:
|
||||
esbuild:
|
||||
specifier: ^0.23.0
|
||||
version: 0.23.0
|
||||
execa:
|
||||
specifier: ^9.3.0
|
||||
version: 9.3.0
|
||||
fast-glob:
|
||||
specifier: ^3.3.2
|
||||
version: 3.3.2
|
||||
@@ -327,7 +370,7 @@ importers:
|
||||
version: 2.1.3
|
||||
unimport:
|
||||
specifier: ^3.9.1
|
||||
version: 3.9.1(rollup@3.29.4)
|
||||
version: 3.9.1(rollup@4.19.0)
|
||||
vite:
|
||||
specifier: ^5.3.5
|
||||
version: 5.3.5(@types/node@20.14.12)(sass@1.77.8)
|
||||
@@ -368,9 +411,6 @@ importers:
|
||||
'@types/prompts':
|
||||
specifier: ^2.4.9
|
||||
version: 2.4.9
|
||||
execa:
|
||||
specifier: ^9.3.0
|
||||
version: 9.3.0
|
||||
extract-zip:
|
||||
specifier: ^2.0.1
|
||||
version: 2.0.1
|
||||
@@ -404,6 +444,9 @@ importers:
|
||||
|
||||
packages/wxt-demo:
|
||||
dependencies:
|
||||
'@wxt-dev/i18n':
|
||||
specifier: workspace:*
|
||||
version: link:../i18n
|
||||
react:
|
||||
specifier: ^18.3.1
|
||||
version: 18.3.1
|
||||
@@ -441,8 +484,8 @@ importers:
|
||||
|
||||
packages:
|
||||
|
||||
'@aklinker1/buildc@1.0.11':
|
||||
resolution: {integrity: sha512-pUdwGQxTfhozZmC4HceoJye+Iq4srLCTPGOJK/VXrta30Bsfnp0N0zvIh9yhch/c7hvHGKf3vFdDPFOthGBdqw==}
|
||||
'@aklinker1/buildc@1.1.0':
|
||||
resolution: {integrity: sha512-qqIulkYDiRm3S4PcKVCQU4A+y0sIBjoQA2gUA+FxzUHPgpPUHnWPHpqZa0qJVzgUls23/UrFyH2Rq2YE5HV5+Q==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
typescript: ^5.0.0
|
||||
@@ -1261,6 +1304,10 @@ packages:
|
||||
resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
'@jest/schemas@29.6.3':
|
||||
resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.5':
|
||||
resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
@@ -1450,6 +1497,9 @@ packages:
|
||||
'@shikijs/transformers@1.10.3':
|
||||
resolution: {integrity: sha512-MNjsyye2WHVdxfZUSr5frS97sLGe6G1T+1P41QjyBFJehZphMcr4aBlRLmq6OSPBslYe9byQPVvt/LJCOfxw8Q==}
|
||||
|
||||
'@sinclair/typebox@0.27.8':
|
||||
resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}
|
||||
|
||||
'@sindresorhus/is@5.4.1':
|
||||
resolution: {integrity: sha512-axlrvsHlHlFmKKMEg4VyvMzFr93JWJj4eIfXY1STVuO2fsImCa7ncaiG5gC8HKOX590AW5RtRsC41/B+OfrSqw==}
|
||||
engines: {node: '>=14.16'}
|
||||
@@ -1493,6 +1543,9 @@ packages:
|
||||
'@types/babel__traverse@7.20.6':
|
||||
resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==}
|
||||
|
||||
'@types/chrome@0.0.268':
|
||||
resolution: {integrity: sha512-7N1QH9buudSJ7sI8Pe4mBHJr5oZ48s0hcanI9w3wgijAlv1OZNUZve9JR4x42dn5lJ5Sm87V1JNfnoh10EnQlA==}
|
||||
|
||||
'@types/chrome@0.0.269':
|
||||
resolution: {integrity: sha512-vF7x8YywnhXX2F06njQ/OE7a3Qeful43C5GUOsUksXWk89WoSFUU3iLeZW8lDpVO9atm8iZIEiLQTRC3H7NOXQ==}
|
||||
|
||||
@@ -1602,21 +1655,36 @@ packages:
|
||||
peerDependencies:
|
||||
vitest: 2.0.4
|
||||
|
||||
'@vitest/expect@1.6.0':
|
||||
resolution: {integrity: sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==}
|
||||
|
||||
'@vitest/expect@2.0.4':
|
||||
resolution: {integrity: sha512-39jr5EguIoanChvBqe34I8m1hJFI4+jxvdOpD7gslZrVQBKhh8H9eD7J/LJX4zakrw23W+dITQTDqdt43xVcJw==}
|
||||
|
||||
'@vitest/pretty-format@2.0.4':
|
||||
resolution: {integrity: sha512-RYZl31STbNGqf4l2eQM1nvKPXE0NhC6Eq0suTTePc4mtMQ1Fn8qZmjV4emZdEdG2NOWGKSCrHZjmTqDCDoeFBw==}
|
||||
|
||||
'@vitest/runner@1.6.0':
|
||||
resolution: {integrity: sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==}
|
||||
|
||||
'@vitest/runner@2.0.4':
|
||||
resolution: {integrity: sha512-Gk+9Su/2H2zNfNdeJR124gZckd5st4YoSuhF1Rebi37qTXKnqYyFCd9KP4vl2cQHbtuVKjfEKrNJxHHCW8thbQ==}
|
||||
|
||||
'@vitest/snapshot@1.6.0':
|
||||
resolution: {integrity: sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==}
|
||||
|
||||
'@vitest/snapshot@2.0.4':
|
||||
resolution: {integrity: sha512-or6Mzoz/pD7xTvuJMFYEtso1vJo1S5u6zBTinfl+7smGUhqybn6VjzCDMhmTyVOFWwkCMuNjmNNxnyXPgKDoPw==}
|
||||
|
||||
'@vitest/spy@1.6.0':
|
||||
resolution: {integrity: sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==}
|
||||
|
||||
'@vitest/spy@2.0.4':
|
||||
resolution: {integrity: sha512-uTXU56TNoYrTohb+6CseP8IqNwlNdtPwEO0AWl+5j7NelS6x0xZZtP0bDWaLvOfUbaYwhhWp1guzXUxkC7mW7Q==}
|
||||
|
||||
'@vitest/utils@1.6.0':
|
||||
resolution: {integrity: sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==}
|
||||
|
||||
'@vitest/utils@2.0.4':
|
||||
resolution: {integrity: sha512-Zc75QuuoJhOBnlo99ZVUkJIuq4Oj0zAkrQ2VzCqNCx6wAwViHEh5Fnp4fiJTE9rA+sAoXRf00Z9xGgfEzV6fzQ==}
|
||||
|
||||
@@ -1720,6 +1788,10 @@ packages:
|
||||
'@webext-core/match-patterns@1.0.3':
|
||||
resolution: {integrity: sha512-NY39ACqCxdKBmHgw361M9pfJma8e4AZo20w9AY+5ZjIj1W2dvXC8J31G5fjfOGbulW9w4WKpT8fPooi0mLkn9A==}
|
||||
|
||||
acorn-walk@8.3.3:
|
||||
resolution: {integrity: sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
|
||||
acorn@8.11.3:
|
||||
resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
@@ -1763,6 +1835,10 @@ packages:
|
||||
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
ansi-styles@5.2.0:
|
||||
resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
ansi-styles@6.2.1:
|
||||
resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -1788,6 +1864,9 @@ packages:
|
||||
resolution: {integrity: sha512-1OvF9IbWwaeiM9VhzYXVQacMibxpXOMYVNIvMtKRyX9SImBXpKcFr8XvFDeEslCyuH/t6KRt7HEO94AlP8Iatw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
assertion-error@1.1.0:
|
||||
resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==}
|
||||
|
||||
assertion-error@2.0.1:
|
||||
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -1937,6 +2016,10 @@ packages:
|
||||
caniuse-lite@1.0.30001633:
|
||||
resolution: {integrity: sha512-6sT0yf/z5jqf8tISAgpJDrmwOpLsrpnyCdD/lOZKvKkkJK4Dn0X5i7KF7THEZhOq+30bmhwBlNEaqPUiHiKtZg==}
|
||||
|
||||
chai@4.5.0:
|
||||
resolution: {integrity: sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
chai@5.1.1:
|
||||
resolution: {integrity: sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -1957,6 +2040,9 @@ packages:
|
||||
resolution: {integrity: sha512-IzgToIJ/R9NhVKmL+PW33ozYkv53bXvufDNUSH3GTKXq1iCHGgkbgbtqEWbo8tnWNnt7nPDpjL8PwSG2iS8RVw==}
|
||||
hasBin: true
|
||||
|
||||
check-error@1.0.3:
|
||||
resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==}
|
||||
|
||||
check-error@2.1.1:
|
||||
resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==}
|
||||
engines: {node: '>= 16'}
|
||||
@@ -2076,9 +2162,6 @@ packages:
|
||||
resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==}
|
||||
engines: {'0': node >= 0.8}
|
||||
|
||||
confbox@0.1.3:
|
||||
resolution: {integrity: sha512-eH3ZxAihl1PhKfpr4VfEN6/vUd87fmgb6JkldHgg/YR6aEBhW63qUDgzP2Y6WM0UumdsYp5H3kibalXAdHfbgg==}
|
||||
|
||||
confbox@0.1.7:
|
||||
resolution: {integrity: sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==}
|
||||
|
||||
@@ -2205,6 +2288,10 @@ packages:
|
||||
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
deep-eql@4.1.4:
|
||||
resolution: {integrity: sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
deep-eql@5.0.2:
|
||||
resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -2273,6 +2360,10 @@ packages:
|
||||
resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
diff-sequences@29.6.3:
|
||||
resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
|
||||
dir-glob@3.0.1:
|
||||
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -2558,12 +2649,10 @@ packages:
|
||||
|
||||
glob@6.0.4:
|
||||
resolution: {integrity: sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A==}
|
||||
deprecated: Glob versions prior to v9 are no longer supported
|
||||
|
||||
glob@8.1.0:
|
||||
resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==}
|
||||
engines: {node: '>=12'}
|
||||
deprecated: Glob versions prior to v9 are no longer supported
|
||||
|
||||
global-dirs@3.0.1:
|
||||
resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==}
|
||||
@@ -2713,7 +2802,6 @@ packages:
|
||||
|
||||
inflight@1.0.6:
|
||||
resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
|
||||
deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
|
||||
|
||||
inherits@2.0.4:
|
||||
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||
@@ -3089,6 +3177,9 @@ packages:
|
||||
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
|
||||
hasBin: true
|
||||
|
||||
loupe@2.3.7:
|
||||
resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==}
|
||||
|
||||
loupe@3.1.1:
|
||||
resolution: {integrity: sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==}
|
||||
|
||||
@@ -3421,6 +3512,10 @@ packages:
|
||||
resolution: {integrity: sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==}
|
||||
engines: {node: '>=12.20'}
|
||||
|
||||
p-limit@5.0.0:
|
||||
resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
p-map@7.0.2:
|
||||
resolution: {integrity: sha512-z4cYYMMdKHzw4O5UkWJImbZynVIo0lSGTXc7bzB1e/rrDqkgGUNysK/o4bTr+0+xKvvLoTyGqYC4Fgljy9qe1Q==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -3490,6 +3585,9 @@ packages:
|
||||
pathe@1.1.2:
|
||||
resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==}
|
||||
|
||||
pathval@1.1.1:
|
||||
resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==}
|
||||
|
||||
pathval@2.0.0:
|
||||
resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==}
|
||||
engines: {node: '>= 14.16'}
|
||||
@@ -3724,6 +3822,10 @@ packages:
|
||||
resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==}
|
||||
engines: {node: ^14.13.1 || >=16.0.0}
|
||||
|
||||
pretty-format@29.7.0:
|
||||
resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
|
||||
pretty-ms@9.0.0:
|
||||
resolution: {integrity: sha512-E9e9HJ9R9NasGOgPaPE8VMeiPKAyWR5jcFpNnwIejslIhWqdqOrb2wShBsncMPUb+BcCd2OPYfh7p2W6oemTng==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -3784,6 +3886,9 @@ packages:
|
||||
peerDependencies:
|
||||
react: ^18.3.1
|
||||
|
||||
react-is@18.3.1:
|
||||
resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
|
||||
|
||||
react-refresh@0.14.2:
|
||||
resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -3860,7 +3965,6 @@ packages:
|
||||
|
||||
rimraf@2.4.5:
|
||||
resolution: {integrity: sha512-J5xnxTyqaiw06JjMftq7L9ouA448dw/E7dKghkP9WpKNuwmARNNg+Gk8/u5ryb9N/Yo2+z3MCwuqFK/+qPOPfQ==}
|
||||
deprecated: Rimraf versions prior to v4 are no longer supported
|
||||
hasBin: true
|
||||
|
||||
rollup-plugin-dts@6.1.1:
|
||||
@@ -4225,6 +4329,10 @@ packages:
|
||||
tinybench@2.8.0:
|
||||
resolution: {integrity: sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw==}
|
||||
|
||||
tinypool@0.8.4:
|
||||
resolution: {integrity: sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
tinypool@1.0.0:
|
||||
resolution: {integrity: sha512-KIKExllK7jp3uvrNtvRBYBWBOAXSX8ZvoaD8T+7KB/QHIuoJW3Pmr60zucywjAlMb5TeXUkcs/MWeWLu0qvuAQ==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
@@ -4233,6 +4341,10 @@ packages:
|
||||
resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
tinyspy@2.2.1:
|
||||
resolution: {integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
tinyspy@3.0.0:
|
||||
resolution: {integrity: sha512-q5nmENpTHgiPVd1cJDDc9cVoYN5x4vCvwT3FMilvKPKneCBZAxn2YWQjDF0UMcE9k0Cay1gBiDfTMU0g+mPMQA==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
@@ -4269,6 +4381,10 @@ packages:
|
||||
engines: {node: '>=18.0.0'}
|
||||
hasBin: true
|
||||
|
||||
type-detect@4.1.0:
|
||||
resolution: {integrity: sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
type-fest@1.4.0:
|
||||
resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -4390,6 +4506,11 @@ packages:
|
||||
validate-npm-package-license@3.0.4:
|
||||
resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==}
|
||||
|
||||
vite-node@1.6.0:
|
||||
resolution: {integrity: sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
hasBin: true
|
||||
|
||||
vite-node@2.0.4:
|
||||
resolution: {integrity: sha512-ZpJVkxcakYtig5iakNeL7N3trufe3M6vGuzYAr4GsbCTwobDeyPJpE4cjDhhPluv8OvQCFzu2LWp6GkoKRITXA==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
@@ -4464,6 +4585,31 @@ packages:
|
||||
peerDependencies:
|
||||
vite: ^4.0.0 || ^5.0.0
|
||||
|
||||
vitest@1.6.0:
|
||||
resolution: {integrity: sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@edge-runtime/vm': '*'
|
||||
'@types/node': ^18.0.0 || >=20.0.0
|
||||
'@vitest/browser': 1.6.0
|
||||
'@vitest/ui': 1.6.0
|
||||
happy-dom: '*'
|
||||
jsdom: '*'
|
||||
peerDependenciesMeta:
|
||||
'@edge-runtime/vm':
|
||||
optional: true
|
||||
'@types/node':
|
||||
optional: true
|
||||
'@vitest/browser':
|
||||
optional: true
|
||||
'@vitest/ui':
|
||||
optional: true
|
||||
happy-dom:
|
||||
optional: true
|
||||
jsdom:
|
||||
optional: true
|
||||
|
||||
vitest@2.0.4:
|
||||
resolution: {integrity: sha512-luNLDpfsnxw5QSW4bISPe6tkxVvv5wn2BBs/PuDRkhXZ319doZyLOBr1sjfB5yCEpTiU7xCAdViM8TNVGPwoog==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
@@ -4659,6 +4805,10 @@ packages:
|
||||
yauzl@2.10.0:
|
||||
resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==}
|
||||
|
||||
yocto-queue@1.1.1:
|
||||
resolution: {integrity: sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==}
|
||||
engines: {node: '>=12.20'}
|
||||
|
||||
yoctocolors@2.0.2:
|
||||
resolution: {integrity: sha512-Ct97huExsu7cWeEjmrXlofevF8CvzUglJ4iGUet5B8xn1oumtAZBpHU4GzYuoE6PVqcZ5hghtBrSlhwHuR1Jmw==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -4671,7 +4821,7 @@ packages:
|
||||
|
||||
snapshots:
|
||||
|
||||
'@aklinker1/buildc@1.0.11(typescript@5.5.4)':
|
||||
'@aklinker1/buildc@1.1.0(typescript@5.5.4)':
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
consola: 3.2.3
|
||||
@@ -4691,14 +4841,14 @@ snapshots:
|
||||
citty: 0.1.6
|
||||
typescript: 5.5.4
|
||||
|
||||
'@aklinker1/rollup-plugin-visualizer@5.12.0(rollup@3.29.4)':
|
||||
'@aklinker1/rollup-plugin-visualizer@5.12.0(rollup@4.19.0)':
|
||||
dependencies:
|
||||
open: 8.4.2
|
||||
picomatch: 2.3.1
|
||||
source-map: 0.7.4
|
||||
yargs: 17.7.2
|
||||
optionalDependencies:
|
||||
rollup: 3.29.4
|
||||
rollup: 4.19.0
|
||||
|
||||
'@algolia/autocomplete-core@1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)(search-insights@2.15.0)':
|
||||
dependencies:
|
||||
@@ -4959,7 +5109,7 @@ snapshots:
|
||||
'@babel/helper-split-export-declaration': 7.24.7
|
||||
'@babel/parser': 7.24.7
|
||||
'@babel/types': 7.24.7
|
||||
debug: 4.3.4
|
||||
debug: 4.3.5
|
||||
globals: 11.12.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -4988,7 +5138,7 @@ snapshots:
|
||||
'@devicefarmer/adbkit-monkey': 1.2.1
|
||||
bluebird: 3.7.2
|
||||
commander: 9.5.0
|
||||
debug: 4.3.4
|
||||
debug: 4.3.5
|
||||
node-forge: 1.3.1
|
||||
split: 1.0.1
|
||||
transitivePeerDependencies:
|
||||
@@ -5324,6 +5474,10 @@ snapshots:
|
||||
|
||||
'@istanbuljs/schema@0.1.3': {}
|
||||
|
||||
'@jest/schemas@29.6.3':
|
||||
dependencies:
|
||||
'@sinclair/typebox': 0.27.8
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.5':
|
||||
dependencies:
|
||||
'@jridgewell/set-array': 1.2.1
|
||||
@@ -5417,6 +5571,14 @@ snapshots:
|
||||
optionalDependencies:
|
||||
rollup: 3.29.4
|
||||
|
||||
'@rollup/pluginutils@5.1.0(rollup@4.19.0)':
|
||||
dependencies:
|
||||
'@types/estree': 1.0.5
|
||||
estree-walker: 2.0.2
|
||||
picomatch: 2.3.1
|
||||
optionalDependencies:
|
||||
rollup: 4.19.0
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.19.0':
|
||||
optional: true
|
||||
|
||||
@@ -5475,6 +5637,8 @@ snapshots:
|
||||
dependencies:
|
||||
shiki: 1.10.3
|
||||
|
||||
'@sinclair/typebox@0.27.8': {}
|
||||
|
||||
'@sindresorhus/is@5.4.1': {}
|
||||
|
||||
'@sindresorhus/merge-streams@4.0.0': {}
|
||||
@@ -5529,6 +5693,11 @@ snapshots:
|
||||
dependencies:
|
||||
'@babel/types': 7.24.7
|
||||
|
||||
'@types/chrome@0.0.268':
|
||||
dependencies:
|
||||
'@types/filesystem': 0.0.36
|
||||
'@types/har-format': 1.2.15
|
||||
|
||||
'@types/chrome@0.0.269':
|
||||
dependencies:
|
||||
'@types/filesystem': 0.0.36
|
||||
@@ -5656,6 +5825,12 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@vitest/expect@1.6.0':
|
||||
dependencies:
|
||||
'@vitest/spy': 1.6.0
|
||||
'@vitest/utils': 1.6.0
|
||||
chai: 4.5.0
|
||||
|
||||
'@vitest/expect@2.0.4':
|
||||
dependencies:
|
||||
'@vitest/spy': 2.0.4
|
||||
@@ -5667,21 +5842,44 @@ snapshots:
|
||||
dependencies:
|
||||
tinyrainbow: 1.2.0
|
||||
|
||||
'@vitest/runner@1.6.0':
|
||||
dependencies:
|
||||
'@vitest/utils': 1.6.0
|
||||
p-limit: 5.0.0
|
||||
pathe: 1.1.2
|
||||
|
||||
'@vitest/runner@2.0.4':
|
||||
dependencies:
|
||||
'@vitest/utils': 2.0.4
|
||||
pathe: 1.1.2
|
||||
|
||||
'@vitest/snapshot@1.6.0':
|
||||
dependencies:
|
||||
magic-string: 0.30.10
|
||||
pathe: 1.1.2
|
||||
pretty-format: 29.7.0
|
||||
|
||||
'@vitest/snapshot@2.0.4':
|
||||
dependencies:
|
||||
'@vitest/pretty-format': 2.0.4
|
||||
magic-string: 0.30.10
|
||||
pathe: 1.1.2
|
||||
|
||||
'@vitest/spy@1.6.0':
|
||||
dependencies:
|
||||
tinyspy: 2.2.1
|
||||
|
||||
'@vitest/spy@2.0.4':
|
||||
dependencies:
|
||||
tinyspy: 3.0.0
|
||||
|
||||
'@vitest/utils@1.6.0':
|
||||
dependencies:
|
||||
diff-sequences: 29.6.3
|
||||
estree-walker: 3.0.3
|
||||
loupe: 2.3.7
|
||||
pretty-format: 29.7.0
|
||||
|
||||
'@vitest/utils@2.0.4':
|
||||
dependencies:
|
||||
'@vitest/pretty-format': 2.0.4
|
||||
@@ -5803,6 +6001,10 @@ snapshots:
|
||||
|
||||
'@webext-core/match-patterns@1.0.3': {}
|
||||
|
||||
acorn-walk@8.3.3:
|
||||
dependencies:
|
||||
acorn: 8.12.1
|
||||
|
||||
acorn@8.11.3: {}
|
||||
|
||||
acorn@8.12.1: {}
|
||||
@@ -5848,6 +6050,8 @@ snapshots:
|
||||
dependencies:
|
||||
color-convert: 2.0.1
|
||||
|
||||
ansi-styles@5.2.0: {}
|
||||
|
||||
ansi-styles@6.2.1: {}
|
||||
|
||||
any-promise@1.3.0: {}
|
||||
@@ -5870,6 +6074,8 @@ snapshots:
|
||||
|
||||
array-union@3.0.1: {}
|
||||
|
||||
assertion-error@1.1.0: {}
|
||||
|
||||
assertion-error@2.0.1: {}
|
||||
|
||||
async-mutex@0.5.0:
|
||||
@@ -5994,7 +6200,7 @@ snapshots:
|
||||
c12@1.10.0:
|
||||
dependencies:
|
||||
chokidar: 3.6.0
|
||||
confbox: 0.1.3
|
||||
confbox: 0.1.7
|
||||
defu: 6.1.4
|
||||
dotenv: 16.4.5
|
||||
giget: 1.2.3
|
||||
@@ -6053,6 +6259,16 @@ snapshots:
|
||||
|
||||
caniuse-lite@1.0.30001633: {}
|
||||
|
||||
chai@4.5.0:
|
||||
dependencies:
|
||||
assertion-error: 1.1.0
|
||||
check-error: 1.0.3
|
||||
deep-eql: 4.1.4
|
||||
get-func-name: 2.0.2
|
||||
loupe: 2.3.7
|
||||
pathval: 1.1.1
|
||||
type-detect: 4.1.0
|
||||
|
||||
chai@5.1.1:
|
||||
dependencies:
|
||||
assertion-error: 2.0.1
|
||||
@@ -6092,6 +6308,10 @@ snapshots:
|
||||
std-env: 3.6.0
|
||||
yaml: 2.5.0
|
||||
|
||||
check-error@1.0.3:
|
||||
dependencies:
|
||||
get-func-name: 2.0.2
|
||||
|
||||
check-error@2.1.1: {}
|
||||
|
||||
chokidar@3.6.0:
|
||||
@@ -6218,8 +6438,6 @@ snapshots:
|
||||
readable-stream: 2.3.8
|
||||
typedarray: 0.0.6
|
||||
|
||||
confbox@0.1.3: {}
|
||||
|
||||
confbox@0.1.7: {}
|
||||
|
||||
config-chain@1.1.13:
|
||||
@@ -6361,6 +6579,10 @@ snapshots:
|
||||
dependencies:
|
||||
mimic-response: 3.1.0
|
||||
|
||||
deep-eql@4.1.4:
|
||||
dependencies:
|
||||
type-detect: 4.1.0
|
||||
|
||||
deep-eql@5.0.2: {}
|
||||
|
||||
deep-extend@0.6.0: {}
|
||||
@@ -6413,6 +6635,8 @@ snapshots:
|
||||
|
||||
detect-libc@2.0.3: {}
|
||||
|
||||
diff-sequences@29.6.3: {}
|
||||
|
||||
dir-glob@3.0.1:
|
||||
dependencies:
|
||||
path-type: 4.0.0
|
||||
@@ -7347,6 +7571,10 @@ snapshots:
|
||||
dependencies:
|
||||
js-tokens: 4.0.0
|
||||
|
||||
loupe@2.3.7:
|
||||
dependencies:
|
||||
get-func-name: 2.0.2
|
||||
|
||||
loupe@3.1.1:
|
||||
dependencies:
|
||||
get-func-name: 2.0.2
|
||||
@@ -7696,6 +7924,10 @@ snapshots:
|
||||
|
||||
p-cancelable@3.0.0: {}
|
||||
|
||||
p-limit@5.0.0:
|
||||
dependencies:
|
||||
yocto-queue: 1.1.1
|
||||
|
||||
p-map@7.0.2: {}
|
||||
|
||||
package-json-from-dist@1.0.0: {}
|
||||
@@ -7756,6 +7988,8 @@ snapshots:
|
||||
|
||||
pathe@1.1.2: {}
|
||||
|
||||
pathval@1.1.1: {}
|
||||
|
||||
pathval@2.0.0: {}
|
||||
|
||||
pend@1.2.0: {}
|
||||
@@ -7969,6 +8203,12 @@ snapshots:
|
||||
|
||||
pretty-bytes@6.1.1: {}
|
||||
|
||||
pretty-format@29.7.0:
|
||||
dependencies:
|
||||
'@jest/schemas': 29.6.3
|
||||
ansi-styles: 5.2.0
|
||||
react-is: 18.3.1
|
||||
|
||||
pretty-ms@9.0.0:
|
||||
dependencies:
|
||||
parse-ms: 4.0.0
|
||||
@@ -8055,6 +8295,8 @@ snapshots:
|
||||
react: 18.3.1
|
||||
scheduler: 0.23.2
|
||||
|
||||
react-is@18.3.1: {}
|
||||
|
||||
react-refresh@0.14.2: {}
|
||||
|
||||
react@18.3.1:
|
||||
@@ -8544,10 +8786,14 @@ snapshots:
|
||||
|
||||
tinybench@2.8.0: {}
|
||||
|
||||
tinypool@0.8.4: {}
|
||||
|
||||
tinypool@1.0.0: {}
|
||||
|
||||
tinyrainbow@1.2.0: {}
|
||||
|
||||
tinyspy@2.2.1: {}
|
||||
|
||||
tinyspy@3.0.0: {}
|
||||
|
||||
titleize@3.0.0: {}
|
||||
@@ -8573,6 +8819,8 @@ snapshots:
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
|
||||
type-detect@4.1.0: {}
|
||||
|
||||
type-fest@1.4.0: {}
|
||||
|
||||
type-fest@2.19.0: {}
|
||||
@@ -8659,9 +8907,9 @@ snapshots:
|
||||
|
||||
undici-types@5.26.5: {}
|
||||
|
||||
unimport@3.9.1(rollup@3.29.4):
|
||||
unimport@3.9.1(rollup@4.19.0):
|
||||
dependencies:
|
||||
'@rollup/pluginutils': 5.1.0(rollup@3.29.4)
|
||||
'@rollup/pluginutils': 5.1.0(rollup@4.19.0)
|
||||
acorn: 8.12.1
|
||||
escape-string-regexp: 5.0.0
|
||||
estree-walker: 3.0.3
|
||||
@@ -8740,6 +8988,23 @@ snapshots:
|
||||
spdx-correct: 3.2.0
|
||||
spdx-expression-parse: 3.0.1
|
||||
|
||||
vite-node@1.6.0(@types/node@20.14.12)(sass@1.77.8):
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
debug: 4.3.5
|
||||
pathe: 1.1.2
|
||||
picocolors: 1.0.1
|
||||
vite: 5.3.5(@types/node@20.14.12)(sass@1.77.8)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- less
|
||||
- lightningcss
|
||||
- sass
|
||||
- stylus
|
||||
- sugarss
|
||||
- supports-color
|
||||
- terser
|
||||
|
||||
vite-node@2.0.4(@types/node@20.14.12)(sass@1.77.8):
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
@@ -8841,6 +9106,40 @@ snapshots:
|
||||
dependencies:
|
||||
vite: 5.3.5(@types/node@20.14.12)(sass@1.77.8)
|
||||
|
||||
vitest@1.6.0(@types/node@20.14.12)(happy-dom@14.12.3)(sass@1.77.8):
|
||||
dependencies:
|
||||
'@vitest/expect': 1.6.0
|
||||
'@vitest/runner': 1.6.0
|
||||
'@vitest/snapshot': 1.6.0
|
||||
'@vitest/spy': 1.6.0
|
||||
'@vitest/utils': 1.6.0
|
||||
acorn-walk: 8.3.3
|
||||
chai: 4.5.0
|
||||
debug: 4.3.5
|
||||
execa: 8.0.1
|
||||
local-pkg: 0.5.0
|
||||
magic-string: 0.30.10
|
||||
pathe: 1.1.2
|
||||
picocolors: 1.0.1
|
||||
std-env: 3.7.0
|
||||
strip-literal: 2.1.0
|
||||
tinybench: 2.8.0
|
||||
tinypool: 0.8.4
|
||||
vite: 5.3.5(@types/node@20.14.12)(sass@1.77.8)
|
||||
vite-node: 1.6.0(@types/node@20.14.12)(sass@1.77.8)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@types/node': 20.14.12
|
||||
happy-dom: 14.12.3
|
||||
transitivePeerDependencies:
|
||||
- less
|
||||
- lightningcss
|
||||
- sass
|
||||
- stylus
|
||||
- sugarss
|
||||
- supports-color
|
||||
- terser
|
||||
|
||||
vitest@2.0.4(@types/node@20.14.12)(happy-dom@14.12.3)(sass@1.77.8):
|
||||
dependencies:
|
||||
'@ampproject/remapping': 2.3.0
|
||||
@@ -9062,6 +9361,8 @@ snapshots:
|
||||
buffer-crc32: 0.2.13
|
||||
fd-slicer: 1.1.0
|
||||
|
||||
yocto-queue@1.1.1: {}
|
||||
|
||||
yoctocolors@2.0.2: {}
|
||||
|
||||
zip-dir@2.0.0:
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
diff --git a/packages/wxt/src/core/runners/web-ext.ts b/packages/wxt/src/core/runners/web-ext.ts
|
||||
index 09819c3..a0f0df2 100644
|
||||
--- a/packages/wxt/src/core/runners/web-ext.ts
|
||||
+++ b/packages/wxt/src/core/runners/web-ext.ts
|
||||
@@ -3,6 +3,8 @@ import { ExtensionRunner } from '../../types';
|
||||
import { formatDuration } from '../utils/time';
|
||||
import defu from 'defu';
|
||||
import { wxt } from '../wxt';
|
||||
+import fs from 'node:fs';
|
||||
+import stream from 'node:stream/promises';
|
||||
|
||||
/**
|
||||
* Create an `ExtensionRunner` backed by `web-ext`.
|
||||
@@ -78,6 +80,12 @@ export function createWebExtRunner(): ExtensionRunner {
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
wxt.logger.success(`Opened browser in ${formatDuration(duration)}`);
|
||||
+ await runner.exit();
|
||||
+ const s = fs.createWriteStream('stats.txt', { flags: 'a' });
|
||||
+ s.write(duration + ' ');
|
||||
+ s.end();
|
||||
+ await stream.finished(s);
|
||||
+ process.exit(0);
|
||||
},
|
||||
|
||||
async closeBrowser() {
|
||||
Executable
+41
@@ -0,0 +1,41 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
#
|
||||
# Benchmark how long it takes for the browser to open in dev mode.
|
||||
#
|
||||
# To run:
|
||||
# cd <root>
|
||||
# ./scripts/benchmarks/browser-startup.sh
|
||||
#
|
||||
# You can set N below to change the number of samples per git ref.
|
||||
#
|
||||
|
||||
N=20
|
||||
|
||||
function benchmark_ref() {
|
||||
# Prep
|
||||
git checkout $1
|
||||
pnpm buildc clean
|
||||
git apply scripts/benchmarks/browser-startup.patch
|
||||
pnpm i --ignore-scripts
|
||||
pnpm -r --filter wxt build
|
||||
echo -n "$1 " >> stats.txt
|
||||
|
||||
# Run benchmark
|
||||
for i in $(seq $N); do
|
||||
pnpm wxt packages/wxt-demo
|
||||
done
|
||||
git checkout HEAD -- packages/wxt/src/core/runners/web-ext.ts pnpm-lock.yaml
|
||||
echo "" >> stats.txt
|
||||
}
|
||||
|
||||
rm -f stats.txt
|
||||
|
||||
benchmark_ref "HEAD"
|
||||
|
||||
# Benchmark a commit:
|
||||
# benchmark_ref "3109bba"
|
||||
#
|
||||
# Benchmark a version:
|
||||
# benchmark_ref "v0.19.0"
|
||||
Reference in New Issue
Block a user