Compare commits
134 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1c3009a48f | |||
| 6c6087249f | |||
| 539d482f71 | |||
| cab97bca1e | |||
| 6020083b89 | |||
| 947b24f1b0 | |||
| 2c5ba64b61 | |||
| faa4fcb718 | |||
| 8e9b67fea2 | |||
| 659bdd46a0 | |||
| edcc411d26 | |||
| e022423bad | |||
| 84e1fd44eb | |||
| b68d1ff828 | |||
| c31a5c30ed | |||
| 5b408c2bc2 | |||
| ca29419db6 | |||
| ab26bdc7c1 | |||
| d270250633 | |||
| d75f64df79 | |||
| 5faa5d7537 | |||
| 177223c625 | |||
| a64ff22917 | |||
| 0bf08329b0 | |||
| e3f00d2797 | |||
| 09de72895e | |||
| c0aa12089e | |||
| 0248f58785 | |||
| 247cd66f74 | |||
| 4a1bd41df9 | |||
| efcb593718 | |||
| a82c7cbf71 | |||
| db60e79a99 | |||
| 722eff145f | |||
| 282458748d | |||
| 26197f11a3 | |||
| 9d560f9798 | |||
| 786292c95e | |||
| e760ee444e | |||
| 78f91fefb6 | |||
| fda1e18f48 | |||
| c94168e2d1 | |||
| 3eb505dbde | |||
| fbe502fddf | |||
| c143a9978d | |||
| e3555b663d | |||
| df934d252c | |||
| 447d011b8f | |||
| abe1263d27 | |||
| 143b5ac81f | |||
| 87fbb1f4c3 | |||
| 62f11bf605 | |||
| 3ead54b58e | |||
| 580f4f3cfe | |||
| da0accebe8 | |||
| eb65d0b42a | |||
| c2784135b0 | |||
| 61d54bd1f9 | |||
| 0bd94fce3b | |||
| 2a97c06443 | |||
| 8213aa776a | |||
| 026f78254d | |||
| 0ebb013ba6 | |||
| c5f78d0c8d | |||
| 8771676e69 | |||
| aebbbb0685 | |||
| 68183b648e | |||
| 45809c0198 | |||
| 13163c9ba1 | |||
| aedbba3f3d | |||
| 763dbd5ee8 | |||
| 5821ae0e8c | |||
| 751706d2ae | |||
| cf8554718d | |||
| 5f653b3ff9 | |||
| 5f2e1c38a7 | |||
| 8a9d8be8a4 | |||
| 067b2eccf3 | |||
| 2e1bd6ec93 | |||
| b463cef1c2 | |||
| adf9d39d69 | |||
| b1699592e4 | |||
| 8675abfced | |||
| a1fc19e47c | |||
| 8235de04d2 | |||
| 46a324adc2 | |||
| c0af62316d | |||
| 2fd3503694 | |||
| e6529e6e41 | |||
| 72673ca366 | |||
| cbda49ba7b | |||
| 0ab32e5afa | |||
| 36cfcd0acb | |||
| f746d46247 | |||
| a2de6cbe44 | |||
| e632a2964a | |||
| f736a14651 | |||
| a8b0d40959 | |||
| 4c5504d139 | |||
| fb86deb388 | |||
| 3a8e6135ac | |||
| a38de0c03f | |||
| ea97410fb4 | |||
| e36549ddce | |||
| 10851ae01c | |||
| d553ef6b29 | |||
| 15bf0da82e | |||
| fc246ffaac | |||
| 9785eff21e | |||
| 52fbd2c99e | |||
| 648ae4fb8d | |||
| bbeeabc9ea | |||
| 35cf6e7bc0 | |||
| 78822acdac | |||
| aea123890f | |||
| c369f4a955 | |||
| 16d8240406 | |||
| 24cd01dd31 | |||
| fb22beadb8 | |||
| fa85177648 | |||
| ed07a49dd2 | |||
| 09c9de0a60 | |||
| 523296b29a | |||
| ef67ef47aa | |||
| cb5eb68d62 | |||
| 2e9ab75e2b | |||
| 0c2d795800 | |||
| 97aa173f7e | |||
| 9eb64d94d6 | |||
| a0971cab52 | |||
| 127562e9d0 | |||
| 038e7450e0 | |||
| 6c00763187 | |||
| 2da0f3d686 |
@@ -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
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: npm
|
||||
directory: '/' # Location of package manifests
|
||||
directories:
|
||||
- /
|
||||
- packages/*
|
||||
schedule:
|
||||
interval: 'monthly'
|
||||
- package-ecosystem: 'github-actions'
|
||||
|
||||
@@ -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/*'
|
||||
@@ -7,11 +7,13 @@ on:
|
||||
default: wxt
|
||||
type: choice
|
||||
options:
|
||||
- wxt
|
||||
- auto-icons
|
||||
- i18n
|
||||
- module-react
|
||||
- module-vue
|
||||
- module-svelte
|
||||
- module-solid
|
||||
- module-svelte
|
||||
- module-vue
|
||||
- wxt
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
|
||||
@@ -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@v5
|
||||
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'
|
||||
@@ -21,3 +21,4 @@ docs/api/reference
|
||||
stats.html
|
||||
.tool-versions
|
||||
.cache
|
||||
*-stats.txt
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -59,13 +70,20 @@ Or see the [installation guide](https://wxt.dev/guide/installation.html) to get
|
||||
- 🦾 Auto-imports
|
||||
- 🤖 Automated publishing
|
||||
- 🎨 Frontend framework agnostic: works with Vue, React, Svelte, etc
|
||||
- 📦 Modular architecture with [WXT modules](https://wxt.dev/guide/go-further/reusable-modules.html#overview)
|
||||
- 🖍️ Quickly bootstrap a new project
|
||||
- 📏 Bundle analysis
|
||||
- ⬇️ Download and bundle remote URL imports
|
||||
- 📦 Modular architecture with [modules](https://wxt.dev/guide/go-further/reusable-modules.html#overview)
|
||||
|
||||
## 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 -->
|
||||
|
||||
@@ -35,6 +35,15 @@ const chromeExtensionIds = [
|
||||
'bcpgdpedphodjcjlminjbdeejccjbimp', // 汇率转换-中文版本
|
||||
'loeilaonggnalkaiiaepbegccilkmjjp', // Currency Converter Plus
|
||||
'npcnninnjghigjfiecefheeibomjpkak', // Respond Easy
|
||||
'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
|
||||
'dbichmdlbjdeplpkhcejgkakobjbjalc', // 社媒助手 - https://github.com/iszhouhua/social-media-copilot
|
||||
'opepfpjeogkbgeigkbepobceinnfmjdd', // Dofollow Links for SEO
|
||||
'pdnenlnelpdomajfejgapbdpmjkfpjkp', // ChatGPT Writer: Use AI on Any Site (GPT-4o, Claude, Gemini, and More)
|
||||
'jobnhifpphkgoelnhnopgkdhbdkiadmj', // discord message translator
|
||||
];
|
||||
|
||||
const { data, err, isLoading } = useListExtensionDetails(chromeExtensionIds);
|
||||
|
||||
@@ -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'),
|
||||
]),
|
||||
@@ -142,12 +143,19 @@ export default defineConfig({
|
||||
menuItem('Custom Events', 'custom-events'),
|
||||
menuItem('Reusable Modules', 'reusable-modules'),
|
||||
menuItem('Remote Code', 'remote-code'),
|
||||
menuItem('Entrypoint Side Effects', 'entrypoint-side-effects'),
|
||||
menuItem('Entrypoint Loaders', 'entrypoint-loaders'),
|
||||
menuItem('How WXT Works', 'how-wxt-works'),
|
||||
]),
|
||||
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/', [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { resolve, join } from 'node:path';
|
||||
import { resolve } from 'node:path';
|
||||
import consola from 'consola';
|
||||
import { execaCommand } from 'execa';
|
||||
import spawn from 'nano-spawn';
|
||||
|
||||
const cliDir = resolve('packages/wxt/src/cli/commands');
|
||||
const cliDirGlob = resolve(cliDir, '**');
|
||||
@@ -37,7 +37,8 @@ export default {
|
||||
};
|
||||
|
||||
async function getHelp(command: string): Promise<string> {
|
||||
const res = await execaCommand(command + ' --help', {
|
||||
const args = command.split(' ');
|
||||
const res = await spawn(args[0], [...args.slice(1), '--help'], {
|
||||
cwd: 'packages/wxt',
|
||||
});
|
||||
return res.stdout;
|
||||
|
||||
|
After Width: | Height: | Size: 136 KiB |
@@ -25,6 +25,10 @@ npx wxt@latest init <project-name>
|
||||
npx wxt@latest init <project-name>
|
||||
```
|
||||
|
||||
### Demo
|
||||
|
||||

|
||||
|
||||
:::
|
||||
|
||||
There are several starting templates available.
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -53,9 +53,9 @@ declare module 'wxt/sandbox' {
|
||||
}
|
||||
|
||||
export default defineAppConfig({
|
||||
bugReportingDisabled: process.env.VITE_BUG_REPORTING_DISABLED === 'true',
|
||||
apiKey: process.env.VITE_API_KEY,
|
||||
bugReportingDisabled: import.meta.env.VITE_BUG_REPORTING_DISABLED === 'true',
|
||||
apiKey: import.meta.env.VITE_API_KEY,
|
||||
});
|
||||
```
|
||||
|
||||
> You don't have to do this, you can use `process.env.VITE_*` anywhere in your runtime code, but putting them here consolidates them to one place and defines what variables are expected.
|
||||
> You don't have to do this, you can use `import.meta.env.VITE_*` anywhere in your runtime code, but putting them here consolidates them to one place and defines what variables are expected.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
@@ -1,6 +1,6 @@
|
||||
# Entrypoint Loaders
|
||||
|
||||
Because entrypoint options, like content script matches, are listed in the entrypoint's JS file, WXT has to import them during the build process to use those options when generating the manifest.
|
||||
Because entrypoint options, like content script `matches`, are listed in the entrypoint's JS file, WXT has to import them during the build process to use those options when generating the manifest.
|
||||
|
||||
There are two options for loading your entrypoints:
|
||||
|
||||
@@ -11,7 +11,50 @@ There are two options for loading your entrypoints:
|
||||
|
||||
Since 0.19.0, WXT uses `vite-node`, the same tool that powers Vitest and Nuxt, to import your entrypoint files.
|
||||
|
||||
There isn't really anything to add here... By default, it should "just work".
|
||||
If you use any runtime packages that depend on `webextension-polyfill`, you need to add them to [Vite's `ssr.noExternal` option](https://vitejs.dev/config/ssr-options#ssr-noexternal):
|
||||
|
||||
```ts
|
||||
export default defineConfig({
|
||||
vite: () => ({
|
||||
ssr: {
|
||||
noExternal: ['@webext-core/messaging', '@webext-core/proxy-service'],
|
||||
},
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
:::details Why?
|
||||
This tells Vite it needs process these module's, letting WXT properly disable the polyfill in the NodeJS environment so it doesn't cause any build errors like this:
|
||||
|
||||
```
|
||||
ERROR This script should only be loaded in a browser extension
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
To get a list of installed packages that use on `webextension-polyfill`, run your package manager's `list` command. Here's an example with PNPM:
|
||||
|
||||
```sh
|
||||
$ pnpm why webextension-polyfill
|
||||
|
||||
dependencies:
|
||||
@webext-core/messaging 1.4.0
|
||||
└── webextension-polyfill 0.10.0
|
||||
@webext-core/proxy-service 1.2.0
|
||||
├─┬ @webext-core/messaging 1.4.0 peer
|
||||
│ └── webextension-polyfill 0.10.0
|
||||
└── webextension-polyfill 0.12.0 peer
|
||||
|
||||
devDependencies:
|
||||
@wxt-dev/module-vue 1.0.0
|
||||
└─┬ wxt 0.19.0-alpha1 peer
|
||||
└── webextension-polyfill 0.12.0
|
||||
webextension-polyfill 0.12.0
|
||||
wxt 0.19.0-alpha1
|
||||
└── webextension-polyfill 0.12.0
|
||||
```
|
||||
|
||||
Ignoring WXT itself (it's added automatically for you), there are three packages that depend on the polyfill: `@wxt-dev/module-vue`, `@webext-core/messaging`, and `@webext-core/proxy-service`. Since the vue module is a build dependency, with no runtime code, you don't have to add it. That means for this case, you need to add `@webext-core/messaging`, and `@webext-core/proxy-service`, as shown in the original code snippet.
|
||||
|
||||
## jiti
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -25,12 +25,12 @@ There are two ways to add a module to your project:
|
||||
});
|
||||
```
|
||||
|
||||
2. **NPM package**: By convention, any package on NPM that starts with `@wxt-dev/module-*` or `wxt-module-*` can be installed and included in your project:
|
||||
2. **NPM package**: Find WXT modules on NPM and include them in your project:
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
export default defineConfig({
|
||||
// Add the module to your project
|
||||
modules: ['wxt-module-example'],
|
||||
modules: ['@wxt-dev/auto-icons'],
|
||||
});
|
||||
```
|
||||
|
||||
@@ -99,9 +99,9 @@ export default defineConfig({
|
||||
|
||||
### Actually Doing Something
|
||||
|
||||
The first argument of the setup function, `wxt`, provides full access to the current build's context. You can access the resolved configuration via `wxt.config`, or setup hooks to manipulate the build at different steps of the build process.
|
||||
The first argument of the setup function, `wxt`, provides full access to the current build's context. You can access the resolved configuration via `wxt.config`, or setup hooks to manipulate the build at different steps of the build process with `wxt.hooks`.
|
||||
|
||||
Here's an example that shows you how to update the `outDir` based on the build mode. It does a good job showing how to access config, set up a hook, and do something when the hook is called.
|
||||
Here's an example that updates the `outDir` based on the build mode. It's a very simple example of how to access config and setup a hook.
|
||||
|
||||
```ts
|
||||
export default defineWxtModule((wxt) => {
|
||||
@@ -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: {},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
# Build Integrations
|
||||
|
||||
To use the custom messages file format, you'll need to use `@wxt-dev/i18n/build` to transform the custom format to the one expected by browsers.
|
||||
|
||||
Here's a list of build tools that already have an integration:
|
||||
|
||||
[[toc]]
|
||||
|
||||
:::info
|
||||
If you want to contribute, please do! In particular, an `unplugin` integration would be awesome!
|
||||
:::
|
||||
|
||||
## WXT
|
||||
|
||||
See [Installation with WXT](./installation#with-wxt).
|
||||
|
||||
But TLDR, all you need is:
|
||||
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
export default defineConfig({
|
||||
modules: ['@wxt-dev/i18n/module'],
|
||||
});
|
||||
```
|
||||
|
||||
Types are generated whenever you run `wxt prepare` or another build command:
|
||||
|
||||
```sh
|
||||
wxt prepare
|
||||
wxt
|
||||
wxt build
|
||||
wxt zip
|
||||
# etc
|
||||
```
|
||||
|
||||
## Custom
|
||||
|
||||
If you're not using WXT, you'll have to pre-process the localization files yourself. Here's a basic script to generate the `_locales/.../messages.json` and `wxt-i18n-structure.d.ts` files:
|
||||
|
||||
```ts
|
||||
// build-i18n.js
|
||||
import {
|
||||
parseMessagesFile,
|
||||
generateChromeMessagesFile,
|
||||
generateTypeFile,
|
||||
} from '@wxt-dev/i18n/build';
|
||||
|
||||
// Read your localization files
|
||||
const messages = {
|
||||
en: await parseMessagesFile('path/locales/en.yml'),
|
||||
de: await parseMessagesFile('path/locales/de.yml'),
|
||||
// ...
|
||||
};
|
||||
|
||||
// Generate JSON files for the browser
|
||||
await generateChromeMessagesFile('dist/_locales/en/messages.json', messages.en);
|
||||
await generateChromeMessagesFile('dist/_locales/de/messages.json', messages.de);
|
||||
// ...
|
||||
|
||||
// Generate a types file based on your default_locale
|
||||
await generateTypeFile('wxt-i18n-structure.d.ts', messages.en);
|
||||
```
|
||||
|
||||
Then run the script:
|
||||
|
||||
```sh
|
||||
node generate-i18n.js
|
||||
```
|
||||
|
||||
If your build tool has a dev mode, you'll also want to rerun the script whenever you change a localization file.
|
||||
|
||||
### Type Safety
|
||||
|
||||
Once you've generated `wxt-i18n-structure.d.ts` (see the [Custom](#custom) section), you can use it to pass the generated type into `createI18n`:
|
||||
|
||||
```ts
|
||||
import type { WxtI18nStructure } from './wxt-i18n-structure';
|
||||
|
||||
export const i18n = createI18n<WxtI18nStructure>();
|
||||
```
|
||||
|
||||
And just like that, you have type safety!
|
||||
@@ -0,0 +1,48 @@
|
||||
# Editor Support
|
||||
|
||||
For better DX, you can configure your editor with plugins and extensions.
|
||||
|
||||
[[toc]]
|
||||
|
||||
## VS Code
|
||||
|
||||
Install the [I18n Ally Extension](https://marketplace.visualstudio.com/items?itemName=lokalise.i18n-ally) to:
|
||||
|
||||
- Go to translation definition
|
||||
- Inline previews of text
|
||||
- Hover to see other translations
|
||||
|
||||
You'll need to configure it the extension so it knows where your localization files are and what function represents getting a translation:
|
||||
|
||||
`.vscode/i18n-ally-custom-framework.yml`:
|
||||
|
||||
```yml
|
||||
# An array of strings which contain Language Ids defined by VS Code
|
||||
# You can check available language ids here: https://code.visualstudio.com/docs/languages/identifiers
|
||||
languageIds:
|
||||
- typescript
|
||||
|
||||
# Look for t("...")
|
||||
usageMatchRegex:
|
||||
- "[^\\w\\d]t\\(['\"`]({key})['\"`]"
|
||||
|
||||
# Disable other built-in i18n ally frameworks
|
||||
monopoly: true
|
||||
```
|
||||
|
||||
`.vscode/settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"i18n-ally.localesPaths": ["src/locales"],
|
||||
"i18n-ally.keystyle": "nested"
|
||||
}
|
||||
```
|
||||
|
||||
## Zed
|
||||
|
||||
As of time of writing, Aug 18, 2024, no extensions exist for Zed to add I18n support.
|
||||
|
||||
## IntelliJ
|
||||
|
||||
Unknown - Someone who uses IntelliJ will have to open a PR for this!
|
||||
@@ -0,0 +1,81 @@
|
||||
# Installation
|
||||
|
||||
[[toc]]
|
||||
|
||||
### With WXT
|
||||
|
||||
1. Install `@wxt-dev/i18n` via your package manager:
|
||||
|
||||
```sh
|
||||
pnpm i @wxt-dev/i18n
|
||||
```
|
||||
|
||||
2. Add the WXT module to your `wxt.config.ts` file and setup a default locale:
|
||||
|
||||
```ts
|
||||
export default defineConfig({
|
||||
modules: ['@wxt-dev/i18n/module'],
|
||||
manifest: {
|
||||
default_locale: 'en',
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
3. Create a localization file at `<srcDir>/locales/<default_locale>.yml` or move your existing localization files there.
|
||||
|
||||
```yml
|
||||
# <srcDir>/locales/en.yml
|
||||
helloWorld: Hello world!
|
||||
```
|
||||
|
||||
:::tip
|
||||
`@wxt-dev/i18n` supports the standard messages format, so if you already have localization files at `<srcDir>/public/_locale/<lang>/messages.json`, you don't need to convert them to YAML or refactor them - just move them to `<srcDir>/locales/<lang>.json` and they'll just work out of the box!
|
||||
:::
|
||||
|
||||
4. To get a translation, use the auto-imported `i18n` object or import it manually:
|
||||
|
||||
```ts
|
||||
import { i18n } from '#i18n';
|
||||
|
||||
i18n.t('helloWorld'); // "Hello world!"
|
||||
```
|
||||
|
||||
And you're done! Using WXT, you get type-safety out of the box.
|
||||
|
||||
### Without WXT
|
||||
|
||||
1. Install `@wxt-dev/i18n` via your package manager:
|
||||
|
||||
```sh
|
||||
pnpm i @wxt-dev/i18n
|
||||
```
|
||||
|
||||
2. Create a messages file at `_locales/<lang>/messages.json` or move your existing translations there:
|
||||
|
||||
```json
|
||||
{
|
||||
"helloWorld": {
|
||||
"message": "Hello world!"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
:::info
|
||||
For the best DX, you should to integrate `@wxt-dev/i18n` into your build process. This enables:
|
||||
|
||||
1. Plural forms
|
||||
2. Simple messages file format
|
||||
3. Type safety
|
||||
|
||||
See [Build Integrations](./build-integrations) to set it up.
|
||||
:::
|
||||
|
||||
3. Create the `i18n` object, export it, and use it wherever you want!
|
||||
|
||||
```ts
|
||||
import { createI18n } from '@wxt-dev/i18n';
|
||||
|
||||
export const i18n = createI18n();
|
||||
|
||||
i18n.t('helloWorld'); // "Hello world!";
|
||||
```
|
||||
@@ -0,0 +1,22 @@
|
||||
# Introduction
|
||||
|
||||
:::info
|
||||
You don't have to use `wxt` to use this package - it will work in any bundler setup. See [Installation without WXT](./installation#without-wxt) for more details.
|
||||
:::
|
||||
|
||||
`@wxt-dev/i18n` is a simple, type-safe wrapper around the `browser.i18n` APIs. It provides several benefits over the standard API:
|
||||
|
||||
1. Simple messages file format
|
||||
2. Plural form support
|
||||
3. Integrates with the [I18n Ally VS Code extension](./editor-support#vscode)
|
||||
|
||||
It also provides several benefits over other non-web extension specific i18n packages:
|
||||
|
||||
1. Does not bundle localization files into every entrypoint
|
||||
2. Don't need to fetch the localization files asynchronously
|
||||
3. Can localize extension name in manifest
|
||||
4. Can access localized strings inside CSS files
|
||||
|
||||
However, it does have one major downside:
|
||||
|
||||
1. Like the `browser.i18n` API, to change the language, users must change the browser's language
|
||||
@@ -0,0 +1,170 @@
|
||||
# Messages File Format
|
||||
|
||||
You can only use the file format discussed on this page if you have [integrated `@wxt-dev/i18n` into your build process](./build-integrations). If you have not integrated it into your build process, you must use JSON files in the `_locales` directory, like a normal web extension.
|
||||
|
||||
[[toc]]
|
||||
|
||||
## File Extensions
|
||||
|
||||
You can define your messages in several different file types:
|
||||
|
||||
- `.yml`
|
||||
- `.yaml`
|
||||
- `.json`
|
||||
- `.jsonc`
|
||||
- `.json5`
|
||||
- `.toml`
|
||||
|
||||
## Nested Keys
|
||||
|
||||
You can have translations at the top level or nest them into groups:
|
||||
|
||||
```yml
|
||||
ok: OK
|
||||
cancel: Cancel
|
||||
welcome:
|
||||
title: Welcome to XYZ
|
||||
dialogs:
|
||||
confirmation:
|
||||
title: 'Are you sure?'
|
||||
```
|
||||
|
||||
To access a nested key, use `.`:
|
||||
|
||||
```ts
|
||||
i18n.t('ok'); // "OK"
|
||||
i18n.t('cancel'); // "Cancel"
|
||||
i18n.t('welcome.title'); // "Welcome to XYZ"
|
||||
i18n.t('dialogs.confirmation.title'); // "Are you sure?"
|
||||
```
|
||||
|
||||
## Substitutions
|
||||
|
||||
Because `@wxt-dev/i18n` is based on `browser.i18n`, you define substitutions the same way, with `$1`-`$9`:
|
||||
|
||||
```yml
|
||||
hello: Hello $1!
|
||||
order: Thanks for ordering your $1
|
||||
```
|
||||
|
||||
### Escapting `$`
|
||||
|
||||
To escape the dollar sign, put another `$` in front of it:
|
||||
|
||||
```yml
|
||||
dollars: $$$1
|
||||
```
|
||||
|
||||
```ts
|
||||
i18n.t('dollars', ['1.00']); // "$1.00"
|
||||
```
|
||||
|
||||
## Plural Forms
|
||||
|
||||
:::warning
|
||||
Plural support languages like Arabic, that have different forms for "few" or "many", is not supported right now. Feel free to open a PR if you are interested in this!
|
||||
:::
|
||||
|
||||
To get a different translation based on a count:
|
||||
|
||||
```yml
|
||||
items:
|
||||
1: 1 item
|
||||
n: $1 items
|
||||
```
|
||||
|
||||
Then you pass in the count as the second argument to `i18n.t`:
|
||||
|
||||
```ts
|
||||
i18n.t('items', 0); // "0 items"
|
||||
i18n.t('items', 1); // "1 item"
|
||||
i18n.t('items', 2); // "2 items"
|
||||
```
|
||||
|
||||
To add a custom translation for 0 items:
|
||||
|
||||
```yml
|
||||
items:
|
||||
0: No items
|
||||
1: 1 item
|
||||
n: $1 items
|
||||
```
|
||||
|
||||
```ts
|
||||
i18n.t('items', 0); // "No items"
|
||||
i18n.t('items', 1); // "1 item"
|
||||
i18n.t('items', 2); // "2 items"
|
||||
```
|
||||
|
||||
If you need to pass a custom substitution for `$1` instead of the count, just add the substitution:
|
||||
|
||||
```yml
|
||||
items:
|
||||
0: No items
|
||||
1: $1 item
|
||||
n: $1 items
|
||||
```
|
||||
|
||||
```ts
|
||||
i18n.t('items', 0, ['Zero']); // "No items"
|
||||
i18n.t('items', 1, ['One']); // "One item"
|
||||
i18n.t('items', 2, ['Multiple']); // "Multiple items"
|
||||
```
|
||||
|
||||
## Verbose Keys
|
||||
|
||||
`@wxt-dev/i18n` is compatible with the message format used by [`browser.i18n`](https://developer.chrome.com/docs/extensions/reference/api/i18n).
|
||||
|
||||
:::info
|
||||
This means if you're migrating to `@wxt-dev/i18n` and you're already using the verbose format, you don't have to change anything!
|
||||
:::
|
||||
|
||||
A key is treated as "verbose" when it is:
|
||||
|
||||
1. At the top level (not nested)
|
||||
2. Only contains the following properties: `message`, `description` and/or `placeholder`
|
||||
|
||||
:::code-group
|
||||
|
||||
```json [JSON]
|
||||
{
|
||||
"appName": {
|
||||
"message": "GitHub - Better Line Counts",
|
||||
"description": "The app's name, should not be translated",
|
||||
},
|
||||
"ok": "OK",
|
||||
"deleteConfirmation": {
|
||||
"title": "Delete XYZ?"
|
||||
"message": "You cannot undo this action once taken."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```yml [YAML]
|
||||
appName:
|
||||
message: GitHub - Better Line Counts
|
||||
description: The app's name, should not be translated
|
||||
ok: OK
|
||||
deleteConfirmation:
|
||||
title: Delete XYZ?
|
||||
message: You cannot undo this action once taken.
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
In this example, only `appName` is considered verbose. `deleteConfirmation` is not verbose because it contains the additional property `title`.
|
||||
|
||||
```ts
|
||||
i18n.t('appName'); // ✅ "GitHub - Better Line Counts"
|
||||
i18n.t('appName.message'); // ❌
|
||||
i18n.t('ok'); // ✅ "OK"
|
||||
i18n.t('deleteConfirmation'); // ❌
|
||||
i18n.t('deleteConfirmation.title'); // ✅ "Delete XYZ?"
|
||||
i18n.t('deleteConfirmation.message'); // ✅ "You cannot undo this action once taken."
|
||||
```
|
||||
|
||||
If this is confusing, don't worry! With type-safety, you'll get a type error if you do it wrong. If type-safety is disabled, you'll get a runtime warning in the devtools console.
|
||||
|
||||
:::warning
|
||||
Using the verbose format is not recommended. I have yet to see a translation service and software that supports this format out of the box. Stick with the simple format when you can.
|
||||
:::
|
||||
@@ -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`).
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -10,41 +10,60 @@ pnpm i wxt@latest
|
||||
|
||||
If there was a major version change, follow the steps below to fix breaking changes.
|
||||
|
||||
## v0.18.0 → v0.19.0
|
||||
## v0.18.5 → v0.19.0
|
||||
|
||||
### `vite-node` Entrypoint Loader
|
||||
|
||||
The default entrypoint loader has changed to `vite-node`. This change enables:
|
||||
The default entrypoint loader has changed to `vite-node`. If you use any NPM packages that depend on the `webextension-polyfill`, you need to add them to Vite's `ssr.noExternal` option:
|
||||
|
||||
- Importing variables and using them in the entrypoint options.
|
||||
<!-- prettier-ignore -->
|
||||
```ts
|
||||
export default defineConfig({
|
||||
vite: () => ({ // [!code ++]
|
||||
ssr: { // [!code ++]
|
||||
noExternal: ['@webext-core/messaging', '@webext-core/proxy-service'], // [!code ++]
|
||||
}, // [!code ++]
|
||||
}), // [!code ++]
|
||||
});
|
||||
```
|
||||
|
||||
```ts
|
||||
// entrypoints/content.ts
|
||||
import { GOOGLE_MATCHES } from '~/utils/constants'
|
||||
> [Read the full docs](/guide/go-further/entrypoint-loaders#vite-node) for more information.
|
||||
|
||||
export default defineContentScript({
|
||||
matches: [GOOGLE_MATCHES],
|
||||
main: () => ...,
|
||||
})
|
||||
```
|
||||
:::details This change enables:
|
||||
|
||||
- Use Vite-specific APIs like `import.meta.glob` to define entrypoint options.
|
||||
Importing variables and using them in the entrypoint options:
|
||||
|
||||
```ts
|
||||
// entrypoints/content.ts
|
||||
const providers: Record<string, any> = import.meta.glob('../providers/*', {
|
||||
eager: true,
|
||||
});
|
||||
```ts
|
||||
// entrypoints/content.ts
|
||||
import { GOOGLE_MATCHES } from '~/utils/constants'
|
||||
|
||||
export default defineContentScript({
|
||||
matches: Object.values(providers).flatMap(
|
||||
(provider) => provider.default.paths,
|
||||
),
|
||||
async main() {
|
||||
console.log('Hello content.');
|
||||
},
|
||||
});
|
||||
```
|
||||
export default defineContentScript({
|
||||
matches: [GOOGLE_MATCHES],
|
||||
main: () => ...,
|
||||
})
|
||||
```
|
||||
|
||||
Using Vite-specific APIs like `import.meta.glob` to define entrypoint options:
|
||||
|
||||
```ts
|
||||
// entrypoints/content.ts
|
||||
const providers: Record<string, any> = import.meta.glob('../providers/*', {
|
||||
eager: true,
|
||||
});
|
||||
|
||||
export default defineContentScript({
|
||||
matches: Object.values(providers).flatMap(
|
||||
(provider) => provider.default.paths,
|
||||
),
|
||||
async main() {
|
||||
console.log('Hello content.');
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Basically, you can now import and do things outside the `main` function of the entrypoint - you could not do that before. Still though, be careful. It is recommended to avoid running code outside the `main` function to keep your builds fast.
|
||||
|
||||
:::
|
||||
|
||||
To continue using the old approach, add the following to your `wxt.config.ts` file:
|
||||
|
||||
@@ -67,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 → 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 → v0.18.0
|
||||
|
||||
### Automatic MV3 `host_permissions` to MV2 `permissions`
|
||||
|
||||
@@ -53,6 +53,11 @@ features:
|
||||
details: Works with any front-end framework with a Vite plugin.
|
||||
link: /guide/key-concepts/frontend-frameworks
|
||||
linkText: Add a framework
|
||||
- icon: 📦
|
||||
title: Modular Architecture
|
||||
details: Reuse build-time and runtime-code across multiple extensions.
|
||||
link: /guide/go-further/reusable-modules
|
||||
linkText: Read docs
|
||||
- icon: 🖍️
|
||||
title: Bootstrap a New Project
|
||||
details: Get started quickly with several awesome project templates.
|
||||
|
||||
@@ -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
|
||||
@@ -2,10 +2,9 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=18.20.3",
|
||||
"pnpm": "^8.6.3"
|
||||
"node": ">=18.20.3"
|
||||
},
|
||||
"packageManager": "pnpm@8.15.9",
|
||||
"packageManager": "pnpm@9.12.0",
|
||||
"scripts": {
|
||||
"check": "check && pnpm -r --sequential run check",
|
||||
"test": "pnpm -r --sequential run test run",
|
||||
@@ -18,18 +17,18 @@
|
||||
"docs:preview": "pnpm -s docs:gen && vitepress preview docs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/buildc": "^1.0.11",
|
||||
"@aklinker1/check": "^1.3.1",
|
||||
"@aklinker1/buildc": "^1.1.4",
|
||||
"@aklinker1/check": "^1.4.5",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@vitest/coverage-v8": "^2.0.0",
|
||||
"changelogen": "^0.5.5",
|
||||
"@vitest/coverage-v8": "^2.1.2",
|
||||
"changelogen": "^0.5.7",
|
||||
"consola": "^3.2.3",
|
||||
"dependency-graph": "^1.0.0",
|
||||
"execa": "^9.3.0",
|
||||
"fast-glob": "^3.3.2",
|
||||
"fs-extra": "^11.2.0",
|
||||
"hasha": "^6.0.0",
|
||||
"lint-staged": "^15.2.7",
|
||||
"lint-staged": "^15.2.10",
|
||||
"nano-spawn": "^0.1.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^3.3.3",
|
||||
"simple-git-hooks": "^2.11.1",
|
||||
@@ -37,12 +36,12 @@
|
||||
"typedoc": "^0.25.4",
|
||||
"typedoc-plugin-markdown": "4.0.0-next.23",
|
||||
"typedoc-vitepress-theme": "1.0.0-next.3",
|
||||
"typescript": "^5.5.3",
|
||||
"vitepress": "^1.3.1",
|
||||
"vitest-mock-extended": "^1.3.2",
|
||||
"vue": "^3.4.33",
|
||||
"typescript": "^5.6.2",
|
||||
"vitepress": "^1.3.4",
|
||||
"vitest-mock-extended": "^2.0.2",
|
||||
"vue": "^3.5.11",
|
||||
"wxt": "workspace:*",
|
||||
"yaml": "^2.4.5"
|
||||
"yaml": "^2.5.1"
|
||||
},
|
||||
"simple-git-hooks": {
|
||||
"pre-commit": "pnpm lint-staged"
|
||||
@@ -57,10 +56,5 @@
|
||||
"search-insights"
|
||||
]
|
||||
}
|
||||
},
|
||||
"changelog": {
|
||||
"excludeAuthors": [
|
||||
"aaronklinker1@gmail.com"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# Changelog
|
||||
|
||||
## v1.0.2
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/auto-icons-v1.0.1...auto-icons-v1.0.2)
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- **auto-icons:** Fix configuration example typo ([#905](https://github.com/wxt-dev/wxt/pull/905))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Add more metadata for npm ([#885](https://github.com/wxt-dev/wxt/pull/885))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Uncenter ([@uncenter](http://github.com/uncenter))
|
||||
- Florian Metz ([@Timeraa](http://github.com/Timeraa))
|
||||
|
||||
## v1.0.1
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/auto-icons-v1.0.0...auto-icons-v1.0.1)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- **auto-icons:** Path option ([#880](https://github.com/wxt-dev/wxt/pull/880))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- **deps:** Upgrade all dependencies ([#869](https://github.com/wxt-dev/wxt/pull/869))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Florian Metz ([@Timeraa](http://github.com/Timeraa))
|
||||
@@ -0,0 +1,42 @@
|
||||
# WXT Auto Icons
|
||||
|
||||
## Features
|
||||
|
||||
- Generate extension icons with the correct sizes
|
||||
- Make the icon greyscale during development
|
||||
|
||||
## Usage
|
||||
|
||||
Install the package:
|
||||
|
||||
```sh
|
||||
npm i --save-dev @wxt-dev/auto-icons
|
||||
pnpm i -D @wxt-dev/auto-icons
|
||||
yarn add --dev @wxt-dev/auto-icons
|
||||
bun i -D @wxt-dev/auto-icons
|
||||
```
|
||||
|
||||
Add the module to `wxt.config.ts`:
|
||||
|
||||
```ts
|
||||
export default defineConfig({
|
||||
modules: ['@wxt-dev/auto-icons'],
|
||||
});
|
||||
```
|
||||
|
||||
And finally, save the base icon to `<srcDir>/assets/icon.png`.
|
||||
|
||||
## Configuration
|
||||
|
||||
The module can be configured via the `autoIcons` config:
|
||||
|
||||
```ts
|
||||
export default defineConfig({
|
||||
modules: ['@wxt-dev/auto-icons'],
|
||||
autoIcons: {
|
||||
// ...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Options have JSDocs available in your editor, or you can read them in the source code: [`AutoIconsOptions`](./src/index.ts).
|
||||
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"name": "@wxt-dev/auto-icons",
|
||||
"description": "WXT module for automatically generating extension icons in different sizes",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/wxt-dev/wxt.git",
|
||||
"directory": "packages/auto-icons"
|
||||
},
|
||||
"homepage": "https://github.com/wxt-dev/wxt/blob/main/packages/auto-icons/README.md",
|
||||
"keywords": [
|
||||
"wxt",
|
||||
"module",
|
||||
"icons",
|
||||
"manifest"
|
||||
],
|
||||
"author": {
|
||||
"name": "Florian Metz",
|
||||
"email": "me@timeraa.dev"
|
||||
},
|
||||
"contributors": [
|
||||
{
|
||||
"name": "Aaron Klinker",
|
||||
"email": "aaronklinker1+wxt@gmail.com"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"version": "1.0.2",
|
||||
"type": "module",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.mts",
|
||||
"default": "./dist/index.mjs"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "buildc -- unbuild",
|
||||
"check": "pnpm build && check"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"wxt": ">=0.19.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "^1.4.5",
|
||||
"oxlint": "^0.9.9",
|
||||
"publint": "^0.2.11",
|
||||
"typescript": "^5.6.2",
|
||||
"unbuild": "^2.0.0",
|
||||
"wxt": "workspace:*"
|
||||
},
|
||||
"dependencies": {
|
||||
"defu": "^6.1.4",
|
||||
"fs-extra": "^11.2.0",
|
||||
"sharp": "^0.33.5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import 'wxt';
|
||||
import { defineWxtModule } from 'wxt/modules';
|
||||
import { resolve, relative } from 'node:path';
|
||||
import defu from 'defu';
|
||||
import sharp from 'sharp';
|
||||
import { ensureDir, exists } from 'fs-extra';
|
||||
|
||||
export default defineWxtModule<AutoIconsOptions>({
|
||||
name: '@wxt-dev/auto-icons',
|
||||
configKey: 'autoIcons',
|
||||
async setup(wxt, options) {
|
||||
const parsedOptions = defu<Required<AutoIconsOptions>, AutoIconsOptions[]>(
|
||||
options,
|
||||
{
|
||||
enabled: true,
|
||||
baseIconPath: resolve(wxt.config.srcDir, 'assets/icon.png'),
|
||||
grayscaleOnDevelopment: true,
|
||||
sizes: [128, 48, 32, 16],
|
||||
},
|
||||
);
|
||||
|
||||
const resolvedPath = resolve(wxt.config.srcDir, parsedOptions.baseIconPath);
|
||||
|
||||
if (!parsedOptions.enabled)
|
||||
return wxt.logger.warn(`\`[auto-icons]\` ${this.name} disabled`);
|
||||
|
||||
if (!(await exists(resolvedPath))) {
|
||||
return wxt.logger.warn(
|
||||
`\`[auto-icons]\` Skipping icon generation, no base icon found at ${relative(process.cwd(), resolvedPath)}`,
|
||||
);
|
||||
}
|
||||
|
||||
wxt.hooks.hook('build:manifestGenerated', async (wxt, manifest) => {
|
||||
if (manifest.icons)
|
||||
return wxt.logger.warn(
|
||||
'`[auto-icons]` icons property found in manifest, overwriting with auto-generated icons',
|
||||
);
|
||||
|
||||
manifest.icons = Object.fromEntries(
|
||||
parsedOptions.sizes.map((size) => [size, `icons/${size}.png`]),
|
||||
);
|
||||
});
|
||||
|
||||
wxt.hooks.hook('build:done', async (wxt, output) => {
|
||||
const image = sharp(resolvedPath).png();
|
||||
|
||||
if (
|
||||
wxt.config.mode === 'development' &&
|
||||
parsedOptions.grayscaleOnDevelopment
|
||||
) {
|
||||
image.grayscale();
|
||||
}
|
||||
|
||||
const outputFolder = wxt.config.outDir;
|
||||
|
||||
for (const size of parsedOptions.sizes) {
|
||||
const resized = image.resize(size);
|
||||
ensureDir(resolve(outputFolder, 'icons'));
|
||||
await resized.toFile(resolve(outputFolder, `icons/${size}.png`));
|
||||
|
||||
output.publicAssets.push({
|
||||
type: 'asset',
|
||||
fileName: `icons/${size}.png`,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
wxt.hooks.hook('prepare:publicPaths', (wxt, paths) => {
|
||||
for (const size of parsedOptions.sizes) {
|
||||
paths.push(`icons/${size}.png`);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Options for the auto-icons module
|
||||
*/
|
||||
export interface AutoIconsOptions {
|
||||
/**
|
||||
* Enable auto-icons generation
|
||||
* @default true
|
||||
*/
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* Path to the image to use.
|
||||
*
|
||||
* Path is relative to the project's src directory.
|
||||
* @default "<srcDir>/assets/icon.png"
|
||||
*/
|
||||
baseIconPath?: string;
|
||||
/**
|
||||
* Grayscale the image when in development mode to indicate development
|
||||
* @default true
|
||||
*/
|
||||
grayscaleOnDevelopment?: boolean;
|
||||
/**
|
||||
* Sizes to generate icons for
|
||||
* @default [128, 48, 32, 16]
|
||||
*/
|
||||
sizes?: number[];
|
||||
}
|
||||
|
||||
declare module 'wxt' {
|
||||
export interface InlineConfig {
|
||||
autoIcons?: AutoIconsOptions;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"exclude": ["node_modules/**", "dist/**"]
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
# Changelog
|
||||
|
||||
## v0.2.0
|
||||
|
||||
[⚠️ breaking changes](https://wxt.dev/guide/upgrade-guide/wxt) • [compare changes](https://github.com/wxt-dev/wxt/compare/i18n-v0.1.1...i18n-v0.2.0)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- ⚠️ Remove invalid options argument ([#1048](https://github.com/wxt-dev/wxt/pull/1048))
|
||||
|
||||
#### ⚠️ Breaking Changes
|
||||
|
||||
- ⚠️ Remove invalid options argument ([#1048](https://github.com/wxt-dev/wxt/pull/1048))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron ([@aklinker1](http://github.com/aklinker1))
|
||||
|
||||
## v0.1.1
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/i18n-v0.1.0...i18n-v0.1.1)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Friendly error messages for `null` and `undefined` values inside message files ([#1041](https://github.com/wxt-dev/wxt/pull/1041))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Add `oxlint` for linting ([#947](https://github.com/wxt-dev/wxt/pull/947))
|
||||
- Upgrade all non-major dependencies ([#1040](https://github.com/wxt-dev/wxt/pull/1040))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Windmillcode0 <shieldmousetower734@gmail.com>
|
||||
- Aaron ([@aklinker1](http://github.com/aklinker1))
|
||||
@@ -0,0 +1,3 @@
|
||||
# WXT I18n
|
||||
|
||||
[Read the docs](https://wxt.dev/guide/i18n/installation) to get started.
|
||||
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"name": "@wxt-dev/i18n",
|
||||
"description": "Type-safe wrapper around browser.i18n.getMessage with additional features",
|
||||
"version": "0.2.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.16.10",
|
||||
"oxlint": "^0.9.9",
|
||||
"publint": "^0.2.11",
|
||||
"typescript": "^5.6.2",
|
||||
"unbuild": "^2.0.0",
|
||||
"vitest": "^2.0.0",
|
||||
"vitest-plugin-random-seed": "^1.1.0",
|
||||
"wxt": "workspace:*"
|
||||
},
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": {
|
||||
"types": "./dist/index.d.mts",
|
||||
"default": "./dist/index.mjs"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/index.d.cts",
|
||||
"default": "./dist/index.cjs"
|
||||
}
|
||||
},
|
||||
"./build": {
|
||||
"import": {
|
||||
"types": "./dist/build.d.mts",
|
||||
"default": "./dist/build.mjs"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/build.d.cts",
|
||||
"default": "./dist/build.cjs"
|
||||
}
|
||||
},
|
||||
"./module": {
|
||||
"import": {
|
||||
"types": "./dist/module.d.mts",
|
||||
"default": "./dist/module.mjs"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/module.d.cts",
|
||||
"default": "./dist/module.cjs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
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');
|
||||
});
|
||||
|
||||
it('should throw an error if messages file contains null or undefined', async () => {
|
||||
const invalidFileContent = stringifyYAML({
|
||||
simple: 'example',
|
||||
invalidField: null,
|
||||
});
|
||||
|
||||
mockReadFile.mockResolvedValue(invalidFileContent);
|
||||
|
||||
await expect(parseMessagesFile('invalid.yml')).rejects.toThrowError(
|
||||
'Messages file should not contain `null` (found at "invalidField")',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { createI18n } from '../index';
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
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)]);
|
||||
},
|
||||
);
|
||||
|
||||
it('should allow overriding the plural substitutions', () => {
|
||||
const i18n = createI18n();
|
||||
i18n.t('key', 3, ['custom']);
|
||||
expect(getMessageMock).toBeCalledWith('key', ['custom']);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
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', ['one']);
|
||||
i18n.t('any', ['one', 'two']);
|
||||
i18n.t('any', n, ['one', 'two']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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');
|
||||
// @ts-expect-error
|
||||
i18n.t('simple', []);
|
||||
// @ts-expect-error
|
||||
i18n.t('simple', ['one']);
|
||||
// @ts-expect-error
|
||||
i18n.t('simple', n);
|
||||
|
||||
i18n.t('simpleSub1', ['one']);
|
||||
// @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']);
|
||||
// @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);
|
||||
// @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, undefined);
|
||||
i18n.t('pluralSub1', n, ['one']);
|
||||
// @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']);
|
||||
// @ts-expect-error
|
||||
i18n.t('pluralSub2');
|
||||
// @ts-expect-error
|
||||
i18n.t('pluralSub2', ['one', 'two']);
|
||||
// @ts-expect-error
|
||||
i18n.t('pluralSub2', n, ['one']);
|
||||
// @ts-expect-error
|
||||
i18n.t('pluralSub2', n, ['one', 'two', 'three']);
|
||||
// @ts-expect-error
|
||||
i18n.t('pluralSub2', n);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { ChromeMessage } from '../build';
|
||||
import { applyChromeMessagePlaceholders, getSubstitionCount } from '../utils';
|
||||
|
||||
describe('Utils', () => {
|
||||
describe('applyChromeMessagePlaceholders', () => {
|
||||
it('should return the combined stirng', () => {
|
||||
const input = {
|
||||
message: 'Hello $username$, welcome to $appName$',
|
||||
placeholders: {
|
||||
username: { content: '$1' },
|
||||
appName: { content: 'Example' },
|
||||
},
|
||||
} satisfies ChromeMessage;
|
||||
const expected = input.message
|
||||
.replace('$username$', input.placeholders.username.content)
|
||||
.replace('$appName$', input.placeholders.appName.content);
|
||||
|
||||
const actual = applyChromeMessagePlaceholders(input);
|
||||
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
|
||||
it('should ignore the case', () => {
|
||||
const input = {
|
||||
message: 'Hello $USERNAME$, welcome $username$',
|
||||
placeholders: {
|
||||
username: { content: '$1' },
|
||||
},
|
||||
} satisfies ChromeMessage;
|
||||
const expected = input.message
|
||||
.replace('$USERNAME$', input.placeholders.username.content)
|
||||
.replace('$username$', input.placeholders.username.content);
|
||||
|
||||
const actual = applyChromeMessagePlaceholders(input);
|
||||
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSubstitionCount', () => {
|
||||
it('should return the last substution present in the message', () => {
|
||||
expect(getSubstitionCount('I like $1, but I like $2 better')).toBe(2);
|
||||
});
|
||||
|
||||
it('should return 0 when no substitutions are present', () => {
|
||||
expect(getSubstitionCount('test')).toBe(0);
|
||||
});
|
||||
|
||||
it('should ignore escaped dollar signs', () => {
|
||||
expect(getSubstitionCount('buy $1 now for $$2 dollars')).toBe(1);
|
||||
});
|
||||
|
||||
it('should return the highest substitution when skipping numbers', () => {
|
||||
expect(getSubstitionCount('I like $1, but I like $8 better')).toBe(8);
|
||||
});
|
||||
|
||||
it('should only allow up to 9 substitutions', () => {
|
||||
expect(getSubstitionCount('Hello $9')).toBe(9);
|
||||
expect(getSubstitionCount('Hello $10')).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,296 @@
|
||||
/**
|
||||
* 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 ([null, undefined].includes(object)) {
|
||||
throw new Error(
|
||||
`Messages file should not contain \`${object}\` (found at "${path.join('.')}")`,
|
||||
);
|
||||
}
|
||||
if (Array.isArray(object))
|
||||
return object.flatMap((item, i) =>
|
||||
_parseMessagesObject(path.concat(String(i)), item, depth + 1),
|
||||
);
|
||||
if (isPluralMessage(object)) {
|
||||
const message = Object.values(object).join('|');
|
||||
const substitutions = getSubstitionCount(message);
|
||||
return [
|
||||
{
|
||||
type: 'plural',
|
||||
key: path,
|
||||
substitutions,
|
||||
plurals: object,
|
||||
},
|
||||
];
|
||||
}
|
||||
if (depth === 1 && isChromeMessage(object)) {
|
||||
const message = applyChromeMessagePlaceholders(object);
|
||||
const substitutions = getSubstitionCount(message);
|
||||
return [
|
||||
{
|
||||
type: 'chrome',
|
||||
key: path,
|
||||
substitutions,
|
||||
...object,
|
||||
},
|
||||
];
|
||||
}
|
||||
return Object.entries(object).flatMap(([key, value]) =>
|
||||
_parseMessagesObject(path.concat(key), value, depth + 1),
|
||||
);
|
||||
default:
|
||||
throw Error(`"Could not parse object of type "${typeof object}"`);
|
||||
}
|
||||
}
|
||||
|
||||
function isPluralMessage(object: any): object is Record<number | 'n', string> {
|
||||
return Object.keys(object).every(
|
||||
(key) => key === 'n' || isFinite(Number(key)),
|
||||
);
|
||||
}
|
||||
|
||||
function isChromeMessage(object: any): object is ChromeMessage {
|
||||
return Object.keys(object).every((key) =>
|
||||
ALLOWED_CHROME_MESSAGE_KEYS.has(key),
|
||||
);
|
||||
}
|
||||
|
||||
//
|
||||
// OUTPUT
|
||||
//
|
||||
|
||||
export function generateTypeText(messages: ParsedMessage[]): string {
|
||||
const renderMessageEntry = (message: ParsedMessage): string => {
|
||||
// Use . for deep keys at runtime and types
|
||||
const key = message.key.join('.');
|
||||
|
||||
const features = [
|
||||
`substitutions: ${message.substitutions}`,
|
||||
`plural: ${message.type === 'plural'}`,
|
||||
];
|
||||
return ` "${key}": { ${features.join(', ')} };`;
|
||||
};
|
||||
|
||||
return `export type GeneratedI18nStructure = {
|
||||
${messages.map(renderMessageEntry).join('\n')}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
export async function generateTypeFile(
|
||||
outFile: string,
|
||||
messages: ParsedMessage[],
|
||||
): Promise<void> {
|
||||
const text = generateTypeText(messages);
|
||||
await mkdir(dirname(outFile), { recursive: true });
|
||||
await writeFile(outFile, text, 'utf8');
|
||||
}
|
||||
|
||||
export function generateChromeMessages(
|
||||
messages: ParsedMessage[],
|
||||
): Record<string, ChromeMessage> {
|
||||
return messages.reduce<Record<string, ChromeMessage>>((acc, message) => {
|
||||
// Use _ for deep keys in _locales/.../messages.json
|
||||
const key = message.key.join('_');
|
||||
// Don't output predefined messages
|
||||
if (PREDEFINED_MESSAGES[key]) return acc;
|
||||
|
||||
switch (message.type) {
|
||||
case 'chrome':
|
||||
acc[key] = {
|
||||
message: message.message,
|
||||
description: message.description,
|
||||
placeholders: message.placeholders,
|
||||
};
|
||||
break;
|
||||
case 'plural':
|
||||
acc[key] = {
|
||||
message: Object.values(message.plurals).join(' | '),
|
||||
};
|
||||
break;
|
||||
case 'simple':
|
||||
acc[key] = {
|
||||
message: message.message,
|
||||
};
|
||||
break;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
export function generateChromeMessagesText(messages: ParsedMessage[]): string {
|
||||
const raw = generateChromeMessages(messages);
|
||||
return JSON.stringify(raw, null, 2);
|
||||
}
|
||||
|
||||
export async function generateChromeMessagesFile(
|
||||
file: string,
|
||||
messages: ParsedMessage[],
|
||||
): Promise<void> {
|
||||
const text = generateChromeMessagesText(messages);
|
||||
await writeFile(file, text + '\n', 'utf8');
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* @module @wxt-dev/i18n
|
||||
*/
|
||||
import {
|
||||
I18nStructure,
|
||||
DefaultI18nStructure,
|
||||
I18n,
|
||||
Substitution,
|
||||
} 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;
|
||||
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 {
|
||||
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);
|
||||
} else {
|
||||
message = chrome.i18n.getMessage(key.replaceAll('.', '_'));
|
||||
}
|
||||
if (!message) {
|
||||
console.warn(`[i18n] Message not found: "${key}"`);
|
||||
}
|
||||
if (count == null) return message;
|
||||
|
||||
// Apply pluralization
|
||||
const plural = message.split(' | ');
|
||||
switch (plural.length) {
|
||||
// "n items"
|
||||
case 1:
|
||||
return plural[0];
|
||||
// "1 item | n items"
|
||||
case 2:
|
||||
return plural[count === 1 ? 0 : 1];
|
||||
// "0 items | 1 item | n items"
|
||||
case 3:
|
||||
return plural[count === 0 || count === 1 ? count : 2];
|
||||
default:
|
||||
throw Error('Unknown plural formatting');
|
||||
}
|
||||
};
|
||||
|
||||
return { t } as I18n<T>;
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* The WXT Module to integrate `@wxt-dev/i18n` into your project.
|
||||
*
|
||||
* ```ts
|
||||
* export default defineConfig({
|
||||
* modules: ["@wxt-dev/i18n/module"],
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @module @wxt-dev/i18n/module
|
||||
*/
|
||||
|
||||
import 'wxt';
|
||||
import { addAlias, defineWxtModule } from 'wxt/modules';
|
||||
import {
|
||||
generateChromeMessagesText,
|
||||
parseMessagesFile,
|
||||
generateTypeText,
|
||||
} from './build';
|
||||
import glob from 'fast-glob';
|
||||
import { basename, extname, join, resolve } from 'node:path';
|
||||
import { watch } from 'chokidar';
|
||||
import { GeneratedPublicFile, WxtDirFileEntry } from 'wxt';
|
||||
import { writeFile } from 'node:fs/promises';
|
||||
|
||||
export default defineWxtModule({
|
||||
name: '@wxt-dev/i18n',
|
||||
imports: [{ from: '#i18n', name: 'i18n' }],
|
||||
|
||||
setup(wxt) {
|
||||
if (wxt.config.manifest.default_locale == null) {
|
||||
wxt.logger.warn(
|
||||
`\`[i18n]\` manifest.default_locale not set, \`@wxt-dev/i18n\` disabled.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
wxt.logger.info(
|
||||
'`[i18n]` Default locale: ' + wxt.config.manifest.default_locale,
|
||||
);
|
||||
|
||||
const getLocalizationFiles = async () => {
|
||||
const files = await glob('locales/*', {
|
||||
cwd: wxt.config.srcDir,
|
||||
absolute: true,
|
||||
});
|
||||
return files.map((file) => ({
|
||||
file,
|
||||
locale: basename(file).replace(extname(file), ''),
|
||||
}));
|
||||
};
|
||||
|
||||
const generateOutputJsonFiles = async (): Promise<
|
||||
GeneratedPublicFile[]
|
||||
> => {
|
||||
const files = await getLocalizationFiles();
|
||||
return await Promise.all(
|
||||
files.map(async ({ file, locale }) => {
|
||||
const messages = await parseMessagesFile(file);
|
||||
return {
|
||||
contents: generateChromeMessagesText(messages),
|
||||
relativeDest: join('_locales', locale, 'messages.json'),
|
||||
};
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const generateTypes = async (): Promise<WxtDirFileEntry> => {
|
||||
const files = await getLocalizationFiles();
|
||||
const defaultLocaleFile = files.find(
|
||||
({ locale }) => locale === wxt.config.manifest.default_locale,
|
||||
)!;
|
||||
if (defaultLocaleFile == null) {
|
||||
throw Error(
|
||||
`\`[i18n]\` Required localization file does not exist: \`<srcDir>/locales/${wxt.config.manifest.default_locale}.{json|json5|yml|yaml|toml}\``,
|
||||
);
|
||||
}
|
||||
|
||||
const messages = await parseMessagesFile(defaultLocaleFile.file);
|
||||
return {
|
||||
path: typesPath,
|
||||
text: generateTypeText(messages),
|
||||
};
|
||||
};
|
||||
|
||||
const updateLocalizations = async (file: string): Promise<void> => {
|
||||
wxt.logger.info(
|
||||
`\`[i18n]\` Localization file changed: \`${basename(file)}\``,
|
||||
);
|
||||
|
||||
// Regenerate files
|
||||
const [typesFile, jsonFiles] = await Promise.all([
|
||||
generateTypes(),
|
||||
generateOutputJsonFiles(),
|
||||
]);
|
||||
|
||||
// Write files to disk
|
||||
await Promise.all([
|
||||
writeFile(
|
||||
resolve(wxt.config.wxtDir, typesFile.path),
|
||||
typesFile.text,
|
||||
'utf8',
|
||||
),
|
||||
...jsonFiles.map((file) =>
|
||||
writeFile(
|
||||
resolve(wxt.config.outDir, file.relativeDest),
|
||||
file.contents,
|
||||
'utf8',
|
||||
),
|
||||
),
|
||||
]);
|
||||
|
||||
// TODO: Implement HMR instead of reloading extension. The reload is
|
||||
// fast, but it causes the popup to close, which I'd like to prevent.
|
||||
wxt.server?.reloadExtension();
|
||||
wxt.logger.success(`\`[i18n]\` Extension reloaded`);
|
||||
};
|
||||
|
||||
// Create .wxt/i18n.ts
|
||||
|
||||
const sourcePath = resolve(wxt.config.wxtDir, 'i18n/index.ts');
|
||||
const typesPath = resolve(wxt.config.wxtDir, 'i18n/structure.d.ts');
|
||||
|
||||
wxt.hooks.hook('prepare:types', async (_, entries) => {
|
||||
entries.push({
|
||||
path: sourcePath,
|
||||
text: `import { createI18n } from '@wxt-dev/i18n';
|
||||
import type { GeneratedI18nStructure } from './structure';
|
||||
|
||||
export const i18n = createI18n<GeneratedI18nStructure>();
|
||||
|
||||
export { GeneratedI18nStructure }
|
||||
`,
|
||||
});
|
||||
});
|
||||
|
||||
addAlias(wxt, '#i18n', sourcePath);
|
||||
|
||||
// Generate separate declaration file containing types - this prevents
|
||||
// firing the dev server's default file watcher when updating the types,
|
||||
// which would cause a full rebuild and reload of the extension.
|
||||
|
||||
wxt.hooks.hook('prepare:types', async (_, entries) => {
|
||||
entries.push(await generateTypes());
|
||||
});
|
||||
|
||||
// Generate _locales/.../messages.json files
|
||||
|
||||
wxt.hooks.hook('build:publicAssets', async (_, assets) => {
|
||||
const outFiles = await generateOutputJsonFiles();
|
||||
assets.push(...outFiles);
|
||||
});
|
||||
|
||||
// Reload extension during development
|
||||
|
||||
if (wxt.config.command === 'serve') {
|
||||
wxt.hooks.hookOnce('build:done', () => {
|
||||
const watcher = watch(resolve(wxt.config.srcDir, 'locales'));
|
||||
watcher.on('change', updateLocalizations);
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
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],
|
||||
): 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,
|
||||
): 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>,
|
||||
): 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,
|
||||
): 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,
|
||||
): string;
|
||||
};
|
||||
|
||||
export interface I18n<T extends DefaultI18nStructure> {
|
||||
t: TFunction<T>;
|
||||
}
|
||||
|
||||
export type Substitution = string | number;
|
||||
|
||||
type SubstitutionCount = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ChromeMessage } from './build';
|
||||
|
||||
export function applyChromeMessagePlaceholders(message: ChromeMessage): string {
|
||||
if (message.placeholders == null) return message.message;
|
||||
|
||||
return Object.entries(message.placeholders ?? {}).reduce(
|
||||
(text, [name, value]) => {
|
||||
return text.replaceAll(new RegExp(`\\$${name}\\$`, 'gi'), value.content);
|
||||
},
|
||||
message.message,
|
||||
);
|
||||
}
|
||||
|
||||
export function getSubstitionCount(message: string): number {
|
||||
return (
|
||||
1 +
|
||||
Array.from({ length: MAX_SUBSTITUTIONS }).findLastIndex((_, i) =>
|
||||
message.match(new RegExp(`(?<!\\$)\\$${i + 1}`)),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const MAX_SUBSTITUTIONS = 9;
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"types": ["chrome", "node"]
|
||||
},
|
||||
"exclude": ["node_modules/**", "dist/**"]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { defineProject } from 'vitest/config';
|
||||
import RandomSeed from 'vitest-plugin-random-seed';
|
||||
|
||||
export default defineProject({
|
||||
test: {
|
||||
mockReset: true,
|
||||
restoreMocks: true,
|
||||
},
|
||||
plugins: [RandomSeed()],
|
||||
});
|
||||
@@ -1,5 +1,19 @@
|
||||
# Changelog
|
||||
|
||||
## v1.1.1
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/module-react-v1.1.0...module-react-v1.1.1)
|
||||
|
||||
### 🏡 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.0
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/module-react-v1.0.0...module-react-v1.1.0)
|
||||
|
||||
@@ -9,7 +9,6 @@ export default defineWxtModule<ReactModuleOptions>({
|
||||
const { vite } = options ?? {};
|
||||
|
||||
addViteConfig(wxt, () => ({
|
||||
// @ts-expect-error: Vite version conflicts
|
||||
plugins: [react(vite)],
|
||||
}));
|
||||
|
||||
|
||||
@@ -1,6 +1,23 @@
|
||||
{
|
||||
"name": "@wxt-dev/module-react",
|
||||
"version": "1.1.0",
|
||||
"description": "WXT module to enable React support",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/wxt-dev/wxt.git",
|
||||
"directory": "packages/module-react"
|
||||
},
|
||||
"homepage": "https://github.com/wxt-dev/wxt/blob/main/packages/module-react/README.md",
|
||||
"keywords": [
|
||||
"wxt",
|
||||
"module",
|
||||
"react"
|
||||
],
|
||||
"author": {
|
||||
"name": "Aaron Klinker",
|
||||
"email": "aaronklinker1+wxt@gmail.com"
|
||||
},
|
||||
"license": "MIT",
|
||||
"version": "1.1.1",
|
||||
"type": "module",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.mjs",
|
||||
@@ -22,7 +39,7 @@
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "wxt",
|
||||
"check": "buildc --deps-only -- check",
|
||||
"check": "pnpm build && check",
|
||||
"build": "buildc -- unbuild",
|
||||
"prepare": "buildc --deps-only -- wxt prepare"
|
||||
},
|
||||
@@ -30,16 +47,16 @@
|
||||
"wxt": ">=0.18.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vitejs/plugin-react": "^4.3.1"
|
||||
"@vitejs/plugin-react": "^4.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "^1.3.1",
|
||||
"@types/react": "^18.3.3",
|
||||
"@aklinker1/check": "^1.4.5",
|
||||
"@types/react": "^18.3.11",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"publint": "^0.2.9",
|
||||
"publint": "^0.2.11",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"typescript": "^5.5.3",
|
||||
"typescript": "^5.6.2",
|
||||
"unbuild": "^2.0.0",
|
||||
"wxt": "workspace:*"
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -9,7 +9,6 @@ export default defineWxtModule<SolidModuleOptions>({
|
||||
const { vite } = options ?? {};
|
||||
|
||||
addViteConfig(wxt, () => ({
|
||||
// @ts-expect-error: Vite version mismatch
|
||||
plugins: [solid(vite)],
|
||||
build: {
|
||||
target: 'esnext',
|
||||
|
||||
@@ -1,6 +1,23 @@
|
||||
{
|
||||
"name": "@wxt-dev/module-solid",
|
||||
"version": "1.1.1",
|
||||
"description": "WXT module to enable SolidJS support",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/wxt-dev/wxt.git",
|
||||
"directory": "packages/module-solid"
|
||||
},
|
||||
"homepage": "https://github.com/wxt-dev/wxt/blob/main/packages/module-solid/README.md",
|
||||
"keywords": [
|
||||
"wxt",
|
||||
"module",
|
||||
"solidjs"
|
||||
],
|
||||
"author": {
|
||||
"name": "Aaron Klinker",
|
||||
"email": "aaronklinker1+wxt@gmail.com"
|
||||
},
|
||||
"license": "MIT",
|
||||
"version": "1.1.2",
|
||||
"type": "module",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.mjs",
|
||||
@@ -22,7 +39,7 @@
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "wxt",
|
||||
"check": "buildc --deps-only -- check",
|
||||
"check": "pnpm build && check",
|
||||
"build": "buildc -- unbuild",
|
||||
"prepare": "buildc --deps-only -- wxt prepare"
|
||||
},
|
||||
@@ -33,10 +50,10 @@
|
||||
"vite-plugin-solid": "^2.10.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "^1.3.1",
|
||||
"publint": "^0.2.9",
|
||||
"solid-js": "^1.8.18",
|
||||
"typescript": "^5.5.3",
|
||||
"@aklinker1/check": "^1.4.5",
|
||||
"publint": "^0.2.11",
|
||||
"solid-js": "^1.9.1",
|
||||
"typescript": "^5.6.2",
|
||||
"unbuild": "^2.0.0",
|
||||
"wxt": "workspace:*"
|
||||
}
|
||||
|
||||
@@ -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 🎉
|
||||
@@ -1,6 +1,23 @@
|
||||
{
|
||||
"name": "@wxt-dev/module-svelte",
|
||||
"version": "1.0.0",
|
||||
"description": "WXT module to enable Svelte support",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/wxt-dev/wxt.git",
|
||||
"directory": "packages/module-svelte"
|
||||
},
|
||||
"homepage": "https://github.com/wxt-dev/wxt/blob/main/packages/module-svelte/README.md",
|
||||
"keywords": [
|
||||
"wxt",
|
||||
"module",
|
||||
"svelte"
|
||||
],
|
||||
"author": {
|
||||
"name": "Aaron Klinker",
|
||||
"email": "aaronklinker1+wxt@gmail.com"
|
||||
},
|
||||
"license": "MIT",
|
||||
"version": "1.0.1",
|
||||
"type": "module",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.mjs",
|
||||
@@ -22,18 +39,18 @@
|
||||
],
|
||||
"scripts": {
|
||||
"build": "buildc -- unbuild",
|
||||
"check": "buildc --deps-only -- check"
|
||||
"check": "pnpm build && check"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"wxt": ">=0.18.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^3.1.1"
|
||||
"@sveltejs/vite-plugin-svelte": "^3.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "^1.3.1",
|
||||
"publint": "^0.2.9",
|
||||
"typescript": "^5.5.3",
|
||||
"@aklinker1/check": "^1.4.5",
|
||||
"publint": "^0.2.11",
|
||||
"typescript": "^5.6.2",
|
||||
"unbuild": "^2.0.0",
|
||||
"wxt": "workspace:*"
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ export default defineWxtModule<SvelteModuleOptions>({
|
||||
|
||||
addViteConfig(wxt, () => ({
|
||||
plugins: [
|
||||
// @ts-expect-error: Vite version mismatch
|
||||
svelte({
|
||||
// Using a svelte.config.js file causes a segmentation fault when importing the file
|
||||
configFile: false,
|
||||
|
||||
@@ -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 🎉
|
||||
@@ -1,6 +1,23 @@
|
||||
{
|
||||
"name": "@wxt-dev/module-vue",
|
||||
"version": "1.0.0",
|
||||
"description": "WXT module to enable Vue support",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/wxt-dev/wxt.git",
|
||||
"directory": "packages/module-vue"
|
||||
},
|
||||
"homepage": "https://github.com/wxt-dev/wxt/blob/main/packages/module-vue/README.md",
|
||||
"keywords": [
|
||||
"wxt",
|
||||
"module",
|
||||
"vue"
|
||||
],
|
||||
"author": {
|
||||
"name": "Aaron Klinker",
|
||||
"email": "aaronklinker1+wxt@gmail.com"
|
||||
},
|
||||
"license": "MIT",
|
||||
"version": "1.0.1",
|
||||
"type": "module",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.mjs",
|
||||
@@ -22,18 +39,18 @@
|
||||
],
|
||||
"scripts": {
|
||||
"build": "buildc -- unbuild",
|
||||
"check": "buildc --deps-only -- check"
|
||||
"check": "pnpm build && check"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"wxt": ">=0.18.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vitejs/plugin-vue": "^5.0.5"
|
||||
"@vitejs/plugin-vue": "^5.1.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "^1.3.1",
|
||||
"publint": "^0.2.9",
|
||||
"typescript": "^5.5.3",
|
||||
"@aklinker1/check": "^1.4.5",
|
||||
"publint": "^0.2.11",
|
||||
"typescript": "^5.6.2",
|
||||
"unbuild": "^2.0.0",
|
||||
"wxt": "workspace:*"
|
||||
}
|
||||
|
||||
@@ -17,20 +17,22 @@
|
||||
"postinstall": "buildc --deps-only -- wxt prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"@wxt-dev/i18n": "workspace:*",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/chrome": "^0.0.268",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/chrome": "^0.0.269",
|
||||
"@types/react": "^18.3.11",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"sass": "^1.77.8",
|
||||
"typescript": "^5.5.3",
|
||||
"vitest": "^2.0.3",
|
||||
"@wxt-dev/auto-icons": "workspace:*",
|
||||
"sass": "^1.79.4",
|
||||
"typescript": "^5.6.2",
|
||||
"vitest": "^2.1.2",
|
||||
"vitest-plugin-random-seed": "^1.1.0",
|
||||
"wxt": "workspace:*"
|
||||
},
|
||||
"buildc": {
|
||||
"outDir": ".output/chrome-mv3"
|
||||
"cachable": false
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
@@ -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());
|
||||
@@ -20,18 +17,21 @@ export default defineBackground({
|
||||
// @ts-expect-error: should only accept entrypoints or public assets
|
||||
browser.runtime.getURL('/');
|
||||
browser.runtime.getURL('/background.js');
|
||||
browser.runtime.getURL('/icon/128.png');
|
||||
browser.runtime.getURL('/icons/128.png');
|
||||
browser.runtime.getURL('/example.html#hash');
|
||||
browser.runtime.getURL('/example.html?query=param');
|
||||
// @ts-expect-error: should only allow hashes/query params on HTML files
|
||||
browser.runtime.getURL('/icon-128.png?query=param');
|
||||
|
||||
// @ts-expect-error: should only accept known message names
|
||||
browser.i18n.getMessage('test');
|
||||
browser.i18n.getMessage('prompt_for_name');
|
||||
browser.i18n.getMessage('hello', 'Aaron');
|
||||
browser.i18n.getMessage('bye', ['Aaron']);
|
||||
browser.i18n.getMessage('@@extension_id');
|
||||
i18n.t('test');
|
||||
i18n.t('prompt_for_name');
|
||||
i18n.t('hello', ['test']);
|
||||
i18n.t('bye', ['Aaron']);
|
||||
i18n.t('@@extension_id');
|
||||
i18n.t('deep.example');
|
||||
i18n.t('items', 0);
|
||||
i18n.t('items', 0, ['one']);
|
||||
|
||||
console.log('WXT MODE:', {
|
||||
MODE: import.meta.env.MODE,
|
||||
|
||||
@@ -13,7 +13,7 @@ export default defineContentScript({
|
||||
anchor: 'form[role=search]',
|
||||
onMount: (container) => {
|
||||
const app = document.createElement('div');
|
||||
app.textContent = browser.i18n.getMessage('prompt_for_name');
|
||||
app.textContent = i18n.t('prompt_for_name');
|
||||
container.append(app);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
prompt_for_name:
|
||||
message: What's your name?
|
||||
description: Ask for the user's name
|
||||
hello:
|
||||
message: Hello, $USER$
|
||||
description: Greet the user
|
||||
placeholders:
|
||||
user:
|
||||
content: $1
|
||||
example: Cira
|
||||
bye:
|
||||
message: Goodbye, $USER$. Come back to $OUR_SITE$ soon!
|
||||
description: Say goodbye to the user
|
||||
placeholders:
|
||||
our_site:
|
||||
content: Example.com
|
||||
user:
|
||||
content: $1
|
||||
example: Cira
|
||||
deep:
|
||||
example: 'this is deep'
|
||||
items:
|
||||
0: Zero items
|
||||
1: 1 item
|
||||
n: $1 items
|
||||
@@ -0,0 +1,3 @@
|
||||
import autoIcons from '@wxt-dev/auto-icons';
|
||||
|
||||
export default autoIcons;
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 504 B |
|
Before Width: | Height: | Size: 936 B |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
@@ -6,8 +6,5 @@ export default defineProject({
|
||||
mockReset: true,
|
||||
restoreMocks: true,
|
||||
},
|
||||
plugins: [
|
||||
// @ts-expect-error: Vite version mismatch
|
||||
WxtVitest(),
|
||||
],
|
||||
plugins: [WxtVitest()],
|
||||
});
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
src/core/utils/building/__tests__/test-entrypoints
|
||||
@@ -1,8 +1,249 @@
|
||||
# Changelog
|
||||
|
||||
## v0.19.11
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.10...wxt-v0.19.11)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- **zip:** Hooks ([#993](https://github.com/wxt-dev/wxt/pull/993))
|
||||
- **zip:** `wxt zip --sources` and auto sources for opera ([#1014](https://github.com/wxt-dev/wxt/pull/1014))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Reverse env files priority ([#1016](https://github.com/wxt-dev/wxt/pull/1016))
|
||||
- #1005 fixed, by updating type-definations to getItem method. ([#1007](https://github.com/wxt-dev/wxt/pull/1007), [#1005](https://github.com/wxt-dev/wxt/issues/1005))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Move some files around ([#996](https://github.com/wxt-dev/wxt/pull/996))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Florian Metz ([@Timeraa](http://github.com/Timeraa))
|
||||
- Gurvir Singh ([@baraich](http://github.com/baraich))
|
||||
- Mezannic ([@mezannic](http://github.com/mezannic))
|
||||
- Aaron ([@aklinker1](http://github.com/aklinker1))
|
||||
|
||||
## 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)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Add `injectScript` helper ([#900](https://github.com/wxt-dev/wxt/pull/900))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Do not clear `.wxt/tsconfig.json` in `findEntrypoints` if it exists ([#898](https://github.com/wxt-dev/wxt/pull/898))
|
||||
- **types:** `PublicPath` type resolution with `extensionApi: "chrome"` ([#901](https://github.com/wxt-dev/wxt/pull/901))
|
||||
- Fix `createIframeUi` `page` option types ([3a8e613](https://github.com/wxt-dev/wxt/commit/3a8e613))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Hikiko4ern ([@hikiko4ern](http://github.com/hikiko4ern))
|
||||
|
||||
## v0.19.3
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.2...wxt-v0.19.3)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Add `consola` to `wxt` dependencies ([#892](https://github.com/wxt-dev/wxt/pull/892))
|
||||
- **content-script-context:** Fix initialization logic for Firefox ([#895](https://github.com/wxt-dev/wxt/pull/895))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Improve `prepare:types` hook JSDoc ([#886](https://github.com/wxt-dev/wxt/pull/886))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Hikiko4ern ([@hikiko4ern](http://github.com/hikiko4ern))
|
||||
- Himanshu Patil ([@mehimanshupatil](https://github.com/mehimanshupatil))
|
||||
|
||||
## v0.19.2
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.1...wxt-v0.19.2)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Remove unused top-level functions and variables when loading entrypoints with the `vite-node` loader ([#875](https://github.com/wxt-dev/wxt/pull/875))
|
||||
- Don't default to dev mode for production builds when using `vite-node` loader ([#877](https://github.com/wxt-dev/wxt/pull/877))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Update README and homepage features with WXT modules ([ed07a49](https://github.com/wxt-dev/wxt/commit/ed07a49))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- **deps:** Upgrade all dependencies ([#869](https://github.com/wxt-dev/wxt/pull/869))
|
||||
|
||||
## v0.19.1
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.0...wxt-v0.19.1)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Auto Icons Module ([#851](https://github.com/wxt-dev/wxt/pull/851))
|
||||
|
||||
### 🔥 Performance
|
||||
|
||||
- Tree-shake unrelated code inside `vite-node` entrypoint loader ([#867](https://github.com/wxt-dev/wxt/pull/867))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Define web globals when importing entrypoints ([#865](https://github.com/wxt-dev/wxt/pull/865))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Florian Metz ([@Timeraa](http://github.com/Timeraa))
|
||||
|
||||
## v0.19.0
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.18.15...wxt-v0.19.0)
|
||||
[⚠️ breaking changes](https://wxt.dev/guide/upgrade-guide/wxt) • [compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.18.15...wxt-v0.19.0)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
@@ -36,11 +277,6 @@
|
||||
- **deps:** Upgrade `minimatch` from 9 to 10 ([#840](https://github.com/wxt-dev/wxt/pull/840))
|
||||
- **dev-deps:** Upgrade `happy-dom` from 13 to 14 ([#843](https://github.com/wxt-dev/wxt/pull/843))
|
||||
|
||||
#### ⚠️ Breaking Changes
|
||||
|
||||
- ⚠️ Use `vite-node` to load entrypoints by default ([#859](https://github.com/wxt-dev/wxt/pull/859))
|
||||
- **size:** ⚠️ Switch from `tsup` to `unbuild` for building WXT ([#848](https://github.com/wxt-dev/wxt/pull/848))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Florian Metz ([@Timeraa](http://github.com/Timeraa))
|
||||
|
||||
@@ -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
|
||||
@@ -59,12 +71,20 @@ Or see the [installation guide](https://wxt.dev/guide/installation.html) to get
|
||||
- 🦾 Auto-imports
|
||||
- 🤖 Automated publishing
|
||||
- 🎨 Frontend framework agnostic: works with Vue, React, Svelte, etc
|
||||
- 📦 Modular architecture with [WXT modules](https://wxt.dev/guide/go-further/reusable-modules.html#overview)
|
||||
- 🖍️ Quickly bootstrap a new project
|
||||
- 📏 Bundle analysis
|
||||
- ⬇️ Download and bundle remote URL imports
|
||||
|
||||
## 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 -->
|
||||
|
||||
@@ -19,6 +19,7 @@ exports[`Auto Imports > eslintrc > "enabled: 8" should output a JSON config file
|
||||
"defineUnlistedScript": true,
|
||||
"defineWxtPlugin": true,
|
||||
"fakeBrowser": true,
|
||||
"injectScript": true,
|
||||
"storage": true,
|
||||
"useAppConfig": true
|
||||
}
|
||||
@@ -44,6 +45,7 @@ const globals = {
|
||||
"defineUnlistedScript": true,
|
||||
"defineWxtPlugin": true,
|
||||
"fakeBrowser": true,
|
||||
"injectScript": true,
|
||||
"storage": true,
|
||||
"useAppConfig": true
|
||||
}
|
||||
@@ -77,6 +79,7 @@ exports[`Auto Imports > eslintrc > "enabled: true" should output a JSON config f
|
||||
"defineUnlistedScript": true,
|
||||
"defineWxtPlugin": true,
|
||||
"fakeBrowser": true,
|
||||
"injectScript": true,
|
||||
"storage": true,
|
||||
"useAppConfig": true
|
||||
}
|
||||
@@ -103,6 +106,7 @@ exports[`Auto Imports > eslintrc > should allow customizing the output 1`] = `
|
||||
"defineUnlistedScript": "readonly",
|
||||
"defineWxtPlugin": "readonly",
|
||||
"fakeBrowser": "readonly",
|
||||
"injectScript": "readonly",
|
||||
"storage": "readonly",
|
||||
"useAppConfig": "readonly"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { TestProject } from '../utils';
|
||||
import { execaCommand } from 'execa';
|
||||
import spawn from 'nano-spawn';
|
||||
|
||||
describe('Auto Imports', () => {
|
||||
describe('imports: { ... }', () => {
|
||||
@@ -31,6 +31,7 @@ describe('Auto Imports', () => {
|
||||
const defineUnlistedScript: typeof import('wxt/sandbox')['defineUnlistedScript']
|
||||
const defineWxtPlugin: typeof import('wxt/sandbox')['defineWxtPlugin']
|
||||
const fakeBrowser: typeof import('wxt/testing')['fakeBrowser']
|
||||
const injectScript: typeof import('wxt/client')['injectScript']
|
||||
const storage: typeof import('wxt/storage')['storage']
|
||||
const useAppConfig: typeof import('wxt/client')['useAppConfig']
|
||||
}
|
||||
@@ -179,7 +180,7 @@ describe('Auto Imports', () => {
|
||||
await project.prepare({
|
||||
imports: { eslintrc: { enabled: version } },
|
||||
});
|
||||
return await execaCommand('pnpm eslint entrypoints/background.js', {
|
||||
return await spawn('pnpm', ['eslint', 'entrypoints/background.js'], {
|
||||
cwd: project.root,
|
||||
});
|
||||
}
|
||||
@@ -204,7 +205,8 @@ describe('Auto Imports', () => {
|
||||
);
|
||||
|
||||
await expect(runEslint(project, 9)).rejects.toMatchObject({
|
||||
message: expect.stringContaining(
|
||||
exitCode: 1,
|
||||
stdout: expect.stringContaining(
|
||||
"'defineBackground' is not defined",
|
||||
),
|
||||
});
|
||||
@@ -252,7 +254,8 @@ describe('Auto Imports', () => {
|
||||
);
|
||||
|
||||
await expect(runEslint(project, 8)).rejects.toMatchObject({
|
||||
message: expect.stringContaining(
|
||||
exitCode: 1,
|
||||
stdout: expect.stringContaining(
|
||||
"'defineBackground' is not defined",
|
||||
),
|
||||
});
|
||||
|
||||
@@ -14,6 +14,12 @@ const hooks: WxtHooks = {
|
||||
'entrypoints:grouped': vi.fn(),
|
||||
'vite:build:extendConfig': vi.fn(),
|
||||
'vite:devServer:extendConfig': vi.fn(),
|
||||
'zip:start': vi.fn(),
|
||||
'zip:extension:start': vi.fn(),
|
||||
'zip:extension:done': vi.fn(),
|
||||
'zip:sources:start': vi.fn(),
|
||||
'zip:sources:done': vi.fn(),
|
||||
'zip:done': vi.fn(),
|
||||
};
|
||||
|
||||
function expectHooksToBeCalled(
|
||||
@@ -53,6 +59,12 @@ describe('Hooks', () => {
|
||||
'entrypoints:resolved': true,
|
||||
'vite:build:extendConfig': false,
|
||||
'vite:devServer:extendConfig': false,
|
||||
'zip:start': false,
|
||||
'zip:extension:start': false,
|
||||
'zip:extension:done': false,
|
||||
'zip:sources:start': false,
|
||||
'zip:sources:done': false,
|
||||
'zip:done': false,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -74,6 +86,12 @@ describe('Hooks', () => {
|
||||
'entrypoints:resolved': true,
|
||||
'vite:build:extendConfig': 1,
|
||||
'vite:devServer:extendConfig': false,
|
||||
'zip:start': false,
|
||||
'zip:extension:start': false,
|
||||
'zip:extension:done': false,
|
||||
'zip:sources:start': false,
|
||||
'zip:sources:done': false,
|
||||
'zip:done': false,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -95,6 +113,39 @@ describe('Hooks', () => {
|
||||
'entrypoints:resolved': true,
|
||||
'vite:build:extendConfig': 1,
|
||||
'vite:devServer:extendConfig': false,
|
||||
'zip:start': true,
|
||||
'zip:extension:start': true,
|
||||
'zip:extension:done': true,
|
||||
'zip:sources:start': false,
|
||||
'zip:sources:done': false,
|
||||
'zip:done': true,
|
||||
});
|
||||
});
|
||||
|
||||
it('zip -b firefox should call hooks', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/popup.html', '<html></html>');
|
||||
|
||||
await project.zip({ hooks, browser: 'firefox' });
|
||||
|
||||
expectHooksToBeCalled({
|
||||
ready: true,
|
||||
'prepare:types': true,
|
||||
'prepare:publicPaths': true,
|
||||
'build:before': true,
|
||||
'build:done': true,
|
||||
'build:publicAssets': true,
|
||||
'build:manifestGenerated': true,
|
||||
'entrypoints:grouped': true,
|
||||
'entrypoints:resolved': true,
|
||||
'vite:build:extendConfig': 1,
|
||||
'vite:devServer:extendConfig': false,
|
||||
'zip:start': true,
|
||||
'zip:extension:start': true,
|
||||
'zip:extension:done': true,
|
||||
'zip:sources:start': true,
|
||||
'zip:sources:done': true,
|
||||
'zip:done': true,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -122,6 +173,12 @@ describe('Hooks', () => {
|
||||
'entrypoints:resolved': true,
|
||||
'vite:build:extendConfig': 2,
|
||||
'vite:devServer:extendConfig': 1,
|
||||
'zip:start': false,
|
||||
'zip:extension:start': false,
|
||||
'zip:extension:done': false,
|
||||
'zip:sources:start': false,
|
||||
'zip:sources:done': false,
|
||||
'zip:done': false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { TestProject, WXT_PACKAGE_DIR } from '../utils';
|
||||
import { execaCommand } from 'execa';
|
||||
import spawn from 'nano-spawn';
|
||||
import glob from 'fast-glob';
|
||||
import { mkdir, writeJson } from 'fs-extra';
|
||||
|
||||
@@ -8,11 +8,15 @@ describe('Init command', () => {
|
||||
it('should download and create a template', async () => {
|
||||
const project = new TestProject();
|
||||
|
||||
await execaCommand(`pnpm -s wxt init ${project.root} -t vue --pm npm`, {
|
||||
env: { ...process.env, CI: 'true' },
|
||||
stdio: 'ignore',
|
||||
cwd: WXT_PACKAGE_DIR,
|
||||
});
|
||||
await spawn(
|
||||
'pnpm',
|
||||
['-s', 'wxt', 'init', project.root, '-t', 'vue', '--pm', 'npm'],
|
||||
{
|
||||
env: { CI: 'true' },
|
||||
stdio: 'ignore',
|
||||
cwd: WXT_PACKAGE_DIR,
|
||||
},
|
||||
);
|
||||
const files = await glob('**/*', {
|
||||
cwd: project.root,
|
||||
onlyFiles: true,
|
||||
@@ -51,11 +55,15 @@ describe('Init command', () => {
|
||||
await writeJson(project.resolvePath('package.json'), {});
|
||||
|
||||
await expect(() =>
|
||||
execaCommand(`pnpm -s wxt init ${project.root} -t vue --pm npm`, {
|
||||
env: { ...process.env, CI: 'true' },
|
||||
stdio: 'ignore',
|
||||
cwd: WXT_PACKAGE_DIR,
|
||||
}),
|
||||
spawn(
|
||||
'pnpm',
|
||||
['-s', 'wxt', 'init', project.root, '-t', 'vue', '--pm', 'npm'],
|
||||
{
|
||||
env: { CI: 'true' },
|
||||
stdio: 'ignore',
|
||||
cwd: WXT_PACKAGE_DIR,
|
||||
},
|
||||
),
|
||||
).rejects.toThrowError('Command failed with exit code 1:');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { test, expect } from 'vitest';
|
||||
import spawn from 'nano-spawn';
|
||||
import {
|
||||
NpmListDependency,
|
||||
NpmListProject,
|
||||
} from '../../src/core/package-managers/npm';
|
||||
|
||||
// Tests to ensure the total size of the WXT module is as small as possible
|
||||
// https://pkg-size.dev/wxt
|
||||
|
||||
test('Only one version of esbuild should be installed (each version is ~20mb of node_modules)', async () => {
|
||||
const { stdout } = await spawn('pnpm', [
|
||||
'why',
|
||||
'esbuild',
|
||||
'--prod',
|
||||
'--json',
|
||||
]);
|
||||
const projects: NpmListProject[] = JSON.parse(stdout);
|
||||
const esbuildVersions = new Set<string>();
|
||||
iterateDependencies(projects, (name, meta) => {
|
||||
if (name === 'esbuild') esbuildVersions.add(meta.version);
|
||||
});
|
||||
|
||||
expect([...esbuildVersions]).toHaveLength(1);
|
||||
});
|
||||
|
||||
function iterateDependencies(
|
||||
projects: NpmListProject[],
|
||||
cb: (name: string, meta: NpmListDependency) => void,
|
||||
) {
|
||||
const recurse = (dependencies: Record<string, NpmListDependency>) => {
|
||||
Object.entries(dependencies).forEach(([name, meta]) => {
|
||||
cb(name, meta);
|
||||
if (meta.dependencies) recurse(meta.dependencies);
|
||||
});
|
||||
};
|
||||
projects.forEach((project) => {
|
||||
if (project.dependencies) recurse(project.dependencies);
|
||||
});
|
||||
}
|
||||
@@ -280,7 +280,7 @@ describe('Output Directory Structure', () => {
|
||||
var _a, _b;
|
||||
import { l as logHello, i as initPlugins } from "./chunks/_virtual_wxt-plugins-OjKtWpmY.js";
|
||||
function defineBackground(arg) {
|
||||
if (typeof arg === "function") return { main: arg };
|
||||
if (arg == null || typeof arg === "function") return { main: arg };
|
||||
return arg;
|
||||
}
|
||||
const definition = defineBackground({
|
||||
@@ -362,23 +362,23 @@ describe('Output Directory Structure', () => {
|
||||
.toMatchInlineSnapshot(`
|
||||
".output/chrome-mv3/background.js
|
||||
----------------------------------------
|
||||
var _background = function() {
|
||||
var background = function() {
|
||||
"use strict";
|
||||
var _a, _b;
|
||||
function defineBackground(arg) {
|
||||
if (typeof arg === "function") return { main: arg };
|
||||
if (arg == null || typeof arg === "function") return { main: arg };
|
||||
return arg;
|
||||
}
|
||||
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;
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import { TestProject } from '../utils';
|
||||
|
||||
describe('Remote Code', () => {
|
||||
it('should download "url:*" modules and include them in the final bundle', async () => {
|
||||
const url = 'https://code.jquery.com/jquery-3.7.1.slim.min.js';
|
||||
const url = 'https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js';
|
||||
const project = new TestProject();
|
||||
project.addFile(
|
||||
'entrypoints/popup.ts',
|
||||
@@ -16,7 +16,7 @@ describe('Remote Code', () => {
|
||||
const output = await project.serializeFile('.output/chrome-mv3/popup.js');
|
||||
expect(output).toContain(
|
||||
// Some text that will hopefully be in future versions of this script
|
||||
'jQuery v3.7.1',
|
||||
'lodash.com',
|
||||
);
|
||||
expect(output).not.toContain(url);
|
||||
expect(
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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],
|
||||
}),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { TestProject } from '../utils';
|
||||
import extract from 'extract-zip';
|
||||
import { execaCommand } from 'execa';
|
||||
import spawn from 'nano-spawn';
|
||||
import { readFile, writeFile } from 'fs-extra';
|
||||
|
||||
process.env.WXT_PNPM_IGNORE_WORKSPACE = 'true';
|
||||
@@ -41,13 +41,15 @@ describe('Zipping', () => {
|
||||
|
||||
// Build zipped extension
|
||||
await expect(
|
||||
execaCommand('pnpm i --ignore-workspace --frozen-lockfile false', {
|
||||
spawn('pnpm', ['i', '--ignore-workspace', '--frozen-lockfile', 'false'], {
|
||||
cwd: unzipDir,
|
||||
}),
|
||||
).resolves.toMatchObject({ exitCode: 0 });
|
||||
).resolves.not.toHaveProperty('exitCode');
|
||||
await expect(
|
||||
execaCommand('pnpm wxt build -b firefox', { cwd: unzipDir }),
|
||||
).resolves.toMatchObject({ exitCode: 0 });
|
||||
spawn('pnpm', ['wxt', 'build', '-b', 'firefox'], {
|
||||
cwd: unzipDir,
|
||||
}),
|
||||
).resolves.not.toHaveProperty('exitCode');
|
||||
|
||||
await expect(project.fileExists(unzipDir, '.output')).resolves.toBe(true);
|
||||
expect(
|
||||
@@ -97,8 +99,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 +109,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 +118,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 +158,95 @@ 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,
|
||||
);
|
||||
});
|
||||
|
||||
it.each(['firefox', 'opera'])(
|
||||
'should create sources zip for "%s" browser when sourcesZip is undefined',
|
||||
async (browser) => {
|
||||
const project = new TestProject({
|
||||
name: 'test',
|
||||
version: '1.0.0',
|
||||
});
|
||||
project.addFile(
|
||||
'entrypoints/background.ts',
|
||||
'export default defineBackground(() => {});',
|
||||
);
|
||||
const sourcesZip = project.resolvePath('.output/test-1.0.0-sources.zip');
|
||||
|
||||
await project.zip({
|
||||
browser,
|
||||
});
|
||||
|
||||
expect(await project.fileExists(sourcesZip)).toBe(true);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(['firefox', 'chrome'])(
|
||||
'should create sources zip for "%s" when sourcesZip is true',
|
||||
async (browser) => {
|
||||
const project = new TestProject({
|
||||
name: 'test',
|
||||
version: '1.0.0',
|
||||
});
|
||||
project.addFile(
|
||||
'entrypoints/background.ts',
|
||||
'export default defineBackground(() => {});',
|
||||
);
|
||||
const sourcesZip = project.resolvePath('.output/test-1.0.0-sources.zip');
|
||||
|
||||
await project.zip({
|
||||
browser,
|
||||
zip: {
|
||||
zipSources: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(await project.fileExists(sourcesZip)).toBe(true);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(['firefox', 'chrome'])(
|
||||
'should not create sources zip for "%s" when sourcesZip is false',
|
||||
async (browser) => {
|
||||
const project = new TestProject({
|
||||
name: 'test',
|
||||
version: '1.0.0',
|
||||
});
|
||||
project.addFile(
|
||||
'entrypoints/background.ts',
|
||||
'export default defineBackground(() => {});',
|
||||
);
|
||||
const sourcesZip = project.resolvePath('.output/test-1.0.0-sources.zip');
|
||||
|
||||
await project.zip({
|
||||
browser,
|
||||
zip: {
|
||||
zipSources: false,
|
||||
},
|
||||
});
|
||||
|
||||
expect(await project.fileExists(sourcesZip)).toBe(false);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { dirname, relative, resolve } from 'path';
|
||||
import fs, { mkdir } from 'fs-extra';
|
||||
import glob from 'fast-glob';
|
||||
import { execaCommand } from 'execa';
|
||||
import spawn from 'nano-spawn';
|
||||
import {
|
||||
InlineConfig,
|
||||
UserConfig,
|
||||
@@ -119,7 +119,7 @@ export class TestProject {
|
||||
await fs.writeFile(filePath, content ?? '', 'utf-8');
|
||||
}
|
||||
|
||||
await execaCommand('pnpm --ignore-workspace i --ignore-scripts', {
|
||||
await spawn('pnpm', ['--ignore-workspace', 'i', '--ignore-scripts'], {
|
||||
cwd: this.root,
|
||||
});
|
||||
await mkdir(resolve(this.root, 'public'), { recursive: true }).catch(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "wxt",
|
||||
"type": "module",
|
||||
"version": "0.19.0",
|
||||
"version": "0.19.11",
|
||||
"description": "Next gen framework for developing web extensions",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -72,7 +72,7 @@
|
||||
"scripts": {
|
||||
"wxt": "tsx src/cli/index.ts",
|
||||
"build": "buildc -- unbuild",
|
||||
"check": "buildc --deps-only -- run-s -c check:*",
|
||||
"check": "pnpm build && run-s -c check:*",
|
||||
"check:default": "check",
|
||||
"check:tsc-virtual": "tsc --noEmit -p src/virtual",
|
||||
"test": "buildc --deps-only -- vitest",
|
||||
@@ -82,20 +82,23 @@
|
||||
},
|
||||
"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",
|
||||
"@webext-core/match-patterns": "^1.0.3",
|
||||
"async-mutex": "^0.5.0",
|
||||
"c12": "^1.11.1",
|
||||
"c12": "^1.11.2",
|
||||
"cac": "^6.7.14",
|
||||
"chokidar": "^3.6.0",
|
||||
"ci-info": "^4.0.0",
|
||||
"consola": "^3.2.3",
|
||||
"defu": "^6.1.4",
|
||||
"dequal": "^2.0.3",
|
||||
"esbuild": "^0.23.0",
|
||||
"dotenv": "^16.4.5",
|
||||
"esbuild": "^0.21.5",
|
||||
"fast-glob": "^3.3.2",
|
||||
"filesize": "^10.1.4",
|
||||
"filesize": "^10.1.6",
|
||||
"fs-extra": "^11.2.0",
|
||||
"get-port": "^7.1.0",
|
||||
"giget": "^1.2.3",
|
||||
@@ -104,49 +107,48 @@
|
||||
"jiti": "^1.21.6",
|
||||
"json5": "^2.2.3",
|
||||
"jszip": "^3.10.1",
|
||||
"linkedom": "^0.18.4",
|
||||
"magicast": "^0.3.4",
|
||||
"linkedom": "^0.18.5",
|
||||
"magicast": "^0.3.5",
|
||||
"minimatch": "^10.0.1",
|
||||
"nano-spawn": "^0.1.0",
|
||||
"natural-compare": "^1.4.0",
|
||||
"normalize-path": "^3.0.0",
|
||||
"nypm": "^0.3.9",
|
||||
"ohash": "^1.1.3",
|
||||
"nypm": "^0.3.12",
|
||||
"ohash": "^1.1.4",
|
||||
"open": "^10.1.0",
|
||||
"ora": "^8.0.1",
|
||||
"picocolors": "^1.0.1",
|
||||
"ora": "^8.1.0",
|
||||
"perfect-debounce": "^1.0.0",
|
||||
"picocolors": "^1.1.0",
|
||||
"prompts": "^2.4.2",
|
||||
"publish-browser-extension": "^2.1.3",
|
||||
"unimport": "^3.9.0",
|
||||
"vite": "^5.3.4",
|
||||
"vite-node": "^2.0.0",
|
||||
"publish-browser-extension": "^2.2.2",
|
||||
"scule": "^1.3.0",
|
||||
"unimport": "^3.13.1",
|
||||
"vite": "^5.4.8",
|
||||
"vite-node": "^2.1.2",
|
||||
"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.268",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/lodash.merge": "^4.6.9",
|
||||
"@types/natural-compare": "^1.4.3",
|
||||
"@types/node": "^20.14.11",
|
||||
"@types/node": "^20.16.10",
|
||||
"@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.9",
|
||||
"p-map": "^7.0.2",
|
||||
"publint": "^0.2.9",
|
||||
"publint": "^0.2.11",
|
||||
"tsx": "4.15.7",
|
||||
"typescript": "^5.5.3",
|
||||
"typescript": "^5.6.2",
|
||||
"unbuild": "^2.0.0",
|
||||
"vitest": "^2.0.3",
|
||||
"vitest": "^2.1.2",
|
||||
"vitest-plugin-random-seed": "^1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/chrome": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/chrome": {
|
||||
"optional": true
|
||||
|
||||
@@ -903,5 +903,24 @@ describe('Storage Utils', () => {
|
||||
// @ts-expect-error
|
||||
await storage.getItem('loca:test').catch(() => {});
|
||||
});
|
||||
|
||||
it('should return a nullable type when getItem is called without a fallback', async () => {
|
||||
const res = await storage.getItem<string>('local:test');
|
||||
expectTypeOf(res).toBeNullable();
|
||||
});
|
||||
|
||||
it('should return a non-null type when getItem is called with a fallback', async () => {
|
||||
const res = await storage.getItem('local:test', {
|
||||
fallback: 'test',
|
||||
});
|
||||
expectTypeOf(res).not.toBeNullable();
|
||||
});
|
||||
|
||||
it('should return a non-null type when getItem is called with a fallback and the first type parameter is passed', async () => {
|
||||
const res = await storage.getItem<string>('local:test', {
|
||||
fallback: 'test',
|
||||
});
|
||||
expectTypeOf(res).not.toBeNullable();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,18 +6,11 @@
|
||||
*
|
||||
* @module wxt/browser/chrome
|
||||
*/
|
||||
import type { WxtRuntime, WxtI18n } from './index';
|
||||
|
||||
export interface WxtRuntime {
|
||||
// Overriden per-project
|
||||
}
|
||||
export interface WxtI18n {
|
||||
// Overriden per-project
|
||||
}
|
||||
|
||||
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 =
|
||||
|
||||
@@ -4,20 +4,24 @@
|
||||
* @module wxt/browser
|
||||
*/
|
||||
|
||||
import originalBrowser, { Browser, Runtime, I18n } from 'webextension-polyfill';
|
||||
import originalBrowser, { Browser } from 'webextension-polyfill';
|
||||
|
||||
export interface AugmentedBrowser extends Browser {
|
||||
runtime: WxtRuntime;
|
||||
i18n: WxtI18n;
|
||||
}
|
||||
export type AugmentedBrowser = Omit<Browser, 'runtime' | 'i18n'> & {
|
||||
runtime: WxtRuntime & Omit<Browser['runtime'], 'getURL'>;
|
||||
i18n: WxtI18n & Omit<Browser['i18n'], 'getMessage'>;
|
||||
};
|
||||
|
||||
export interface WxtRuntime extends Runtime.Static {
|
||||
// Overriden per-project
|
||||
}
|
||||
/**
|
||||
* This interface is empty because it is generated per-project when running `wxt prepare`. See:
|
||||
* - `.wxt/types/paths.d.ts`
|
||||
*/
|
||||
export interface WxtRuntime {}
|
||||
|
||||
export interface WxtI18n extends I18n.Static {
|
||||
// Overriden per-project
|
||||
}
|
||||
/**
|
||||
* This interface is empty because it is generated per-project when running `wxt prepare`. See:
|
||||
* - `.wxt/types/i18n.d.ts`
|
||||
*/
|
||||
export interface WxtI18n {}
|
||||
|
||||
export const browser: AugmentedBrowser = originalBrowser;
|
||||
|
||||
|
||||
@@ -237,7 +237,9 @@ describe('CLI', () => {
|
||||
mockArgv('zip');
|
||||
await importCli();
|
||||
|
||||
expect(zipMock).toBeCalledWith({});
|
||||
expect(zipMock).toBeCalledWith({
|
||||
zip: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect passing a custom root', async () => {
|
||||
@@ -246,6 +248,7 @@ describe('CLI', () => {
|
||||
|
||||
expect(zipMock).toBeCalledWith({
|
||||
root: 'path/to/root',
|
||||
zip: {},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -255,6 +258,7 @@ describe('CLI', () => {
|
||||
|
||||
expect(zipMock).toBeCalledWith({
|
||||
configFile: './path/to/config.ts',
|
||||
zip: {},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -264,6 +268,7 @@ describe('CLI', () => {
|
||||
|
||||
expect(zipMock).toBeCalledWith({
|
||||
mode: 'development',
|
||||
zip: {},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -273,6 +278,7 @@ describe('CLI', () => {
|
||||
|
||||
expect(zipMock).toBeCalledWith({
|
||||
browser: 'firefox',
|
||||
zip: {},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -282,6 +288,7 @@ describe('CLI', () => {
|
||||
|
||||
expect(zipMock).toBeCalledWith({
|
||||
manifestVersion: 2,
|
||||
zip: {},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -291,6 +298,7 @@ describe('CLI', () => {
|
||||
|
||||
expect(zipMock).toBeCalledWith({
|
||||
manifestVersion: 3,
|
||||
zip: {},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -300,6 +308,40 @@ describe('CLI', () => {
|
||||
|
||||
expect(zipMock).toBeCalledWith({
|
||||
debug: true,
|
||||
zip: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('should pass undefined for zipSources when --sources is not passed', async () => {
|
||||
mockArgv('zip');
|
||||
await importCli();
|
||||
|
||||
expect(zipMock).toBeCalledWith({
|
||||
zip: {
|
||||
zipSources: undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should pass true for zipSources when --sources is passed', async () => {
|
||||
mockArgv('zip', '--sources');
|
||||
await importCli();
|
||||
|
||||
expect(zipMock).toBeCalledWith({
|
||||
zip: {
|
||||
zipSources: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should pass false for zipSources when --sources=false is passed', async () => {
|
||||
mockArgv('zip', '--sources=false');
|
||||
await importCli();
|
||||
|
||||
expect(zipMock).toBeCalledWith({
|
||||
zip: {
|
||||
zipSources: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import { printHeader } from '../core/utils/log';
|
||||
import { formatDuration } from '../core/utils/time';
|
||||
import { ValidationError } from '../core/utils/validation';
|
||||
import { registerWxt } from '../core/wxt';
|
||||
import spawn from 'nano-spawn';
|
||||
|
||||
/**
|
||||
* Wrap an action handler to add a timer, error handling, and maybe enable debug mode.
|
||||
@@ -87,8 +88,7 @@ export function createAliasedCommand(
|
||||
const args = process.argv.slice(
|
||||
process.argv.indexOf(aliasedCommand.name) + 1,
|
||||
);
|
||||
const { execa } = await import('execa');
|
||||
await execa(bin, args, {
|
||||
await spawn(bin, args, {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
} catch {
|
||||
|
||||
@@ -95,6 +95,7 @@ cli
|
||||
.option('-b, --browser <browser>', 'specify a browser')
|
||||
.option('--mv3', 'target manifest v3')
|
||||
.option('--mv2', 'target manifest v2')
|
||||
.option('--sources', 'always create sources zip')
|
||||
.action(
|
||||
wrapAction(async (root, flags) => {
|
||||
await zip({
|
||||
@@ -104,6 +105,9 @@ cli
|
||||
manifestVersion: flags.mv3 ? 3 : flags.mv2 ? 2 : undefined,
|
||||
configFile: flags.config,
|
||||
debug: flags.debug,
|
||||
zip: {
|
||||
zipSources: flags.sources,
|
||||
},
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/** @vitest-environment happy-dom */
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { ContentScriptContext } from '..';
|
||||
import { fakeBrowser } from '@webext-core/fake-browser';
|
||||
import { sleep } from '../../../core/utils/time';
|
||||
|
||||
describe('Content Script Context', () => {
|
||||
beforeEach(() => {
|
||||
vi.useRealTimers();
|
||||
fakeBrowser.runtime.id = 'anything';
|
||||
});
|
||||
|
||||
it("should recognize when the content script has lost it's connection to the extension API", () => {
|
||||
const ctx = new ContentScriptContext('test');
|
||||
const onInvalidated = vi.fn();
|
||||
|
||||
ctx.onInvalidated(onInvalidated);
|
||||
// @ts-expect-error
|
||||
delete fakeBrowser.runtime.id;
|
||||
const isValid = ctx.isValid;
|
||||
|
||||
expect(onInvalidated).toBeCalled();
|
||||
expect(isValid).toBe(false);
|
||||
});
|
||||
|
||||
it('should invalidate the current content script when a new context is created', async () => {
|
||||
const name = 'test';
|
||||
const onInvalidated = vi.fn();
|
||||
const ctx = new ContentScriptContext(name);
|
||||
ctx.onInvalidated(onInvalidated);
|
||||
|
||||
// Wait for events to run before next tick next tick
|
||||
await sleep(0);
|
||||
|
||||
// Create a new context after first is initialized, and wait for it to initialize
|
||||
new ContentScriptContext(name);
|
||||
await sleep(0);
|
||||
|
||||
expect(onInvalidated).toBeCalled();
|
||||
expect(ctx.isValid).toBe(false);
|
||||
});
|
||||
|
||||
it('should not invalidate the current content script when a new context is created with a different name', async () => {
|
||||
const onInvalidated = vi.fn();
|
||||
const ctx = new ContentScriptContext('test1');
|
||||
ctx.onInvalidated(onInvalidated);
|
||||
|
||||
// Wait for events to run before next tick next tick
|
||||
await sleep(0);
|
||||
|
||||
// Create a new context after first is initialized, and wait for it to initialize
|
||||
new ContentScriptContext('test2');
|
||||
await sleep(0);
|
||||
|
||||
expect(onInvalidated).not.toBeCalled();
|
||||
expect(ctx.isValid).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -46,13 +46,13 @@ export class ContentScriptContext implements AbortController {
|
||||
public readonly options?: Omit<ContentScriptDefinition, 'main'>,
|
||||
) {
|
||||
this.#abortController = new AbortController();
|
||||
|
||||
if (this.#isTopFrame) {
|
||||
this.#listenForNewerScripts({ ignoreFirstEvent: true });
|
||||
this.#stopOldScripts();
|
||||
}
|
||||
this.setTimeout(() => {
|
||||
// Run on next tick so the listener it adds isn't triggered by stopOldScript
|
||||
} else {
|
||||
this.#listenForNewerScripts();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get signal() {
|
||||
@@ -224,12 +224,18 @@ export class ContentScriptContext implements AbortController {
|
||||
);
|
||||
}
|
||||
|
||||
#listenForNewerScripts() {
|
||||
#listenForNewerScripts(options?: { ignoreFirstEvent?: boolean }) {
|
||||
let isFirst = true;
|
||||
|
||||
const cb = (event: MessageEvent) => {
|
||||
if (
|
||||
event.data?.type === ContentScriptContext.SCRIPT_STARTED_MESSAGE_TYPE &&
|
||||
event.data?.contentScriptName === this.contentScriptName
|
||||
) {
|
||||
const wasFirst = isFirst;
|
||||
isFirst = false;
|
||||
if (wasFirst && options?.ignoreFirstEvent) return;
|
||||
|
||||
this.notifyInvalidated();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -62,6 +62,7 @@ export function createIframeUi<TMounted>(
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.setAttribute('data-wxt-iframe', '');
|
||||
const iframe = document.createElement('iframe');
|
||||
// @ts-expect-error: getURL is defined per-project, but not inside the package
|
||||
iframe.src = browser.runtime.getURL(options.page);
|
||||
wrapper.appendChild(iframe);
|
||||
|
||||
@@ -258,6 +259,7 @@ function mountUi(
|
||||
* Load the CSS for the current entrypoint.
|
||||
*/
|
||||
async function loadCss(): Promise<string> {
|
||||
// @ts-expect-error: getURL is defined per-project, but not inside the package
|
||||
const url = browser.runtime.getURL(
|
||||
`/content-scripts/${import.meta.env.ENTRYPOINT}.css`,
|
||||
);
|
||||
|
||||