Compare commits
211 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 742d2f1536 | |||
| b4b69783d5 | |||
| c8ddc66cfa | |||
| 2b6ff8d30f | |||
| d79d7ed778 | |||
| ab234c0f45 | |||
| eeb43c6bf4 | |||
| 17bda1eb34 | |||
| 96e28c415c | |||
| 1f9f6d9681 | |||
| f1bb7cb0c2 | |||
| 5eb5be7123 | |||
| 886b2081c2 | |||
| c69e51ce50 | |||
| 861efa3415 | |||
| 792b57542d | |||
| 97e6678e5b | |||
| 66c4ec3f99 | |||
| 8988d6a241 | |||
| b52856bea4 | |||
| 16fee19b85 | |||
| 14011c19cb | |||
| 005a9dff16 | |||
| 3109bbadbe | |||
| 34ae689569 | |||
| 1f216dd57b | |||
| a7963e23db | |||
| 069bd6c435 | |||
| 88a1244ca9 | |||
| 7ee391f688 | |||
| 2b3eab9c2a | |||
| a3fc9e1e18 | |||
| 559d32e6eb | |||
| f904b79367 | |||
| 9cd7cca81c | |||
| 3366c1e35f | |||
| 82be31a408 | |||
| d767f3f598 | |||
| 48967c7c5f | |||
| 6c4da7225d | |||
| e9025fb13e | |||
| 8db6b3187b | |||
| a19f9e12c4 | |||
| a82eb46511 | |||
| e50f2b047a | |||
| 1b41cd26ee | |||
| e32fbed1b7 | |||
| f8acdd6352 | |||
| 9ea89c6340 | |||
| a521a80d43 | |||
| 4c6a57dd33 | |||
| 3ce8c8b1f4 | |||
| 4ee8f7837e | |||
| 1e8e5488af | |||
| 5704765cc4 | |||
| 8c7adbf463 | |||
| 8c4436efe8 | |||
| 5e361c0085 | |||
| ccfc207e92 | |||
| 36be4ce771 | |||
| 18f8a387b0 | |||
| 48c2c07557 | |||
| 61548a9478 | |||
| fd678e9572 | |||
| 63fb913f76 | |||
| faffe20a62 | |||
| 3ed8bd10fd | |||
| 6f3f270573 | |||
| 5347cb4626 | |||
| d6b4d2d1f6 | |||
| 0d7ae287da | |||
| 90cb3bd3bf | |||
| 60eb97c4b2 | |||
| 0ffc6eae3e | |||
| 06a8c5fb0e | |||
| eb86413f35 | |||
| b29d49cbf1 | |||
| c0a0edb30c | |||
| b15a933c75 | |||
| 1851be22a7 | |||
| 39f6c29e4f | |||
| 936e89fbfb | |||
| ad365ecde4 | |||
| b4283e37bd | |||
| b467ad760a | |||
| 9fc6408ef9 | |||
| 394cb1a520 | |||
| ffec88549b | |||
| 4758bea187 | |||
| 6671471f75 | |||
| ae78dc7fb3 | |||
| b62af3d007 | |||
| 43ae44ecd5 | |||
| 5bab9d2f59 | |||
| 7edf1c8103 | |||
| 53b4eac955 | |||
| f9a10c2c52 | |||
| 8206ca9164 | |||
| 1a823cc008 | |||
| efc011eb81 | |||
| 35f6d28977 | |||
| 43fcbec0eb | |||
| a0d56b4d27 | |||
| 38ccb6ace6 | |||
| aaf75f1de1 | |||
| 21e8ca0a2c |
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup
|
||||
- run: pnpm -r --sequential build
|
||||
- run: pnpm buildc all
|
||||
build-demo:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
@@ -33,9 +33,9 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup
|
||||
- uses: oven-sh/setup-bun@v1
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
- name: pnpm test:coverage
|
||||
run: pnpm test:coverage --reporter=default --reporter=hanging-process
|
||||
run: pnpm test:coverage -- --reporter=default --reporter=hanging-process
|
||||
- uses: codecov/codecov-action@v4
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
@@ -44,10 +44,11 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup
|
||||
- run: pnpm test run
|
||||
- run: pnpm test
|
||||
template:
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
template:
|
||||
- react
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
name: vhs
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'docs/tapes/*.tape'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
vhs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup
|
||||
with:
|
||||
install: 'false'
|
||||
- name: Preinstall WXT
|
||||
run: |
|
||||
pnpm store add wxt@latest
|
||||
pnpm dlx wxt@latest --version
|
||||
- uses: charmbracelet/vhs-action@v2.1.0
|
||||
with:
|
||||
path: 'docs/tapes/init-demo.tape'
|
||||
- uses: stefanzweifel/git-auto-commit-action@v4
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
commit_message: 'docs: Update `wxt init` GIF'
|
||||
# https://github.com/charmbracelet/vhs#output
|
||||
file_pattern: 'docs/assets/*.gif'
|
||||
@@ -21,3 +21,4 @@ docs/api/reference
|
||||
stats.html
|
||||
.tool-versions
|
||||
.cache
|
||||
*-stats.txt
|
||||
|
||||
+17
-2
@@ -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="./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,22 +18,22 @@
|
||||
<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">
|
||||
<a href="https://wxt.dev" target="_blank">Get Started</a>
|
||||
•
|
||||
<a href="https://wxt.dev/guide/installation.html" target="_blank">Installation</a>
|
||||
<a href="https://wxt.dev/guide/installation.html" target="_blank">Get Started</a>
|
||||
•
|
||||
<a href="https://wxt.dev/api/config.html" target="_blank">Configuration</a>
|
||||
•
|
||||
<a href="https://wxt.dev/examples.html" target="_blank">Examples</a>
|
||||
•
|
||||
<a href="https://github.com/wxt-dev/wxt/blob/main/packages/wxt/CHANGELOG.md" target="_blank">Changelog</a>
|
||||
•
|
||||
<a href="https://discord.gg/ZFsZqGery9" target="_blank">Discord</a>
|
||||
</p>
|
||||
|
||||

|
||||

|
||||
|
||||
## Demo
|
||||
|
||||
@@ -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,12 +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
|
||||
|
||||
## 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 -->
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
tag?: string;
|
||||
}>();
|
||||
|
||||
const examples = ref();
|
||||
onMounted(async () => {
|
||||
const res = await fetch(
|
||||
'https://raw.githubusercontent.com/wxt-dev/wxt-examples/main/examples.json',
|
||||
);
|
||||
examples.value = await res.json();
|
||||
});
|
||||
|
||||
const filteredExamples = computed(() => {
|
||||
if (props.tag == null) return examples.value;
|
||||
|
||||
return examples.value.filter((example) => {
|
||||
return example.tags?.includes(props.tag);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ul>
|
||||
<li v-if="examples == null">Loading...</li>
|
||||
<template v-else>
|
||||
<li v-for="example of filteredExamples">
|
||||
<a :href="example.url" target="_blank">{{ example.name }}</a>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</template>
|
||||
@@ -0,0 +1,226 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, computed, toRaw, Ref } from 'vue';
|
||||
import ExampleSearchFilterByItem from './ExampleSearchFilterByItem.vue';
|
||||
import ExampleSearchResult from './ExampleSearchResult.vue';
|
||||
import { ExamplesMetadata, KeySelectedObject } from '../utils/types';
|
||||
|
||||
const props = defineProps<{
|
||||
tag?: string;
|
||||
}>();
|
||||
|
||||
const exampleMetadata = ref<ExamplesMetadata>();
|
||||
onMounted(async () => {
|
||||
const res = await fetch(
|
||||
'https://raw.githubusercontent.com/wxt-dev/examples/main/metadata.json',
|
||||
);
|
||||
exampleMetadata.value = await res.json();
|
||||
});
|
||||
|
||||
const searchText = ref('');
|
||||
const selectedApis = ref<KeySelectedObject>({});
|
||||
const selectedPermissions = ref<KeySelectedObject>({});
|
||||
const selectedPackages = ref<KeySelectedObject>({});
|
||||
|
||||
function useRequiredItems(selectedItems: Ref<KeySelectedObject>) {
|
||||
return computed(() =>
|
||||
Array.from(
|
||||
Object.entries(toRaw(selectedItems.value)).reduce(
|
||||
(set, [pkg, checked]) => {
|
||||
if (checked) set.add(pkg);
|
||||
return set;
|
||||
},
|
||||
new Set<string>(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
const requiredApis = useRequiredItems(selectedApis);
|
||||
const requiredPermissions = useRequiredItems(selectedPermissions);
|
||||
const requiredPackages = useRequiredItems(selectedPackages);
|
||||
|
||||
function doesExampleMatchSelected(
|
||||
exampleItems: string[],
|
||||
requiredItems: Ref<string[]>,
|
||||
) {
|
||||
const exampleItemsSet = new Set(exampleItems);
|
||||
return !requiredItems.value.find((item) => !exampleItemsSet.has(item));
|
||||
}
|
||||
|
||||
const filteredExamples = computed(() => {
|
||||
const text = searchText.value.toLowerCase();
|
||||
return exampleMetadata.value.examples.filter((example) => {
|
||||
const matchesText = example.searchText.toLowerCase().includes(text);
|
||||
const matchesApis = doesExampleMatchSelected(example.apis, requiredApis);
|
||||
const matchesPermissions = doesExampleMatchSelected(
|
||||
example.permissions,
|
||||
requiredPermissions,
|
||||
);
|
||||
const matchesPackages = doesExampleMatchSelected(
|
||||
example.packages,
|
||||
requiredPackages,
|
||||
);
|
||||
return matchesText && matchesApis && matchesPermissions && matchesPackages;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="example-layout">
|
||||
<div class="search">
|
||||
<input v-model="searchText" placeholder="Search for an example..." />
|
||||
</div>
|
||||
|
||||
<div class="filters">
|
||||
<ExampleSearchFilterByItem
|
||||
label="APIs"
|
||||
:items="exampleMetadata?.allApis"
|
||||
v-model="selectedApis"
|
||||
/>
|
||||
<ExampleSearchFilterByItem
|
||||
label="Permissions"
|
||||
:items="exampleMetadata?.allPermissions"
|
||||
v-model="selectedPermissions"
|
||||
/>
|
||||
<ExampleSearchFilterByItem
|
||||
label="Packages"
|
||||
:items="exampleMetadata?.allPackages"
|
||||
v-model="selectedPackages"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="results">
|
||||
<p v-if="exampleMetadata == null">Loading examples...</p>
|
||||
<template v-else>
|
||||
<ul class="search-results">
|
||||
<ExampleSearchResult
|
||||
v-for="example of filteredExamples"
|
||||
:key="example.name"
|
||||
:example
|
||||
/>
|
||||
</ul>
|
||||
<p v-if="filteredExamples.length === 0">No matching examples</p>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.example-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: 1fr;
|
||||
grid-template-areas:
|
||||
'search'
|
||||
'results';
|
||||
gap: 16px;
|
||||
}
|
||||
@media only screen and (min-width: 720px) {
|
||||
.example-layout {
|
||||
grid-template-columns: 256px 1fr;
|
||||
grid-template-rows: auto 1fr;
|
||||
grid-template-areas:
|
||||
'filters search'
|
||||
'filters results';
|
||||
}
|
||||
}
|
||||
.search {
|
||||
grid-area: search;
|
||||
background: var(--vp-c-bg-soft);
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
border-radius: 16px;
|
||||
}
|
||||
.filters {
|
||||
display: none;
|
||||
grid-area: filters;
|
||||
}
|
||||
@media only screen and (min-width: 720px) {
|
||||
.filters {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
align-self: flex-start;
|
||||
}
|
||||
}
|
||||
.results {
|
||||
grid-area: results;
|
||||
}
|
||||
|
||||
.box {
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.search input {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.checkbox-col {
|
||||
flex: 1;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
max-height: 200px;
|
||||
font-size: 14px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.filter-btn {
|
||||
color: var(--vp-c-brand-1);
|
||||
}
|
||||
|
||||
.checkbox-col .header {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
opacity: 50%;
|
||||
}
|
||||
|
||||
.checkbox-col p {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: flex-start;
|
||||
text-wrap: wrap;
|
||||
overflow-wrap: anywhere;
|
||||
line-height: 140%;
|
||||
}
|
||||
|
||||
span {
|
||||
padding-top: 1px;
|
||||
}
|
||||
|
||||
.checkbox-col input[type='checkbox'] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.checkbox-col-container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.search-results {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(1, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
@media only screen and (min-width: 800px) {
|
||||
.search-results {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
@media only screen and (min-width: 1024px) {
|
||||
.search-results {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
background-color: red;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,100 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, toRaw } from 'vue';
|
||||
import { KeySelectedObject } from '../utils/types';
|
||||
|
||||
const props = defineProps<{
|
||||
label: string;
|
||||
items?: string[];
|
||||
}>();
|
||||
|
||||
const selectedItems = defineModel<KeySelectedObject>({
|
||||
required: true,
|
||||
});
|
||||
|
||||
const count = computed(() => {
|
||||
return Object.values(toRaw(selectedItems.value)).filter(Boolean).length;
|
||||
});
|
||||
|
||||
function toggleItem(pkg: string) {
|
||||
selectedItems.value = {
|
||||
...toRaw(selectedItems.value),
|
||||
[pkg]: !selectedItems.value[pkg],
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="filter-container">
|
||||
<p class="header">
|
||||
<span>Filter by {{ label }}</span> <span v-if="count">({{ count }})</span>
|
||||
</p>
|
||||
<div class="scroll-container">
|
||||
<ul>
|
||||
<li v-for="item in items">
|
||||
<label :title="item">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="selectedItems[item]"
|
||||
@input="toggleItem(item)"
|
||||
/>
|
||||
<span>{{ item }}</span>
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.filter-container {
|
||||
height: 300px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--vp-c-bg-soft);
|
||||
}
|
||||
|
||||
.scroll-container {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.scroll-container ul {
|
||||
position: absolute;
|
||||
overflow-y: auto;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
font-size: small;
|
||||
padding: 8px 16px 16px 16px;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 8px 16px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
opacity: 50%;
|
||||
}
|
||||
label {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: flex-start;
|
||||
text-wrap: wrap;
|
||||
overflow-wrap: anywhere;
|
||||
line-height: 140%;
|
||||
cursor: pointer;
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
span {
|
||||
padding-top: 1px;
|
||||
}
|
||||
input[type='checkbox'] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,72 @@
|
||||
<script lang="ts" setup>
|
||||
import { Example } from '../utils/types';
|
||||
|
||||
const props = defineProps<{
|
||||
example: Example;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li>
|
||||
<a :href="example.url" target="_blank">
|
||||
<p class="name">{{ example.name }}</p>
|
||||
<p class="description">{{ example.description }}</p>
|
||||
<p class="link">Open →</p>
|
||||
</a>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
* {
|
||||
min-width: 0;
|
||||
}
|
||||
a {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 2px solid var(--vp-c-bg-soft);
|
||||
border-radius: 16px;
|
||||
color: var(--vp-c-text-1) !important;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
outline: 2px solid var(--vp-c-brand-2);
|
||||
}
|
||||
.name {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex-shrink: 0;
|
||||
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.description {
|
||||
height: 53px;
|
||||
opacity: 70%;
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
line-height: 120%;
|
||||
min-height: 0;
|
||||
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.link {
|
||||
opacity: 0;
|
||||
transition: 250ms;
|
||||
color: var(--vp-c-brand-2);
|
||||
font-weight: bold;
|
||||
text-align: right;
|
||||
}
|
||||
a:hover .link {
|
||||
opacity: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -29,6 +29,17 @@ const chromeExtensionIds = [
|
||||
'nedcanggplmbbgmlpcjiafgjcpdimpea', // YTBlock - Block any content from YouTube™
|
||||
'oadbjpccljkplmhnjekgjamejnbadlne', // demo.fun - Interactive product demos that convert
|
||||
'iopdafdcollfgaoffingmahpffckmjni', // SmartEReply: Elevate Your LinkedIn™ Engagement with AI 🚀📈
|
||||
'khjdmjcmpolknpccmaaipmidphjokhdf', // WorkFlowy MultiFlow
|
||||
'fencadnndhdeggodopebjgdfdlhcimfk', // 香草布丁🌿🍮- https://github.com/Xdy1579883916/vanilla-pudding
|
||||
'bnacincmbaknlbegecpioobkfgejlojp', // MaxFocus: Link Preview
|
||||
'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
|
||||
];
|
||||
|
||||
const { data, err, isLoading } = useListExtensionDetails(chromeExtensionIds);
|
||||
|
||||
@@ -78,7 +78,6 @@ export default defineConfig({
|
||||
menuItem('Configuration', 'configuration'),
|
||||
menuItem('Entrypoints', 'entrypoints'),
|
||||
menuItem('Assets', 'assets'),
|
||||
menuItem('Testing', 'testing'),
|
||||
menuItem('Publishing', 'publishing'),
|
||||
menuItem('Migrate to WXT', 'migrate-to-wxt'),
|
||||
menuItem('Compare', 'compare'),
|
||||
@@ -122,6 +121,7 @@ export default defineConfig({
|
||||
|
||||
// Files
|
||||
menuItem('.env', 'env'),
|
||||
menuItem('app.config.ts', 'app-config'),
|
||||
menuItem('package.json', 'package'),
|
||||
menuItem('tsconfig.json', 'tsconfig'),
|
||||
menuItem('web-ext.config.ts', 'web-ext-config'),
|
||||
@@ -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,9 +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/', [
|
||||
|
||||
@@ -2,7 +2,7 @@ import DefaultTheme from 'vitepress/theme';
|
||||
import Icon from '../components/Icon.vue';
|
||||
import EntrypointPatterns from '../components/EntrypointPatterns.vue';
|
||||
import UsingWxtSection from '../components/UsingWxtSection.vue';
|
||||
import ExampleList from '../components/ExampleList.vue';
|
||||
import ExampleSearch from '../components/ExampleSearch.vue';
|
||||
import './custom.css';
|
||||
|
||||
export default {
|
||||
@@ -11,6 +11,6 @@ export default {
|
||||
ctx.app.component('Icon', Icon);
|
||||
ctx.app.component('EntrypointPatterns', EntrypointPatterns);
|
||||
ctx.app.component('UsingWxtSection', UsingWxtSection);
|
||||
ctx.app.component('ExampleList', ExampleList);
|
||||
ctx.app.component('ExampleSearch', ExampleSearch);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
export interface Example {
|
||||
name: string;
|
||||
description?: string;
|
||||
url: string;
|
||||
searchText: string;
|
||||
apis: string[];
|
||||
permissions: string[];
|
||||
packages: string[];
|
||||
}
|
||||
|
||||
export type ExamplesMetadata = {
|
||||
examples: Example[];
|
||||
allApis: string[];
|
||||
allPermissions: string[];
|
||||
allPackages: string[];
|
||||
};
|
||||
|
||||
export type KeySelectedObject = Record<string, boolean | undefined>;
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 137 KiB |
+16
-4
@@ -1,7 +1,19 @@
|
||||
# Examples
|
||||
---
|
||||
layout: page
|
||||
---
|
||||
|
||||
Simple walkthroughs to accomplish common tasks or patterns with WXT.
|
||||
<style>
|
||||
.examples-container {
|
||||
padding: 32px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<ExampleList />
|
||||
<div class="examples-container">
|
||||
<div class="vp-doc">
|
||||
<h1>Examples</h1>
|
||||
</div>
|
||||
|
||||
> Full code available at [`wxt-dev/wxt-examples`](https://github.com/wxt-dev/wxt-examples)
|
||||
<br />
|
||||
|
||||
<ExampleSearch />
|
||||
</div>
|
||||
|
||||
+41
-27
@@ -4,30 +4,44 @@ Lets compare the features of WXT vs [Plasmo](https://docs.plasmo.com/framework)
|
||||
|
||||
## Overview
|
||||
|
||||
| Features | WXT | Plasmo | CRXJS |
|
||||
| ---------------------------------------------------- | :-------------------------: | :--------------------------------------: | :---------------------------------------------------------------------: |
|
||||
| Supports all browsers | ✅ | ✅ | ❌ See [#56](https://github.com/crxjs/chrome-extension-tools/issues/56) |
|
||||
| MV2 Support | ✅ | ✅ | 🟡 Either MV2 or MV3 |
|
||||
| MV3 Support | ✅ | ✅ | 🟡 Either MV2 or MV3 |
|
||||
| Create Extension ZIPs | ✅ | ✅ | ❌ |
|
||||
| Create Firefox Sources ZIP | ✅ | ❌ | ❌ |
|
||||
| First-class TypeScript support | ✅ | ✅ | ✅ |
|
||||
| Entrypoint discovery | File based | File based | ❌ |
|
||||
| Inline entrypoint config | ✅ | ✅ | Manifest based |
|
||||
| Auto-imports | ✅ | ❌ | ❌ |
|
||||
| Supports all frontend frameworks | ✅ | 🟡 Only React, Vue, and Svelte | ✅ |
|
||||
| Framework specific entrypoints (like `Popup.tsx`) | 🟡 `.html` `.ts` `.tsx` | ✅ `.html` `.ts` `.tsx` `.vue` `.svelte` | ❌ |
|
||||
| Automated publishing | ✅ | ✅ | ❌ |
|
||||
| Remote Code Bundling (Google Analytics) | ✅ | ✅ | ❌ |
|
||||
| <strong style="opacity: 50%">Dev Mode</strong> | | |
|
||||
| `.env` Files | ✅ | ✅ | ✅ |
|
||||
| Opens browser and install extension | ✅ | ❌ | ❌ |
|
||||
| HMR for UIs | ✅ | 🟡 React only | ✅ |
|
||||
| Reload HTML Files on Change | ✅ | 🟡 Reloads entire extension | ✅ |
|
||||
| Reload Content Scripts on Change | ✅ | 🟡 Reloads entire extension | ✅ |
|
||||
| Reload Background on Change | 🟡 Reloads entire extension | 🟡 Reloads entire extension | 🟡 Reloads entire extension |
|
||||
| Respects Content Script `run_at` | ✅ | ✅ | ❌ ESM-style loaders run asynchronously |
|
||||
| <strong style="opacity: 50%">Built-in Utils</strong> | | | |
|
||||
| Storage | ✅ | ✅ | ❌ |
|
||||
| Messaging | ❌ | ✅ | ❌ |
|
||||
| Content Script UI | ✅ | ✅ | ❌ |
|
||||
| Features | WXT | Plasmo | CRXJS |
|
||||
| ---------------------------------------------------- | :--------------: | :-------------: | :--------------: |
|
||||
| Supports all browsers | ✅ | ✅ | 🟡 <sup>10</sup> |
|
||||
| MV2 Support | ✅ | ✅ | 🟡 <sup>1</sup> |
|
||||
| MV3 Support | ✅ | ✅ | 🟡 <sup>1</sup> |
|
||||
| Create Extension ZIPs | ✅ | ✅ | ❌ |
|
||||
| Create Firefox Sources ZIP | ✅ | ❌ | ❌ |
|
||||
| First-class TypeScript support | ✅ | ✅ | ✅ |
|
||||
| Entrypoint discovery | ✅ <sup>2</sup> | ✅ <sup>2</sup> | ❌ |
|
||||
| Inline entrypoint config | ✅ | ✅ | ❌ <sup>9</sup> |
|
||||
| Auto-imports | ✅ | ❌ | ❌ |
|
||||
| Supports all frontend frameworks | ✅ | 🟡 <sup>3</sup> | ✅ |
|
||||
| Framework specific entrypoints (like `Popup.tsx`) | 🟡 <sup>4</sup> | ✅ <sup>5</sup> | ❌ |
|
||||
| Automated publishing | ✅ | ✅ | ❌ |
|
||||
| Remote Code Bundling (Google Analytics) | ✅ | ✅ | ❌ |
|
||||
| <strong style="opacity: 50%">Dev Mode</strong> | | |
|
||||
| `.env` Files | ✅ | ✅ | ✅ |
|
||||
| Opens browser with extension installed | ✅ | ❌ | ❌ |
|
||||
| HMR for UIs | ✅ | 🟡 <sup>6</sup> | ✅ |
|
||||
| Reload HTML Files on Change | ✅ | 🟡 <sup>7</sup> | ✅ |
|
||||
| Reload Content Scripts on Change | ✅ | 🟡 <sup>7</sup> | ✅ |
|
||||
| Reload Background on Change | 🟡 <sup>7</sup> | 🟡 <sup>7</sup> | 🟡 <sup>7</sup> |
|
||||
| Respects Content Script `run_at` | ✅ | ✅ | ❌ <sup>8</sup> |
|
||||
| <strong style="opacity: 50%">Built-in Utils</strong> | | | |
|
||||
| Storage | ✅ | ✅ | ❌ <sup>11</sup> |
|
||||
| Messaging | ❌ <sup>11</sup> | ✅ | ❌ <sup>11</sup> |
|
||||
| Content Script UI | ✅ | ✅ | ❌ <sup>11</sup> |
|
||||
|
||||
<small>
|
||||
<sup>1</sup>: Either MV2 or MV3, not both.
|
||||
<br/><sup>2</sup>: File based.
|
||||
<br/><sup>3</sup>: Only React, Vue, and Svelte.
|
||||
<br/><sup>4</sup>: <code>.html</code> <code>.ts</code> <code>.tsx</code>.
|
||||
<br/><sup>5</sup>: <code>.html</code> <code>.ts</code> <code>.tsx</code>. <code>.vue</code> <code>.svelte</code>.
|
||||
<br/><sup>6</sup>: React only.
|
||||
<br/><sup>7</sup>: Reloads entire extension.
|
||||
<br/><sup>8</sup>: ESM-style loaders run asynchronously.
|
||||
<br/><sup>9</sup>: Entrypoint options all configured in `manifest.json`.
|
||||
<br/><sup>10</sup>: As of <code>v2.0.0-beta.23</code>, but v2 stable hasn't been released yet.
|
||||
<br/><sup>11</sup>: There is no built-in wrapper around this API. However, you can still access the standard APIs via <code>chrome</code>/<code>browser</code> globals or use any 3rd party NPM package.
|
||||
</small>
|
||||
|
||||
@@ -17,7 +17,7 @@ In WXT, you create an entrypoint by adding a file to the `entrypoints/` director
|
||||
|
||||
Some entrypoint filesname patterns are reserved by WXT and effect how the manifest is generated.
|
||||
|
||||
- `popup` adds a `action` to the manifest
|
||||
- `popup` adds an `action` to the manifest
|
||||
- `background` adds a background script/service worker
|
||||
- `*.content.ts` adds a content script
|
||||
- ...
|
||||
|
||||
@@ -15,9 +15,20 @@ npx wxt@latest init <project-name>
|
||||
```
|
||||
|
||||
```sh [bun]
|
||||
bunx wxt@latest init <project-name>
|
||||
# The "wxt init" command currently fails when ran with bunx.
|
||||
# Use NPX as a workaround, and select "bun" as your package
|
||||
# manager. To stay up to date with this issue, follow
|
||||
# https://github.com/wxt-dev/wxt/issues/707
|
||||
#
|
||||
# bunx wxt@latest init <project-name>
|
||||
|
||||
npx wxt@latest init <project-name>
|
||||
```
|
||||
|
||||
### Demo
|
||||
|
||||

|
||||
|
||||
:::
|
||||
|
||||
There are several starting templates available.
|
||||
@@ -96,7 +107,6 @@ pnpm dev
|
||||
```
|
||||
|
||||
:::tip 🎉 Well done!
|
||||
|
||||
The dev command will build the extension for development, open the browser, and reload the different parts of the extension when you save changes.
|
||||
:::
|
||||
|
||||
@@ -107,4 +117,4 @@ You're ready to build your web extension!
|
||||
- Read the rest of the "Get Started" pages for a high-overview of what WXT can do
|
||||
- Read the [Guide](/guide/key-concepts/manifest) to learn in-depth about each feature WXT supports
|
||||
- [Configure WXT](./configuration) by creating a `wxt.config.ts` file
|
||||
- Checkout [example projects](https://github.com/wxt-dev/wxt-examples) to see how to perform common tasks with WXT
|
||||
- Checkout [example projects](https://github.com/wxt-dev/examples) to see how to perform common tasks with WXT
|
||||
|
||||
@@ -9,9 +9,9 @@ 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 of [Nuxt](https://nuxt.com), and aims to provide the same great DX with TypeScript, auto-imports, and an opinionated project structure.
|
||||
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.
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ In general, you'll need to:
|
||||
 <input type="checkbox" /> Convert custom import syntax to be compatible with Vite<br />
|
||||
 <input type="checkbox" /> Add a default export to JS entrypoints<br />
|
||||
 <input type="checkbox" /> Use the `browser` global instead of `chrome`<br />
|
||||
 <input type="checkbox" /> Compare final `manifest.json` files, making sure permissions and host permissions are unchanged<br />
|
||||
 <input type="checkbox" /> Compare final `manifest.json` files, making sure permissions and host permissions are unchanged. Use [Google's update testing tool](https://github.com/GoogleChromeLabs/extension-update-testing-tool) if your extension is already live on the Chrome Web Store<br />
|
||||
 <input type="checkbox" /> Extension output by `wxt build` works the same way as before the migration<br />
|
||||
|
||||
Every project is different, so there's no one-solution-fits-all to migrating your project. Just make sure `wxt dev` runs, `wxt build` results in a working extension, and the list of permissions in the `manifest.json` hasn't changed. If all that looks good, you've finished migrating your extension!
|
||||
|
||||
@@ -176,6 +176,8 @@ bun zip:firefox
|
||||
|
||||
:::
|
||||
|
||||
Ensure that you have a `README.md` or `SOURCE_CODE_REVIEW.md` file with the above commands so that the Firefox team knows how to build your extension.
|
||||
|
||||
Make sure the build output is the exact same when running `wxt build -b firefox` in your main project and inside the zipped sources.
|
||||
|
||||
:::warning
|
||||
@@ -184,8 +186,6 @@ If you use a `.env` files, they can effect the chunk hashes in the output direct
|
||||
See Issue [#377](https://github.com/wxt-dev/wxt/issues/377) for more details.
|
||||
:::
|
||||
|
||||
Ensure that you have a `README.md` or `SOURCE_CODE_REVIEW.md` file with the above commands so that the Firefox team knows how to build your extension.
|
||||
|
||||
#### Private Packages
|
||||
|
||||
If you use private packages and you don't want to provide your auth token to the Firefox team during the review process, you can use `zip.downloadPackages` to download any private packages and include them in the zip.
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
# Testing
|
||||
|
||||
## Official Frameworks
|
||||
|
||||
WXT officially supports [Vitest](https://vitest.dev/) for unit tests and either [Playwright](https://playwright.dev/) or [Puppeteer](https://pptr.dev/) for E2E tests against Chromium browsers.
|
||||
|
||||
For details setting up each testing framework, see the official examples:
|
||||
|
||||
<ExampleList tag="testing" />
|
||||
|
||||
## Unofficial Frameworks
|
||||
|
||||
Puppeteer and Playwright are the only E2E test runners that support Chrome Extensions. There are no other options at the time of writing.
|
||||
|
||||
There are other options for unit tests however, like [Jest](https://jestjs.io/), [Mocha](https://mochajs.org/), or [`node:test`](https://nodejs.org/api/test.html). **_WXT does not claim to support any of them_** because none of them support all of WXT's features, like TypeScript or auto-imports.
|
||||
|
||||
If you want to try to use a different framework for unit tests, you will need to configure the environment manually:
|
||||
|
||||
- **Auto-imports**: Add `unimport` to your test environment or disable them by setting `imports: false` in your `wxt.config.ts` file
|
||||
- **`browser` mock**: Mock the `webextension-polyfill` module globally with `wxt/dist/virtual/mock-browser.js`
|
||||
- **[Remote Code Bundling](/guide/go-further/remote-code)**: If you use it, configure your environment to handle the `url:` module prefix
|
||||
- **Global Variables**: If you consume them, manually define globals provided by WXT (like `import.meta.env.BROWSER`) by adding them to the global scope before accessing them (`import.meta.env.BROWSER = "chrome"`)
|
||||
- **Import paths**: If you use the `@/` or `~/` path aliases, add them to your test environment
|
||||
|
||||
[Here's how Vitest is configured](https://github.com/wxt-dev/wxt/blob/main/packages/wxt/src/testing/wxt-vitest-plugin.ts) for reference.
|
||||
@@ -0,0 +1,61 @@
|
||||
# `<srcDir>/app.config.ts`
|
||||
|
||||
:::warning Nuxt Users
|
||||
If you're familiar with Nuxt, this file is meant to be a direct equivalent to Nuxt's `app.config.ts` file.
|
||||
|
||||
However, some of Nuxt's features, like overriding the app config based on a `.env` file or automatically generating the config's types, are not implemented. They are planned, just not implemented yet. Feel free to open a PR!
|
||||
:::
|
||||
|
||||
## Overview
|
||||
|
||||
Define runtime configuration in a single place.
|
||||
|
||||
```ts
|
||||
// <srcDir>/app.config.ts
|
||||
import { defineAppConfig } from 'wxt/sandbox';
|
||||
|
||||
// Define types for your config
|
||||
declare module 'wxt/sandbox' {
|
||||
export interface WxtAppConfig {
|
||||
theme?: 'light' | 'dark';
|
||||
}
|
||||
}
|
||||
|
||||
export default defineAppConfig({
|
||||
theme: 'dark',
|
||||
});
|
||||
```
|
||||
|
||||
Then access the config in your extension by calling `useAppConfig`:
|
||||
|
||||
```ts
|
||||
console.log(useAppConfig()); // { theme: "dark" }
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
If you have a `.env` file, you can access any variables defined in it here. You can convert them to better types (like booleans), add types for them, or leave them as is.
|
||||
|
||||
```txt
|
||||
# .env
|
||||
VITE_BUG_REPORTING_DISABLED=true
|
||||
VITE_API_KEY=...
|
||||
```
|
||||
|
||||
```ts
|
||||
// <srcDir>/app.config.ts
|
||||
|
||||
declare module 'wxt/sandbox' {
|
||||
export interface WxtAppConfig {
|
||||
bugReportingDisabled: boolean;
|
||||
apiKey?: string;
|
||||
}
|
||||
}
|
||||
|
||||
export default defineAppConfig({
|
||||
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 `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,
|
||||
|
||||
@@ -37,4 +37,4 @@ Chrome extensions allow you to add panels and side panes to the devtools window.
|
||||
|
||||
See the WXT's examples for a full walkthrough of extending the devtools window:
|
||||
|
||||
<ExampleList tag="devtools" />
|
||||
- [Devtools Setup](https://github.com/wxt-dev/wxt-examples/tree/main/examples/vanilla-devtools#readme)
|
||||
|
||||
@@ -12,8 +12,8 @@ Firefox does not support sandboxed pages.
|
||||
:patterns="[
|
||||
['sandbox.html', 'sandbox.html'],
|
||||
['sandbox/index.html', 'sandbox.html'],
|
||||
['<name>.sandbox.html', '<name>.html` '],
|
||||
['<name>.sandbox/index.html', '<name>.html` '],
|
||||
['<name>.sandbox.html', '<name>.html'],
|
||||
['<name>.sandbox/index.html', '<name>.html'],
|
||||
]"
|
||||
/>
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -121,7 +121,7 @@ Storage items contain the same APIs as the `storage` variable, but you can confi
|
||||
const showChangelogOnUpdate = storage.defineItem<boolean>(
|
||||
'local:showChangelogOnUpdate',
|
||||
{
|
||||
defaultValue: true,
|
||||
fallback: true,
|
||||
},
|
||||
);
|
||||
```
|
||||
@@ -153,7 +153,7 @@ type IgnoredWebsiteV1 = string;
|
||||
export const ignoredWebsites = storage.defineItem<IgnoredWebsiteV1[]>(
|
||||
'local:ignoredWebsites',
|
||||
{
|
||||
defaultValue: [],
|
||||
fallback: [],
|
||||
version: 1,
|
||||
},
|
||||
);
|
||||
@@ -173,7 +173,7 @@ export const ignoredWebsites = storage.defineItem<IgnoredWebsiteV1[]>( // [!code
|
||||
export const ignoredWebsites = storage.defineItem<IgnoredWebsiteV2[]>( // [!code ++]
|
||||
'local:ignoredWebsites',
|
||||
{
|
||||
defaultValue: [],
|
||||
fallback: [],
|
||||
version: 1, // [!code --]
|
||||
version: 2, // [!code ++]
|
||||
migrations: { // [!code ++]
|
||||
@@ -205,7 +205,7 @@ export const ignoredWebsites = storage.defineItem<IgnoredWebsiteV2[]>( // [!code
|
||||
export const ignoredWebsites = storage.defineItem<IgnoredWebsiteV3[]>( // [!code ++]
|
||||
'local:ignoredWebsites',
|
||||
{
|
||||
defaultValue: [],
|
||||
fallback: [],
|
||||
version: 2, // [!code --]
|
||||
version: 3, // [!code ++]
|
||||
migrations: {
|
||||
@@ -242,7 +242,7 @@ Lets look at the same ignored websites example from before, but start with an un
|
||||
export const ignoredWebsites = storage.defineItem<string[]>(
|
||||
'local:ignoredWebsites',
|
||||
{
|
||||
defaultValue: [],
|
||||
fallback: [],
|
||||
},
|
||||
);
|
||||
```
|
||||
@@ -262,7 +262,7 @@ export const ignoredWebsites = storage.defineItem<string[]>( // [!code --]
|
||||
export const ignoredWebsites = storage.defineItem<IgnoredWebsiteV2[]>( // [!code ++]
|
||||
'local:ignoredWebsites',
|
||||
{
|
||||
defaultValue: [],
|
||||
fallback: [],
|
||||
version: 2, // [!code ++]
|
||||
migrations: { // [!code ++]
|
||||
// Ran when migrating from v1 to v2 // [!code ++]
|
||||
@@ -279,3 +279,35 @@ export const ignoredWebsites = storage.defineItem<IgnoredWebsiteV2[]>( // [!code
|
||||
### Running Migrations
|
||||
|
||||
As soon as `storage.defineItem` is called, WXT checks if migrations need to be ran, and if so, runs them. Calls to get or update the storage item's value or metadata (`getValue`, `setValue`, `removeValue`, `getMeta`, etc) will automatically wait for the migration process to finish before actually reading or writing values.
|
||||
|
||||
### Default Values
|
||||
|
||||
With `storage.defineItem`, there are multiple ways of defining default values:
|
||||
|
||||
1. `fallback` - Return this value from `getValue` instead of `null` if the value is missing.
|
||||
|
||||
This option is great for providing default values for settings:
|
||||
|
||||
```ts
|
||||
const theme = storage.defineItem('local:theme', {
|
||||
fallback: 'dark',
|
||||
});
|
||||
const allowEditing = storage.defineItem('local:allow-editing', {
|
||||
fallback: true,
|
||||
});
|
||||
```
|
||||
|
||||
2. `init` - Initialize and save a value in storage if it is not already saved.
|
||||
|
||||
This is great for values that need to be initialized or set once:
|
||||
|
||||
```ts
|
||||
const userId = storage.defineItem('local:user-id', {
|
||||
init: () => globalThis.crypto.randomUUID(),
|
||||
});
|
||||
const installDate = storage.defineItem('local:install-date', {
|
||||
init: () => new Date().getTime(),
|
||||
});
|
||||
```
|
||||
|
||||
The value is initialized in storage immediately.
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
# 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.
|
||||
|
||||
There are two options for loading your entrypoints:
|
||||
|
||||
1. `vite-node` - default as of `v0.19.0`
|
||||
2. `jiti` (**DEPRECATED, will be removed in `v0.20.0`**) - Default before `v0.19.0`
|
||||
|
||||
## vite-node
|
||||
|
||||
Since 0.19.0, WXT uses `vite-node`, the same tool that powers Vitest and Nuxt, to import your entrypoint files.
|
||||
|
||||
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
|
||||
|
||||
The original method WXT used to import TS files. However, because it doesn't support vite plugins like `vite-node`, there is one main caveot to it's usage: **_module side-effects_**.
|
||||
|
||||
To enable `jiti`:
|
||||
|
||||
```ts
|
||||
export default defineConfig({
|
||||
entrypointLoader: 'jiti',
|
||||
});
|
||||
```
|
||||
|
||||
You cannot use imported variables outside the `main` function in JS entrypoints. This includes options, as shown below:
|
||||
|
||||
```ts
|
||||
// entrypoints/content.ts
|
||||
import { GOOGLE_MATCHES } from '~/utils/match-patterns';
|
||||
|
||||
export default defineContentScript({
|
||||
matches: GOOGLE_MATCHES,
|
||||
main() {
|
||||
// ...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```
|
||||
$ wxt build
|
||||
wxt build
|
||||
|
||||
WXT 0.14.1
|
||||
ℹ Building chrome-mv3 for production with Vite 5.0.5
|
||||
✖ Command failed after 360 ms
|
||||
|
||||
[8:55:54 AM] ERROR entrypoints/content.ts: Cannot use imported variable "GOOGLE_MATCHES" before main function. See https://wxt.dev/guide/entrypoints.html#side-effects
|
||||
```
|
||||
|
||||
This throws an error because WXT needs to import each entrypoint during the build process to extract its definition (containing the `match`, `runAt`, `include`/`exclude`, etc.) to render the `manifest.json` correctly. Before loading an entrypoint, a transformation is applied to remove all imports. This prevents imported modules (local or NPM) with side-effects from running during the build process, potentially throwing an error.
|
||||
|
||||
:::details Why?
|
||||
|
||||
When importing your entrypoint to get its definition, the file is imported in a **_node environment_**, and doesn't have access to the `window`, `chrome`, or `browser` globals a web extension usually has access to. If WXT doesn't remove all the imports from the file, the imported modules could try and access one of these variables, throwing an error.
|
||||
|
||||
:::
|
||||
|
||||
:::warning
|
||||
See [`wxt-dev/wxt#336`](https://github.com/wxt-dev/wxt/issues/336) to track the status of this bug.
|
||||
:::
|
||||
|
||||
Usually, this error occurs when you try to extract options into a shared file or try to run code outside the `main` function. To fix the example from above, use literal values when defining an entrypoint instead of importing them:
|
||||
|
||||
```ts
|
||||
import { GOOGLE_MATCHES } from '~/utils/match-patterns'; // [!code --]
|
||||
|
||||
export default defineContentScript({
|
||||
matches: GOOGLE_MATCHES, // [!code --]
|
||||
matches: ['*//*.google.com/*'], // [!code ++]
|
||||
main() {
|
||||
// ...
|
||||
},
|
||||
});
|
||||
```
|
||||
@@ -1,52 +0,0 @@
|
||||
# Entrypoint Side Effects
|
||||
|
||||
You cannot use imported variables outside the `main` function in JS entrypoints. This includes options, as shown below:
|
||||
|
||||
```ts
|
||||
// entrypoints/content.ts
|
||||
import { GOOGLE_MATCHES } from '~/utils/match-patterns';
|
||||
|
||||
export default defineContentScript({
|
||||
matches: GOOGLE_MATCHES,
|
||||
main() {
|
||||
// ...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```
|
||||
$ wxt build
|
||||
wxt build
|
||||
|
||||
WXT 0.14.1
|
||||
ℹ Building chrome-mv3 for production with Vite 5.0.5
|
||||
✖ Command failed after 360 ms
|
||||
|
||||
[8:55:54 AM] ERROR entrypoints/content.ts: Cannot use imported variable "GOOGLE_MATCHES" before main function. See https://wxt.dev/guide/entrypoints.html#side-effects
|
||||
```
|
||||
|
||||
This throws an error because WXT needs to import each entrypoint during the build process to extract its definition (containing the `match`, `runAt`, `include`/`exclude`, etc.) to render the `manifest.json` correctly. Before loading an entrypoint, a transformation is applied to remove all imports. This prevents imported modules (local or NPM) with side-effects from running during the build process, potentially throwing an error.
|
||||
|
||||
:::details Why?
|
||||
|
||||
When importing your entrypoint to get its definition, the file is imported in a **_node environment_**, and doesn't have access to the `window`, `chrome`, or `browser` globals a web extension usually has access to. If WXT doesn't remove all the imports from the file, the imported modules could try and access one of these variables, throwing an error.
|
||||
|
||||
:::
|
||||
|
||||
:::warning
|
||||
See [`wxt-dev/wxt#336`](https://github.com/wxt-dev/wxt/issues/336) to track the status of this bug.
|
||||
:::
|
||||
|
||||
Usually, this error occurs when you try to extract options into a shared file or try to run code outside the `main` function. To fix the example from above, use literal values when defining an entrypoint instead of importing them:
|
||||
|
||||
```ts
|
||||
import { GOOGLE_MATCHES } from '~/utils/match-patterns'; // [!code --]
|
||||
|
||||
export default defineContentScript({
|
||||
matches: GOOGLE_MATCHES, // [!code --]
|
||||
matches: ['*//*.google.com/*'], // [!code ++]
|
||||
main() {
|
||||
// ...
|
||||
},
|
||||
});
|
||||
```
|
||||
@@ -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.
|
||||
|
||||
@@ -6,7 +6,7 @@ When releasing an update to your extension, there's a couple of things you need
|
||||
|
||||
## Content Script Cleanup
|
||||
|
||||
Old content scripts are not automatically stopped when an extension updates and reloads. Often, this leads to "Invalidated context" errors in production when a content script from an old version of your extension tries to use a web extension API (ie, the `browser` or `chrome` globals).
|
||||
Old content scripts are not automatically stopped when an extension updates and reloads. Often, this leads to "Invalidated context" errors in production when a content script from an old version of your extension tries to use an extension API.
|
||||
|
||||
WXT provides a utility for handling this process: `ContentScriptContext`. An instance of this class is provided to you automatically inside the `main` function of each content script.
|
||||
|
||||
@@ -51,33 +51,13 @@ This prevents old content scripts from interfering with new content scripts, and
|
||||
|
||||
## Testing Permission Changes
|
||||
|
||||
When `permissions`/`host_permissions` change during an update, depending on what exactly changed, Chrome will disable your extension until the user accepts the new permissions.
|
||||
When `permissions`/`host_permissions` change during an update, depending on what exactly changed, the browser will disable your extension until the user accepts the new permissions.
|
||||
|
||||
It is possible to test this before you release an update, but it's not a simple process:
|
||||
You can test if your permission changes will result in a disabled extension:
|
||||
|
||||
1. Get 2 ZIPs of your extension, both generated by `wxt zip`. The first contains a previous version of your extension, the second contains the latest version. Make sure the second ZIP's version is higher than the first's.
|
||||
2. Unzip the two ZIP files somewhere next to each other that's easy to locate.
|
||||
3. In Chrome, open `chrome://extensions` and make sure developer mode is enabled
|
||||
4. Pack the first extension into a CRX, generating a new private key:
|
||||
1. Click "Pack Extension" in the top left
|
||||
2. For "Extension root directory", enter the path to the first extracted zip directory. The directory should contain a `manifest.json` file
|
||||
3. Leave "Private key file" blank
|
||||
4. Click "Pack Extension". This will generate a `.crx` and `.pem` file
|
||||
5. Pack the second extension into a CRX, reusing the private key generated by the previous step
|
||||
1. Click "Pack Extension" in the top left
|
||||
2. For "Extension root directory", enter the path to the second extracted zip directory.
|
||||
3. For "Private key file", enter the path to the generated `.pem` private key file
|
||||
4. Click "Pack Extension". This will generate a second `.crx` file.
|
||||
6. Install the first CRX file by dragging and dropping it onto the `chrome://extensions` page
|
||||
7. Install the second CRX file by dragging and dropping it onthe the `chrome://extensions` page
|
||||
|
||||
If a new permission must be accepted, you'll be prompted to accept it after dropping the second CRX file onto the page.
|
||||
|
||||
:::Info Note
|
||||
Note: Chrome no longer allows self-signed CRX extensions to run, but that's OK for this test case. We're still prompted to accept new permissions, even if we can't interact with the installed extension.
|
||||
|
||||
To validate this, you can create a third ZIP file with a rare permission like `geolocation` in the manifest, that's guarenteed to reprompt permissions when added.
|
||||
:::
|
||||
- Chromium: Use [Google's Extension Update Testing tool](https://github.com/GoogleChromeLabs/extension-update-testing-tool)
|
||||
- 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
|
||||
|
||||
@@ -94,23 +74,4 @@ browser.runtime.onInstalled.addListener(({ reason }) => {
|
||||
If the logic is simple, write a unit test to cover this logic. If you feel the need to manually test this callback, you can either:
|
||||
|
||||
1. In dev mode, remove the `if` statement and reload the extension from `chrome://extensions`
|
||||
2. Build two ZIPs with the same runtime ID and actually update the extension
|
||||
|
||||
The first approach is very straightforward. The second is more complicated...
|
||||
|
||||
Here are the steps:
|
||||
|
||||
So the steps:
|
||||
|
||||
1. Checkout an old commit.
|
||||
2. [Add a `key`](https://developer.chrome.com/docs/extensions/reference/manifest/key#keep-consistent-id) to the `manifest` in your `wxt.config.ts`.
|
||||
3. Run `wxt zip` to create the first ZIP.
|
||||
4. Stash or reset changes and checkout your latest code.
|
||||
5. Add back the same `key` to your manifest.
|
||||
6. Make sure the extension's version is higher than the first zip. It can be any version that's higher, since you won't be releasing this version.
|
||||
7. Run `wxt zip` to create the second ZIP.
|
||||
8. In a fresh chrome profile, go to `chrome://extensions`, enable dev mode, and drag and drop the first zip onto the page to install it.
|
||||
9. In the extension, play around and setup your test case.
|
||||
10. Back on `chrome://extensions`, drag and drop your second zip onto the page.
|
||||
|
||||
If you setup the `key` correctly, it will cause the extension to act like it was updated instead of installing a second version of your extension.
|
||||
2. Use [Google's Extension Update Testing tool](https://github.com/GoogleChromeLabs/extension-update-testing-tool)
|
||||
|
||||
@@ -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: {},
|
||||
});
|
||||
},
|
||||
@@ -176,7 +176,7 @@ export default defineWxtPlugin(() => {
|
||||
:::
|
||||
|
||||
:::warning Async Plugins
|
||||
Unlike modules, **_plugins cannot be async_**!! If you need to do some async work and expose that result to the rest of the extension, wrap it in an object and handle awaiting the promise internally.
|
||||
Unlike modules, **_plugins cannot be async_**!! If you need to do some async work and expose that result to the rest of the extension, store the result's promise synchronously and await it later on.
|
||||
:::
|
||||
|
||||
## Publishing to NPM
|
||||
|
||||
@@ -6,7 +6,9 @@ WXT officially supports [Vitest](https://vitest.dev/) for unit tests and either
|
||||
|
||||
For details setting up each testing framework, see the official examples:
|
||||
|
||||
<ExampleList tag="testing" />
|
||||
- [Vitest](https://github.com/wxt-dev/wxt-examples/tree/main/examples/vanilla-vitest#readme)
|
||||
- [Playwright](https://github.com/wxt-dev/wxt-examples/tree/main/examples/vanilla-playwright#readme)
|
||||
- [Puppeteer](https://github.com/wxt-dev/wxt-examples/tree/main/examples/vanilla-puppeteer#readme)
|
||||
|
||||
### Unofficial Frameworks
|
||||
|
||||
@@ -17,7 +19,7 @@ There are other options for unit tests however, like [Jest](https://jestjs.io/),
|
||||
If you want to try to use a different framework for unit tests, you will need to configure the environment manually:
|
||||
|
||||
- **Auto-imports**: Add `unimport` to your test environment or disable them by setting `imports: false` in your `wxt.config.ts` file
|
||||
- **`browser` mock**: Mock the `webextension-polyfill` module globally with `wxt/dist/virtual/mock-browser.js`
|
||||
- **`browser` mock**: Mock the `webextension-polyfill` module globally with `wxt/dist/virtual/mock-browser.mjs`
|
||||
- **[Remote Code Bundling](/guide/go-further/remote-code)**: If you use it, configure your environment to handle the `url:` module prefix
|
||||
- **Global Variables**: If you consume them, manually define globals provided by WXT (like `import.meta.env.BROWSER`) by adding them to the global scope before accessing them (`import.meta.env.BROWSER = "chrome"`)
|
||||
- **Import paths**: If you use the `@/` or `~/` path aliases, add them to your test environment
|
||||
|
||||
@@ -18,9 +18,7 @@ export default defineConfig({
|
||||
|
||||
## Using Plugins
|
||||
|
||||
Plugins can be passed into the `vite` configuration in you `wxt.config.ts` file, just like any other option.
|
||||
|
||||
All plugins should work in WXT, but it is worth pointing out that since WXT orchestrates multiple vite builds to bundle an extension, plugins will be executed multiple times if necessary.
|
||||
Plugins can be passed into the `vite` configuration in your `wxt.config.ts` file, just like any other option.
|
||||
|
||||
```ts
|
||||
import { defineConfig } from 'wxt';
|
||||
@@ -33,3 +31,9 @@ export default defineConfig({
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
:::warning UNEXPECTED BEHAVIOR
|
||||
Due to the way WXT orchestrates Vite builds, some plugins may not work as expected. Search [GitHub issues](https://github.com/wxt-dev/wxt/issues?q=is%3Aissue+label%3A%22vite+plugin%22) if you run into issues with a specific plugin.
|
||||
|
||||
If one doesn't exist, please open a [new issue](https://github.com/wxt-dev/wxt/issues/new/choose)!
|
||||
:::
|
||||
|
||||
@@ -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.
|
||||
:::
|
||||
@@ -79,24 +79,39 @@ export default defineConfig({
|
||||
|
||||
## ESLint
|
||||
|
||||
ESLint doesn't understand auto-imports; it thinks all auto-imported variables are undeclared globals and will report lint errors for each. To fix this, extend the ESLint file generated inside the `.wxt` directory:
|
||||
|
||||
```js
|
||||
// .eslintrc.js
|
||||
module.exports = {
|
||||
extends: ['./.wxt/eslintrc-auto-import.json'],
|
||||
};
|
||||
```
|
||||
|
||||
By default, this file will be generated when ESLint is a direct dependency. If ESLint is a subdependency or your project is a monorepo, it may not be generated automatically. In this case, you can tell WXT to generate it:
|
||||
ESLint doesn't know about the auto-imported variables unless they are explicitly defined in the `globals` config. By default, WXT will generate the config if it detects ESLint is installed in your project. If the config isn't generated automatically, you can manually tell WXT to generate it.
|
||||
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
export default defineConfig({
|
||||
imports: {
|
||||
eslintrc: {
|
||||
enabled: true,
|
||||
enabled: 8, // Generate ESLint v8 compatible config
|
||||
// or
|
||||
enabled: 9, // Generate ESLint v9 compatible config
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### ESLint 9 and above
|
||||
|
||||
WXT supports the "flat config" file format introduced in ESLint 9. Just import the generated file and add it to the array of config to extend.
|
||||
|
||||
```js
|
||||
// eslint.config.mjs
|
||||
import autoImports from './.wxt/eslint-auto-imports.mjs';
|
||||
|
||||
export default [autoImports];
|
||||
```
|
||||
|
||||
### ESLint 8 and below
|
||||
|
||||
Just extend the generated file:
|
||||
|
||||
```js
|
||||
// .eslintrc.mjs
|
||||
export default {
|
||||
extends: ['./.wxt/eslintrc-auto-import.json'],
|
||||
};
|
||||
```
|
||||
|
||||
@@ -1,28 +1,59 @@
|
||||
# Frontend Frameworks
|
||||
|
||||
WXT supports all frontend frameworks with a Vite plugin:
|
||||
## Built-in Modules
|
||||
|
||||
- `@vitejs/plugin-vue`
|
||||
- `@vitejs/plugin-react`
|
||||
- `@vitejs/plugin-react-swc`
|
||||
- And more!
|
||||
WXT has preconfigured modules for 4 frameworks:
|
||||
|
||||
Just add the vite plugin to your config and you're good to go! Use the framework in HTML pages or content scripts, it will just work 👍
|
||||
- [`@wxt-dev/module-react`](https://github.com/wxt-dev/wxt/tree/main/packages/module-react)
|
||||
- [`@wxt-dev/module-vue`](https://github.com/wxt-dev/wxt/tree/main/packages/module-vue)
|
||||
- [`@wxt-dev/module-svelte`](https://github.com/wxt-dev/wxt/tree/main/packages/module-svelte)
|
||||
- [`@wxt-dev/module-solid`](https://github.com/wxt-dev/wxt/tree/main/packages/module-solid)
|
||||
|
||||
Install the module for your framework, then add it to your config:
|
||||
|
||||
:::code-group
|
||||
|
||||
```ts [Vue]
|
||||
```ts [React]
|
||||
import { defineConfig } from 'wxt';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
|
||||
export default defineConfig({
|
||||
vite: () => ({
|
||||
plugins: [vue()],
|
||||
}),
|
||||
modules: ['@wxt-dev/module-react'],
|
||||
});
|
||||
```
|
||||
|
||||
```ts [React]
|
||||
```ts [Vue]
|
||||
import { defineConfig } from 'wxt';
|
||||
|
||||
export default defineConfig({
|
||||
modules: ['@wxt-dev/module-vue'],
|
||||
});
|
||||
```
|
||||
|
||||
```ts [Svelte]
|
||||
import { defineConfig } from 'wxt';
|
||||
|
||||
export default defineConfig({
|
||||
modules: ['@wxt-dev/module-svelte'],
|
||||
});
|
||||
```
|
||||
|
||||
```ts [Solid]
|
||||
import { defineConfig } from 'wxt';
|
||||
|
||||
export default defineConfig({
|
||||
modules: ['@wxt-dev/module-solid'],
|
||||
});
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
## Adding Vite Plugins
|
||||
|
||||
If your framework doesn't have an official WXT module, no worries! WXT supports any framework with a Vite plugin.
|
||||
|
||||
Just add the Vite plugin to your config and you're good to go! Use the framework in HTML pages or content scripts and it will just work 👍
|
||||
|
||||
```ts
|
||||
import { defineConfig } from 'wxt';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
@@ -33,18 +64,7 @@ export default defineConfig({
|
||||
});
|
||||
```
|
||||
|
||||
```ts [Svelte]
|
||||
import { defineConfig } from 'wxt';
|
||||
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
export default defineConfig({
|
||||
vite: () => ({
|
||||
plugins: [svelte()],
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
:::
|
||||
The WXT modules just simplify the configuration and add auto-imports. They're not much different than the above.
|
||||
|
||||
## Multiple Apps
|
||||
|
||||
@@ -75,18 +95,18 @@ Usually, this means each entrypoint should be a directory with it's own files in
|
||||
├ App.vue
|
||||
├ main.ts
|
||||
├ style.css
|
||||
└ rotuer.ts
|
||||
└ router.ts
|
||||
```
|
||||
|
||||
## 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`)
|
||||
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`).
|
||||
|
||||
Refer to your router's docs for information about "hash" mode and how to enable it. Here's a non-extensive list of a few popular routers:
|
||||
Refer to your router's docs for information about hash mode and how to enable it. Here's a non-extensive list of a few popular routers:
|
||||
|
||||
- [React](https://reactrouter.com/en/main/routers/create-hash-router)
|
||||
- [Vue](https://router.vuejs.org/guide/essentials/history-mode.html#Hash-Mode)
|
||||
- [Svelte](https://www.npmjs.com/package/svelte-spa-router#hash-based-routing)
|
||||
- [Solid](https://github.com/solidjs/solid-router?tab=readme-ov-file#hash-mode-router)
|
||||
- [`react-router`](https://reactrouter.com/en/main/routers/create-hash-router)
|
||||
- [`vue-router`](https://router.vuejs.org/guide/essentials/history-mode.html#Hash-Mode)
|
||||
- [`svelte-spa-router`](https://www.npmjs.com/package/svelte-spa-router#hash-based-routing)
|
||||
- [`solid-router`](https://github.com/solidjs/solid-router?tab=readme-ov-file#hash-mode-router)
|
||||
|
||||
@@ -25,7 +25,7 @@ Here's an example `wxt.config.ts` file:
|
||||
import { defineConfig } from 'wxt';
|
||||
|
||||
export default defineConfig({
|
||||
mainfest: {
|
||||
manifest: {
|
||||
action: {
|
||||
default_title: 'Some Title',
|
||||
},
|
||||
@@ -102,7 +102,7 @@ If a version is not found, a warning is logged and the version defaults to `"0.0
|
||||
}
|
||||
```
|
||||
|
||||
### `icons`
|
||||
## `icons`
|
||||
|
||||
By default, WXT will discover icons in your [`public` directory](/guide/directory-structure/public/) and use them for the [manifest's `icons`](https://developer.chrome.com/docs/extensions/mv3/manifest/icons/).
|
||||
|
||||
@@ -135,7 +135,7 @@ export default defineConfig({
|
||||
});
|
||||
```
|
||||
|
||||
### Permissions
|
||||
## Permissions
|
||||
|
||||
[Permissions](https://developer.chrome.com/docs/extensions/reference/permissions/) must be listed in the manifest config.
|
||||
|
||||
@@ -147,36 +147,34 @@ export default defineConfig({
|
||||
});
|
||||
```
|
||||
|
||||
## Localization
|
||||
## Host Permissions
|
||||
|
||||
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.
|
||||
[Host Permissions](https://developer.chrome.com/docs/extensions/develop/concepts/declare-permissions#host-permissions) must be listed in the manifest config.
|
||||
|
||||
```ts
|
||||
export default defineConfig({
|
||||
manifest: {
|
||||
name: '__MSG_extName__',
|
||||
description: '__MSG_extDescription__',
|
||||
default_locale: 'en',
|
||||
host_permissions: ['*://*.google.com/*'],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
See the official localization examples for more details:
|
||||
:::warning
|
||||
If you use host permissions and target both MV2 and MV3, make sure to only include the required host permissions for each version:
|
||||
|
||||
<ExampleList tag="i18n" />
|
||||
```ts
|
||||
export default defineConfig({
|
||||
manifest: ({ manifestVersion }) => ({
|
||||
host_permissions: manifestVersion === 2 ? [...] : [...],
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
## Default Locale
|
||||
|
||||
See the dedicated [I18n docs](/guide/extension-apis/i18n) for setting up localization and a `default_locale`.
|
||||
|
||||
## Actions
|
||||
|
||||
@@ -228,7 +226,7 @@ The `manifest` option can also be set equal to a function, letting you use logic
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
export default defineConfig({
|
||||
manifest: ({ manifestVersion, browser, mode, comamnd }) => {
|
||||
manifest: ({ manifestVersion, browser, mode, command }) => {
|
||||
return { ... }
|
||||
}
|
||||
})
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
WXT is built on top [`webextension-polyfill` by Mozilla](https://www.npmjs.com/package/webextension-polyfill). The polyfill standardizes much of web extension APIs so they behave the same across different browsers and manifest versions.
|
||||
|
||||
Unlike with Chrome Extension development, which uses a `chrome` global, you need to import the `browser` variable from WXT to use the polyfill:
|
||||
Unlike with Chrome Extension development, which uses a `chrome` global, you need to import the `browser` variable from WXT to access the extension APIs:
|
||||
|
||||
```ts
|
||||
import { browser } from 'wxt/browser';
|
||||
@@ -80,11 +80,11 @@ WXT uses Vite, so all of Vite's `import.meta.env` variables are also available:
|
||||
|
||||
Based on the files in your project, WXT will modify some of the polyfill's types to be type-safe.
|
||||
|
||||
For example, `browser.runtime.getURL` will be typed to only allow getting the URL of known files.
|
||||
For example, `browser.runtime.getURL` will be typed to only allow getting the URL of known files. `browser.i18n.getMessage` will only allow getting translations of messages defined in your `public/_locales/<default-locale>/messages.json` file.
|
||||
|
||||
## Missing Types
|
||||
|
||||
Some newer APIs that Chrome provides are missing types. But don't worry, the APIs are present at runtime! The polyfill only provides types for standard and stable APIs that work on all browsers, so just be careful when you use them.
|
||||
Some newer APIs that Chrome provides are missing types. Don't worry, the APIs are present at runtime! The polyfill only provides types for standard and stable APIs that work on all browsers, so just be careful when you use them.
|
||||
|
||||
If you're using TypeScript, you can use `@ts-expect-error` to ignore any errors when using an API that doesn't have any types.
|
||||
|
||||
|
||||
@@ -0,0 +1,312 @@
|
||||
# Upgrading WXT
|
||||
|
||||
## Overview
|
||||
|
||||
To upgrade WXT to the latest version... just install it!
|
||||
|
||||
```sh
|
||||
pnpm i wxt@latest
|
||||
```
|
||||
|
||||
If there was a major version change, follow the steps below to fix breaking changes.
|
||||
|
||||
## v0.18.5 → v0.19.0
|
||||
|
||||
### `vite-node` Entrypoint Loader
|
||||
|
||||
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:
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```ts
|
||||
export default defineConfig({
|
||||
vite: () => ({ // [!code ++]
|
||||
ssr: { // [!code ++]
|
||||
noExternal: ['@webext-core/messaging', '@webext-core/proxy-service'], // [!code ++]
|
||||
}, // [!code ++]
|
||||
}), // [!code ++]
|
||||
});
|
||||
```
|
||||
|
||||
> [Read the full docs](/guide/go-further/entrypoint-loaders#vite-node) for more information.
|
||||
|
||||
:::details This change enables:
|
||||
|
||||
Importing variables and using them in the entrypoint options:
|
||||
|
||||
```ts
|
||||
// entrypoints/content.ts
|
||||
import { GOOGLE_MATCHES } from '~/utils/constants'
|
||||
|
||||
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:
|
||||
|
||||
```ts
|
||||
export default defineConfig({
|
||||
entrypointLoader: 'jiti', // [!code ++]
|
||||
});
|
||||
```
|
||||
|
||||
:::warning
|
||||
`entrypointLoader: "jiti"` is deprecated and will be removed in the next major version.
|
||||
:::
|
||||
|
||||
### Drop CJS Support
|
||||
|
||||
WXT no longer ships with Common JS support. If you're using CJS, here's your migration steps:
|
||||
|
||||
1. Add [`"type": "module"`](https://nodejs.org/api/packages.html#type) to your `package.json`.
|
||||
2. Change the file extension of any `.js` files that use CJS syntax to `.cjs`, or update them to use EMS syntax.
|
||||
|
||||
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`
|
||||
|
||||
> Out of an abundance of caution, this change has been marked as a breaking change because permission generation is different.
|
||||
|
||||
If you list `host_permissions` in your `wxt.config.ts`'s manifest and have released your extension, double check that your `permissions` and `host_permissions` have not changed for all browsers you target in your `.output/*/manifest.json` files. Permission changes can cause the extension to be disabled on update, and can cause a drop in users, so be sure to double check for differences compared to the previous manifest version.
|
||||
|
||||
## v0.16.0 → v0.17.0
|
||||
|
||||
### Storage - `defineItem` Requires `defaultValue` Option
|
||||
|
||||
If you were using `defineItem` with versioning and no default value, you will need to add `defaultValue: null` to the options and update the first type parameter:
|
||||
|
||||
```ts
|
||||
const item = storage.defineItem<number>("local:count", { // [!code --]
|
||||
const item = storage.defineItem<number | null>("local:count", { // [!code ++]
|
||||
defaultValue: null, // [!code ++]
|
||||
version: ...,
|
||||
migrations: ...,
|
||||
})
|
||||
```
|
||||
|
||||
The `defaultValue` property is now required if passing in the second options argument.
|
||||
|
||||
If you exclude the second options argument, it will default to being nullable, as before.
|
||||
|
||||
```ts
|
||||
const item: WxtStorageItem<number | null> =
|
||||
storage.defineItem<number>('local:count');
|
||||
const value: number | null = await item.getValue();
|
||||
```
|
||||
|
||||
### Storage - Fix Types In `watch` Callback
|
||||
|
||||
> If you don't use TypeScript, this isn't a breaking change, this is just a type change.
|
||||
|
||||
```ts
|
||||
const item = storage.defineItem<number>('local:count', { defaultValue: 0 });
|
||||
item.watch((newValue: number | null, oldValue: number | null) => { // [!code --]
|
||||
item.watch((newValue: number, oldValue: number) => { // [!code ++]
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
## v0.15.0 → v0.16.0
|
||||
|
||||
### Output Directory Structure Changed
|
||||
|
||||
JS entrypoints in the output directory have been moved. Unless you're doing some kind of post-build work referencing files, you don't have to make any changes.
|
||||
|
||||
```
|
||||
.output/
|
||||
<target>/
|
||||
chunks/
|
||||
some-shared-chunk-<hash>.js
|
||||
popup-<hash>.js // [!code --]
|
||||
popup.html
|
||||
popup.html
|
||||
popup.js // [!code ++]
|
||||
```
|
||||
|
||||
## v0.14.0 → v0.15.0
|
||||
|
||||
### Renamed `zip.ignoredSources` to `zip.excludeSources`
|
||||
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
export default defineConfig({
|
||||
zip: {
|
||||
ignoredSources: [
|
||||
/*...*/
|
||||
], // [!code --]
|
||||
excludeSources: [
|
||||
/*...*/
|
||||
], // [!code ++]
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Renamed Undocumented Constants
|
||||
|
||||
Renamed undocumented constants for detecting the build config at runtime in [#380](https://github.com/wxt-dev/wxt/pull/380). Now documented here: https://wxt.dev/guide/multiple-browsers.html#runtime
|
||||
|
||||
- `__BROWSER__` → `import.meta.env.BROWSER`
|
||||
- `__COMMAND__` → `import.meta.env.COMMAND`
|
||||
- `__MANIFEST_VERSION__` → `import.meta.env.MANIFEST_VERSION`
|
||||
- `__IS_CHROME__` → `import.meta.env.CHROME`
|
||||
- `__IS_FIREFOX__` → `import.meta.env.FIREFOX`
|
||||
- `__IS_SAFARI__` → `import.meta.env.SAFARI`
|
||||
- `__IS_EDGE__` → `import.meta.env.EDGE`
|
||||
- `__IS_OPERA__` → `import.meta.env.OPERA`
|
||||
|
||||
## v0.13.0 → v0.14.0
|
||||
|
||||
### Content Script UI API changes
|
||||
|
||||
`createContentScriptUi` and `createContentScriptIframe`, and some of their options, have been renamed:
|
||||
|
||||
- `createContentScriptUi({ ... })` → `createShadowRootUi({ ... })`
|
||||
- `createContentScriptIframe({ ... })` → `createIframeUi({ ... })`
|
||||
- `type: "inline" | "overlay" | "modal"` has been changed to `position: "inline" | "overlay" | "modal"`
|
||||
- `onRemove` is now called **_before_** the UI is removed from the DOM, previously it was called after the UI was removed
|
||||
- `mount` option has been renamed to `onMount`, to better match the related option, `onRemove`.
|
||||
|
||||
## v0.12.0 → v0.13.0
|
||||
|
||||
### New `wxt/storage` APIs
|
||||
|
||||
`wxt/storage` no longer relies on [`unstorage`](https://www.npmjs.com/package/unstorage). Some `unstorage` APIs, like `prefixStorage`, have been removed, while others, like `snapshot`, are methods on the new `storage` object. Most of the standard usage remains the same. See https://wxt.dev/guide/storage and https://wxt.dev/api/reference/wxt/storage/ for more details ([#300](https://github.com/wxt-dev/wxt/pull/300))
|
||||
|
||||
## v0.11.0 → v0.12.0
|
||||
|
||||
### API Exports Changed
|
||||
|
||||
`defineContentScript` and `defineBackground` are now exported from `wxt/sandbox` instead of `wxt/client`. ([#284](https://github.com/wxt-dev/wxt/pull/284))
|
||||
|
||||
- If you use auto-imports, no changes are required.
|
||||
- If you have disabled auto-imports, you'll need to manually update your import statements:
|
||||
```ts
|
||||
import { defineBackground, defineContentScript } from 'wxt/client'; // [!code --]
|
||||
import { defineBackground, defineContentScript } from 'wxt/sandbox'; // [!code ++]
|
||||
```
|
||||
|
||||
## v0.10.0 → v0.11.0
|
||||
|
||||
### Vite 5
|
||||
|
||||
You will need to update any other Vite plugins to a version that supports Vite 5.
|
||||
|
||||
## v0.9.0 → v0.10.0
|
||||
|
||||
### Extension Icon Discovery
|
||||
|
||||
WXT no longer discovers icons other than `.png` files. If you previously used `.jpg`, `.jpeg`, `.bmp`, or `.svg`, you'll need to convert your icons to `.png` files or manually add them to the manifest inside your `wxt.config.ts` file.
|
||||
|
||||
## v0.8.0 → v0.9.0
|
||||
|
||||
### Removed `WebWorker` Types by Default
|
||||
|
||||
Removed [`"WebWorker"` types](https://www.typescriptlang.org/tsconfig/lib.html) from `.wxt/tsconfig.json`. These types are useful for MV3 projects using a service worker.
|
||||
|
||||
To add them back to your project, add the following to your project's TSConfig:
|
||||
|
||||
```json
|
||||
{
|
||||
"extends": "./.wxt/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
// [!code ++]
|
||||
"lib": ["ESNext", "DOM", "WebWorker"] // [!code ++]
|
||||
} // [!code ++]
|
||||
}
|
||||
```
|
||||
|
||||
## v0.7.0 → v0.8.0
|
||||
|
||||
### `defineUnlistedScript`
|
||||
|
||||
Unlisted scripts must now `export default defineUnlistedScript(...)`.
|
||||
|
||||
### `BackgroundDefinition` Type
|
||||
|
||||
Rename `BackgroundScriptDefintition` to `BackgroundDefinition`.
|
||||
|
||||
## v0.6.0 → v0.7.0
|
||||
|
||||
### Content Script CSS Output Location Changed
|
||||
|
||||
Content script CSS used to be output to `assets/<name>.css`, but is now `content-scripts/<name>.css` to match the docs.
|
||||
|
||||
## v0.5.0 → v0.6.0
|
||||
|
||||
### Require a Function for `vite` Config
|
||||
|
||||
The `vite` config option must now be a function. If you were using an object before, change it from `vite: { ... }` to `vite: () => ({ ... })`.
|
||||
|
||||
## v0.4.0 → v0.5.0
|
||||
|
||||
### Revert Move Public Directory
|
||||
|
||||
Change default `publicDir` to from `<rootDir>/public` to `<srcDir>/public`.
|
||||
|
||||
## v0.3.0 → v0.4.0
|
||||
|
||||
### Update Default Path Aliases
|
||||
|
||||
Use relative path aliases inside `.wxt/tsconfig.json`.
|
||||
|
||||
## v0.2.0 → v0.3.0
|
||||
|
||||
### Move Public Directory
|
||||
|
||||
Change default `publicDir` to from `<srcDir>/public` to `<rootDir>/public`.
|
||||
|
||||
### Improve Type Safety
|
||||
|
||||
Add type safety to `browser.runtime.getURL`.
|
||||
|
||||
## v0.1.0 → v0.2.0
|
||||
|
||||
### Rename `defineBackground`
|
||||
|
||||
Rename `defineBackgroundScript` to `defineBackground`.
|
||||
@@ -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.
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
# https://docs.netlify.com/routing/redirects/
|
||||
|
||||
# Old URLs -> New URLs
|
||||
|
||||
# OLD
|
||||
/config.html /api/reference/wxt/interfaces/InlineConfig.html
|
||||
/api/config.html /api/reference/wxt/interfaces/InlineConfig.html
|
||||
/entrypoints /entrypoints/background.html
|
||||
@@ -29,3 +31,6 @@
|
||||
/guide/build-targets.html /guide/multiple-browsers.html
|
||||
/guide/installation.html /get-started/installation.html
|
||||
/guide/introduction.html /get-started/introduction.html
|
||||
|
||||
# 0.19.0
|
||||
/guide/go-further/entrypoint-side-effects.html /guide/go-further/entrypoint-loaders.html
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB |
@@ -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
|
||||
+16
-19
@@ -2,49 +2,46 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=18.20.3",
|
||||
"pnpm": "^8.6.3"
|
||||
"node": ">=18.20.3"
|
||||
},
|
||||
"packageManager": "pnpm@8.15.8",
|
||||
"packageManager": "pnpm@9.6.0",
|
||||
"scripts": {
|
||||
"check": "check && pnpm -r --sequential run check",
|
||||
"test": "vitest",
|
||||
"test:coverage": "vitest run --coverage.enabled \"--coverage.include=packages/wxt/src/**\" \"--coverage.exclude=packages/wxt/src/core/utils/testing/**\" \"--coverage.exclude=**/*.d.ts\" \"--coverage.exclude=**/fixtures/**\"",
|
||||
"test": "pnpm -r --sequential run test run",
|
||||
"test:coverage": "pnpm -r --sequential run test:coverage",
|
||||
"prepare": "simple-git-hooks",
|
||||
"prepublish": "pnpm -s build",
|
||||
"docs:gen": "typedoc --options docs/typedoc.json",
|
||||
"docs:dev": "pnpm -s docs:gen && vitepress dev docs",
|
||||
"docs:build": "pnpm -s docs:gen && vitepress build docs",
|
||||
"docs:preview": "pnpm -s docs:gen && vitepress preview docs",
|
||||
"build-deps": "pnpm tsx scripts/build-deps.ts"
|
||||
"docs:preview": "pnpm -s docs:gen && vitepress preview docs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "^1.3.1",
|
||||
"@aklinker1/buildc": "^1.1.1",
|
||||
"@aklinker1/check": "^1.4.5",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@vitest/coverage-v8": "^1.6.0",
|
||||
"@vitest/coverage-v8": "^2.0.4",
|
||||
"changelogen": "^0.5.5",
|
||||
"consola": "^3.2.3",
|
||||
"dependency-graph": "^1.0.0",
|
||||
"execa": "^9.2.0",
|
||||
"execa": "^9.3.1",
|
||||
"fast-glob": "^3.3.2",
|
||||
"fs-extra": "^11.2.0",
|
||||
"hasha": "^6.0.0",
|
||||
"lint-staged": "^15.2.7",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^3.3.2",
|
||||
"prettier": "^3.3.3",
|
||||
"simple-git-hooks": "^2.11.1",
|
||||
"tsx": "^4.15.4",
|
||||
"tsx": "4.15.7",
|
||||
"typedoc": "^0.25.4",
|
||||
"typedoc-plugin-markdown": "4.0.0-next.23",
|
||||
"typedoc-vitepress-theme": "1.0.0-next.3",
|
||||
"typescript": "^5.4.5",
|
||||
"vitepress": "^1.2.3",
|
||||
"vitest": "^1.6.0",
|
||||
"vitest-mock-extended": "^1.3.1",
|
||||
"vitest-plugin-random-seed": "^1.1.0",
|
||||
"vue": "^3.4.27",
|
||||
"typescript": "^5.5.4",
|
||||
"vitepress": "^1.3.1",
|
||||
"vitest-mock-extended": "^2.0.0",
|
||||
"vue": "^3.4.34",
|
||||
"wxt": "workspace:*",
|
||||
"yaml": "^2.4.5"
|
||||
"yaml": "^2.5.0"
|
||||
},
|
||||
"simple-git-hooks": {
|
||||
"pre-commit": "pnpm lint-staged"
|
||||
|
||||
@@ -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.1",
|
||||
"publint": "^0.2.9",
|
||||
"typescript": "^5.5.4",
|
||||
"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,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.1.0",
|
||||
"type": "module",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/wxt-dev/wxt.git",
|
||||
"directory": "packages/i18n"
|
||||
},
|
||||
"homepage": "http://wxt.dev/guide/i18n/installation.html",
|
||||
"keywords": [
|
||||
"wxt",
|
||||
"module",
|
||||
"i18n"
|
||||
],
|
||||
"author": {
|
||||
"name": "Aaron Klinker",
|
||||
"email": "aaronklinker1+wxt@gmail.com"
|
||||
},
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "buildc -- unbuild",
|
||||
"check": "buildc --deps-only -- check",
|
||||
"test": "buildc --deps-only -- vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"chokidar": "^3.6.0",
|
||||
"confbox": "^0.1.7",
|
||||
"fast-glob": "^3.3.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"wxt": ">=0.19.7"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"wxt": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "^1.4.5",
|
||||
"@types/chrome": "^0.0.268",
|
||||
"@types/node": "^20.14.2",
|
||||
"oxlint": "^0.9.1",
|
||||
"publint": "^0.2.8",
|
||||
"typescript": "^5.4.5",
|
||||
"unbuild": "^2.0.0",
|
||||
"vitest": "^1.6.0",
|
||||
"vitest-plugin-random-seed": "^1.1.0",
|
||||
"wxt": "workspace:*"
|
||||
},
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": {
|
||||
"types": "./dist/index.d.mts",
|
||||
"default": "./dist/index.mjs"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/index.d.cts",
|
||||
"default": "./dist/index.cjs"
|
||||
}
|
||||
},
|
||||
"./build": {
|
||||
"import": {
|
||||
"types": "./dist/build.d.mts",
|
||||
"default": "./dist/build.mjs"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/build.d.cts",
|
||||
"default": "./dist/build.cjs"
|
||||
}
|
||||
},
|
||||
"./module": {
|
||||
"import": {
|
||||
"types": "./dist/module.d.mts",
|
||||
"default": "./dist/module.mjs"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/module.d.cts",
|
||||
"default": "./dist/module.cjs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import {
|
||||
parseMessagesFile,
|
||||
generateChromeMessagesFile,
|
||||
generateTypeFile,
|
||||
} from '../build';
|
||||
import {
|
||||
stringifyTOML,
|
||||
stringifyYAML,
|
||||
stringifyJSON5,
|
||||
stringifyJSON,
|
||||
stringifyJSONC,
|
||||
} from 'confbox';
|
||||
import { writeFile, readFile } from 'node:fs/promises';
|
||||
|
||||
vi.mock('node:fs/promises');
|
||||
const mockWriteFile = vi.mocked(writeFile);
|
||||
const mockReadFile = vi.mocked(readFile);
|
||||
|
||||
describe('Built Tools', () => {
|
||||
it('should correctly convert all types of message formats', async () => {
|
||||
const fileText = stringifyYAML({
|
||||
simple: 'example',
|
||||
sub: 'Hello $1',
|
||||
nested: {
|
||||
example: 'This is nested',
|
||||
array: ['One', 'Two'],
|
||||
notChrome: {
|
||||
message: 'test 1',
|
||||
},
|
||||
},
|
||||
chrome: {
|
||||
message: 'test 2',
|
||||
description: 'test',
|
||||
},
|
||||
plural0: {
|
||||
0: 'Zero items',
|
||||
1: 'One item',
|
||||
n: '$1 items',
|
||||
},
|
||||
plural1: {
|
||||
1: 'One item',
|
||||
n: '$1 items',
|
||||
},
|
||||
pluralN: {
|
||||
n: '$1 items',
|
||||
},
|
||||
pluralSub: {
|
||||
1: 'Hello $2, I have one problem',
|
||||
n: 'Hello $2, I have $1 problems',
|
||||
},
|
||||
});
|
||||
|
||||
mockReadFile.mockResolvedValue(fileText);
|
||||
|
||||
const messages = await parseMessagesFile(`file.yml`);
|
||||
await generateChromeMessagesFile('output.json', messages);
|
||||
await generateTypeFile('output.d.ts', messages);
|
||||
const actualChromeMessagesFile = mockWriteFile.mock.calls[0][1];
|
||||
const actualDtsFile = mockWriteFile.mock.calls[1][1];
|
||||
|
||||
expect(mockWriteFile).toBeCalledTimes(2);
|
||||
expect(actualChromeMessagesFile).toMatchInlineSnapshot(`
|
||||
"{
|
||||
"simple": {
|
||||
"message": "example"
|
||||
},
|
||||
"sub": {
|
||||
"message": "Hello $1"
|
||||
},
|
||||
"nested_example": {
|
||||
"message": "This is nested"
|
||||
},
|
||||
"nested_array_0": {
|
||||
"message": "One"
|
||||
},
|
||||
"nested_array_1": {
|
||||
"message": "Two"
|
||||
},
|
||||
"nested_notChrome_message": {
|
||||
"message": "test 1"
|
||||
},
|
||||
"chrome": {
|
||||
"message": "test 2",
|
||||
"description": "test"
|
||||
},
|
||||
"plural0": {
|
||||
"message": "Zero items | One item | $1 items"
|
||||
},
|
||||
"plural1": {
|
||||
"message": "One item | $1 items"
|
||||
},
|
||||
"pluralN": {
|
||||
"message": "$1 items"
|
||||
},
|
||||
"pluralSub": {
|
||||
"message": "Hello $2, I have one problem | Hello $2, I have $1 problems"
|
||||
}
|
||||
}
|
||||
"
|
||||
`);
|
||||
expect(actualDtsFile).toMatchInlineSnapshot(`
|
||||
"export type GeneratedI18nStructure = {
|
||||
"simple": { substitutions: 0, plural: false };
|
||||
"sub": { substitutions: 1, plural: false };
|
||||
"nested.example": { substitutions: 0, plural: false };
|
||||
"nested.array.0": { substitutions: 0, plural: false };
|
||||
"nested.array.1": { substitutions: 0, plural: false };
|
||||
"nested.notChrome.message": { substitutions: 0, plural: false };
|
||||
"chrome": { substitutions: 0, plural: false };
|
||||
"plural0": { substitutions: 1, plural: true };
|
||||
"plural1": { substitutions: 1, plural: true };
|
||||
"pluralN": { substitutions: 1, plural: true };
|
||||
"pluralSub": { substitutions: 2, plural: true };
|
||||
"@@extension_id": { substitutions: 0, plural: false };
|
||||
"@@ui_locale": { substitutions: 0, plural: false };
|
||||
"@@bidi_dir": { substitutions: 0, plural: false };
|
||||
"@@bidi_reversed_dir": { substitutions: 0, plural: false };
|
||||
"@@bidi_start_edge": { substitutions: 0, plural: false };
|
||||
"@@bidi_end_edge": { substitutions: 0, plural: false };
|
||||
}
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['yaml', stringifyYAML],
|
||||
['yml', stringifyYAML],
|
||||
['toml', stringifyTOML],
|
||||
['json', stringifyJSON],
|
||||
['jsonc', stringifyJSONC],
|
||||
['json5', stringifyJSON5],
|
||||
])('Parse and generate: %s', async (extension, stringify) => {
|
||||
const fileText = stringify({
|
||||
simple: 'example',
|
||||
});
|
||||
const expectedDts = `export type GeneratedI18nStructure = {
|
||||
"simple": { substitutions: 0, plural: false };
|
||||
"@@extension_id": { substitutions: 0, plural: false };
|
||||
"@@ui_locale": { substitutions: 0, plural: false };
|
||||
"@@bidi_dir": { substitutions: 0, plural: false };
|
||||
"@@bidi_reversed_dir": { substitutions: 0, plural: false };
|
||||
"@@bidi_start_edge": { substitutions: 0, plural: false };
|
||||
"@@bidi_end_edge": { substitutions: 0, plural: false };
|
||||
}
|
||||
`;
|
||||
const expectedChromeMessages =
|
||||
JSON.stringify({ simple: { message: 'example' } }, null, 2) + '\n';
|
||||
|
||||
mockReadFile.mockResolvedValue(fileText);
|
||||
|
||||
const messages = await parseMessagesFile(`file.${extension}`);
|
||||
await generateChromeMessagesFile('output.json', messages);
|
||||
await generateTypeFile('output.d.ts', messages);
|
||||
|
||||
expect(mockWriteFile).toBeCalledTimes(2);
|
||||
expect(mockWriteFile).toBeCalledWith(
|
||||
'output.json',
|
||||
expectedChromeMessages,
|
||||
'utf8',
|
||||
);
|
||||
expect(mockWriteFile).toBeCalledWith('output.d.ts', expectedDts, 'utf8');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { createI18n } from '../index';
|
||||
import { GetMessageOptions } from '../types';
|
||||
|
||||
const getMessageMock = vi.fn();
|
||||
|
||||
vi.stubGlobal('chrome', {
|
||||
i18n: {
|
||||
getMessage: getMessageMock,
|
||||
},
|
||||
});
|
||||
|
||||
describe('createI18n', () => {
|
||||
beforeEach(() => {
|
||||
getMessageMock.mockReturnValue('Some text.');
|
||||
});
|
||||
|
||||
it.each([
|
||||
['key', 'key'],
|
||||
['some_key', 'some_key'],
|
||||
['some.nested.key', 'some_nested_key'],
|
||||
])('should retrieve "%s" as "%s"', (input, expectedKey) => {
|
||||
const i18n = createI18n();
|
||||
const expectedValue = String(Math.random());
|
||||
getMessageMock.mockReturnValue(expectedValue);
|
||||
|
||||
const actual = i18n.t(input);
|
||||
|
||||
expect(actual).toBe(expectedValue);
|
||||
expect(getMessageMock).toBeCalledTimes(1);
|
||||
expect(getMessageMock).toBeCalledWith(expectedKey, undefined);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['n items', 0, 'n items'],
|
||||
['n items', 1, 'n items'],
|
||||
['n items', 2, 'n items'],
|
||||
['n items', 3, 'n items'],
|
||||
['1 item | n items', 0, 'n items'],
|
||||
['1 item | n items', 1, '1 item'],
|
||||
['1 item | n items', 2, 'n items'],
|
||||
['1 item | n items', 3, 'n items'],
|
||||
['0 items | 1 item | n items', 0, '0 items'],
|
||||
['0 items | 1 item | n items', 1, '1 item'],
|
||||
['0 items | 1 item | n items', 2, 'n items'],
|
||||
['0 items | 1 item | n items', 3, 'n items'],
|
||||
])(
|
||||
'should retrieve plural forms correctly',
|
||||
(rawMessage, count, expected) => {
|
||||
const i18n = createI18n();
|
||||
getMessageMock.mockReturnValue(rawMessage);
|
||||
const key = 'items';
|
||||
|
||||
const actual = i18n.t(key, count);
|
||||
|
||||
expect(actual).toBe(expected);
|
||||
expect(getMessageMock).toBeCalledTimes(1);
|
||||
expect(getMessageMock).toBeCalledWith(key, [String(count)], undefined);
|
||||
},
|
||||
);
|
||||
|
||||
it('should allow overriding the plural substitutions', () => {
|
||||
const i18n = createI18n();
|
||||
i18n.t('key', 3, ['custom']);
|
||||
expect(getMessageMock).toBeCalledWith('key', ['custom'], undefined);
|
||||
});
|
||||
|
||||
it('should pass options into browser.i18n.getMessage', () => {
|
||||
const i18n = createI18n();
|
||||
const options: GetMessageOptions = {
|
||||
escapeLt: true,
|
||||
};
|
||||
|
||||
i18n.t('key', options);
|
||||
i18n.t('key', [''], options);
|
||||
i18n.t('key', 1, options);
|
||||
i18n.t('key', 1, [''], options);
|
||||
getMessageMock.mock.calls.forEach((call) => {
|
||||
expect(call.pop()).toEqual(options);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,119 @@
|
||||
import { beforeEach, describe, it, vi } from 'vitest';
|
||||
import { createI18n } from '..';
|
||||
|
||||
const getMessageMock = vi.fn();
|
||||
|
||||
vi.stubGlobal('chrome', {
|
||||
i18n: {
|
||||
getMessage: getMessageMock,
|
||||
},
|
||||
});
|
||||
|
||||
const n: number = 1;
|
||||
|
||||
describe('I18n Types', () => {
|
||||
beforeEach(() => {
|
||||
getMessageMock.mockReturnValue('Some text.');
|
||||
});
|
||||
|
||||
describe('No type-safety', () => {
|
||||
const i18n = createI18n();
|
||||
|
||||
describe('t', () => {
|
||||
it('should allow passing any combination of arguments', () => {
|
||||
i18n.t('any');
|
||||
i18n.t('any', { escapeLt: true });
|
||||
i18n.t('any', ['one']);
|
||||
i18n.t('any', ['one'], { escapeLt: true });
|
||||
i18n.t('any', ['one', 'two']);
|
||||
i18n.t('any', ['one', 'two'], { escapeLt: true });
|
||||
i18n.t('any', n, ['one', 'two']);
|
||||
i18n.t('any', n, ['one', 'two'], { escapeLt: true });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('With type-safety', () => {
|
||||
const i18n = createI18n<{
|
||||
simple: { plural: false; substitutions: 0 };
|
||||
simpleSub1: { plural: false; substitutions: 1 };
|
||||
simpleSub2: { plural: false; substitutions: 2 };
|
||||
plural: { plural: true; substitutions: 0 };
|
||||
pluralSub1: { plural: true; substitutions: 1 };
|
||||
pluralSub2: { plural: true; substitutions: 2 };
|
||||
}>();
|
||||
|
||||
describe('t', () => {
|
||||
it('should only allow passing valid combinations of arguments', () => {
|
||||
i18n.t('simple');
|
||||
i18n.t('simple', { escapeLt: true });
|
||||
// @ts-expect-error
|
||||
i18n.t('simple', []);
|
||||
// @ts-expect-error
|
||||
i18n.t('simple', ['one']);
|
||||
// @ts-expect-error
|
||||
i18n.t('simple', n);
|
||||
|
||||
i18n.t('simpleSub1', ['one']);
|
||||
i18n.t('simpleSub1', ['one'], { escapeLt: true });
|
||||
// @ts-expect-error
|
||||
i18n.t('simpleSub1');
|
||||
// @ts-expect-error
|
||||
i18n.t('simpleSub1', []);
|
||||
// @ts-expect-error
|
||||
i18n.t('simpleSub1', ['one', 'two']);
|
||||
// @ts-expect-error
|
||||
i18n.t('simpleSub1', n);
|
||||
|
||||
i18n.t('simpleSub2', ['one', 'two']);
|
||||
i18n.t('simpleSub2', ['one', 'two'], { escapeLt: true });
|
||||
// @ts-expect-error
|
||||
i18n.t('simpleSub2');
|
||||
// @ts-expect-error
|
||||
i18n.t('simpleSub2', ['one']);
|
||||
// @ts-expect-error
|
||||
i18n.t('simpleSub2', ['one', 'two', 'three']);
|
||||
// @ts-expect-error
|
||||
i18n.t('simpleSub2', n);
|
||||
|
||||
i18n.t('plural', n);
|
||||
i18n.t('plural', n, { escapeLt: true });
|
||||
// @ts-expect-error
|
||||
i18n.t('plural');
|
||||
// @ts-expect-error
|
||||
i18n.t('plural', []);
|
||||
// @ts-expect-error
|
||||
i18n.t('plural', ['one']);
|
||||
// @ts-expect-error
|
||||
i18n.t('plural', n, ['sub']);
|
||||
|
||||
i18n.t('pluralSub1', n);
|
||||
i18n.t('pluralSub1', n, { escapeLt: true });
|
||||
i18n.t('pluralSub1', n, undefined, { escapeLt: true });
|
||||
i18n.t('pluralSub1', n, ['one']);
|
||||
i18n.t('pluralSub1', n, ['one'], { escapeLt: true });
|
||||
// @ts-expect-error
|
||||
i18n.t('pluralSub1');
|
||||
// @ts-expect-error
|
||||
i18n.t('pluralSub1', ['one']);
|
||||
// @ts-expect-error
|
||||
i18n.t('pluralSub1', n, []);
|
||||
// @ts-expect-error
|
||||
i18n.t('pluralSub1', n, ['one', 'two']);
|
||||
|
||||
i18n.t('pluralSub2', n, ['one', 'two']);
|
||||
i18n.t('pluralSub2', n, ['one', 'two'], { escapeLt: true });
|
||||
// @ts-expect-error
|
||||
i18n.t('pluralSub2');
|
||||
// @ts-expect-error
|
||||
i18n.t('pluralSub2', ['one', 'two']);
|
||||
// @ts-expect-error
|
||||
i18n.t('pluralSub2', n, ['one']);
|
||||
// @ts-expect-error
|
||||
i18n.t('pluralSub2', n, ['one', 'two', 'three']);
|
||||
// @ts-expect-error
|
||||
i18n.t('pluralSub2', n);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { ChromeMessage } from '../build';
|
||||
import { applyChromeMessagePlaceholders, getSubstitionCount } from '../utils';
|
||||
|
||||
describe('Utils', () => {
|
||||
describe('applyChromeMessagePlaceholders', () => {
|
||||
it('should return the combined stirng', () => {
|
||||
const input = {
|
||||
message: 'Hello $username$, welcome to $appName$',
|
||||
placeholders: {
|
||||
username: { content: '$1' },
|
||||
appName: { content: 'Example' },
|
||||
},
|
||||
} satisfies ChromeMessage;
|
||||
const expected = input.message
|
||||
.replace('$username$', input.placeholders.username.content)
|
||||
.replace('$appName$', input.placeholders.appName.content);
|
||||
|
||||
const actual = applyChromeMessagePlaceholders(input);
|
||||
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
|
||||
it('should ignore the case', () => {
|
||||
const input = {
|
||||
message: 'Hello $USERNAME$, welcome $username$',
|
||||
placeholders: {
|
||||
username: { content: '$1' },
|
||||
},
|
||||
} satisfies ChromeMessage;
|
||||
const expected = input.message
|
||||
.replace('$USERNAME$', input.placeholders.username.content)
|
||||
.replace('$username$', input.placeholders.username.content);
|
||||
|
||||
const actual = applyChromeMessagePlaceholders(input);
|
||||
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSubstitionCount', () => {
|
||||
it('should return the last substution present in the message', () => {
|
||||
expect(getSubstitionCount('I like $1, but I like $2 better')).toBe(2);
|
||||
});
|
||||
|
||||
it('should return 0 when no substitutions are present', () => {
|
||||
expect(getSubstitionCount('test')).toBe(0);
|
||||
});
|
||||
|
||||
it('should ignore escaped dollar signs', () => {
|
||||
expect(getSubstitionCount('buy $1 now for $$2 dollars')).toBe(1);
|
||||
});
|
||||
|
||||
it('should return the highest substitution when skipping numbers', () => {
|
||||
expect(getSubstitionCount('I like $1, but I like $8 better')).toBe(8);
|
||||
});
|
||||
|
||||
it('should only allow up to 9 substitutions', () => {
|
||||
expect(getSubstitionCount('Hello $9')).toBe(9);
|
||||
expect(getSubstitionCount('Hello $10')).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,291 @@
|
||||
/**
|
||||
* This module contains utils for generating types and `messages.json` files
|
||||
* based on the custom messages file format.
|
||||
*
|
||||
* @module @wxt-dev/i18n/build
|
||||
*/
|
||||
|
||||
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
||||
import { parseYAML, parseJSON5, parseTOML } from 'confbox';
|
||||
import { dirname, extname } from 'node:path';
|
||||
import { applyChromeMessagePlaceholders, getSubstitionCount } from './utils';
|
||||
|
||||
//
|
||||
// TYPES
|
||||
//
|
||||
|
||||
export interface ChromeMessage {
|
||||
message: string;
|
||||
description?: string;
|
||||
placeholders?: Record<string, { content: string; example?: string }>;
|
||||
}
|
||||
|
||||
export interface ParsedBaseMessage {
|
||||
key: string[];
|
||||
substitutions: number;
|
||||
}
|
||||
|
||||
export interface ParsedChromeMessage extends ParsedBaseMessage, ChromeMessage {
|
||||
type: 'chrome';
|
||||
}
|
||||
export interface ParsedSimpleMessage extends ParsedBaseMessage {
|
||||
type: 'simple';
|
||||
message: string;
|
||||
}
|
||||
export interface ParsedPluralMessage extends ParsedBaseMessage {
|
||||
type: 'plural';
|
||||
plurals: { [count: string]: string };
|
||||
}
|
||||
|
||||
export type ParsedMessage =
|
||||
| ParsedChromeMessage
|
||||
| ParsedSimpleMessage
|
||||
| ParsedPluralMessage;
|
||||
|
||||
export type MessageFormat = 'JSON5' | 'YAML' | 'TOML';
|
||||
|
||||
//
|
||||
// CONSTANTS
|
||||
//
|
||||
|
||||
/**
|
||||
* See https://developer.chrome.com/docs/extensions/reference/api/i18n#overview-predefined
|
||||
*/
|
||||
const PREDEFINED_MESSAGES: Record<string, ChromeMessage> = {
|
||||
'@@extension_id': {
|
||||
message: '<browser.runtime.id>',
|
||||
description:
|
||||
"The extension or app ID; you might use this string to construct URLs for resources inside the extension. Even unlocalized extensions can use this message.\nNote: You can't use this message in a manifest file.",
|
||||
},
|
||||
'@@ui_locale': {
|
||||
message: '<browser.i18n.getUiLocale()>',
|
||||
description: '',
|
||||
},
|
||||
'@@bidi_dir': {
|
||||
message: '<ltr|rtl>',
|
||||
description:
|
||||
'The text direction for the current locale, either "ltr" for left-to-right languages such as English or "rtl" for right-to-left languages such as Japanese.',
|
||||
},
|
||||
'@@bidi_reversed_dir': {
|
||||
message: '<rtl|ltr>',
|
||||
description:
|
||||
'If the `@@bidi_dir` is "ltr", then this is "rtl"; otherwise, it\'s "ltr".',
|
||||
},
|
||||
'@@bidi_start_edge': {
|
||||
message: '<left|right>',
|
||||
description:
|
||||
'If the `@@bidi_dir` is "ltr", then this is "left"; otherwise, it\'s "right".',
|
||||
},
|
||||
'@@bidi_end_edge': {
|
||||
message: '<right|left>',
|
||||
description:
|
||||
'If the `@@bidi_dir` is "ltr", then this is "right"; otherwise, it\'s "left".',
|
||||
},
|
||||
};
|
||||
|
||||
const EXT_FORMATS_MAP: Record<string, MessageFormat> = {
|
||||
'.json': 'JSON5',
|
||||
'.jsonc': 'JSON5',
|
||||
'.json5': 'JSON5',
|
||||
'.yaml': 'YAML',
|
||||
'.yml': 'YAML',
|
||||
'.toml': 'TOML',
|
||||
};
|
||||
|
||||
const PARSERS: Record<MessageFormat, (text: string) => any> = {
|
||||
YAML: parseYAML,
|
||||
JSON5: parseJSON5,
|
||||
TOML: parseTOML,
|
||||
};
|
||||
|
||||
const ALLOWED_CHROME_MESSAGE_KEYS: Set<string> = new Set<keyof ChromeMessage>([
|
||||
'description',
|
||||
'message',
|
||||
'placeholders',
|
||||
]);
|
||||
|
||||
//
|
||||
// PARSING
|
||||
//
|
||||
|
||||
/**
|
||||
* Parse a messages file, extract the messages. Supports JSON, JSON5, and YAML.
|
||||
*/
|
||||
export async function parseMessagesFile(
|
||||
file: string,
|
||||
): Promise<ParsedMessage[]> {
|
||||
const text = await readFile(file, 'utf8');
|
||||
const ext = extname(file).toLowerCase();
|
||||
return parseMessagesText(text, EXT_FORMATS_MAP[ext] ?? 'JSON5');
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a string, extracting the messages. Supports JSON, JSON5, and YAML.
|
||||
*/
|
||||
export function parseMessagesText(
|
||||
text: string,
|
||||
format: 'JSON5' | 'YAML' | 'TOML',
|
||||
): ParsedMessage[] {
|
||||
return parseMessagesObject(PARSERS[format](text));
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the JS object form of a raw messages file, extract the messages.
|
||||
*/
|
||||
export function parseMessagesObject(object: any): ParsedMessage[] {
|
||||
return _parseMessagesObject(
|
||||
[],
|
||||
{
|
||||
...object,
|
||||
...PREDEFINED_MESSAGES,
|
||||
},
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
function _parseMessagesObject(
|
||||
path: string[],
|
||||
object: any,
|
||||
depth: number,
|
||||
): ParsedMessage[] {
|
||||
switch (typeof object) {
|
||||
case 'string':
|
||||
case 'bigint':
|
||||
case 'boolean':
|
||||
case 'number':
|
||||
case 'symbol': {
|
||||
const message = String(object);
|
||||
const substitutions = getSubstitionCount(message);
|
||||
return [
|
||||
{
|
||||
type: 'simple',
|
||||
key: path,
|
||||
substitutions,
|
||||
message,
|
||||
},
|
||||
];
|
||||
}
|
||||
case 'object':
|
||||
if (Array.isArray(object))
|
||||
return object.flatMap((item, i) =>
|
||||
_parseMessagesObject(path.concat(String(i)), item, depth + 1),
|
||||
);
|
||||
if (isPluralMessage(object)) {
|
||||
const message = Object.values(object).join('|');
|
||||
const substitutions = getSubstitionCount(message);
|
||||
return [
|
||||
{
|
||||
type: 'plural',
|
||||
key: path,
|
||||
substitutions,
|
||||
plurals: object,
|
||||
},
|
||||
];
|
||||
}
|
||||
if (depth === 1 && isChromeMessage(object)) {
|
||||
const message = applyChromeMessagePlaceholders(object);
|
||||
const substitutions = getSubstitionCount(message);
|
||||
return [
|
||||
{
|
||||
type: 'chrome',
|
||||
key: path,
|
||||
substitutions,
|
||||
...object,
|
||||
},
|
||||
];
|
||||
}
|
||||
return Object.entries(object).flatMap(([key, value]) =>
|
||||
_parseMessagesObject(path.concat(key), value, depth + 1),
|
||||
);
|
||||
default:
|
||||
throw Error(`"Could not parse object of type "${typeof object}"`);
|
||||
}
|
||||
}
|
||||
|
||||
function isPluralMessage(object: any): object is Record<number | 'n', string> {
|
||||
return Object.keys(object).every(
|
||||
(key) => key === 'n' || isFinite(Number(key)),
|
||||
);
|
||||
}
|
||||
|
||||
function isChromeMessage(object: any): object is ChromeMessage {
|
||||
return Object.keys(object).every((key) =>
|
||||
ALLOWED_CHROME_MESSAGE_KEYS.has(key),
|
||||
);
|
||||
}
|
||||
|
||||
//
|
||||
// OUTPUT
|
||||
//
|
||||
|
||||
export function generateTypeText(messages: ParsedMessage[]): string {
|
||||
const renderMessageEntry = (message: ParsedMessage): string => {
|
||||
// Use . for deep keys at runtime and types
|
||||
const key = message.key.join('.');
|
||||
|
||||
const features = [
|
||||
`substitutions: ${message.substitutions}`,
|
||||
`plural: ${message.type === 'plural'}`,
|
||||
];
|
||||
return ` "${key}": { ${features.join(', ')} };`;
|
||||
};
|
||||
|
||||
return `export type GeneratedI18nStructure = {
|
||||
${messages.map(renderMessageEntry).join('\n')}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
export async function generateTypeFile(
|
||||
outFile: string,
|
||||
messages: ParsedMessage[],
|
||||
): Promise<void> {
|
||||
const text = generateTypeText(messages);
|
||||
await mkdir(dirname(outFile), { recursive: true });
|
||||
await writeFile(outFile, text, 'utf8');
|
||||
}
|
||||
|
||||
export function generateChromeMessages(
|
||||
messages: ParsedMessage[],
|
||||
): Record<string, ChromeMessage> {
|
||||
return messages.reduce<Record<string, ChromeMessage>>((acc, message) => {
|
||||
// Use _ for deep keys in _locales/.../messages.json
|
||||
const key = message.key.join('_');
|
||||
// Don't output predefined messages
|
||||
if (PREDEFINED_MESSAGES[key]) return acc;
|
||||
|
||||
switch (message.type) {
|
||||
case 'chrome':
|
||||
acc[key] = {
|
||||
message: message.message,
|
||||
description: message.description,
|
||||
placeholders: message.placeholders,
|
||||
};
|
||||
break;
|
||||
case 'plural':
|
||||
acc[key] = {
|
||||
message: Object.values(message.plurals).join(' | '),
|
||||
};
|
||||
break;
|
||||
case 'simple':
|
||||
acc[key] = {
|
||||
message: message.message,
|
||||
};
|
||||
break;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
export function generateChromeMessagesText(messages: ParsedMessage[]): string {
|
||||
const raw = generateChromeMessages(messages);
|
||||
return JSON.stringify(raw, null, 2);
|
||||
}
|
||||
|
||||
export async function generateChromeMessagesFile(
|
||||
file: string,
|
||||
messages: ParsedMessage[],
|
||||
): Promise<void> {
|
||||
const text = generateChromeMessagesText(messages);
|
||||
await writeFile(file, text + '\n', 'utf8');
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* @module @wxt-dev/i18n
|
||||
*/
|
||||
import {
|
||||
I18nStructure,
|
||||
DefaultI18nStructure,
|
||||
I18n,
|
||||
Substitution,
|
||||
GetMessageOptions,
|
||||
} from './types';
|
||||
|
||||
export function createI18n<
|
||||
T extends I18nStructure = DefaultI18nStructure,
|
||||
>(): I18n<T> {
|
||||
const t = (key: string, ...args: any[]) => {
|
||||
// Resolve args
|
||||
let sub: Substitution[] | undefined;
|
||||
let count: number | undefined;
|
||||
let options: GetMessageOptions | undefined;
|
||||
args.forEach((arg, i) => {
|
||||
if (arg == null) {
|
||||
// ignore nullish args
|
||||
} else if (typeof arg === 'number') {
|
||||
count = arg;
|
||||
} else if (Array.isArray(arg)) {
|
||||
sub = arg;
|
||||
} else if (typeof arg === 'object') {
|
||||
options = arg;
|
||||
} else {
|
||||
throw Error(
|
||||
`Unknown argument at index ${i}. Must be a number for pluralization, substitution array, or options object.`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Default substitutions to [n]
|
||||
if (count != null && sub == null) {
|
||||
sub = [String(count)];
|
||||
}
|
||||
|
||||
// Load the localization
|
||||
let message: string;
|
||||
if (sub?.length) {
|
||||
// Convert all substitutions to strings
|
||||
const stringSubs = sub?.map((sub) => String(sub));
|
||||
message = chrome.i18n.getMessage(
|
||||
key.replaceAll('.', '_'),
|
||||
stringSubs,
|
||||
// @ts-ignore - @types/chrome doesn't type the options object, but it's there
|
||||
options,
|
||||
);
|
||||
} else {
|
||||
message = chrome.i18n.getMessage(
|
||||
key.replaceAll('.', '_'),
|
||||
// @ts-ignore - @types/chrome doesn't type the options object, but it's there
|
||||
options,
|
||||
);
|
||||
}
|
||||
if (!message) {
|
||||
console.warn(`[i18n] Message not found: "${key}"`);
|
||||
}
|
||||
if (count == null) return message;
|
||||
|
||||
// Apply pluralization
|
||||
const plural = message.split(' | ');
|
||||
switch (plural.length) {
|
||||
// "n items"
|
||||
case 1:
|
||||
return plural[0];
|
||||
// "1 item | n items"
|
||||
case 2:
|
||||
return plural[count === 1 ? 0 : 1];
|
||||
// "0 items | 1 item | n items"
|
||||
case 3:
|
||||
return plural[count === 0 || count === 1 ? count : 2];
|
||||
default:
|
||||
throw Error('Unknown plural formatting');
|
||||
}
|
||||
};
|
||||
|
||||
return { t } as I18n<T>;
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* The WXT Module to integrate `@wxt-dev/i18n` into your project.
|
||||
*
|
||||
* ```ts
|
||||
* export default defineConfig({
|
||||
* modules: ["@wxt-dev/i18n/module"],
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @module @wxt-dev/i18n/module
|
||||
*/
|
||||
|
||||
import 'wxt';
|
||||
import { addAlias, defineWxtModule } from 'wxt/modules';
|
||||
import {
|
||||
generateChromeMessagesText,
|
||||
parseMessagesFile,
|
||||
generateTypeText,
|
||||
} from './build';
|
||||
import glob from 'fast-glob';
|
||||
import { basename, extname, join, resolve } from 'node:path';
|
||||
import { watch } from 'chokidar';
|
||||
import { GeneratedPublicFile, WxtDirFileEntry } from 'wxt';
|
||||
import { writeFile } from 'node:fs/promises';
|
||||
|
||||
export default defineWxtModule({
|
||||
name: '@wxt-dev/i18n',
|
||||
imports: [{ from: '#i18n', name: 'i18n' }],
|
||||
|
||||
setup(wxt) {
|
||||
if (wxt.config.manifest.default_locale == null) {
|
||||
wxt.logger.warn(
|
||||
`\`[i18n]\` manifest.default_locale not set, \`@wxt-dev/i18n\` disabled.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
wxt.logger.info(
|
||||
'`[i18n]` Default locale: ' + wxt.config.manifest.default_locale,
|
||||
);
|
||||
|
||||
const getLocalizationFiles = async () => {
|
||||
const files = await glob('locales/*', {
|
||||
cwd: wxt.config.srcDir,
|
||||
absolute: true,
|
||||
});
|
||||
return files.map((file) => ({
|
||||
file,
|
||||
locale: basename(file).replace(extname(file), ''),
|
||||
}));
|
||||
};
|
||||
|
||||
const generateOutputJsonFiles = async (): Promise<
|
||||
GeneratedPublicFile[]
|
||||
> => {
|
||||
const files = await getLocalizationFiles();
|
||||
return await Promise.all(
|
||||
files.map(async ({ file, locale }) => {
|
||||
const messages = await parseMessagesFile(file);
|
||||
return {
|
||||
contents: generateChromeMessagesText(messages),
|
||||
relativeDest: join('_locales', locale, 'messages.json'),
|
||||
};
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const generateTypes = async (): Promise<WxtDirFileEntry> => {
|
||||
const files = await getLocalizationFiles();
|
||||
const defaultLocaleFile = files.find(
|
||||
({ locale }) => locale === wxt.config.manifest.default_locale,
|
||||
)!;
|
||||
if (defaultLocaleFile == null) {
|
||||
throw Error(
|
||||
`\`[i18n]\` Required localization file does not exist: \`<srcDir>/locales/${wxt.config.manifest.default_locale}.{json|json5|yml|yaml|toml}\``,
|
||||
);
|
||||
}
|
||||
|
||||
const messages = await parseMessagesFile(defaultLocaleFile.file);
|
||||
return {
|
||||
path: typesPath,
|
||||
text: generateTypeText(messages),
|
||||
};
|
||||
};
|
||||
|
||||
const updateLocalizations = async (file: string): Promise<void> => {
|
||||
wxt.logger.info(
|
||||
`\`[i18n]\` Localization file changed: \`${basename(file)}\``,
|
||||
);
|
||||
|
||||
// Regenerate files
|
||||
const [typesFile, jsonFiles] = await Promise.all([
|
||||
generateTypes(),
|
||||
generateOutputJsonFiles(),
|
||||
]);
|
||||
|
||||
// Write files to disk
|
||||
await Promise.all([
|
||||
writeFile(
|
||||
resolve(wxt.config.wxtDir, typesFile.path),
|
||||
typesFile.text,
|
||||
'utf8',
|
||||
),
|
||||
...jsonFiles.map((file) =>
|
||||
writeFile(
|
||||
resolve(wxt.config.outDir, file.relativeDest),
|
||||
file.contents,
|
||||
'utf8',
|
||||
),
|
||||
),
|
||||
]);
|
||||
|
||||
// TODO: Implement HMR instead of reloading extension. The reload is
|
||||
// fast, but it causes the popup to close, which I'd like to prevent.
|
||||
wxt.server?.reloadExtension();
|
||||
wxt.logger.success(`\`[i18n]\` Extension reloaded`);
|
||||
};
|
||||
|
||||
// Create .wxt/i18n.ts
|
||||
|
||||
const sourcePath = resolve(wxt.config.wxtDir, 'i18n/index.ts');
|
||||
const typesPath = resolve(wxt.config.wxtDir, 'i18n/structure.d.ts');
|
||||
|
||||
wxt.hooks.hook('prepare:types', async (_, entries) => {
|
||||
entries.push({
|
||||
path: sourcePath,
|
||||
text: `import { createI18n } from '@wxt-dev/i18n';
|
||||
import type { GeneratedI18nStructure } from './structure';
|
||||
|
||||
export const i18n = createI18n<GeneratedI18nStructure>();
|
||||
|
||||
export { GeneratedI18nStructure }
|
||||
`,
|
||||
});
|
||||
});
|
||||
|
||||
addAlias(wxt, '#i18n', sourcePath);
|
||||
|
||||
// Generate separate declaration file containing types - this prevents
|
||||
// firing the dev server's default file watcher when updating the types,
|
||||
// which would cause a full rebuild and reload of the extension.
|
||||
|
||||
wxt.hooks.hook('prepare:types', async (_, entries) => {
|
||||
entries.push(await generateTypes());
|
||||
});
|
||||
|
||||
// Generate _locales/.../messages.json files
|
||||
|
||||
wxt.hooks.hook('build:publicAssets', async (_, assets) => {
|
||||
const outFiles = await generateOutputJsonFiles();
|
||||
assets.push(...outFiles);
|
||||
});
|
||||
|
||||
// Reload extension during development
|
||||
|
||||
if (wxt.config.command === 'serve') {
|
||||
wxt.hooks.hookOnce('build:done', () => {
|
||||
const watcher = watch(resolve(wxt.config.srcDir, 'locales'));
|
||||
watcher.on('change', updateLocalizations);
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
export interface I18nFeatures {
|
||||
plural: boolean;
|
||||
substitutions: SubstitutionCount;
|
||||
}
|
||||
|
||||
export type I18nStructure = {
|
||||
[K: string]: I18nFeatures;
|
||||
};
|
||||
|
||||
export type DefaultI18nStructure = {
|
||||
[K: string]: any;
|
||||
};
|
||||
|
||||
// prettier-ignore
|
||||
export type SubstitutionTuple<T extends SubstitutionCount> =
|
||||
T extends 1 ? [$1: Substitution]
|
||||
: T extends 2 ? [$1: Substitution, $2: Substitution]
|
||||
: T extends 3 ? [$1: Substitution, $2: Substitution, $3: Substitution]
|
||||
: T extends 4 ? [$1: Substitution, $2: Substitution, $3: Substitution, $4: Substitution]
|
||||
: T extends 5 ? [$1: Substitution, $2: Substitution, $3: Substitution, $4: Substitution, $5: Substitution]
|
||||
: T extends 6 ? [$1: Substitution, $2: Substitution, $3: Substitution, $4: Substitution, $5: Substitution, $6: Substitution]
|
||||
: T extends 7 ? [$1: Substitution, $2: Substitution, $3: Substitution, $4: Substitution, $5: Substitution, $6: Substitution, $7: Substitution]
|
||||
: T extends 8 ? [$1: Substitution, $2: Substitution, $3: Substitution, $4: Substitution, $5: Substitution, $6: Substitution, $7: Substitution, $8: Substitution]
|
||||
: T extends 9 ? [$1: Substitution, $2: Substitution, $3: Substitution, $4: Substitution, $5: Substitution, $6: Substitution, $7: Substitution, $8: Substitution, $9: Substitution]
|
||||
: never
|
||||
|
||||
export type TFunction<T extends I18nStructure> = {
|
||||
// Non-plural, no substitutions
|
||||
<K extends keyof T>(
|
||||
// prettier-ignore
|
||||
key: K & { [P in keyof T]: T[P] extends { plural: false; substitutions: 0 } ? P : never; }[keyof T],
|
||||
options?: GetMessageOptions,
|
||||
): string;
|
||||
|
||||
// Non-plural with substitutions
|
||||
<K extends keyof T>(
|
||||
// prettier-ignore
|
||||
key: K & { [P in keyof T]: T[P] extends { plural: false; substitutions: SubstitutionCount } ? P : never; }[keyof T],
|
||||
substitutions: T[K] extends I18nFeatures
|
||||
? SubstitutionTuple<T[K]['substitutions']>
|
||||
: never,
|
||||
options?: GetMessageOptions,
|
||||
): string;
|
||||
|
||||
// Plural with 1 substitution
|
||||
<K extends keyof T>(
|
||||
// prettier-ignore
|
||||
key: K & { [P in keyof T]: T[P] extends { plural: true; substitutions: 1 } ? P : never; }[keyof T],
|
||||
n: number,
|
||||
substitutions?: SubstitutionTuple<1>,
|
||||
options?: GetMessageOptions,
|
||||
): string;
|
||||
|
||||
// Plural without substitutions
|
||||
<K extends keyof T>(
|
||||
// prettier-ignore
|
||||
key: K & { [P in keyof T]: T[P] extends { plural: true; substitutions: 0 | 1 } ? P : never; }[keyof T],
|
||||
n: number,
|
||||
options?: GetMessageOptions,
|
||||
): string;
|
||||
|
||||
// Plural with substitutions
|
||||
<K extends keyof T>(
|
||||
// prettier-ignore
|
||||
key: K & { [P in keyof T]: T[P] extends { plural: true; substitutions: SubstitutionCount } ? P : never; }[keyof T],
|
||||
n: number,
|
||||
substitutions: T[K] extends I18nFeatures
|
||||
? SubstitutionTuple<T[K]['substitutions']>
|
||||
: never,
|
||||
options?: GetMessageOptions,
|
||||
): string;
|
||||
};
|
||||
|
||||
export interface I18n<T extends DefaultI18nStructure> {
|
||||
t: TFunction<T>;
|
||||
}
|
||||
|
||||
export type Substitution = string | number;
|
||||
|
||||
export interface GetMessageOptions {
|
||||
/**
|
||||
* Escape `<` in translation to `<`. This applies only to the message itself, not to the placeholders. Developers might want to use this if the translation is used in an HTML context. Closure Templates used with Closure Compiler generate this automatically.
|
||||
*
|
||||
* See https://developer.chrome.com/docs/extensions/reference/api/i18n#type-getMessage-options
|
||||
*/
|
||||
escapeLt?: boolean;
|
||||
}
|
||||
|
||||
type SubstitutionCount = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ChromeMessage } from './build';
|
||||
|
||||
export function applyChromeMessagePlaceholders(message: ChromeMessage): string {
|
||||
if (message.placeholders == null) return message.message;
|
||||
|
||||
return Object.entries(message.placeholders ?? {}).reduce(
|
||||
(text, [name, value]) => {
|
||||
return text.replaceAll(new RegExp(`\\$${name}\\$`, 'gi'), value.content);
|
||||
},
|
||||
message.message,
|
||||
);
|
||||
}
|
||||
|
||||
export function getSubstitionCount(message: string): number {
|
||||
return (
|
||||
1 +
|
||||
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()],
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
# 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)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Enable auto-imports for JSX/TSX files ([#773](https://github.com/wxt-dev/wxt/pull/773))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Upgrade wxt peer to >= 0.18.6 ([7edf1c8](https://github.com/wxt-dev/wxt/commit/7edf1c8))
|
||||
- Use `prepare` instead of `postinstall` for local dev setup ([#788](https://github.com/wxt-dev/wxt/pull/788))
|
||||
|
||||
### 🏡 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))
|
||||
|
||||
## v1.0.0
|
||||
|
||||
Initial release 🎉
|
||||
@@ -0,0 +1,12 @@
|
||||
import { defineBuildConfig } from 'unbuild';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
export default defineBuildConfig({
|
||||
rootDir: 'modules',
|
||||
outDir: resolve(__dirname, 'dist'),
|
||||
entries: [{ input: 'react.ts', name: 'index' }],
|
||||
rollup: {
|
||||
emitCJS: true,
|
||||
},
|
||||
declaration: true,
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
export default function () {
|
||||
const [count, setCount] = useState(0);
|
||||
const increment = () => setCount((count) => count + 1);
|
||||
return <button onClick={increment}>Count: {count}</button>;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { defineContentScript } from 'wxt/sandbox';
|
||||
import { ContentScriptContext, createShadowRootUi } from 'wxt/client';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ['*://*/*'],
|
||||
|
||||
async main(ctx) {
|
||||
const ui = await createUi(ctx);
|
||||
ui.mount();
|
||||
},
|
||||
});
|
||||
|
||||
function createUi(ctx: ContentScriptContext) {
|
||||
return createShadowRootUi(ctx, {
|
||||
name: 'react-ui',
|
||||
position: 'inline',
|
||||
append: 'first',
|
||||
onMount(container) {
|
||||
const root = ReactDOM.createRoot(container);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
return root;
|
||||
},
|
||||
onRemove(root) {
|
||||
root?.unmount();
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Document</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="./main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
|
||||
const root = document.getElementById('app')!;
|
||||
|
||||
ReactDOM.createRoot(root).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
@@ -13,6 +13,17 @@ export default defineWxtModule<ReactModuleOptions>({
|
||||
}));
|
||||
|
||||
addImportPreset(wxt, 'react');
|
||||
|
||||
// Enable auto-imports for JSX files
|
||||
wxt.hooks.hook('ready', (wxt) => {
|
||||
if (wxt.config.imports === false) return;
|
||||
|
||||
wxt.config.imports.dirsScanOptions ??= {};
|
||||
wxt.config.imports.dirsScanOptions.filePatterns = [
|
||||
// Default plus JSX/TSX
|
||||
'*.{ts,js,mjs,cjs,mts,cts,jsx,tsx}',
|
||||
];
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,6 +1,23 @@
|
||||
{
|
||||
"name": "@wxt-dev/module-react",
|
||||
"version": "1.0.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",
|
||||
@@ -21,20 +38,25 @@
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "pnpm -s build-deps && unbuild",
|
||||
"check": "pnpm -s build-deps && check",
|
||||
"build-deps": "pnpm -ws build-deps @wxt-dev/module-react"
|
||||
"dev": "wxt",
|
||||
"check": "pnpm build && check",
|
||||
"build": "buildc -- unbuild",
|
||||
"prepare": "buildc --deps-only -- wxt prepare"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"wxt": ">=0.18.5"
|
||||
"wxt": ">=0.18.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vitejs/plugin-react": "^4.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "^1.3.1",
|
||||
"publint": "^0.2.8",
|
||||
"typescript": "^5.4.5",
|
||||
"@aklinker1/check": "^1.4.5",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"publint": "^0.2.9",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"typescript": "^5.5.4",
|
||||
"unbuild": "^2.0.0",
|
||||
"wxt": "workspace:*"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"extends": ["../../tsconfig.base.json", "./.wxt/tsconfig.json"],
|
||||
"compilerOptions": {
|
||||
"allowImportingTsExtensions": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"exclude": ["node_modules/**", "dist/**"]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
# 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)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Use `prepare` instead of `postinstall` for local dev setup ([#788](https://github.com/wxt-dev/wxt/pull/788))
|
||||
|
||||
## v1.1.0
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/module-solid-v1.0.1...module-solid-v1.1.0)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Enable auto-imports for JSX/TSX files ([#773](https://github.com/wxt-dev/wxt/pull/773))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Upgrade wxt peer to >= 0.18.6 ([7edf1c8](https://github.com/wxt-dev/wxt/commit/7edf1c8))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- 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))
|
||||
|
||||
## v1.0.1
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/module-solid-v1.0.0...module-solid-v1.0.1)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Add `target: esnext` by default ([#733](https://github.com/wxt-dev/wxt/pull/733))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Add changelog ([21e8ca0](https://github.com/wxt-dev/wxt/commit/21e8ca0))
|
||||
|
||||
## v1.0.0
|
||||
|
||||
Initial release 🎉
|
||||
@@ -4,7 +4,7 @@ Enables the use of [SolidJS](https://www.solidjs.com/) in your web extension, in
|
||||
|
||||
This plugin makes a few changes:
|
||||
|
||||
1. Adds `vite-plugin-solid` to vite
|
||||
1. Adds `vite-plugin-solid` to and sets `build.target: esnext` in the vite config
|
||||
2. Adds the [`solid-js` preset](https://github.com/unjs/unimport/blob/main/src/presets/solid.ts) to auto-imports
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { defineBuildConfig } from 'unbuild';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
export default defineBuildConfig({
|
||||
rootDir: 'modules',
|
||||
outDir: resolve(__dirname, 'dist'),
|
||||
entries: [{ input: 'solid.ts', name: 'index' }],
|
||||
rollup: {
|
||||
emitCJS: true,
|
||||
},
|
||||
declaration: true,
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Component } from 'solid-js';
|
||||
|
||||
export const App: Component = () => {
|
||||
const [count, setCount] = createSignal(0);
|
||||
const increment = () => setCount((count) => count + 1);
|
||||
return <button onClick={increment}>Count: {count()}</button>;
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import { defineContentScript } from 'wxt/sandbox';
|
||||
import { ContentScriptContext, createShadowRootUi } from 'wxt/client';
|
||||
import { render } from 'solid-js/web';
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ['*://*/*'],
|
||||
|
||||
async main(ctx) {
|
||||
const ui = await createUi(ctx);
|
||||
ui.mount();
|
||||
},
|
||||
});
|
||||
|
||||
function createUi(ctx: ContentScriptContext) {
|
||||
return createShadowRootUi(ctx, {
|
||||
name: 'solid-ui',
|
||||
position: 'inline',
|
||||
append: 'first',
|
||||
onMount(container) {
|
||||
return render(() => <App />, container);
|
||||
},
|
||||
onRemove(unmount) {
|
||||
unmount?.();
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Document</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="./main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,5 @@
|
||||
import { render } from 'solid-js/web';
|
||||
|
||||
const root = document.getElementById('app')!;
|
||||
|
||||
render(() => <App />, root);
|
||||
@@ -10,9 +10,23 @@ export default defineWxtModule<SolidModuleOptions>({
|
||||
|
||||
addViteConfig(wxt, () => ({
|
||||
plugins: [solid(vite)],
|
||||
build: {
|
||||
target: 'esnext',
|
||||
},
|
||||
}));
|
||||
|
||||
addImportPreset(wxt, 'solid-js');
|
||||
|
||||
// Enable auto-imports for JSX files
|
||||
wxt.hooks.hook('ready', (wxt) => {
|
||||
if (wxt.config.imports === false) return;
|
||||
|
||||
wxt.config.imports.dirsScanOptions ??= {};
|
||||
wxt.config.imports.dirsScanOptions.filePatterns = [
|
||||
// Default plus JSX/TSX
|
||||
'*.{ts,js,mjs,cjs,mts,cts,jsx,tsx}',
|
||||
];
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,6 +1,23 @@
|
||||
{
|
||||
"name": "@wxt-dev/module-solid",
|
||||
"version": "1.0.0",
|
||||
"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",
|
||||
@@ -21,20 +38,22 @@
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "pnpm -s build-deps && unbuild",
|
||||
"check": "pnpm -s build-deps && check",
|
||||
"build-deps": "pnpm -ws build-deps @wxt-dev/module-solid"
|
||||
"dev": "wxt",
|
||||
"check": "pnpm build && check",
|
||||
"build": "buildc -- unbuild",
|
||||
"prepare": "buildc --deps-only -- wxt prepare"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"wxt": ">=0.18.5"
|
||||
"wxt": ">=0.18.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"vite-plugin-solid": "^2.10.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "^1.3.1",
|
||||
"publint": "^0.2.8",
|
||||
"typescript": "^5.4.5",
|
||||
"@aklinker1/check": "^1.4.5",
|
||||
"publint": "^0.2.9",
|
||||
"solid-js": "^1.8.19",
|
||||
"typescript": "^5.5.4",
|
||||
"unbuild": "^2.0.0",
|
||||
"wxt": "workspace:*"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"extends": ["../../tsconfig.base.json", "./.wxt/tsconfig.json"],
|
||||
"compilerOptions": {
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "solid-js"
|
||||
},
|
||||
"exclude": ["node_modules/**", "dist/**"]
|
||||
}
|
||||
|
||||
@@ -0,0 +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 🎉
|
||||
@@ -5,7 +5,7 @@ Enables the use of [Svelte](https://svelte.dev/) in your web extension, in HTML
|
||||
This plugin makes a few changes:
|
||||
|
||||
1. Adds `@sveltejs/vite-plugin-svelte` to vite
|
||||
2. Adds the [`svelte` preset](https://github.com/unjs/unimport/blob/main/src/presets/vue.ts) to auto-imports
|
||||
2. Adds the [`svelte` preset](https://github.com/unjs/unimport/blob/main/src/presets/svelte.ts) to auto-imports
|
||||
|
||||
## Usage
|
||||
|
||||
|
||||
@@ -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",
|
||||
@@ -21,20 +38,19 @@
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "pnpm -s build-deps && unbuild",
|
||||
"check": "pnpm -s build-deps && check",
|
||||
"build-deps": "pnpm -ws build-deps @wxt-dev/module-svelte"
|
||||
"build": "buildc -- unbuild",
|
||||
"check": "pnpm build && check"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"wxt": ">=0.18.5"
|
||||
"wxt": ">=0.18.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^3.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "^1.3.1",
|
||||
"publint": "^0.2.8",
|
||||
"typescript": "^5.4.5",
|
||||
"@aklinker1/check": "^1.4.5",
|
||||
"publint": "^0.2.9",
|
||||
"typescript": "^5.5.4",
|
||||
"unbuild": "^2.0.0",
|
||||
"wxt": "workspace:*"
|
||||
}
|
||||
|
||||
@@ -0,0 +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 🎉
|
||||
@@ -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",
|
||||
@@ -21,20 +38,19 @@
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "pnpm -s build-deps && unbuild",
|
||||
"check": "pnpm -s build-deps && check",
|
||||
"build-deps": "pnpm -ws build-deps @wxt-dev/module-vue"
|
||||
"build": "buildc -- unbuild",
|
||||
"check": "pnpm build && check"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"wxt": ">=0.18.5"
|
||||
"wxt": ">=0.18.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vitejs/plugin-vue": "^5.0.5"
|
||||
"@vitejs/plugin-vue": "^5.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "^1.3.1",
|
||||
"publint": "^0.2.8",
|
||||
"typescript": "^5.4.5",
|
||||
"@aklinker1/check": "^1.4.5",
|
||||
"publint": "^0.2.9",
|
||||
"typescript": "^5.5.4",
|
||||
"unbuild": "^2.0.0",
|
||||
"wxt": "workspace:*"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import autoImports from './.wxt/eslintrc-auto-import.js';
|
||||
|
||||
export default [
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...autoImports.globals,
|
||||
},
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -4,28 +4,35 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "pnpm -s build-deps && wxt",
|
||||
"build": "pnpm -s build-deps && wxt build",
|
||||
"build:all": "pnpm -s build-deps && run-s -s 'build:all:*'",
|
||||
"dev": "buildc --deps-only -- wxt",
|
||||
"build": "buildc --deps-only -- wxt build",
|
||||
"build:all": "buildc --deps-only -- run-s -s 'build:all:*'",
|
||||
"build:all:chrome-mv3": "wxt build",
|
||||
"build:all:chrome-mv2": "wxt build --mv2",
|
||||
"build:all:firefox-mv3": "wxt build -b firefox --mv3",
|
||||
"build:all:firefox-mv2": "wxt build -b firefox",
|
||||
"test": "pnpm -s build-deps && vitest",
|
||||
"zip": "pnpm -s build-deps && wxt zip",
|
||||
"check": "pnpm -s build-deps && check",
|
||||
"postinstall": "pnpm -s build-deps && wxt prepare",
|
||||
"build-deps": "pnpm -sw build-deps wxt-demo"
|
||||
"test": "buildc --deps-only -- vitest",
|
||||
"zip": "buildc --deps-only -- wxt zip",
|
||||
"check": "buildc --deps-only -- check",
|
||||
"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.269",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"sass": "^1.77.5",
|
||||
"typescript": "^5.4.5",
|
||||
"@wxt-dev/auto-icons": "workspace:*",
|
||||
"sass": "^1.77.8",
|
||||
"typescript": "^5.5.4",
|
||||
"vitest": "^2.0.4",
|
||||
"vitest-plugin-random-seed": "^1.1.0",
|
||||
"wxt": "workspace:*"
|
||||
},
|
||||
"buildc": {
|
||||
"cachable": false
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user