Compare commits

..

66 Commits

Author SHA1 Message Date
GitHub Actions 4a1bd41df9 chore(release): wxt v0.19.10
vhs / vhs (push) Cancelled after 0s
2024-09-22 14:20:12 +00:00
1natsu efcb593718 perf: Reduce hypersensitive onChange of watcher (#978)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2024-09-22 09:12:29 -05:00
Okinea Dev a82c7cbf71 docs: Improved docs and links (#970)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2024-09-22 08:45:30 -05:00
renxinyan db60e79a99 docs: Correct the type example of allFrames (#988) 2024-09-21 19:03:07 -05:00
Jarle Mathiesen 722eff145f docs: Add "YouTube Summarized" to homepage (#986) 2024-09-20 07:55:58 -05:00
Zain Hasan 282458748d docs: Fix small typo in introduction (#983) 2024-09-16 21:20:53 -05:00
the-syndrome 26197f11a3 docs: examples reference outDir vs. outputDir (#982) 2024-09-15 07:58:52 -05:00
aklinker1 9d560f9798 docs: Update wxt init GIF 2024-09-14 20:45:33 +00:00
Okinea Dev 786292c95e docs: Add wxt init demo GIF (#974)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
Co-authored-by: okineadev <okineadev@users.noreply.github.com>
2024-09-14 15:44:15 -05:00
rxliuli e760ee444e chore(template): set type to "module" for Svelte template (#977) 2024-09-12 08:07:11 -05:00
kongmoumou 78f91fefb6 chore: Continuous publish using pkg.pr.new (#955) 2024-09-11 00:40:42 -05:00
Aaron Klinker fda1e18f48 types: Fix ExtensionRunnerConfig.chromiumPref type
Closes #964
2024-09-10 16:29:36 -05:00
hikiko4ern c94168e2d1 fix: Fix config manifest type (#973) 2024-09-10 16:13:08 -05:00
GitHub Actions 3eb505dbde chore(release): wxt v0.19.9 2024-09-04 21:39:24 +00:00
1natsu fbe502fddf chore: Cleanup leftover E2E test artifacts (#968) 2024-09-04 16:35:18 -05:00
1natsu c143a9978d fix: Properly overload import.meta.env with WXT's own environment globals (#966) 2024-09-03 14:35:10 -05:00
Aaron Klinker e3555b663d Update description after 447d011 2024-09-03 14:34:32 -05:00
Aaron Klinker df934d252c Correct version number after 447d011 2024-09-03 14:31:27 -05:00
Aaron Klinker 447d011b8f docs: Retroactively mention breaking change in v0.18.5
See https://github.com/wxt-dev/wxt/discussions/950#discussioncomment-10512337
2024-09-03 14:30:10 -05:00
Aaron abe1263d27 feat: Use @types/chrome for config manifest type (#969) 2024-09-03 14:11:24 -05:00
Aaron Klinker 143b5ac81f docs: Add docs around importEntrypoint to relevant functions 2024-09-03 13:40:59 -05:00
dependabot[bot] 87fbb1f4c3 chore(deps): bump execa from 9.3.0 to 9.3.1 (#957)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-03 12:12:22 -05:00
dependabot[bot] 62f11bf605 chore(deps): bump sharp from 0.33.4 to 0.33.5 (#959)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-03 12:10:02 -05:00
dependabot[bot] 3ead54b58e chore(deps): bump unimport from 3.9.1 to 3.11.1 (#960)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-03 12:08:44 -05:00
dependabot[bot] 580f4f3cfe chore(deps): bump ora from 8.0.1 to 8.1.0 (#961)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-03 12:08:04 -05:00
1natsu da0accebe8 fix: duplicate BuildOutput.publicAssets (#951)
Co-authored-by: Aaron Klinker <aaronklinker1@gmail.com>
2024-09-03 12:06:21 -05:00
Aaron eb65d0b42a docs: Add link to testing permission changes in Firefox 2024-09-03 07:34:43 -05:00
Alex Nguyen c2784135b0 docs: Fix typo in frontend-frameworks.md (#952) 2024-09-01 19:43:11 -05:00
Aaron 61d54bd1f9 fix: Allow adding multiple hyphens in an entrypoint name (#949) 2024-08-30 04:56:23 -05:00
Aaron 0bd94fce3b chore: Add oxlint for linting (#947) 2024-08-30 04:14:20 -05:00
Amal Chandran 2a97c06443 docs: Add "Draftly" to homepage (#941)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2024-08-30 02:51:32 -05:00
Aaron 8213aa776a chore: Fix type error (#946) 2024-08-30 02:45:36 -05:00
Aaron 026f78254d chore: Upgrade unbuild from 1.1.0 to 1.1.1 2024-08-29 08:19:56 -05:00
Aaron 0ebb013ba6 types(modules): Use NestedHooks instead of Partial for hooks object 2024-08-29 07:12:58 -05:00
Aaron c5f78d0c8d feat(modules): Add wxt.hook alias for wxt.hooks.hook 2024-08-29 07:12:13 -05:00
GitHub Actions 8771676e69 chore(release): wxt v0.19.8 2024-08-29 11:53:13 +00:00
1natsu aebbbb0685 fix: Fallback to GitHub API for listing templates when ungh is down (#944)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2024-08-29 06:48:38 -05:00
Aaron 68183b648e chore(types): Cleanup types in wxt/browser/chrome (#932) 2024-08-23 08:49:39 -05:00
Aaron Klinker 45809c0198 chore: Update lockfile 2024-08-19 23:08:40 -05:00
Aaron Klinker 13163c9ba1 chore(dev-deps): Upgrade @aklinker1/buildc from v1.0.11 to v1.1.0 2024-08-19 21:49:01 -05:00
1natsu aedbba3f3d perf: Ignore output directories for all vite.watcher (#924) 2024-08-19 21:44:27 -05:00
GitHub Actions 763dbd5ee8 chore(release): wxt v0.19.7 2024-08-19 15:40:58 +00:00
Aaron 5821ae0e8c feat: New @wxt-dev/i18n package (#758) 2024-08-19 09:18:01 -05:00
Aaron 751706d2ae docs: Fix module name for wxt/browser/chrome 2024-08-19 09:08:53 -05:00
Aaron cf8554718d feat(modules): Add addAlias helper (#928) 2024-08-18 15:20:12 -05:00
Aaron 5f653b3ff9 fix: Ensure TSConfig paths start with ../ or ./ so they are valid (#927) 2024-08-18 15:18:23 -05:00
Aaron 5f2e1c38a7 chore: Remove warning log for missing public directory 2024-08-18 15:10:07 -05:00
Aaron 8a9d8be8a4 feat(testing): Run WXT modules when setting up test environment (#926) 2024-08-18 15:08:45 -05:00
Aaron 067b2eccf3 fix(testing): Stub chrome and browser globals with fakeBrowser automatically (#925) 2024-08-18 14:56:15 -05:00
Aaron 2e1bd6ec93 chore: Update demo package cache behavior 2024-08-18 14:49:29 -05:00
Aaron b463cef1c2 Update changelog 2024-08-16 15:59:58 -05:00
GitHub Actions adf9d39d69 chore(release): wxt v0.19.6 2024-08-16 20:16:57 +00:00
1natsu b1699592e4 perf: Ignore non-source code from the file watcher (#919) 2024-08-16 15:01:18 -05:00
Aaron 8675abfced docs: Update ESM content script docs with example 2024-08-16 09:34:32 -05:00
Aaron a1fc19e47c chore(bench): Setup benchmark script for testing browser open timing (#917) 2024-08-15 16:51:06 -05:00
Aaron 8235de04d2 chore: Add profiling docs to CONTRIBUTING.md 2024-08-15 16:50:42 -05:00
Aaron 46a324adc2 fix(types): Don't report type errors when using string templates with browser.i18n.getMessage (#916) 2024-08-14 23:44:11 -05:00
Aaron c0af62316d fix: Typo in sidepanel options (browse_stylebrowser_style) (#914) 2024-08-14 07:46:42 -05:00
GitHub Actions 2fd3503694 chore(release): wxt v0.19.5 2024-08-12 15:36:58 +00:00
Aaron e6529e6e41 fix: Update chromium setting for enabling content script sourcemaps 2024-08-12 10:32:25 -05:00
1natsu 72673ca366 fix: #907 move wxt devDeps execa to dependencies (#908) 2024-08-10 12:01:44 -05:00
Florian Metz cbda49ba7b feat: allow excluding files when zipping (#906) 2024-08-07 20:15:33 -05:00
hikiko4ern 0ab32e5afa feat: allow zipping hidden files by listing them explicitly in includeSources (#902) 2024-08-07 20:13:33 -05:00
GitHub Actions 36cfcd0acb chore(release): @wxt-dev/module-solid v1.1.2 2024-08-06 13:07:15 +00:00
GitHub Actions f746d46247 chore(release): @wxt-dev/module-svelte v1.0.1 2024-08-06 12:29:43 +00:00
GitHub Actions a2de6cbe44 chore(release): @wxt-dev/module-vue v1.0.1 2024-08-06 04:23:00 +00:00
99 changed files with 3317 additions and 478 deletions
+5
View File
@@ -1,5 +1,9 @@
name: Basic Setup
description: Install PNPM, Node, and dependencies
inputs:
install:
default: 'true'
description: Whether or not to run 'pnpm install'
runs:
using: composite
steps:
@@ -11,5 +15,6 @@ runs:
node-version: 18
cache: pnpm
- name: Install Dependencies
if: ${{ inputs.install == 'true' }}
shell: bash
run: pnpm install
+17
View File
@@ -0,0 +1,17 @@
name: Continuous Publish
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
build:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup
- run: pnpm buildc all
- run: pnpx pkg-pr-new publish --compact './packages/*'
+6 -5
View File
@@ -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:
+29
View File
@@ -0,0 +1,29 @@
name: vhs
on:
push:
paths:
- 'docs/tapes/*.tape'
workflow_dispatch:
jobs:
vhs:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup
with:
install: 'false'
- name: Preinstall WXT
run: |
pnpm store add wxt@latest
pnpm dlx wxt@latest --version
- uses: charmbracelet/vhs-action@v2.1.0
with:
path: 'docs/tapes/init-demo.tape'
- uses: stefanzweifel/git-auto-commit-action@v4
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
commit_message: 'docs: Update `wxt init` GIF'
# https://github.com/charmbracelet/vhs#output
file_pattern: 'docs/assets/*.gif'
+1
View File
@@ -21,3 +21,4 @@ docs/api/reference
stats.html
.tool-versions
.cache
*-stats.txt
+17 -2
View File
@@ -1,6 +1,6 @@
# Contributing
Everyone is welcome to contribute to WXT!
Everyone is welcome to contribute to **WXT**!
If you are changing the docs or fixing a bug, feel free to fork and open a PR.
@@ -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.
@@ -124,7 +139,7 @@ Releases are done with GitHub actions:
Use [`taze`](https://www.npmjs.com/package/taze) to upgrade dependencies throughout the entire monorepo.
```ts
```sh
pnpm dlx taze -r
```
+25 -7
View File
@@ -1,16 +1,16 @@
<h1 align="center">
<img style="vertical-align:middle" width="44" src="https://raw.githubusercontent.com/wxt-dev/wxt/HEAD/docs/public/hero-logo.svg" alt="WXT Logo">
<img align="top" width="44" src="https://raw.githubusercontent.com/wxt-dev/wxt/HEAD/docs/public/hero-logo.svg" alt="WXT Logo">
<span>WXT</span>
</h1>
<p align="center">
<a href="https://www.npmjs.com/package/wxt" target="_blank"><img alt="npm" src="https://img.shields.io/npm/v/wxt?labelColor=black&color=%234fa048"></a>
<a href="https://www.npmjs.com/package/wxt" target="_blank"><img alt="npm version" src="https://img.shields.io/npm/v/wxt?labelColor=black&color=%234fa048"></a>
<span> </span>
<a href="https://www.npmjs.com/package/wxt" target="_blank"><img alt="npm" src="https://img.shields.io/npm/dm/wxt?labelColor=black&color=%234fa048"></a>
<a href="https://www.npmjs.com/package/wxt" target="_blank"><img alt="downloads" src="https://img.shields.io/npm/dm/wxt?labelColor=black&color=%234fa048"></a>
<span> </span>
<a href="https://github.com/wxt-dev/wxt/blob/main/LICENSE" target="_blank"><img alt="NPM" src="https://img.shields.io/npm/l/wxt?labelColor=black&color=%234fa048"></a>
<a href="https://github.com/wxt-dev/wxt/blob/main/LICENSE" target="_blank"><img alt="license | MIT" src="https://img.shields.io/npm/l/wxt?labelColor=black&color=%234fa048"></a>
<span> </span>
<a href="https://codecov.io/github/wxt-dev/wxt" target="_blank"><img alt="Codecov" src="https://img.shields.io/codecov/c/github/wxt-dev/wxt?labelColor=black&color=%234fa048"></a>
<a href="https://codecov.io/github/wxt-dev/wxt" target="_blank"><img alt="coverage" src="https://img.shields.io/codecov/c/github/wxt-dev/wxt?labelColor=black&color=%234fa048"></a>
</p>
<p align="center">
@@ -43,10 +43,21 @@ https://github.com/wxt-dev/wxt/assets/10101283/4d678939-1bdb-495c-9c36-3aa281d84
Bootstrap a new project:
<!-- automd:pm-x version="latest" name="wxt" args="init" -->
```sh
pnpm dlx wxt@latest init <project-name>
# npm
npx wxt@latest init
# pnpm
pnpm dlx wxt@latest init
# bun
bunx wxt@latest init
```
<!-- /automd -->
Or see the [installation guide](https://wxt.dev/guide/installation.html) to get started with WXT.
## Features
@@ -66,6 +77,13 @@ Or see the [installation guide](https://wxt.dev/guide/installation.html) to get
## Contributors
<!-- automd:contributors author="aklinker1" license="MIT" github="wxt-dev/wxt" -->
Published under the [MIT](https://github.com/wxt-dev/wxt/blob/main/LICENSE) license.
Made by [@aklinker1](https://github.com/aklinker1) and [community](https://github.com/wxt-dev/wxt/graphs/contributors) 💛
<br><br>
<a href="https://github.com/wxt-dev/wxt/graphs/contributors">
<img src="https://contrib.rocks/image?repo=wxt-dev/wxt" />
<img src="https://contrib.rocks/image?repo=wxt-dev/wxt" />
</a>
<!-- /automd -->
@@ -38,6 +38,8 @@ const chromeExtensionIds = [
'cfkdcideecefncbglkhneoflfnmhoicc', // mindful - stay focused on your goals
'lnhejcpclabmbgpiiomjbhalblnnbffg', // 1Proompt
'fonflmjnjbkigocpoommgmhljdpljain', // NiceTab - https://github.com/web-dahuyou/NiceTab
'fcffekbnfcfdemeekijbbmgmkognnmkd', // Draftly for LinkedIn
'nkndldfehcidpejfkokbeghpnlbppdmo', // YouTube Summarized - Summarize any YouTube video
];
const { data, err, isLoading } = useListExtensionDetails(chromeExtensionIds);
+8
View File
@@ -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/', [
Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

+4
View File
@@ -25,6 +25,10 @@ npx wxt@latest init <project-name>
npx wxt@latest init <project-name>
```
### Demo
![wxt init demo](/assets/init-demo.gif)
:::
There are several starting templates available.
+1 -1
View File
@@ -9,7 +9,7 @@ head:
## Overview
WXT is a free and open source framework for building web extensions in an conventional, intuitive, and safe way **_for all browsers_**.
WXT is a free and open source framework for building web extensions in a conventional, intuitive, and safe way **_for all browsers_**.
WXT is based on [Nuxt](https://nuxt.com), and aims to provide the same great DX with TypeScript, auto-imports, and an opinionated project structure.
@@ -24,7 +24,7 @@ export default defineContentScript({
excludeMatches: undefined | [],
includeGlobs: undefined | [],
excludeGlobs: undefined | [],
allFrames: undefined | [],
allFrames: undefined | true | false,
runAt: undefined | 'document_start' | 'document_end' | 'document_idle',
matchAboutBlank: undefined | true | false,
matchOriginAsFallback: undefined | true | false,
+56
View File
@@ -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)
+3 -1
View File
@@ -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.
+2 -1
View File
@@ -56,7 +56,8 @@ When `permissions`/`host_permissions` change during an update, depending on what
You can test if your permission changes will result in a disabled extension:
- Chromium: Use [Google's Extension Update Testing tool](https://github.com/GoogleChromeLabs/extension-update-testing-tool)
- Firefox/Safari: Everyone breaks something in production eventually... 🫡 Good luck soldier
- Firefox: See their [Test Permission Requests](https://extensionworkshop.com/documentation/develop/test-permission-requests/) page
- Safari: Everyone breaks something in production eventually... 🫡 Good luck soldier
## Update Event
+1 -1
View File
@@ -139,7 +139,7 @@ export default defineWxtModule({
name: 'changelog',
// Point to the "modules/changelog.html" file
inputPath: resolve(__dirname, 'changelog.html'),
outputDir: wxt.config.outputDir,
outputDir: wxt.config.outDir,
options: {},
});
},
+82
View File
@@ -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!
+48
View File
@@ -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!
+81
View File
@@ -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!";
```
+22
View File
@@ -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
+170
View File
@@ -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.
:::
@@ -100,7 +100,7 @@ Usually, this means each entrypoint should be a directory with it's own files in
## Configuring Routers
Lots of frameworks come with routers for building a multi-page app using the URL's path. Chrome extensions don't don't work like this. Since HTML files are static, `chrome-extension://{id}/popup.html`, there's no way to change the entire path for routing.
Lots of frameworks come with routers for building a multi-page app using the URL's path. Chrome extensions don't work like this. Since HTML files are static, `chrome-extension://{id}/popup.html`, there's no way to change the entire path for routing.
Instead, you need to configure the router to run in "hash" mode, where the routing information is apart of the URL's hash, not the path (ie: `popup.html#/` and `popup.html#/account/settings`).
+2 -30
View File
@@ -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
+22 -1
View File
@@ -10,7 +10,7 @@ pnpm i wxt@latest
If there was a major version change, follow the steps below to fix breaking changes.
## v0.18.0 &rarr; v0.19.0
## v0.18.5 &rarr; v0.19.0
### `vite-node` Entrypoint Loader
@@ -86,6 +86,27 @@ WXT no longer ships with Common JS support. If you're using CJS, here's your mig
Vite also provides steps for migrating to ESM. Check them out for more details: https://vitejs.dev/guide/migration#deprecate-cjs-node-api
## v0.18.0 &rarr; v0.18.5
> When this version was released, it was not considered a breaking change... but it should have been.
### New `modules/` Directory
WXT now recognizes the `modules/` directory as a folder containing [WXT modules](/guide/go-further/reusable-modules).
If you already have `<srcDir>/modules` or `<srcDir>/Modules` directory, `wxt prepare` and other commands will fail.
You have two options:
1. [Recommended] Keep your files where they are and tell WXT to look in a different folder:
```ts
// wxt.config.ts
export default defineConfig({
modulesDir: 'wxt-modules', // defaults to "modules"
});
```
2. Rename your `modules` directory to something else.
## v0.17.0 &rarr; v0.18.0
### Automatic MV3 `host_permissions` to MV2 `permissions`
+59
View File
@@ -0,0 +1,59 @@
# VHS documentation
#
# You can view all VHS documentation on the command line with `vhs manual`.
# Or see https://github.com/charmbracelet/vhs#vhs-command-reference
# Output file
Output docs/assets/init-demo.gif
# The tools we will use
Require pnpm
# === Scene ====
Set Width 1400
Set Height 800
# The maximum FPS for GIF is `50` FPS.
Set Framerate 50
# Terminal theme with WXT brand colors (which was taken from the website)
# Based on the standard charmbracelet/vhs theme:
# https://github.com/charmbracelet/vhs/blob/88e634f4a10bbe305b6aea9a12b4d8dc3dd7f31c/style.go#L7-L28
Set Theme {"background": "#1a1b1a", "foreground": "#dddddd", "black": "#282a2e", "brightBlack": "#4d4d4d", "red": "#D74E6F", "brightRed": "#FE5F86", "green": "#67d45e", "brightGreen": "#67d45e", "yellow": "#D3E561", "brightYellow": "#EBFF71", "blue": "#8056FF", "brightBlue": "#9B79FF", "magenta": "#ED61D7", "brightMagenta": "#FF7AEA", "cyan": "#04D7D7", "brightCyan": "#00FEFE", "white": "#bfbfbf", "brightWhite": "#e6e6e6", "indigo": "#5B56E0"}
Set FontSize 32
# Terminal settings
Set Shell "bash"
# Terminal prompt. It looks like "● mycommand ..."
Env PS1 "\e[0;32m●\e[0m "
# ===== Preparation =====
# Steps to prepare the recording environment, ensuring a clean and isolated setup.
Hide
# Create a temporary folder for demo
Type 'vhs_sandbox="$(mktemp -d)"' Enter
Type 'cd "$vhs_sandbox"' Enter
Type 'clear' Enter
Show
# ===== Actions =====
Type 'pnpm dlx wxt@latest init .' Sleep 1s Enter
Sleep 3.25s
Down@750ms Enter@750ms # Select `vue` template
Sleep 1.25s
Down@750ms Enter@750ms # Select `pnpm` as a package manager
Sleep 5s
# ===== Cleaning =====
Hide
# Delete the temporary folder
Type 'rm -rf "$vhs_sandbox"' Enter
Show
+3 -3
View File
@@ -17,14 +17,14 @@
"docs:preview": "pnpm -s docs:gen && vitepress preview docs"
},
"devDependencies": {
"@aklinker1/buildc": "^1.0.11",
"@aklinker1/check": "^1.3.1",
"@aklinker1/buildc": "^1.1.1",
"@aklinker1/check": "^1.4.5",
"@types/fs-extra": "^11.0.4",
"@vitest/coverage-v8": "^2.0.4",
"changelogen": "^0.5.5",
"consola": "^3.2.3",
"dependency-graph": "^1.0.0",
"execa": "^9.3.0",
"execa": "^9.3.1",
"fast-glob": "^3.3.2",
"fs-extra": "^11.2.0",
"hasha": "^6.0.0",
+3 -2
View File
@@ -45,7 +45,8 @@
"wxt": ">=0.19.0"
},
"devDependencies": {
"@aklinker1/check": "^1.3.1",
"@aklinker1/check": "^1.4.5",
"oxlint": "^0.9.1",
"publint": "^0.2.9",
"typescript": "^5.5.4",
"unbuild": "^2.0.0",
@@ -54,6 +55,6 @@
"dependencies": {
"defu": "^6.1.4",
"fs-extra": "^11.2.0",
"sharp": "^0.33.4"
"sharp": "^0.33.5"
}
}
+3
View File
@@ -0,0 +1,3 @@
# WXT I18n
[Read the docs](https://wxt.dev/guide/i18n/installation) to get started.
+90
View File
@@ -0,0 +1,90 @@
{
"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.4.5",
"@types/chrome": "^0.0.268",
"@types/node": "^20.14.2",
"oxlint": "^0.9.1",
"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"
]
}
+164
View File
@@ -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');
});
});
+82
View File
@@ -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);
});
});
});
+119
View File
@@ -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);
});
});
});
});
+63
View File
@@ -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);
});
});
});
+291
View File
@@ -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');
}
+82
View File
@@ -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>;
}
+162
View File
@@ -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);
});
}
},
});
+89
View File
@@ -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 `&lt;`. 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;
+23
View File
@@ -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 +
Array.from({ length: MAX_SUBSTITUTIONS }).findLastIndex((_, i) =>
message.match(new RegExp(`(?<!\\$)\\$${i + 1}`)),
)
);
}
const MAX_SUBSTITUTIONS = 9;
+7
View File
@@ -0,0 +1,7 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"types": ["chrome", "node"]
},
"exclude": ["node_modules/**", "dist/**"]
}
+10
View File
@@ -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 -1
View File
@@ -50,7 +50,7 @@
"@vitejs/plugin-react": "^4.3.1"
},
"devDependencies": {
"@aklinker1/check": "^1.3.1",
"@aklinker1/check": "^1.4.5",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"publint": "^0.2.9",
+14
View File
@@ -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)
+2 -2
View File
@@ -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",
@@ -50,7 +50,7 @@
"vite-plugin-solid": "^2.10.2"
},
"devDependencies": {
"@aklinker1/check": "^1.3.1",
"@aklinker1/check": "^1.4.5",
"publint": "^0.2.9",
"solid-js": "^1.8.19",
"typescript": "^5.5.4",
+27 -1
View File
@@ -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 🎉
+2 -2
View File
@@ -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",
@@ -48,7 +48,7 @@
"@sveltejs/vite-plugin-svelte": "^3.1.1"
},
"devDependencies": {
"@aklinker1/check": "^1.3.1",
"@aklinker1/check": "^1.4.5",
"publint": "^0.2.9",
"typescript": "^5.5.4",
"unbuild": "^2.0.0",
+22 -1
View File
@@ -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 🎉
+2 -2
View File
@@ -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",
@@ -48,7 +48,7 @@
"@vitejs/plugin-vue": "^5.1.1"
},
"devDependencies": {
"@aklinker1/check": "^1.3.1",
"@aklinker1/check": "^1.4.5",
"publint": "^0.2.9",
"typescript": "^5.5.4",
"unbuild": "^2.0.0",
+2 -1
View File
@@ -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);
},
});
+25
View File
@@ -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
+3
View File
@@ -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
View File
@@ -0,0 +1 @@
src/core/utils/building/__tests__/test-entrypoints
+144 -1
View File
@@ -1,5 +1,148 @@
# Changelog
## v0.19.10
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.9...wxt-v0.19.10)
### 🔥 Performance
- Reduce hypersensitive onChange of watcher ([#978](https://github.com/wxt-dev/wxt/pull/978))
### 🩹 Fixes
- Fix config manifest type ([#973](https://github.com/wxt-dev/wxt/pull/973))
### 📖 Documentation
- Examples reference outDir vs. outputDir ([#982](https://github.com/wxt-dev/wxt/pull/982))
- Improved docs and links ([#970](https://github.com/wxt-dev/wxt/pull/970))
### 🌊 Types
- Fix `ExtensionRunnerConfig.chromiumPref` type ([fda1e18](https://github.com/wxt-dev/wxt/commit/fda1e18))
### ❤️ Contributors
- 1natsu ([@1natsu172](http://github.com/1natsu172))
- Okinea Dev ([@okineadev](http://github.com/okineadev))
- The-syndrome <meltdown-syndrome@proton.me>
- Hikiko4ern ([@hikiko4ern](http://github.com/hikiko4ern))
## v0.19.9
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.8...wxt-v0.19.9)
### 🚀 Enhancements
- **modules:** Add `wxt.hook` alias for `wxt.hooks.hook` ([c5f78d0](https://github.com/wxt-dev/wxt/commit/c5f78d0))
- Use `@types/chrome` for config manifest type ([#969](https://github.com/wxt-dev/wxt/pull/969))
### 🩹 Fixes
- Allow adding multiple hyphens in an entrypoint name ([#949](https://github.com/wxt-dev/wxt/pull/949))
- Duplicate `BuildOutput.publicAssets` ([#951](https://github.com/wxt-dev/wxt/pull/951))
- Properly overload `import.meta.env` with WXT's own environment globals ([#966](https://github.com/wxt-dev/wxt/pull/966))
### 📖 Documentation
- Add docs around `importEntrypoint` to relevant functions ([143b5ac](https://github.com/wxt-dev/wxt/commit/143b5ac))
### 🌊 Types
- **modules:** Use `NestedHooks` instead of `Partial` for hooks object ([0ebb013](https://github.com/wxt-dev/wxt/commit/0ebb013))
### 🏡 Chore
- Fix type error ([#946](https://github.com/wxt-dev/wxt/pull/946))
- Add `oxlint` for linting ([#947](https://github.com/wxt-dev/wxt/pull/947))
- **deps:** Bump ora from 8.0.1 to 8.1.0 ([#961](https://github.com/wxt-dev/wxt/pull/961))
- **deps:** Bump unimport from 3.9.1 to 3.11.1 ([#960](https://github.com/wxt-dev/wxt/pull/960))
- **deps:** Bump execa from 9.3.0 to 9.3.1 ([#957](https://github.com/wxt-dev/wxt/pull/957))
- Cleanup leftover E2E test artifacts ([#968](https://github.com/wxt-dev/wxt/pull/968))
### ❤️ Contributors
- 1natsu ([@1natsu172](http://github.com/1natsu172))
## 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 +2496,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))
+27 -8
View File
@@ -1,16 +1,17 @@
<!-- DO NOT EDIT, THIS FILE WAS GENERATED BY '../../scripts/generate-readmes.sh' -->
<h1 align="center">
<img style="vertical-align:middle" width="44" src="https://raw.githubusercontent.com/wxt-dev/wxt/HEAD/docs/public/hero-logo.svg" alt="WXT Logo">
<img align="top" width="44" src="https://raw.githubusercontent.com/wxt-dev/wxt/HEAD/docs/public/hero-logo.svg" alt="WXT Logo">
<span>WXT</span>
</h1>
<p align="center">
<a href="https://www.npmjs.com/package/wxt" target="_blank"><img alt="npm" src="https://img.shields.io/npm/v/wxt?labelColor=black&color=%234fa048"></a>
<a href="https://www.npmjs.com/package/wxt" target="_blank"><img alt="npm version" src="https://img.shields.io/npm/v/wxt?labelColor=black&color=%234fa048"></a>
<span> </span>
<a href="https://www.npmjs.com/package/wxt" target="_blank"><img alt="npm" src="https://img.shields.io/npm/dm/wxt?labelColor=black&color=%234fa048"></a>
<a href="https://www.npmjs.com/package/wxt" target="_blank"><img alt="downloads" src="https://img.shields.io/npm/dm/wxt?labelColor=black&color=%234fa048"></a>
<span> </span>
<a href="https://github.com/wxt-dev/wxt/blob/main/LICENSE" target="_blank"><img alt="NPM" src="https://img.shields.io/npm/l/wxt?labelColor=black&color=%234fa048"></a>
<a href="https://github.com/wxt-dev/wxt/blob/main/LICENSE" target="_blank"><img alt="license | MIT" src="https://img.shields.io/npm/l/wxt?labelColor=black&color=%234fa048"></a>
<span> </span>
<a href="https://codecov.io/github/wxt-dev/wxt" target="_blank"><img alt="Codecov" src="https://img.shields.io/codecov/c/github/wxt-dev/wxt?labelColor=black&color=%234fa048"></a>
<a href="https://codecov.io/github/wxt-dev/wxt" target="_blank"><img alt="coverage" src="https://img.shields.io/codecov/c/github/wxt-dev/wxt?labelColor=black&color=%234fa048"></a>
</p>
<p align="center">
@@ -18,7 +19,7 @@
<br/>
<span>⚡</span>
<br/>
<q><i>It's like Nuxt, but for Chrome Extensions</i></q>
<q><i>It's like Nuxt, but for Web Extensions</i></q>
</p>
<p align="center">
@@ -43,10 +44,21 @@ https://github.com/wxt-dev/wxt/assets/10101283/4d678939-1bdb-495c-9c36-3aa281d84
Bootstrap a new project:
<!-- automd:pm-x version="latest" name="wxt" args="init" -->
```sh
pnpm dlx wxt@latest init <project-name>
# npm
npx wxt@latest init
# pnpm
pnpm dlx wxt@latest init
# bun
bunx wxt@latest init
```
<!-- /automd -->
Or see the [installation guide](https://wxt.dev/guide/installation.html) to get started with WXT.
## Features
@@ -66,6 +78,13 @@ Or see the [installation guide](https://wxt.dev/guide/installation.html) to get
## Contributors
<!-- automd:contributors author="aklinker1" license="MIT" github="wxt-dev/wxt" -->
Published under the [MIT](https://github.com/wxt-dev/wxt/blob/main/LICENSE) license.
Made by [@aklinker1](https://github.com/aklinker1) and [community](https://github.com/wxt-dev/wxt/graphs/contributors) 💛
<br><br>
<a href="https://github.com/wxt-dev/wxt/graphs/contributors">
<img src="https://contrib.rocks/image?repo=wxt-dev/wxt" />
<img src="https://contrib.rocks/image?repo=wxt-dev/wxt" />
</a>
<!-- /automd -->
@@ -362,7 +362,7 @@ describe('Output Directory Structure', () => {
.toMatchInlineSnapshot(`
".output/chrome-mv3/background.js
----------------------------------------
var _background = function() {
var background = function() {
"use strict";
var _a, _b;
function defineBackground(arg) {
@@ -372,13 +372,13 @@ describe('Output Directory Structure', () => {
function logHello(name) {
console.log(\`Hello \${name}!\`);
}
_background;
background;
const definition = defineBackground({
main() {
logHello("background");
}
});
_background;
background;
function initPlugins() {
}
// @ts-expect-error
@@ -411,7 +411,7 @@ describe('Output Directory Structure', () => {
const result$1 = result;
return result$1;
}();
_background;
background;
"
`);
});
@@ -13,7 +13,6 @@ describe('TypeScript Project', () => {
".wxt/types/globals.d.ts
----------------------------------------
// Generated by wxt
export {}
interface ImportMetaEnv {
readonly MANIFEST_VERSION: 2 | 3;
readonly BROWSER: string;
@@ -126,7 +125,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 +135,6 @@ describe('TypeScript Project', () => {
options?: GetMessageOptions,
): string;
/**
* No message description.
*
* "<browser.i18n.getUiLocale()>"
*/
getMessage(
@@ -215,6 +212,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 +378,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',
+1 -1
View File
@@ -74,7 +74,7 @@ describe('User Config', () => {
);
await project.build({
// @ts-expect-error: Specifically setting an invalid field for the test - it should show up in the snapshot
// Specifically setting an invalid field for the test - it should show up in the snapshot
manifest: ({ manifestVersion, command }) => ({
example_customization: [String(manifestVersion), command],
}),
+42 -5
View File
@@ -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,
);
});
});
+10 -9
View File
@@ -1,7 +1,7 @@
{
"name": "wxt",
"type": "module",
"version": "0.19.4",
"version": "0.19.10",
"description": "Next gen framework for developing web extensions",
"repository": {
"type": "git",
@@ -82,6 +82,7 @@
},
"dependencies": {
"@aklinker1/rollup-plugin-visualizer": "5.12.0",
"@types/chrome": "^0.0.269",
"@types/webextension-polyfill": "^0.10.7",
"@webext-core/fake-browser": "^1.3.1",
"@webext-core/isolated-element": "^1.1.2",
@@ -94,7 +95,9 @@
"consola": "^3.2.3",
"defu": "^6.1.4",
"dequal": "^2.0.3",
"dotenv": "^16.4.5",
"esbuild": "^0.23.0",
"execa": "^9.3.1",
"fast-glob": "^3.3.2",
"filesize": "^10.1.4",
"fs-extra": "^11.2.0",
@@ -113,30 +116,31 @@
"nypm": "^0.3.9",
"ohash": "^1.1.3",
"open": "^10.1.0",
"ora": "^8.0.1",
"ora": "^8.1.0",
"perfect-debounce": "^1.0.0",
"picocolors": "^1.0.1",
"prompts": "^2.4.2",
"publish-browser-extension": "^2.1.3",
"unimport": "^3.9.1",
"scule": "^1.3.0",
"unimport": "^3.11.1",
"vite": "^5.3.5",
"vite-node": "^2.0.4",
"web-ext-run": "^0.2.1",
"webextension-polyfill": "^0.12.0"
},
"devDependencies": {
"@aklinker1/check": "^1.3.1",
"@aklinker1/check": "^1.4.5",
"@faker-js/faker": "^8.4.1",
"@types/chrome": "^0.0.269",
"@types/fs-extra": "^11.0.4",
"@types/lodash.merge": "^4.6.9",
"@types/natural-compare": "^1.4.3",
"@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",
"oxlint": "^0.9.1",
"p-map": "^7.0.2",
"publint": "^0.2.9",
"tsx": "4.15.7",
@@ -145,9 +149,6 @@
"vitest": "^2.0.4",
"vitest-plugin-random-seed": "^1.1.0"
},
"peerDependencies": {
"@types/chrome": "*"
},
"peerDependenciesMeta": {
"@types/chrome": {
"optional": true
+4 -6
View File
@@ -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);
`;
+10 -7
View File
@@ -1,3 +1,4 @@
import { debounce } from 'perfect-debounce';
import {
BuildStepOutput,
EntrypointGroup,
@@ -144,12 +145,7 @@ function createFileReloader(server: WxtDevServer) {
const fileChangedMutex = new Mutex();
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;
const cb = async (event: string, path: string) => {
changeQueue.push([event, path]);
await fileChangedMutex.runExclusive(async () => {
@@ -160,6 +156,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;
@@ -214,11 +212,16 @@ function createFileReloader(server: WxtDevServer) {
wxt.logger.success(`Reloaded: ${getFilenameList(rebuiltNames)}`);
break;
}
} catch (err) {
} catch {
// Catch build errors instead of crashing. Don't log error either, builder should have already logged it
}
});
};
return debounce(cb, wxt.config.dev.server!.watchDebounce, {
leading: true,
trailing: false,
});
}
/**
+55 -32
View File
@@ -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';
+3
View File
@@ -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,
},
},
};
@@ -0,0 +1,18 @@
import { describe, it, expect } from 'vitest';
import { safeStringToNumber } from '../number';
describe('Number Utils', () => {
describe('safeStringToNumber', () => {
it.each([
{ arg: '1000', expected: 1000 },
{ arg: '', expected: 0 },
{ arg: '12abc', expected: null },
{ arg: undefined, expected: null },
])(
'should be safely converted from string to number: safeStringToNumber($arg) -> $expected',
({ arg, expected }) => {
expect(safeStringToNumber(arg)).toBe(expected);
},
);
});
});
@@ -21,9 +21,10 @@ describe('String utils', () => {
describe('safeVarName', () => {
it.each([
['Hello world!', '_hello_world'],
['Hello world!', 'helloWorld'],
['123', '_123'],
['abc-123', '_abc_123'],
['abc-123', 'abc123'],
['abc-123-xyz', 'abc123Xyz'],
['', '_'],
[' ', '_'],
['_', '_'],
@@ -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',
@@ -176,7 +201,6 @@ async function getGlobalsDeclarationEntry(): Promise<WxtDirFileEntry> {
path: 'types/globals.d.ts',
text: [
'// Generated by wxt',
'export {}',
'interface ImportMetaEnv {',
...globals.map((global) => ` readonly ${global.name}: ${global.type};`),
'}',
@@ -208,7 +232,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);
@@ -47,14 +47,16 @@ export async function rebuild(
const newOutput = await buildEntrypoints(entrypointGroups, spinner);
const mergedOutput: Omit<BuildOutput, 'manifest'> = {
steps: [...existingOutput.steps, ...newOutput.steps],
publicAssets: [...existingOutput.publicAssets, ...newOutput.publicAssets],
// Do not merge existing because all publicAssets copied everytime
publicAssets: newOutput.publicAssets,
};
const { manifest: newManifest, warnings: manifestWarnings } =
await generateManifest(allEntrypoints, mergedOutput);
const finalOutput: BuildOutput = {
manifest: newManifest,
...newOutput,
...mergedOutput,
};
// Write manifest
@@ -64,14 +66,7 @@ export async function rebuild(
spinner.clear().stop();
return {
output: {
manifest: newManifest,
steps: [...existingOutput.steps, ...finalOutput.steps],
publicAssets: [
...existingOutput.publicAssets,
...finalOutput.publicAssets,
],
},
output: finalOutput,
manifest: newManifest,
warnings: manifestWarnings,
};
@@ -25,6 +25,8 @@ import { normalizePath } from '../paths';
import glob from 'fast-glob';
import { builtinModules } from '../../../builtin-modules';
import { getEslintVersion } from '../eslint';
import { safeStringToNumber } from '../number';
import { loadEnv } from '../env';
/**
* Given an inline config, discover the config file if necessary, merge the results, resolve any
@@ -72,6 +74,8 @@ export async function resolveConfig(
const mode = mergedConfig.mode ?? COMMAND_MODES[command];
const env: ConfigEnv = { browser, command, manifestVersion, mode };
loadEnv(mode); // Load any environment variables used below
const root = path.resolve(
inlineConfig.root ?? userConfig.root ?? process.cwd(),
);
@@ -86,13 +90,10 @@ export async function resolveConfig(
if (await isDirMissing(entrypointsDir)) {
logMissingDir(logger, 'Entrypoints', entrypointsDir);
}
const filterEntrypoints = !!mergedConfig.filterEntrypoints?.length
const filterEntrypoints = mergedConfig.filterEntrypoints?.length
? new Set(mergedConfig.filterEntrypoints)
: undefined;
const publicDir = path.resolve(srcDir, mergedConfig.publicDir ?? 'public');
if (await isDirMissing(publicDir)) {
logMissingDir(logger, 'Public', publicDir);
}
const typesDir = path.resolve(wxtDir, 'types');
const outBaseDir = path.resolve(root, mergedConfig.outDir ?? '.output');
const outDir = path.resolve(outBaseDir, `${browser}-mv${manifestVersion}`);
@@ -127,6 +128,7 @@ export async function resolveConfig(
devServerConfig = {
port,
hostname: mergedConfig.dev?.server?.hostname ?? 'localhost',
watchDebounce: safeStringToNumber(process.env.WXT_WATCH_DEBOUNCE) ?? 800,
};
}
@@ -268,6 +270,7 @@ function resolveZipConfig(
includeSources: [],
compressionLevel: 9,
...mergedConfig.zip,
exclude: mergedConfig.zip?.exclude ?? [],
excludeSources: [
'**/node_modules',
// WXT files
+1 -1
View File
@@ -14,7 +14,7 @@ export function getEntrypointName(
): string {
const relativePath = path.relative(entrypointsDir, inputPath);
// Grab the string up to the first . or / or \\
const name = relativePath.split(/[\.\/\\]/, 2)[0];
const name = relativePath.split(/[./\\]/, 2)[0];
return name;
}
+10
View File
@@ -0,0 +1,10 @@
import { config } from 'dotenv';
/**
* Load environment files based on the current mode.
*/
export function loadEnv(mode: string) {
return config({
path: [`.env`, `.env.local`, `.env.${mode}`, `.env.${mode}.local`],
});
}
@@ -42,7 +42,7 @@ export function applyGlobals(globals: EnvGlobals): void {
try {
// @ts-expect-error: Untyped key on globalThis
globalThis[key] = envValue;
} catch (err) {
} catch {
// ignore any globals that can't be set
}
});
+2 -1
View File
@@ -5,7 +5,8 @@ export async function getEslintVersion(): Promise<string[]> {
);
const { ESLint } = require('eslint');
return ESLint.version?.split('.') ?? [];
} catch (error) {
} catch {
// Return an empty version when there's an error importing ESLint
return [];
}
}
+4 -4
View File
@@ -272,14 +272,14 @@ function addEntrypoints(
options.browser_style = popup.options.browserStyle;
if (manifest.manifest_version === 3) {
manifest.action = {
...(manifest.action ?? {}),
...manifest.action,
...options,
default_popup,
};
} else {
const key = popup.options.mv2Key ?? 'browser_action';
manifest[key] = {
...(manifest[key] ?? {}),
...manifest[key],
...options,
default_popup,
};
@@ -434,8 +434,8 @@ function discoverIcons(
/^icon@([0-9]+)w\.png$/, // icon@16w.png
/^icon@([0-9]+)h\.png$/, // icon@16h.png
/^icon@([0-9]+)\.png$/, // icon@16.png
/^icons?[\/\\]([0-9]+)\.png$/, // icon/16.png | icons/16.png
/^icons?[\/\\]([0-9]+)x[0-9]+\.png$/, // icon/16x16.png | icons/16x16.png
/^icons?[/\\]([0-9]+)\.png$/, // icon/16.png | icons/16.png
/^icons?[/\\]([0-9]+)x[0-9]+\.png$/, // icon/16x16.png | icons/16x16.png
];
// #endregion snippet
+4
View File
@@ -0,0 +1,4 @@
export function safeStringToNumber(str: string | undefined): number | null {
const num = Number(str);
return isNaN(num) ? null : num;
}
+1
View File
@@ -16,6 +16,7 @@ export async function getPackageJson(): Promise<
} catch (err) {
wxt.logger.debug(
`Failed to read package.json at: ${file}. Returning undefined.`,
err,
);
return {};
}
+13 -2
View File
@@ -1,3 +1,5 @@
import { camelCase } from 'scule';
export function kebabCaseAlphanumeric(str: string): string {
return str
.toLowerCase()
@@ -9,8 +11,17 @@ export function kebabCaseAlphanumeric(str: string): string {
* Return a safe variable name for a given string.
*/
export function safeVarName(str: string): string {
// _ prefix to ensure it doesn't start with a number
return '_' + kebabCaseAlphanumeric(str.trim()).replace('-', '_');
const name = camelCase(kebabCaseAlphanumeric(str));
if (name.match(/^[a-z]/)) return name;
// _ prefix to ensure it doesn't start with a number or other invalid symbol
return '_' + name;
}
/**
* Converts a string to a valid filename (NOT path), stripping out invalid characters.
*/
export function safeFilename(str: string): string {
return kebabCaseAlphanumeric(str);
}
/**
@@ -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(),
@@ -310,6 +311,7 @@ export const fakeResolvedConfig = fakeObjectCreator<ResolvedConfig>(() => {
export const fakeWxt = fakeObjectCreator<Wxt>(() => ({
config: fakeResolvedConfig(),
hook: vi.fn(),
hooks: mock(),
logger: mock(),
reloadConfig: vi.fn(),
+1 -3
View File
@@ -162,9 +162,7 @@ function getSimpleAstJson(ast: any): any {
} else if (typeof ast === 'object') {
return Object.fromEntries(
Object.entries(ast)
.filter(
([key, value]) => key !== 'loc' && key !== 'start' && key !== 'end',
)
.filter(([key]) => key !== 'loc' && key !== 'start' && key !== 'end')
.map(([key, value]) => [key, getSimpleAstJson(value)]),
);
} else {
+1
View File
@@ -40,6 +40,7 @@ export async function registerWxt(
wxt = {
config,
hooks,
hook: hooks.hook.bind(hooks),
get logger() {
return config.logger;
},
+6 -8
View File
@@ -1,7 +1,7 @@
import { InlineConfig } from '../types';
import path from 'node:path';
import fs from 'fs-extra';
import { kebabCaseAlphanumeric } from './utils/strings';
import { safeFilename } from './utils/strings';
import { getPackageJson } from './utils/package';
import { minimatch } from 'minimatch';
import { formatDuration } from './utils/time';
@@ -27,9 +27,7 @@ export async function zip(config?: InlineConfig): Promise<string[]> {
const projectName =
wxt.config.zip.name ??
kebabCaseAlphanumeric(
(await getPackageJson())?.name || path.dirname(process.cwd()),
);
safeFilename((await getPackageJson())?.name || path.dirname(process.cwd()));
const applyTemplate = (template: string): string =>
template
.replaceAll('{{name}}', projectName)
@@ -47,7 +45,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 +99,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 (
+49 -6
View File
@@ -34,17 +34,21 @@ export function defineWxtModule<TOptions extends WxtModuleOptions>(
* the output directory. This will speed up project builds since it just has to
* copy some files instead of bundling them.
*
* To extract entrypoint options from a JS/TS file, use
* `wxt.builder.importEntrypoint` (see example).
*
* @param wxt The wxt instance provided by the module's setup function.
* @param entrypoint The entrypoint to be bundled along with the extension.
*
* @example
* export default defineWxtModule((wxt, options) => {
* export default defineWxtModule(async (wxt, options) => {
* const entrypointPath = "/path/to/my-entrypoint.ts";
* addEntrypoint(wxt, {
* type: "unlisted-page",
* name: "changelog",
* inputPath: "wxt-module-changelog/index.html"
* outputDir: wxt.config.outputDir,
* options: {},
* type: "content-script",
* name: "some-name",
* inputPath: entrypointPath,
* outputDir: wxt.config.outDir,
* options: await wxt.builder.importEntrypoint(entrypointPath),
* });
* });
*/
@@ -172,3 +176,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));
}
+81 -7
View File
@@ -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.
@@ -820,18 +830,55 @@ export type ResolvedPerBrowserOptions<T, TOmitted extends keyof T = never> = {
* Manifest customization available in the `wxt.config.ts` file. You cannot configure entrypoints
* here, they are configured inline.
*/
export type UserManifest = Partial<
Omit<
Manifest.WebExtensionManifest,
export type UserManifest = {
[key in keyof chrome.runtime.ManifestV3 as key extends
| 'action'
| 'background'
| 'chrome_url_overrides'
| 'devtools_page'
| 'manifest_version'
| 'options_page'
| 'options_ui'
| 'permissions'
| 'sandbox'
>
>;
| 'web_accessible_resources'
? never
: key]?: chrome.runtime.ManifestV3[key];
} & {
// Add any Browser-specific or MV2 properties that WXT supports here
action?: chrome.runtime.ManifestV3['action'] & {
browser_style?: boolean;
};
browser_action?: chrome.runtime.ManifestV2['browser_action'] & {
browser_style?: boolean;
};
page_action?: chrome.runtime.ManifestV2['page_action'] & {
browser_style?: boolean;
};
browser_specific_settings?: {
gecko?: {
id?: string;
strict_min_version?: string;
strict_max_version?: string;
update_url?: string;
};
gecko_android?: {
strict_min_version?: string;
strict_max_version?: string;
};
safari?: {
strict_min_version?: string;
strict_max_version?: string;
};
};
permissions?: (
| chrome.runtime.ManifestPermissions
| (string & Record<never, never>)
)[];
web_accessible_resources?:
| string[]
| chrome.runtime.ManifestV3['web_accessible_resources'];
};
export type UserManifestFn = (
env: ConfigEnv,
@@ -920,7 +967,7 @@ export interface ExtensionRunnerConfig {
* }
* }
*/
chromiumPref?: string;
chromiumPref?: Record<string, any>;
/**
* By default, chrome opens a random port for debugging. Set this value to use a specific port.
*/
@@ -1097,6 +1144,9 @@ export interface WxtHooks {
) => HookResult;
/**
* Called once all entrypoints have been loaded from the `entrypointsDir`.
* Use `wxt.builder.importEntrypoint` to load entrypoint options from the
* file, or manually define them.
*
* @param wxt The configured WXT object
* @param entrypoints The list of entrypoints to be built
*/
@@ -1119,6 +1169,10 @@ export interface WxtHooks {
export interface Wxt {
config: ResolvedConfig;
hooks: Hookable<WxtHooks>;
/**
* Alias for `wxt.hooks.hook(...)`.
*/
hook: Hookable<WxtHooks>['hook'];
/**
* Alias for config.logger
*/
@@ -1192,6 +1246,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.
@@ -1222,6 +1277,25 @@ export interface ResolvedConfig {
server?: {
port: number;
hostname: string;
/**
* The milliseconds to debounce when a file is saved before reloading.
* The only way to set this option is to set the `WXT_WATCH_DEBOUNCE`
* environment variable, either globally (like in `.bashrc` file) or
* per-project (in `.env` file).
*
* For example:
* ```
* # ~/.zshrc
* export WXT_WATCH_DEBOUNCE=1000
* ```
* or
* ```
* # .env
* WXT_WATCH_DEBOUNCE=1000
* ```
* @default 800
*/
watchDebounce: number;
};
reloadCommand: string | false;
};
@@ -1364,7 +1438,7 @@ export interface WxtModule<TOptions extends WxtModuleOptions> {
* Alternative to adding hooks in setup function with `wxt.hooks`. Hooks are
* added before the `setup` function is called.
*/
hooks?: Partial<WxtHooks>;
hooks?: NestedHooks<WxtHooks>;
/**
* A custom function that can be used to setup hooks and call module-specific
* APIs.
+1 -1
View File
@@ -6,12 +6,12 @@ export default defineConfig({
test: {
mockReset: true,
restoreMocks: true,
setupFiles: ['vitest.setup.ts'],
testTimeout: 120e3,
coverage: {
include: ['src/**'],
exclude: ['**/dist', '**/__tests__', 'src/utils/testing'],
},
globalSetup: ['./vitest.globalSetup.ts'],
},
server: {
watch: {
+19
View File
@@ -0,0 +1,19 @@
import { exists, rm } from 'fs-extra';
let setupHappened = false;
export async function setup() {
if (setupHappened) {
throw new Error('setup called twice');
}
setupHappened = true;
// @ts-expect-error
globalThis.__ENTRYPOINT__ = 'test';
const e2eDistPath = './e2e/dist/';
if (await exists(e2eDistPath)) {
await rm(e2eDistPath, { recursive: true, force: true });
}
}
-2
View File
@@ -1,2 +0,0 @@
// @ts-expect-error
globalThis.__ENTRYPOINT__ = 'test';
+648 -222
View File
File diff suppressed because it is too large Load Diff
+26
View File
@@ -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() {
+41
View File
@@ -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"
+9
View File
@@ -0,0 +1,9 @@
#!/bin/bash
#
# Generate code in README.md and sync it with wxt/packages/README.md for NPM
#
pnpm dlx automd
echo "<!-- DO NOT EDIT, THIS FILE WAS GENERATED BY '../../scripts/generate-readmes.sh' -->" > packages/wxt/README.md
cat README.md >> packages/wxt/README.md
+1
View File
@@ -3,6 +3,7 @@
"description": "manifest.json description",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "wxt",
"dev:firefox": "wxt -b firefox",