Compare commits
144 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 32500a78be | |||
| 6c9beee71a | |||
| b6a0cbbbed | |||
| 406766b37f | |||
| 6f26eb5072 | |||
| 900d3bbb18 | |||
| 148bc4a6eb | |||
| ee3cbe6925 | |||
| 11ecd3f818 | |||
| 72d4f7de3a | |||
| 6be319f379 | |||
| 84083bd9b0 | |||
| d718f7fb50 | |||
| 7530628eb8 | |||
| 4b3ffc90c8 | |||
| 01e4c1975a | |||
| 65dfded682 | |||
| c712175445 | |||
| 0f71d570ca | |||
| b150a52471 | |||
| 028c601515 | |||
| af382ef065 | |||
| 91ef7f2ab5 | |||
| 0bf7279c8f | |||
| 62525d33c6 | |||
| 88ff40e301 | |||
| 1a72a0cdc6 | |||
| 2b07b4bc12 | |||
| 06c09e7c87 | |||
| 30b96ac87c | |||
| 2e9b5cffe8 | |||
| 3e1641a6ec | |||
| d64ca52bb0 | |||
| 8c645fa1ce | |||
| 79c9c89923 | |||
| 11d93fe724 | |||
| 039c561a80 | |||
| 64a704fc72 | |||
| 4fbf137d15 | |||
| ff065ab88b | |||
| 7fdf3fb968 | |||
| 7ebd451409 | |||
| a7a48a8a75 | |||
| 27298b7f22 | |||
| 565ce93a9a | |||
| 0bc6088b6a | |||
| 2fb99e8fbb | |||
| cd2050c534 | |||
| 5b557d0224 | |||
| 0137d79ba0 | |||
| e05fbf4fb5 | |||
| 72e6ca5063 | |||
| af9e842fe0 | |||
| 230962686f | |||
| 413cd9354f | |||
| 64a42de31f | |||
| f7f03e2669 | |||
| 1dc7c34bfd | |||
| 0fd5c3846c | |||
| e42e92f9df | |||
| e45c91f941 | |||
| 8f71d80f94 | |||
| 78e896e5fb | |||
| a643852387 | |||
| e221252325 | |||
| 88f126e658 | |||
| 17af05acc8 | |||
| fd436a0d63 | |||
| 0d796b63c2 | |||
| cb143d2bbc | |||
| f3e7758521 | |||
| 23ef101954 | |||
| c2f5efbde4 | |||
| 4ce178c592 | |||
| 7aa30afa7f | |||
| 90aaed202c | |||
| c92ed13d66 | |||
| 0e4953fbfc | |||
| 799d687061 | |||
| 866eb0694c | |||
| e5723aab03 | |||
| f51f5dd19f | |||
| aae5f0789d | |||
| 586fe3950c | |||
| 241c907c65 | |||
| aab4244d84 | |||
| 767a3a987c | |||
| a2924d872a | |||
| 54baf21490 | |||
| 5a3790a7d3 | |||
| 236e099b81 | |||
| 9b54bc4704 | |||
| ffb99af719 | |||
| 90bb145d91 | |||
| 190c87d120 | |||
| 4c511d5728 | |||
| b8ecbc5a54 | |||
| 8c3d756187 | |||
| cfdc6b3e6b | |||
| 226e6c9fbe | |||
| db41f27072 | |||
| db025cd3a2 | |||
| 57229c69c1 | |||
| 8f3a90eeb2 | |||
| a360965d92 | |||
| eb1ace6d20 | |||
| 497c902283 | |||
| 4d4453b830 | |||
| ef53580ed1 | |||
| 52e5387d47 | |||
| f5b7f7e0e5 | |||
| 592bdbe4d9 | |||
| 6fc227bad6 | |||
| d74f6b0735 | |||
| 62ea796045 | |||
| 064704c9b7 | |||
| c846a1febf | |||
| c3959e8427 | |||
| e770f9b6b7 | |||
| 67f35615a2 | |||
| 5c7b6098a2 | |||
| f7d12b7486 | |||
| 3481313859 | |||
| 2f23556893 | |||
| 330e39159a | |||
| aa14676f91 | |||
| 199f330324 | |||
| 63dee97ac3 | |||
| b121ed2fe2 | |||
| 607e1162e5 | |||
| b4d569a915 | |||
| 4fac364542 | |||
| 51c7ee5fa8 | |||
| 9b780f2d00 | |||
| f5aec7ec9b | |||
| 5d991eda31 | |||
| 0318a34046 | |||
| e7c66304d3 | |||
| 4603ebb511 | |||
| ea570c12b7 | |||
| c17ce34a27 | |||
| 0e4d3ad8ab | |||
| 4304f71df2 | |||
| bb5ea34396 |
@@ -0,0 +1,15 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [wxt-dev] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||
polar: # Replace with a single Polar username
|
||||
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
|
||||
thanks_dev: # Replace with a single thanks.dev username
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
@@ -4,6 +4,9 @@ inputs:
|
||||
install:
|
||||
default: 'true'
|
||||
description: Whether or not to run 'pnpm install'
|
||||
installArgs:
|
||||
default: ''
|
||||
description: Additional args to append to "pnpm install"
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
@@ -17,4 +20,4 @@ runs:
|
||||
- name: Install Dependencies
|
||||
if: ${{ inputs.install == 'true' }}
|
||||
shell: bash
|
||||
run: pnpm install
|
||||
run: pnpm install ${{ inputs.installArgs }}
|
||||
|
||||
@@ -13,7 +13,9 @@ on:
|
||||
- module-solid
|
||||
- module-svelte
|
||||
- module-vue
|
||||
- storage
|
||||
- unocss
|
||||
- webextension-polyfill
|
||||
- wxt
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -7,13 +7,15 @@ on:
|
||||
default: wxt
|
||||
type: choice
|
||||
options:
|
||||
- wxt
|
||||
- module-react
|
||||
- module-vue
|
||||
- module-svelte
|
||||
- module-solid
|
||||
- auto-icons
|
||||
- i18n
|
||||
- module-react
|
||||
- module-solid
|
||||
- module-svelte
|
||||
- module-vue
|
||||
- storage
|
||||
- webextension-polyfill
|
||||
- wxt
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
@@ -21,6 +23,8 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup
|
||||
with:
|
||||
installArgs: --ignore-scripts
|
||||
- run: pnpm tsx scripts/sync-releases.ts ${{ inputs.package }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
- name: pnpm test:coverage
|
||||
run: pnpm test:coverage -- --reporter=default --reporter=hanging-process
|
||||
- uses: codecov/codecov-action@v4
|
||||
- uses: codecov/codecov-action@v5
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
windows-tests:
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
- Demonstrating empathy and kindness toward other people
|
||||
- Being respectful of differing opinions, viewpoints, and experiences
|
||||
- Giving and gracefully accepting constructive feedback
|
||||
- Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
- Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
- The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
- Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
aaronklinker1@gmail.com.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
@@ -32,6 +32,7 @@ Here are some helpful commands:
|
||||
|
||||
```sh
|
||||
# Build WXT package
|
||||
cd packages/wxt
|
||||
pnpm build
|
||||
```
|
||||
|
||||
@@ -144,3 +145,34 @@ pnpm dlx taze -r
|
||||
```
|
||||
|
||||
Configuration is in [`taze.config.ts`](./taze.config.ts).
|
||||
|
||||
## Install Unreleased Versions
|
||||
|
||||
This repo uses https://pkg.pr.new to publish versions of all it's packages for almost every commit. You can install them via:
|
||||
|
||||
```sh
|
||||
npm i https://pkg.pr.new/[package-name]@[ref]
|
||||
```
|
||||
|
||||
Or use one of the shorthands:
|
||||
|
||||
```sh
|
||||
# Install the latest build of `wxt` from a PR:
|
||||
npm i https://pkg.pr.new/wxt@1283
|
||||
|
||||
# Install the latest build of `@wxt-dev/module-react` on the `main` branch
|
||||
npm i https://pkg.pr.new/@wxt-dev/module-react@main
|
||||
|
||||
# Install `@wxt-dev/storage` from a specific commit:
|
||||
npm i https://pkg.pr.new/@wxt-dev/module-react@426f907
|
||||
```
|
||||
|
||||
## Blog Posts
|
||||
|
||||
Anyone is welcome to submit a blog post on https://wxt.dev/blog!
|
||||
|
||||
> [!NOTE]
|
||||
> Before starting on a blog post, please message Aaron on Discord or start a discussion on GitHub to get permission to write about a topic, but most topics are welcome: Major version updates, tutorials, etc.
|
||||
|
||||
- **English only**: Blog posts should be written in English. Unfortunately, our maintainers doesn't have the bandwidth right now to translate our docs, let alone blog posts. Sorry 😓
|
||||
- **AI**: Please only use AI to translate or proof-read your blog post. Don't generate the whole thing... We don't want to publish that.
|
||||
|
||||
@@ -75,6 +75,12 @@ Or see the [installation guide](https://wxt.dev/guide/installation.html) to get
|
||||
- 📏 Bundle analysis
|
||||
- ⬇️ Download and bundle remote URL imports
|
||||
|
||||
## Sponsors
|
||||
|
||||
WXT is a [MIT-licensed](https://github.com/wxt-dev/wxt/blob/main/LICENSE) open source project with its ongoing development made possible entirely by the support of these awesome backers. If you'd like to join them, please consider [sponsoring WXT's development](https://github.com/sponsors/wxt-dev).
|
||||
|
||||
<a href="https://github.com/sponsors/wxt-dev"><img alt="WXT Sponsors" src="https://raw.githubusercontent.com/wxt-dev/static/refs/heads/main/sponsorkit/sponsors.svg"></a>
|
||||
|
||||
## Contributors
|
||||
|
||||
<!-- automd:contributors author="aklinker1" license="MIT" github="wxt-dev/wxt" -->
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
// @ts-expect-error: Vitepress data-loader magic, this import is correct
|
||||
import { data } from '../loaders/blog.data';
|
||||
import BlogPostPreview from './BlogPostPreview.vue';
|
||||
|
||||
const posts = computed(() =>
|
||||
data
|
||||
.map((post) => ({
|
||||
...post,
|
||||
...post.frontmatter,
|
||||
date: new Date(post.frontmatter.date),
|
||||
}))
|
||||
.sort((a, b) => b.date.getTime() - a.date.getTime()),
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container">
|
||||
<div>
|
||||
<div class="vp-doc">
|
||||
<h1>Blog</h1>
|
||||
</div>
|
||||
|
||||
<ul>
|
||||
<BlogPostPreview v-for="post of posts" :key="post.url" :post />
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
.container > div {
|
||||
padding: 32px;
|
||||
max-width: 900px;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
ul {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
list-style: none;
|
||||
}
|
||||
ul,
|
||||
li {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
ul li {
|
||||
padding-top: 16px;
|
||||
margin-top: 16px;
|
||||
border-top: 1px solid var(--vp-c-default);
|
||||
}
|
||||
ul li:last-child {
|
||||
padding-bottom: 16px;
|
||||
margin-bottom: 16px;
|
||||
border-bottom: 1px solid var(--vp-c-default);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,76 @@
|
||||
<script lang="ts" setup>
|
||||
import useBlogDate from '../composables/useBlogDate';
|
||||
import { useData } from 'vitepress';
|
||||
|
||||
const { frontmatter } = useData();
|
||||
const date = useBlogDate(() => frontmatter.value.date);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="vp-doc">
|
||||
<main class="container-content">
|
||||
<h1 v-html="$frontmatter.title" />
|
||||
<p class="meta-row">
|
||||
<a
|
||||
class="author"
|
||||
v-for="author of $frontmatter.authors"
|
||||
:key="author.github"
|
||||
:href="`https://github.com/${author.github}`"
|
||||
>
|
||||
<img :src="`https://github.com/${author.github}.png?size=96`" />
|
||||
<span>{{ author.name }}</span>
|
||||
</a>
|
||||
<span>•</span>
|
||||
<span>{{ date }}</span>
|
||||
</p>
|
||||
<Content />
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
vp-doc {
|
||||
display: flex;
|
||||
}
|
||||
main {
|
||||
max-width: 1080px;
|
||||
padding: 32px;
|
||||
margin: auto;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
main {
|
||||
padding: 64px;
|
||||
}
|
||||
}
|
||||
.meta-row {
|
||||
display: flex;
|
||||
color: var(--vp-c-text-2);
|
||||
gap: 16px;
|
||||
overflow: hidden;
|
||||
padding-bottom: 32px;
|
||||
}
|
||||
.meta-row > * {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.author {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
color: var(--vp-c-text-2);
|
||||
font-weight: normal;
|
||||
text-decoration: none;
|
||||
}
|
||||
.author img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
.author span {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.author:hover {
|
||||
text-decoration: underline;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,72 @@
|
||||
<script lang="ts" setup>
|
||||
import useBlogDate from '../composables/useBlogDate';
|
||||
|
||||
const props = defineProps<{
|
||||
post: {
|
||||
title: string;
|
||||
description?: string;
|
||||
date: Date;
|
||||
url: string;
|
||||
authors: Array<{ name: string; github: string }>;
|
||||
};
|
||||
}>();
|
||||
|
||||
const date = useBlogDate(() => props.post.date);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li class="blog-list-item">
|
||||
<a :href="post.url">
|
||||
<div class="vp-doc">
|
||||
<h3 class="title" v-html="post.title" />
|
||||
<p class="description" v-html="post.description" />
|
||||
<p class="meta">
|
||||
{{ post.authors.map((author) => author.name).join(', ') }}
|
||||
•
|
||||
{{ date }}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
li {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
h3 {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
li > a > div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0 -16px;
|
||||
padding: 16px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
li > a > div:hover {
|
||||
background: var(--vp-c-default);
|
||||
}
|
||||
li .title {
|
||||
color: var(--vp-c-text);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
li .description {
|
||||
font-size: 16px;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
li .meta {
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
</style>
|
||||
@@ -4,11 +4,8 @@ import useListExtensionDetails, {
|
||||
ChromeExtension,
|
||||
} from '../composables/useListExtensionDetails';
|
||||
|
||||
// Add extension IDs here. Order doesn't matter, will be sorted by weekly active users
|
||||
// During the transition from chrome.google.com/webstore to
|
||||
// chromewebstore.google.com, queue.wxt.dev might return null for your
|
||||
// extension. If it does, use "<slug>/<id>" instead of just the ID. The slug
|
||||
// can be retrieved from the URL of the item on chromewebstore.google.com
|
||||
// Add extension IDs here. Order doesn't matter, will be sorted by a combination of weekly active users and rating.
|
||||
// Change the commit message or PR title to: "docs: Added "[extension name]" to the homepage"
|
||||
const chromeExtensionIds = [
|
||||
'ocfdgncpifmegplaglcnglhioflaimkd', // GitHub: Better Line Counts
|
||||
'mgmdkjcljneegjfajchedjpdhbadklcf', // Anime Skip Player
|
||||
@@ -16,7 +13,7 @@ const chromeExtensionIds = [
|
||||
'elfaihghhjjoknimpccccmkioofjjfkf', // StayFree - Website Blocker & Web Analytics
|
||||
'okifoaikfmpfcamplcfjkpdnhfodpkil', // Doozy: Ai Made Easy
|
||||
'lknmjhcajhfbbglglccadlfdjbaiifig', // tl;dv - Record, Transcribe & ChatGPT for Google Meet
|
||||
'youtube中文配音/oglffgiaiekgeicdgkdlnlkhliajdlja', // Youtube中文配音
|
||||
'oglffgiaiekgeicdgkdlnlkhliajdlja', // Youtube中文配音
|
||||
'agjnjboanicjcpenljmaaigopkgdnihi', // PreMiD
|
||||
'aiakblgmlabokilgljkglggnpflljdgp', // Markdown Sticky Notes
|
||||
'nomnkbngkijpffepcgbbofhcnafpkiep', // DocVersionRedirector
|
||||
@@ -50,6 +47,20 @@ const chromeExtensionIds = [
|
||||
'fcphghnknhkimeagdglkljinmpbagone', // YouTube Auto HD + FPS
|
||||
'lpomjgbicdemjkgmbnkjncgdebogkhlb', // MultiViewer Companion
|
||||
'ggiafipgeeaaahnjamgpjcgkdpanhddg', // Sync Watch - Watch videos together on any site
|
||||
'nmldnjcblcihmegipecakhmnieiofmgl', // Keyword Rank Checker
|
||||
'gppllamhaciichleihemgilcpledblpn', // YouTube Simple View - Hide distractions & more
|
||||
'pccbghdfdnnkkbcdcibchpbffdgednkf', // Propbar - Property Data Enhancer
|
||||
'lfknakglefggmdkjdfhhofkjnnolffkh', // Text Search Pro - Search by case and whole-word match!
|
||||
'mbenhbocjckkbaojacmaepiameldglij', // Invoice Generator
|
||||
'phlfhkmdofajnbhgmbmjkbkdgppgoppb', // Monthly Bill Tracker
|
||||
'macmkmchfoclhpbncclinhjflmdkaoom', // Wandpen - Instantly improve your writing with AI
|
||||
'lhmgechokhmdekdpgkkemoeecelcaonm', // YouTube Hider - Remove Comments By Keywords, Usernames & Tools
|
||||
'imgheieooppmahcgniieddodaliodeeg', // QA Compass - Record standardized bug reports easily
|
||||
'npgghjedpchajflknnbngajkjkdhncdo', // aesthetic Notion, styled
|
||||
'hmdcmlfkchdmnmnmheododdhjedfccka', // Eye Dropper
|
||||
'eihpmapodnppeemkhkbhikmggfojdkjd', // Cursorful - Screen Recorder with Auto Zoom
|
||||
'hjjkgbibknbahijglkffklflidncplkn', // Show IP – Live View of Website IPs for Developers
|
||||
'ilbikcehnpkmldojkcmlldkoelofnbde', // Strong Password Generator
|
||||
];
|
||||
|
||||
const { data, err, isLoading } = useListExtensionDetails(chromeExtensionIds);
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { computed, toValue, MaybeRefOrGetter } from 'vue';
|
||||
|
||||
const MONTH_FORMATTER = new Intl.DateTimeFormat(
|
||||
globalThis?.navigator?.language,
|
||||
{
|
||||
month: 'long',
|
||||
},
|
||||
);
|
||||
|
||||
export default function (date: MaybeRefOrGetter<Date | string>) {
|
||||
return computed(() => {
|
||||
const d = new Date(toValue(date));
|
||||
return `${MONTH_FORMATTER.format(d)} ${d.getDate()}, ${d.getFullYear()}`;
|
||||
});
|
||||
}
|
||||
@@ -13,14 +13,20 @@ import { version as wxtVersion } from '../../packages/wxt/package.json';
|
||||
import { version as i18nVersion } from '../../packages/i18n/package.json';
|
||||
import { version as autoIconsVersion } from '../../packages/auto-icons/package.json';
|
||||
import { version as unocssVersion } from '../../packages/unocss/package.json';
|
||||
import { version as storageVersion } from '../../packages/storage/package.json';
|
||||
import { Feed } from 'feed';
|
||||
import { writeFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
|
||||
const origin = 'https://wxt.dev';
|
||||
|
||||
const title = 'Next-gen Web Extension Framework';
|
||||
const titleSuffix = ' – WXT';
|
||||
const description =
|
||||
"WXT provides the best developer experience, making it quick, easy, and fun to develop chrome extensions for all browsers. With built-in utilities for building, zipping, and publishing your extension, it's easy to get started.";
|
||||
"WXT provides the best developer experience, making it quick, easy, and fun to develop web extensions. With built-in utilities for building, zipping, and publishing your extension, it's easy to get started.";
|
||||
const ogTitle = `${title}${titleSuffix}`;
|
||||
const ogUrl = 'https://wxt.dev';
|
||||
const ogImage = 'https://wxt.dev/social-preview.png';
|
||||
const ogUrl = origin;
|
||||
const ogImage = `${origin}/social-preview.png`;
|
||||
|
||||
// https://vitepress.dev/reference/site-config
|
||||
export default defineConfig({
|
||||
@@ -32,7 +38,30 @@ export default defineConfig({
|
||||
},
|
||||
lastUpdated: true,
|
||||
sitemap: {
|
||||
hostname: 'https://wxt.dev',
|
||||
hostname: origin,
|
||||
},
|
||||
|
||||
async buildEnd(site) {
|
||||
// Only construct the RSS document for production builds
|
||||
const { default: blogDataLoader } = await import('./loaders/blog.data');
|
||||
const posts = await blogDataLoader.load();
|
||||
const feed = new Feed({
|
||||
copyright: 'MIT',
|
||||
id: 'wxt',
|
||||
title: 'WXT Blog',
|
||||
link: `${origin}/blog`,
|
||||
});
|
||||
posts.forEach((post) => {
|
||||
feed.addItem({
|
||||
date: post.frontmatter.date,
|
||||
link: new URL(post.url, origin).href,
|
||||
title: post.frontmatter.title,
|
||||
description: post.frontmatter.description,
|
||||
});
|
||||
});
|
||||
console.log('rss.xml:');
|
||||
console.log(feed.rss2());
|
||||
await writeFile(join(site.outDir, 'rss.xml'), feed.rss2(), 'utf8');
|
||||
},
|
||||
|
||||
head: [
|
||||
@@ -61,6 +90,16 @@ export default defineConfig({
|
||||
alt: 'WXT logo',
|
||||
},
|
||||
|
||||
footer: {
|
||||
message: [
|
||||
'<a class="light-netlify" href="https://www.netlify.com"> <img src="https://www.netlify.com/v3/img/components/netlify-color-bg.svg" alt="Deploys by Netlify" style="display: inline;" /></a>',
|
||||
'<a class="dark-netlify" href="https://www.netlify.com"> <img src="https://www.netlify.com/v3/img/components/netlify-color-accent.svg" alt="Deploys by Netlify" style="display: inline;" /></a>',
|
||||
'Released under the <a href="https://github.com/wxt-dev/wxt/blob/main/LICENSE">MIT License</a>.',
|
||||
].join('<br/>'),
|
||||
copyright:
|
||||
'Copyright © 2023-present <a href="https://github.com/aklinker1">Aaron Klinker</a>',
|
||||
},
|
||||
|
||||
editLink: {
|
||||
pattern: 'https://github.com/wxt-dev/wxt/edit/main/docs/:path',
|
||||
},
|
||||
@@ -78,6 +117,7 @@ export default defineConfig({
|
||||
navItem('Guide', '/guide/installation'),
|
||||
navItem('Examples', '/examples'),
|
||||
navItem('API', '/api/reference/wxt'),
|
||||
navItem('Blog', '/blog'),
|
||||
navItem(`v${wxtVersion}`, [
|
||||
navItem('wxt', [
|
||||
navItem(`v${wxtVersion}`, '/'),
|
||||
@@ -87,7 +127,7 @@ export default defineConfig({
|
||||
),
|
||||
]),
|
||||
navItem('Other Packages', [
|
||||
navItem(`wxt/storage — ${wxtVersion}`, '/storage'),
|
||||
navItem(`@wxt-dev/storage — ${storageVersion}`, '/storage'),
|
||||
navItem(`@wxt-dev/auto-icons — ${autoIconsVersion}`, '/auto-icons'),
|
||||
navItem(`@wxt-dev/i18n — ${i18nVersion}`, '/i18n'),
|
||||
navItem(`@wxt-dev/unocss — ${unocssVersion}`, '/unocss'),
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { createContentLoader } from 'vitepress';
|
||||
|
||||
export default createContentLoader('blog/*.md');
|
||||
@@ -1,72 +1,28 @@
|
||||
/* Colors */
|
||||
:root {
|
||||
--wxt-c-green: #53bc4a;
|
||||
--wxt-c-green-1: #0b8a00;
|
||||
--wxt-c-green-2: #096600;
|
||||
--wxt-c-green-3: #096600;
|
||||
--wxt-c-green-soft: rgba(11, 138, 0, 0.14);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--wxt-c-green-1: #67d45e;
|
||||
--wxt-c-green-2: #4fa048;
|
||||
--wxt-c-green-3: #447e3f;
|
||||
--wxt-c-green-2: #329929;
|
||||
--wxt-c-green-3: #21651b;
|
||||
--wxt-c-green-soft: rgba(103, 212, 94, 0.14);
|
||||
}
|
||||
|
||||
/* https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css */
|
||||
|
||||
:root {
|
||||
--vp-c-brand: var(--wxt-c-green);
|
||||
--vp-c-brand-1: var(--wxt-c-green-1);
|
||||
--vp-c-brand-2: var(--wxt-c-green-2);
|
||||
--vp-c-brand-3: var(--wxt-c-green-3);
|
||||
|
||||
--vp-button-brand-bg: var(--wxt-c-green);
|
||||
--vp-button-brand-hover-bg: var(--wxt-c-green-2);
|
||||
--vp-button-brand-active-bg: var(--wxt-c-green-3);
|
||||
|
||||
--vp-code-link-color: var(--wxt-c-green);
|
||||
|
||||
/* --vp-c-text-1: var(--wxt-c-green-1); */
|
||||
|
||||
--vp-button-brand-text: var(--vp-c-black);
|
||||
--vp-button-brand-hover-text: var(--vp-c-black);
|
||||
--vp-button-brand-active-text: var(--vp-c-black);
|
||||
|
||||
--vp-custom-block-tip-border: var(--wxt-c-green);
|
||||
--vp-custom-block-tip-text: var(--wxt-c-green);
|
||||
|
||||
/* --vp-code-block-bg: #222422;
|
||||
--vp-code-copy-code-bg: #313431;
|
||||
--vp-code-copy-code-hover-bg: #3c403c; */
|
||||
|
||||
--vp-custom-block-tip-bg: var(--vp-code-block-bg);
|
||||
--vp-custom-block-info-bg: var(--vp-code-block-bg);
|
||||
|
||||
--vp-code-color: #476582;
|
||||
--vp-c-brand-soft: var(--wxt-c-green-soft);
|
||||
}
|
||||
|
||||
.vp-doc a {
|
||||
color: var(--wxt-c-green);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--vp-c-bg: #131413;
|
||||
|
||||
--vp-c-bg-soft: #1a1b1a;
|
||||
--vp-c-bg-soft-up: #1f201f;
|
||||
--vp-c-bg-soft-down: #262926;
|
||||
--vp-c-bg-soft-mute: #242424;
|
||||
|
||||
--vp-c-bg-alt: #171817;
|
||||
|
||||
--vp-c-default: #313136;
|
||||
--vp-c-default-1: #3a3a3c;
|
||||
--vp-c-default-2: #505053;
|
||||
--vp-c-default-3: #2c2c30;
|
||||
--vp-c-default-soft: #252529;
|
||||
|
||||
--vp-code-block-bg: #191a19;
|
||||
--vp-code-copy-code-bg: #212321;
|
||||
--vp-code-copy-code-hover-bg: #292d29;
|
||||
|
||||
--vp-custom-block-info-bg: #191a19;
|
||||
|
||||
--vp-code-color: #c9def1;
|
||||
}
|
||||
/* Customize Individual Components */
|
||||
|
||||
.vp-doc .no-vertical-dividers th,
|
||||
.vp-doc .no-vertical-dividers td {
|
||||
@@ -97,3 +53,16 @@ body {
|
||||
vertical-align: middle;
|
||||
background-color: var(--vp-c-default-2);
|
||||
}
|
||||
|
||||
.light-netlify {
|
||||
display: inline;
|
||||
}
|
||||
.dark .light-netlify {
|
||||
display: none;
|
||||
}
|
||||
.dark-netlify {
|
||||
display: none;
|
||||
}
|
||||
.dark .dark-netlify {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
@@ -3,14 +3,17 @@ import Icon from '../components/Icon.vue';
|
||||
import EntrypointPatterns from '../components/EntrypointPatterns.vue';
|
||||
import UsingWxtSection from '../components/UsingWxtSection.vue';
|
||||
import ExampleSearch from '../components/ExampleSearch.vue';
|
||||
import BlogLayout from '../components/BlogLayout.vue';
|
||||
import './custom.css';
|
||||
|
||||
export default {
|
||||
extends: DefaultTheme,
|
||||
enhanceApp(ctx) {
|
||||
ctx.app.component('Icon', Icon);
|
||||
ctx.app.component('EntrypointPatterns', EntrypointPatterns);
|
||||
ctx.app.component('UsingWxtSection', UsingWxtSection);
|
||||
ctx.app.component('ExampleSearch', ExampleSearch);
|
||||
ctx.app
|
||||
.component('Icon', Icon)
|
||||
.component('EntrypointPatterns', EntrypointPatterns)
|
||||
.component('UsingWxtSection', UsingWxtSection)
|
||||
.component('ExampleSearch', ExampleSearch)
|
||||
.component('blog', BlogLayout);
|
||||
},
|
||||
};
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 132 KiB |
@@ -0,0 +1,9 @@
|
||||
---
|
||||
layout: page
|
||||
---
|
||||
|
||||
<script lang="ts" setup>
|
||||
import BlogHome from './.vitepress/components/BlogHome.vue';
|
||||
</script>
|
||||
|
||||
<BlogHome />
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
layout: blog
|
||||
title: Real World Messaging
|
||||
description: |
|
||||
The extension messaging APIs are difficult to learn. Let's go beyond the simple examples from Chrome and Firefox's documentation to build our own simple messaging system from scratch.
|
||||
authors:
|
||||
- name: Aaron Klinker
|
||||
github: aklinker1
|
||||
date: 2024-10-20T04:54:23.601Z
|
||||
---
|
||||
|
||||
Test content **bold** _italic_
|
||||
@@ -0,0 +1,72 @@
|
||||
---
|
||||
layout: blog
|
||||
title: Introducing <code>#imports</code>
|
||||
description: Learn how WXT's new <code>#imports</code> module works and how to use it.
|
||||
authors:
|
||||
- name: Aaron Klinker
|
||||
github: aklinker1
|
||||
date: 2024-12-06T14:39:00.000Z
|
||||
---
|
||||
|
||||
WXT v0.20 introduced a new way of manually importing its APIs: **the `#imports` module**. This module was introduced to simplify import statements and provide more visibility into all the APIs WXT provides.
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```ts
|
||||
import { browser } from 'wxt/browser'; // [!code --]
|
||||
import { createShadowRootUi } from 'wxt/utils/content-script-ui/shadow-root'; // [!code --]
|
||||
import { defineContentScript } from 'wxt/utils/define-content-script'; // [!code --]
|
||||
import { injectScript } from 'wxt/utils/inject-script'; // [!code --]
|
||||
import { // [!code ++]
|
||||
browser, createShadowRootUi, defineContentScript, injectScript // [!code ++]
|
||||
} from '#imports'; // [!code ++]
|
||||
```
|
||||
|
||||
The `#imports` module is considered a "virtual module", because the file doesn't actually exist. At build-time, imports are split into individual statements for each API:
|
||||
|
||||
:::code-group
|
||||
|
||||
```ts [What you write]
|
||||
import { defineContentScript, injectScript } from '#imports';
|
||||
```
|
||||
|
||||
```ts [What the bundler sees]
|
||||
import { defineContentScript } from 'wxt/utils/define-content-script';
|
||||
import { injectScript } from 'wxt/utils/inject-script';
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
Think of `#imports` as a convenient way to access all of WXT's APIs from one place, without impacting performance or bundle size.
|
||||
|
||||
This enables better tree-shaking compared to v0.19 and below.
|
||||
|
||||
:::tip Need to lookup the full import path of an API?
|
||||
Open up your project's `.wxt/types/imports-module.d.ts` file.
|
||||
:::
|
||||
|
||||
## Mocking
|
||||
|
||||
When writing tests, you might need to mock APIs from the `#imports` module. While mocking these APIs is very easy, it may not be immediately clear how to accomplish it.
|
||||
|
||||
Let's look at an example using Vitest. When [configured with `wxt/testing`](/guide/essentials/unit-testing#vitest), Vitest sees the same transformed code as the bundler. That means to mock an API from `#imports`, you need to call `vi.mock` with the real import path, not `#imports`:
|
||||
|
||||
```ts
|
||||
import { injectScript } from '#imports';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
vi.mock('wxt/utils/inject-script')
|
||||
const injectScriptMock = vi.mocked(injectScript);
|
||||
|
||||
injectScriptMock.mockReturnValueOnce(...);
|
||||
```
|
||||
|
||||
## Conclusion
|
||||
|
||||
You don't have to use `#imports` if you don't like - you can continue importing APIs from their submodules. However, using `#imports` is the recommended approach moving forwards.
|
||||
|
||||
- As more APIs are added, you won't have to memorize additional import paths.
|
||||
- If breaking changes are made to import paths in future major versions, `#imports` won't break.
|
||||
|
||||
Happy Coding 😄
|
||||
|
||||
> P.S. Yes, this is exactly how [Nuxt's `#imports`](https://nuxt.com/docs/guide/concepts/auto-imports#explicit-imports) works! We use the exact same library, [`unimport`](https://github.com/unjs/unimport).
|
||||
@@ -29,7 +29,7 @@ img.src = imageUrl;
|
||||
|
||||
## `/public` Directory
|
||||
|
||||
Files inside `<srcDir>/public/` are copied into the output folder as-is, without being processed by WXT's bundler.
|
||||
Files inside `<rootDir>/public/` are copied into the output folder as-is, without being processed by WXT's bundler.
|
||||
|
||||
Here's how you access them:
|
||||
|
||||
@@ -54,6 +54,10 @@ img.src = imageUrl;
|
||||
|
||||
:::
|
||||
|
||||
:::warning
|
||||
Assets in the `public/` directory are **_not_** accessible in content scripts by default. To use a public asset in a content script, you must add it to your manifest's [`web_accessible_resources` array](/api/reference/wxt/type-aliases/UserManifest#web-accessible-resources).
|
||||
:::
|
||||
|
||||
## Inside Content Scripts
|
||||
|
||||
Assets inside content scripts are a little different. By default, when you import an asset, it returns just the path to the asset. This is because Vite assumes you're loading assets from the same hostname.
|
||||
@@ -77,6 +81,84 @@ export default defineContentScript({
|
||||
|
||||
## WASM
|
||||
|
||||
:::warning 🚧 Under construction
|
||||
These docs will be coming soon!
|
||||
:::
|
||||
How a `.wasm` file is loaded varies greatly between packages, but most follow a basic setup: Use a JS API to load and execute the `.wasm` file.
|
||||
|
||||
For an extension, that means two things:
|
||||
|
||||
1. The `.wasm` file needs to be present in output folder so it can be loaded.
|
||||
2. You must import the JS API to load and initialize the `.wasm` file, usually provided by the NPM package.
|
||||
|
||||
For an example, let's say you have a content script needs to parse TS code into AST. We'll use [`@oxc-parser/wasm`](https://www.npmjs.com/package/@oxc-parser/wasm) to do it!
|
||||
|
||||
First, we need to copy the `.wasm` file to the output directory. We'll do it with a [WXT module](/guide/essentials/wxt-modules):
|
||||
|
||||
```ts
|
||||
// modules/oxc-parser-wasm.ts
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
export default defineWxtModule((wxt) => {
|
||||
wxt.hook('build:publicAssets', (_, assets) => {
|
||||
assets.push({
|
||||
absoluteSrc: resolve(
|
||||
'node_modules/@oxc-parser/wasm/web/oxc_parser_wasm_bg.wasm',
|
||||
),
|
||||
relativeDest: 'oxc_parser_wasm_bg.wasm',
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Run `wxt build`, and you should see the WASM file copied into your `.output/chrome-mv3` folder!
|
||||
|
||||
Next, since this is in a content script and we'll be fetching the WASM file over the network to load it, we need to add the file to the `web_accessible_resources`:
|
||||
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
export default defineConfig({
|
||||
manifest: {
|
||||
web_accessible_resources: [
|
||||
{
|
||||
// We'll use this matches in the cotent script as well
|
||||
matches: ['*://*.github.com/*'],
|
||||
// Use the same path as `relativeDest` from the WXT module
|
||||
resources: ['/oxc_parser_wasm_bg.wasm'],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
And finally, we need to load and initialize the `.wasm` file inside the content script to use it:
|
||||
|
||||
```ts
|
||||
// entrypoints/content.ts
|
||||
import initWasm, { parseSync } from '@oxc-parser/wasm';
|
||||
|
||||
export default defineContentScript({
|
||||
matches: '*://*.github.com/*',
|
||||
async main(ctx) {
|
||||
if (!location.pathname.endsWith('.ts')) return;
|
||||
|
||||
// Get text from GitHub
|
||||
const code = document.getElementById(
|
||||
'read-only-cursor-text-area',
|
||||
)?.textContent;
|
||||
if (!code) return;
|
||||
const sourceFilename = document.getElementById('file-name-id')?.textContent;
|
||||
if (!sourceFilename) return;
|
||||
|
||||
// Load the WASM file:
|
||||
await initWasm({
|
||||
module_or_path: browser.runtime.getURL('/oxc_parser_wasm_bg.wasm'),
|
||||
});
|
||||
|
||||
// Once loaded, we can use `parseSync`!
|
||||
const ast = parseSync(code, { sourceFilename });
|
||||
console.log(ast);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
This code is taken directly from `@oxc-parser/wasm` docs with one exception: We manually pass in a file path. In a standard NodeJS or web project, the default path works just fine so you don't have to pass anything in. However, extensions are different. You should always explicitly pass in the full URL to the WASM file in your output directory, which is what `browser.runtime.getURL` returns.
|
||||
|
||||
Run your extension, and you should see OXC parse the TS file!
|
||||
|
||||
@@ -11,19 +11,7 @@ export default defineConfig({
|
||||
});
|
||||
```
|
||||
|
||||
By default, WXT automatically setups up auto-imports for all of it's own APIs:
|
||||
|
||||
- [`browser`](/api/reference/wxt/browser/variables/browser) from `wxt/browser`
|
||||
- [`defineContentScript`](/api/reference/wxt/sandbox/functions/defineContentScript) from `wxt/sandbox`
|
||||
- [`defineBackground`](/api/reference/wxt/sandbox/functions/defineBackground) from `wxt/sandbox`
|
||||
- [`defineUnlistedScript`](/api/reference/wxt/sandbox/functions/defineUnlistedScript) from `wxt/sandbox`
|
||||
- [`createIntegratedUi`](/api/reference/wxt/client/functions/createIntegratedUi) from `wxt/client`
|
||||
- [`createShadowRootUi`](/api/reference/wxt/client/functions/createShadowRootUi) from `wxt/client`
|
||||
- [`createIframeUi`](/api/reference/wxt/client/functions/createIframeUi) from `wxt/client`
|
||||
- [`fakeBrowser`](/api/reference/wxt/testing/variables/fakeBrowser) from `wxt/testing`
|
||||
- And more!
|
||||
|
||||
WXT also adds some project directories as auto-import sources automatically:
|
||||
By default, WXT automatically sets up auto-imports for all of it's own APIs and some of your project directories:
|
||||
|
||||
- `<srcDir>/components/*`
|
||||
- `<srcDir>/composables/*`
|
||||
@@ -32,6 +20,8 @@ WXT also adds some project directories as auto-import sources automatically:
|
||||
|
||||
All named and default exports from files in these directories are available everywhere else in your project without having to import them.
|
||||
|
||||
To see the complete list of auto-imported APIs, run [`wxt prepare`](/api/cli/wxt-prepare) and look at your project's `.wxt/types/imports-module.d.ts` file.
|
||||
|
||||
## TypeScript
|
||||
|
||||
For TypeScript and your editor to recognize auto-imported variables, you need to run the [`wxt prepare` command](/api/cli/wxt-prepare).
|
||||
@@ -110,3 +100,19 @@ export default defineConfig({
|
||||
imports: false, // [!code ++]
|
||||
});
|
||||
```
|
||||
|
||||
## Explicit Imports (`#imports`)
|
||||
|
||||
You can manually import all of WXT's APIs via the `#imports` module:
|
||||
|
||||
```ts
|
||||
import {
|
||||
createShadowRootUi,
|
||||
ContentScriptContext,
|
||||
MatchPattern,
|
||||
} from '#imports';
|
||||
```
|
||||
|
||||
To learn more about how the `#imports` module works, read the [related blog post](/blog/2024-12-06-using-imports-module).
|
||||
|
||||
If you've disabled auto-imports, you should still use `#imports` to import all of WXT's APIs from a single place.
|
||||
|
||||
@@ -4,7 +4,7 @@ outline: deep
|
||||
|
||||
# Browser Startup
|
||||
|
||||
> See the [API Reference](/api/reference/wxt/interfaces/ExtensionRunnerConfig) for a full list of config.
|
||||
> See the [API Reference](/api/reference/wxt/interfaces/WebExtConfig) for a full list of config.
|
||||
|
||||
During development WXT uses [`web-ext` by Mozilla](https://www.npmjs.com/package/web-ext) to automatically open a browser window with your extension installed.
|
||||
|
||||
@@ -15,9 +15,9 @@ You can configure browser startup in 3 places:
|
||||
1. `<rootDir>/web-ext.config.ts`: Ignored from version control, this file lets you configure your own options for a specific project without affecting other developers
|
||||
|
||||
```ts
|
||||
import { defineRunnerConfig } from 'wxt';
|
||||
import { defineWebExtConfig } from 'wxt';
|
||||
|
||||
export default defineRunnerConfig({
|
||||
export default defineWebExtConfig({
|
||||
// ...
|
||||
});
|
||||
```
|
||||
@@ -32,7 +32,7 @@ You can configure browser startup in 3 places:
|
||||
To set or customize the browser opened during development:
|
||||
|
||||
```ts
|
||||
export default defineRunnerConfig({
|
||||
export default defineWebExtConfig({
|
||||
binaries: {
|
||||
chrome: '/path/to/chrome-beta', // Use Chrome Beta instead of regular Chrome
|
||||
firefox: 'firefoxdeveloperedition', // Use Firefox Developer Edition instead of regular Firefox
|
||||
@@ -52,7 +52,7 @@ To persist data, set the `--user-data-dir` flag:
|
||||
:::code-group
|
||||
|
||||
```ts [Mac/Linux]
|
||||
export default defineRunnerConfig({
|
||||
export default defineWebExtConfig({
|
||||
chromiumArgs: ['--user-data-dir=./.wxt/chrome-data'],
|
||||
});
|
||||
```
|
||||
@@ -60,9 +60,10 @@ export default defineRunnerConfig({
|
||||
```ts [Windows]
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
export default defineRunnerConfig({
|
||||
export default defineWebExtConfig({
|
||||
// On Windows, the path must be absolute
|
||||
chromiumArgs: [`--user-data-dir="${resolve(".wxt/chrome-data")}"`
|
||||
chromiumProfile: resolve('.wxt/chrome-data'),
|
||||
keepProfileChanges: true,
|
||||
});
|
||||
```
|
||||
|
||||
@@ -79,7 +80,7 @@ You can use any directory you'd like for `--user-data-dir`, the examples above c
|
||||
If you prefer to load the extension into your browser manually, you can disable the auto-open behavior:
|
||||
|
||||
```ts
|
||||
export default defineRunnerConfig({
|
||||
export default defineWebExtConfig({
|
||||
disabled: true,
|
||||
});
|
||||
```
|
||||
|
||||
@@ -17,63 +17,3 @@ If you're running into errors while importing entrypoints, run `wxt prepare --de
|
||||
:::
|
||||
|
||||
Once the environment has been polyfilled and your code pre-processed, it's up the entrypoint loader to import your code, extracting the options from the default export.
|
||||
|
||||
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. It re-uses the same vite config used when building your extension, making it the most stable entrypoint loader.
|
||||
|
||||
## jiti
|
||||
|
||||
To enable `jiti`:
|
||||
|
||||
```ts
|
||||
export default defineConfig({
|
||||
entrypointLoader: 'jiti',
|
||||
});
|
||||
```
|
||||
|
||||
This is the original method WXT used to import TS files. However, because it doesn't support vite plugins like `vite-node`, it does one additional pre-processing step: It removes **_ALL_** imports from your code.
|
||||
|
||||
That means you cannot use imported variables outside the `main` function in JS entrypoints, like for content script `matches` or other options:
|
||||
|
||||
```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.
|
||||
```
|
||||
|
||||
Usually, this error occurs when you try to extract options into a shared file or when running 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() {
|
||||
// ...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
@@ -56,3 +56,26 @@ Vite provides two other environment variables, but they aren't useful in WXT pro
|
||||
- `import.meta.env.BASE_URL`: Use `browser.runtime.getURL` instead.
|
||||
- `import.meta.env.SSR`: Always `false`.
|
||||
:::
|
||||
|
||||
## Manifest
|
||||
|
||||
To use environment variables in the manifest, you need to use the function syntax:
|
||||
|
||||
```ts
|
||||
export default defineConfig({
|
||||
extensionApi: 'chrome',
|
||||
modules: ['@wxt-dev/module-vue'],
|
||||
manifest: { // [!code --]
|
||||
oauth2: { // [!code --]
|
||||
client_id: import.meta.env.WXT_APP_CLIENT_ID // [!code --]
|
||||
} // [!code --]
|
||||
} // [!code --]
|
||||
manifest: () => ({ // [!code ++]
|
||||
oauth2: { // [!code ++]
|
||||
client_id: import.meta.env.WXT_APP_CLIENT_ID // [!code ++]
|
||||
} // [!code ++]
|
||||
}), // [!code ++]
|
||||
});
|
||||
```
|
||||
|
||||
WXT can't load your `.env` files until after the config file has been loaded. So by using the function syntax for `manifest`, it defers creating the object until after the `.env` files are loaded into the process.
|
||||
|
||||
@@ -156,6 +156,11 @@ Alternatively, you can use [`@wxt-dev/auto-icons`](https://www.npmjs.com/package
|
||||
|
||||
> [Chrome docs](https://developer.chrome.com/docs/extensions/reference/permissions/)
|
||||
|
||||
Most of the time, you need to manually add permissions to your manifest. Only in a few specific situations are permissions added automatically:
|
||||
|
||||
- During development: the `tabs` and `scripting` permissions will be added to enable hot reloading.
|
||||
- When a `sidepanel` entrypoint is present: The `sidepanel` permission is added.
|
||||
|
||||
```ts
|
||||
export default defineConfig({
|
||||
manifest: {
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
Define runtime configuration in a single place, `<srcDir>/app.config.ts`:
|
||||
|
||||
```ts
|
||||
import { defineAppConfig } from 'wxt/sandbox';
|
||||
import { defineAppConfig } from '#imports';
|
||||
|
||||
// Define types for your config
|
||||
declare module 'wxt/sandbox' {
|
||||
declare module 'wxt/utils/define-app-config' {
|
||||
export interface WxtAppConfig {
|
||||
theme?: 'light' | 'dark';
|
||||
}
|
||||
@@ -26,7 +26,7 @@ This file is committed to the repo, so don't put any secrets here. Instead, use
|
||||
To access runtime config, WXT provides the `useAppConfig` function:
|
||||
|
||||
```ts
|
||||
import { useAppConfig } from 'wxt/sandbox';
|
||||
import { useAppConfig } from '#imports';
|
||||
|
||||
console.log(useAppConfig()); // { theme: "dark" }
|
||||
```
|
||||
@@ -36,7 +36,7 @@ console.log(useAppConfig()); // { theme: "dark" }
|
||||
You can use environment variables in the `app.config.ts` file.
|
||||
|
||||
```ts
|
||||
declare module 'wxt/sandbox' {
|
||||
declare module 'wxt/utils/define-app-config' {
|
||||
export interface WxtAppConfig {
|
||||
apiKey?: string;
|
||||
skipWelcome: boolean;
|
||||
|
||||
@@ -239,6 +239,7 @@ export default defineContentScript({
|
||||
onMount: (container) => {
|
||||
// Render your app to the UI container
|
||||
const unmount = render(() => <div>...</div>, container);
|
||||
return unmount;
|
||||
},
|
||||
onRemove: (unmount) => {
|
||||
// Unmount the app when the UI is removed
|
||||
@@ -254,13 +255,13 @@ export default defineContentScript({
|
||||
|
||||
:::
|
||||
|
||||
See the [API Reference](/api/reference/wxt/client/functions/createIntegratedUi) for the complete list of options.
|
||||
See the [API Reference](/api/reference/wxt/utils/content-script-ui/integrated/functions/createIntegratedUi) for the complete list of options.
|
||||
|
||||
### Shadow Root
|
||||
|
||||
Often in web extensions, you don't want your content script's CSS affecting the page, or vise-versa. The [`ShadowRoot`](https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot) API is ideal for this.
|
||||
|
||||
WXT's [`createShadowRootUi`](/api/reference/wxt/client/functions/createShadowRootUi) abstracts all the `ShadowRoot` setup away, making it easy to create UIs whose styles are isolated from the page. It also supports an optional `isolateEvents` parameter to further isolate user interactions.
|
||||
WXT's [`createShadowRootUi`](/api/reference/wxt/utils/content-script-ui/shadow-root/functions/createShadowRootUi) abstracts all the `ShadowRoot` setup away, making it easy to create UIs whose styles are isolated from the page. It also supports an optional `isolateEvents` parameter to further isolate user interactions.
|
||||
|
||||
To use `createShadowRootUi`, follow these steps:
|
||||
|
||||
@@ -443,7 +444,7 @@ export default defineContentScript({
|
||||
|
||||
:::
|
||||
|
||||
See the [API Reference](/api/reference/wxt/client/functions/createShadowRootUi) for the complete list of options.
|
||||
See the [API Reference](/api/reference/wxt/utils/content-script-ui/shadow-root/functions/createShadowRootUi) for the complete list of options.
|
||||
|
||||
Full examples:
|
||||
|
||||
@@ -454,7 +455,7 @@ Full examples:
|
||||
|
||||
If you don't need to run your UI in the same frame as the content script, you can use an IFrame to host your UI instead. Since an IFrame just hosts an HTML page, **_HMR is supported_**.
|
||||
|
||||
WXT provides a helper function, [`createIframeUi`](/api/reference/wxt/client/functions/createIframeUi), which simplifies setting up the IFrame.
|
||||
WXT provides a helper function, [`createIframeUi`](/api/reference/wxt/utils/content-script-ui/iframe/functions/createIframeUi), which simplifies setting up the IFrame.
|
||||
|
||||
1. Create an HTML page that will be loaded into your IFrame:
|
||||
```html
|
||||
@@ -509,7 +510,7 @@ WXT provides a helper function, [`createIframeUi`](/api/reference/wxt/client/fun
|
||||
});
|
||||
```
|
||||
|
||||
See the [API Reference](/api/reference/wxt/client/functions/createIframeUi) for the complete list of options.
|
||||
See the [API Reference](/api/reference/wxt/utils/content-script-ui/iframe/functions/createIframeUi) for the complete list of options.
|
||||
|
||||
## Isolated World vs Main World
|
||||
|
||||
@@ -547,16 +548,21 @@ To use `injectScript`, we need two entrypoints, one content script and one unlis
|
||||
```ts
|
||||
// entrypoints/example-main-world.ts
|
||||
export default defineUnlistedScript(() => {
|
||||
console.log('Hello from the main world!');
|
||||
console.log('Hello from the main world');
|
||||
});
|
||||
```
|
||||
|
||||
```ts
|
||||
// entrypoints/example.content.ts
|
||||
export default defineContentScript(async () => {
|
||||
await injectScript('/example-main-world.js', {
|
||||
keepInDom: true,
|
||||
});
|
||||
export default defineContentScript({
|
||||
matches: ['*://*/*'],
|
||||
async main() {
|
||||
console.log('Injecting script...');
|
||||
await injectScript('/example-main-world.js', {
|
||||
keepInDom: true,
|
||||
});
|
||||
console.log('Done!');
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
@@ -570,6 +576,39 @@ For MV3, `injectScript` is synchronous and the injected script will be evaluated
|
||||
However for MV2, `injectScript` has to `fetch` the script's text content and create an inline `<script>` block. This means for MV2, your script is injected asynchronously and it will not be evaluated at the same time as your content script's `run_at`.
|
||||
:::
|
||||
|
||||
## Mounting UI to dynamic element
|
||||
|
||||
In many cases, you may need to mount a UI to a DOM element that does not exist at the time the web page is initially loaded. To handle this, use the `autoMount` API to automatically mount the UI when the target element appears dynamically and unmount it when the element disappears. In WXT, the `anchor` option is used to target the element, enabling automatic mounting and unmounting based on its appearance and removal.
|
||||
|
||||
```ts
|
||||
export default defineContentScript({
|
||||
matches: ['<all_urls>'],
|
||||
|
||||
main(ctx) {
|
||||
const ui = createIntegratedUi(ctx, {
|
||||
position: 'inline',
|
||||
// It observes the anchor
|
||||
anchor: '#your-target-dynamic-element',
|
||||
onMount: (container) => {
|
||||
// Append children to the container
|
||||
const app = document.createElement('p');
|
||||
app.textContent = '...';
|
||||
container.append(app);
|
||||
},
|
||||
});
|
||||
|
||||
// Call autoMount to observe anchor element for add/remove.
|
||||
ui.autoMount();
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
:::tip
|
||||
When the `ui.remove` is called, `autoMount` also stops.
|
||||
:::
|
||||
|
||||
See the [API Reference](/api/reference/wxt/utils/content-script-ui/types/interfaces/ContentScriptUi#automount) for the complete list of options.
|
||||
|
||||
## Dealing with SPAs
|
||||
|
||||
It is difficult to write content scripts for SPAs (single page applications) and websites using HTML5 history mode for navigation because content scripts are only ran on full page reloads. SPAs and websites that take advantage of HTML5 history mode **_do not perform a full reload when changing paths_**, and thus your content script isn't going to be ran when you expect it to be.
|
||||
@@ -602,7 +641,7 @@ export default defineContentScript({
|
||||
matches: ['*://*.youtube.com/*'],
|
||||
main(ctx) {
|
||||
ctx.addEventListener(window, 'wxt:locationchange', ({ newUrl }) => {
|
||||
if (watchPattern.matches(newUrl)) mainWatch(ctx);
|
||||
if (watchPattern.includes(newUrl)) mainWatch(ctx);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -304,14 +304,14 @@ Follow the [Devtools Example](https://github.com/wxt-dev/examples/tree/main/exam
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta
|
||||
name="viewportYou are responsible for loading/running these scripts where needed. If necessary, don't forget to add the script and/or any related stylesheets to web_accessible_resources."
|
||||
content="width=device-width, initial-scale=1.0"
|
||||
/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Options Title</title>
|
||||
|
||||
<!-- Customize the manifest options -->
|
||||
<meta name="manifest.open_in_tab" content="true|false" />
|
||||
<meta name="manifest.chrome_style" content="true|false" />
|
||||
<meta name="manifest.browser_style" content="true|false" />
|
||||
|
||||
<!-- Set include/exclude if the page should be removed from some builds -->
|
||||
<meta name="manifest.include" content="['chrome', ...]" />
|
||||
<meta name="manifest.exclude" content="['chrome', ...]" />
|
||||
@@ -339,7 +339,11 @@ Follow the [Devtools Example](https://github.com/wxt-dev/examples/tree/main/exam
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
<!-- Set the `action.default_title` in the manifest -->
|
||||
<title>Default Popup Title</title>
|
||||
|
||||
<!-- Customize the manifest options -->
|
||||
<meta
|
||||
name="manifest.default_icon"
|
||||
content="{
|
||||
@@ -350,6 +354,7 @@ Follow the [Devtools Example](https://github.com/wxt-dev/examples/tree/main/exam
|
||||
/>
|
||||
<meta name="manifest.type" content="page_action|browser_action" />
|
||||
<meta name="manifest.browser_style" content="true|false" />
|
||||
|
||||
<!-- Set include/exclude if the page should be removed from some builds -->
|
||||
<meta name="manifest.include" content="['chrome', ...]" />
|
||||
<meta name="manifest.exclude" content="['chrome', ...]" />
|
||||
@@ -384,6 +389,7 @@ Firefox does not support sandboxed pages.
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Title</title>
|
||||
|
||||
<!-- Set include/exclude if the page should be removed from some builds -->
|
||||
<meta name="manifest.include" content="['chrome', ...]" />
|
||||
<meta name="manifest.exclude" content="['chrome', ...]" />
|
||||
@@ -416,6 +422,8 @@ In Chrome, side panels use the `side_panel` API, while Firefox uses the `sidebar
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Default Side Panel Title</title>
|
||||
|
||||
<!-- Customize the manifest options -->
|
||||
<meta
|
||||
name="manifest.default_icon"
|
||||
content="{
|
||||
@@ -426,6 +434,7 @@ In Chrome, side panels use the `side_panel` API, while Firefox uses the `sidebar
|
||||
/>
|
||||
<meta name="manifest.open_at_install" content="true|false" />
|
||||
<meta name="manifest.browser_style" content="true|false" />
|
||||
|
||||
<!-- Set include/exclude if the page should be removed from some builds -->
|
||||
<meta name="manifest.include" content="['chrome', ...]" />
|
||||
<meta name="manifest.exclude" content="['chrome', ...]" />
|
||||
@@ -475,6 +484,7 @@ body {
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Title</title>
|
||||
|
||||
<!-- Set include/exclude if the page should be removed from some builds -->
|
||||
<meta name="manifest.include" content="['chrome', ...]" />
|
||||
<meta name="manifest.exclude" content="['chrome', ...]" />
|
||||
|
||||
@@ -20,7 +20,7 @@ In your background script, set `type: "module"`:
|
||||
|
||||
```ts
|
||||
export default defineBackground({
|
||||
type: 'module', // !code ++
|
||||
type: 'module', // [!code ++]
|
||||
main() {
|
||||
// ...
|
||||
},
|
||||
|
||||
@@ -4,52 +4,29 @@
|
||||
|
||||
Different browsers provide different global variables for accessing the extension APIs (chrome provides `chrome`, firefox provides `browser`, etc).
|
||||
|
||||
WXT simplifies this - always use `browser`:
|
||||
WXT merges these two into a unified API accessed through the `browser` variable.
|
||||
|
||||
```ts
|
||||
import { browser } from 'wxt/browser';
|
||||
|
||||
browser.action.onClicked.addListener(() => {
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
Other than that, refer to Chrome and Mozilla's documentation for how to use specific APIs. Everything a normal extension can do, WXT can do as well, just via `browser` instead of `chrome`.
|
||||
:::tip
|
||||
With auto-imports enabled, you don't even need to import this variable from `wxt/browser`!
|
||||
:::
|
||||
|
||||
## Webextension Polyfill
|
||||
The `browser` variable WXT provides is a simple export of the `browser` or `chrome` globals provided by the browser at runtime:
|
||||
|
||||
> Since `v0.1.0`
|
||||
<<< @/../packages/wxt/src/browser.ts#snippet
|
||||
|
||||
By default, WXT uses the [`webextension-polyfill` by Mozilla](https://www.npmjs.com/package/webextension-polyfill) to make the extension API consistent between browsers.
|
||||
This means you can use the promise-style API for both MV2 and MV3, and it will work across all browsers (Chromium, Firefox, Safari, etc).
|
||||
|
||||
To access types, you should import the relevant namespace from `wxt/browser`:
|
||||
## Accessing Types
|
||||
|
||||
```ts
|
||||
import { Runtime } from 'wxt/browser';
|
||||
|
||||
function handleMessage(message: any, sender: Runtime.Sender) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Disabling the polyfill
|
||||
|
||||
> Since `v0.19.0`
|
||||
|
||||
After the release of MV3 and Chrome's official deprecation of MV2 in June 2024, the polyfill isn't really doing anything useful anymore.
|
||||
|
||||
You can disable it with a single line:
|
||||
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
export default defineConfig({
|
||||
extensionApi: 'chrome',
|
||||
});
|
||||
```
|
||||
|
||||
This will change `wxt/browser` to simply export the `browser` or `chrome` globals based on browser at runtime:
|
||||
|
||||
<<< @/../packages/wxt/src/browser/chrome.ts#snippet
|
||||
|
||||
Accessing types is a little different with the polyfill disabled. They do not need to be imported; they're available on the `browser` object itself:
|
||||
All types can be accessed via WXT's `browser` object:
|
||||
|
||||
```ts
|
||||
function handleMessage(message: any, sender: browser.runtime.Sender) {
|
||||
@@ -59,7 +36,7 @@ function handleMessage(message: any, sender: browser.runtime.Sender) {
|
||||
|
||||
## Feature Detection
|
||||
|
||||
Depending on the manifest version and browser, some APIs are not available at runtime. If an API is not available, it will be `undefined`.
|
||||
Depending on the manifest version, browser, and permissions, some APIs are not available at runtime. If an API is not available, it will be `undefined`.
|
||||
|
||||
:::warning
|
||||
Types will not help you here. The types WXT provides for `browser` assume all APIs exist. You are responsible for knowing whether an API is available or not.
|
||||
|
||||
@@ -61,14 +61,14 @@ After enabling it, your project structure should look like this:
|
||||
📂 {rootDir}/
|
||||
📁 .output/
|
||||
📁 .wxt/
|
||||
📁 modules/
|
||||
📁 public/
|
||||
📂 src/
|
||||
📁 assets/
|
||||
📁 components/
|
||||
📁 composables/
|
||||
📁 entrypoints/
|
||||
📁 hooks/
|
||||
📁 modules/
|
||||
📁 public/
|
||||
📁 utils/
|
||||
📄 app.config.ts
|
||||
📄 .env
|
||||
|
||||
@@ -129,7 +129,7 @@ wxt zip
|
||||
|
||||
Firefox requires you to upload a ZIP of your source code. This allows them to rebuild your extension and review the code in a readable way. More details can be found in [Firefox's docs](https://extensionworkshop.com/documentation/publish/source-code-submission/).
|
||||
|
||||
When running `wxt zip -b firefox`, WXT will zip both your extension and sources. Certain files (such as config files, hidden files, and tests) are automatically excluded from your sources. However, it's important to manually check the ZIP to ensure it only contains the files necessary to rebuild your extension.
|
||||
When running `wxt zip -b firefox`, WXT will zip both your extension and sources. Certain files (such as config files, hidden files, tests, and excluded entrypoints) are automatically excluded from your sources. However, it's important to manually check the ZIP to ensure it only contains the files necessary to rebuild your extension.
|
||||
|
||||
To customize which files are zipped, add the `zip` option to your config file.
|
||||
|
||||
|
||||
@@ -12,15 +12,18 @@ When using `browser.scripting.executeScript`, you can execute content scripts or
|
||||
// entrypoints/background.ts
|
||||
const res = await browser.scripting.executeScript({
|
||||
target: { tabId },
|
||||
files: ['injected.js'],
|
||||
files: ['content-scripts/example.js'],
|
||||
});
|
||||
console.log(res); // "Hello John!"
|
||||
```
|
||||
|
||||
```ts
|
||||
// entrypoints/injected.js
|
||||
export default defineUnlistedScript(() => {
|
||||
console.log('Script was injected!');
|
||||
return 'Hello John!';
|
||||
// entrypoints/example.content.ts
|
||||
export default defineContentScript({
|
||||
registration: 'runtime',
|
||||
main(ctx) {
|
||||
console.log('Script was executed!');
|
||||
return 'Hello John!';
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
@@ -6,7 +6,7 @@ You can use the vanilla APIs (see docs above), use [WXT's built-in storage API](
|
||||
|
||||
## Alternatives
|
||||
|
||||
1. [`wxt/storage`](/storage) (recommended): WXT ships with its own wrapper around the vanilla storage APIs that simplifies common use cases
|
||||
1. [`wxt/utils/storage`](/storage) (recommended): WXT ships with its own wrapper around the vanilla storage APIs that simplifies common use cases
|
||||
|
||||
2. DIY: If you're migrating to WXT and already have a storage wrapper, keep using it. In the future, if you want to delete that code, you can use one of these alternatives, but there's no reason to replace working code during a migration.
|
||||
|
||||
|
||||
@@ -75,3 +75,5 @@ Here are some examples:
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Alternatively, you can use the [`filterEntrypoints` config](/api/reference/wxt/interfaces/InlineConfig#filterentrypoints) to list all the entrypoints you want to build.
|
||||
|
||||
@@ -32,7 +32,7 @@ Here are real projects with unit testing setup. Look at the code and tests to se
|
||||
|
||||
### Example Tests
|
||||
|
||||
This example demonstrates that you don't have to mock `browser.storage` (used by `wxt/storage`) in tests - [`@webext-core/fake-browser`](https://webext-core.aklinker1.io/fake-browser/installation) implements storage in-memory so it behaves like it would in a real extension!
|
||||
This example demonstrates that you don't have to mock `browser.storage` (used by `wxt/utils/storage`) in tests - [`@webext-core/fake-browser`](https://webext-core.aklinker1.io/fake-browser/installation) implements storage in-memory so it behaves like it would in a real extension!
|
||||
|
||||
```ts
|
||||
import { describe, it, expect } from 'vitest';
|
||||
@@ -71,6 +71,34 @@ describe('isLoggedIn', () => {
|
||||
});
|
||||
```
|
||||
|
||||
### Mocking WXT APIs
|
||||
|
||||
First, you need to understand how the `#imports` module works. When WXT (and vitest) sees this import during a preprocessing step, the import is replaced with multiple imports pointing to their "real" import path.
|
||||
|
||||
For example, this is what your write in your source code:
|
||||
|
||||
```ts
|
||||
// What you write
|
||||
import { injectScript, createShadowRootUi } from '#imports';
|
||||
```
|
||||
|
||||
But Vitest sees this:
|
||||
|
||||
```ts
|
||||
import { injectScript } from 'wxt/browser';
|
||||
import { createShadowRootUi } from 'wxt/utils/content-script-ui/shadow-root';
|
||||
```
|
||||
|
||||
So in this case, if you wanted to mock `injectScript`, you need to pass in `"wxt/utils/inject-script"`, not `"#imports"`.
|
||||
|
||||
```ts
|
||||
vi.mock("wxt/utils/inject-script", () => ({
|
||||
injectScript: ...
|
||||
}))
|
||||
```
|
||||
|
||||
Refer to your project's `.wxt/types/imports-module.d.ts` file to lookup real import paths for `#imports`. If the file doesn't exist, run [`wxt prepare`](/guide/essentials/config/typescript).
|
||||
|
||||
## Other Testing Frameworks
|
||||
|
||||
To use a different framework, you will likely have to disable auto-imports, setup import aliases, manually mock the extension APIs, and setup the test environment to support all of WXT's features that you use.
|
||||
|
||||
@@ -75,7 +75,7 @@ import { defineWxtModule } from 'wxt/modules';
|
||||
|
||||
export default defineWxtModule({
|
||||
setup(wxt) {
|
||||
wxt.hook('ready', () => {
|
||||
wxt.hook('config:resolved', () => {
|
||||
wxt.config.outDir = 'dist';
|
||||
});
|
||||
},
|
||||
@@ -112,12 +112,12 @@ export default defineWxtModule<AnalyticModuleOptions>({
|
||||
|
||||
```ts
|
||||
import { defineWxtModule } from 'wxt/modules';
|
||||
import 'wxt/sandbox';
|
||||
import 'wxt/utils/define-app-config';
|
||||
|
||||
export interface MyModuleRuntimeOptions {
|
||||
// Add your runtime options here...
|
||||
}
|
||||
declare module 'wxt/sandbox' {
|
||||
declare module 'wxt/utils/define-app-config' {
|
||||
export interface WxtAppConfig {
|
||||
myModule: MyModuleOptions;
|
||||
}
|
||||
@@ -167,6 +167,31 @@ This file could then be loaded at runtime:
|
||||
const res = await fetch(browser.runtime.getURL('/some-text.txt'));
|
||||
```
|
||||
|
||||
#### Add custom entrypoints
|
||||
|
||||
Once the existing files under the `entrypoints/` directory have been discovered, the `entrypoints:found` hook can be used to add custom entrypoints.
|
||||
|
||||
:::info
|
||||
The `entrypoints:found` hook is triggered before validation is carried out on the list of entrypoints. Thus, any custom entrypoints will still be checked for duplicate names and logged during debugging.
|
||||
:::
|
||||
|
||||
```ts
|
||||
import { defineWxtModule } from 'wxt/modules';
|
||||
|
||||
export default defineWxtModule({
|
||||
setup(wxt) {
|
||||
wxt.hook('entrypoints:found', (_, entrypointInfos) => {
|
||||
// Add your new entrypoint
|
||||
entrypointInfos.push({
|
||||
name: 'my-custom-script',
|
||||
inputPath: 'path/to/custom-script.js',
|
||||
type: 'content-script',
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
#### Generate runtime module
|
||||
|
||||
Create a file in `.wxt`, add an alias to import it, and add auto-imports for exported variables.
|
||||
|
||||
@@ -5,11 +5,11 @@ WXT is a modern, open-source framework for building web extensions. Inspired by
|
||||
- Provide an awesome [DX](https://about.gitlab.com/topics/devops/what-is-developer-experience/)
|
||||
- Provide first-class support for all major browsers
|
||||
|
||||
Check out the [comparison](/guide/resources/compare) to see how WXT compares to other tools for building web extnesions.
|
||||
Check out the [comparison](/guide/resources/compare) to see how WXT compares to other tools for building web extensions.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
These docs assume you have a basic knowledge of how Chrome extensions are structured and how you access the extension APIs.
|
||||
These docs assume you have a basic knowledge of how web extensions are structured and how you access the extension APIs.
|
||||
|
||||
:::warning New to extension development?
|
||||
If you have never written an extension before, follow Chrome's [Hello World tutorial](https://developer.chrome.com/docs/extensions/get-started/tutorial/hello-world) to first **_create an extension without WXT_**, then come back here.
|
||||
|
||||
@@ -8,9 +8,7 @@ Commonly asked questions about how to use WXT or why it behaves the way it does.
|
||||
|
||||
[[toc]]
|
||||
|
||||
---
|
||||
|
||||
### Why are content scripts not showing up in the manifest?
|
||||
## Why aren't content scripts added to the manifest?
|
||||
|
||||
During development, WXT registers content scripts dynamically so they can be reloaded individually when a file is saved without reloading your entire extension.
|
||||
|
||||
@@ -19,3 +17,58 @@ To list the content scripts registered during development, open the service work
|
||||
```js
|
||||
await chrome.scripting.getRegisteredContentScripts();
|
||||
```
|
||||
|
||||
## How do I disable opening the browser automatically during development?
|
||||
|
||||
See https://wxt.dev/guide/essentials/config/browser-startup.html#disable-opening-browser
|
||||
|
||||
## How do I stay logged into a website during development?
|
||||
|
||||
See https://wxt.dev/guide/essentials/config/browser-startup.html#persist-data
|
||||
|
||||
## My component library doesn't work in content scripts!
|
||||
|
||||
Component libraries place their CSS in the document's `<head>` by default. When using `createShadowRootUi`, your UI is isolated from the document's styles because it's inside a ShadowRoot.
|
||||
|
||||
To fix this, you need to tell your component library to insert it's CSS inside the shadow root. Here's the docs for a couple of popular libraries:
|
||||
|
||||
- React
|
||||
- Ant Design: [`StyleProvider`](https://ant.design/docs/react/compatible-style#shadow-dom-usage)
|
||||
- Mantine: [`MantineProvider#getRootElement` and `MantineProvider#cssVariablesSelector`](https://mantine.dev/theming/mantine-provider/)
|
||||
|
||||
> If your library isn't listed above, try searching it's docs/issues for "shadow root", "shadow dom", or "css container".
|
||||
|
||||
`createShadowRootUi` provides it's own `<head>` element inside the shadow root, so that were you should tell the library to add the CSS. Here's an example with Ant Design:
|
||||
|
||||
```tsx
|
||||
import { StyleProvider } from '@ant-design/cssinjs'; // [!code ++]
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App.tsx';
|
||||
|
||||
const ui = await createShadowRootUi(ctx, {
|
||||
name: 'example-ui',
|
||||
position: 'inline',
|
||||
anchor: 'body',
|
||||
onMount: (container) => { // [!code --]
|
||||
onMount: (container, shadow) => { // [!code ++]
|
||||
const cssContainer = shadow.querySelector("head")!; // [!code ++]
|
||||
const root = ReactDOM.createRoot(container);
|
||||
root.render(
|
||||
<StyleProvider container={cssContainer}> // [!code ++]
|
||||
<App />
|
||||
</StyleProvider> // [!code ++]
|
||||
);
|
||||
return root;
|
||||
},
|
||||
onRemove: (root) => {
|
||||
root?.unmount();
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Note that this doesn't effect all component libraries, just ones that inject CSS themselves rather than having you import their CSS. This approach is more prevailent in the React community, but not limited to it. That's why only React libraries are listed above. Vuetify, for example, works just fine because you import its CSS - WXT picks up on this and the CSS is added inside the shadow root automatically:
|
||||
|
||||
```ts
|
||||
import 'vuetify/styles'; // <-- This line imports the CSS, just like importing a .css file
|
||||
import { createVuetify } from 'vuetify';
|
||||
```
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# Upgrading WXT
|
||||
|
||||
## Overview
|
||||
@@ -12,6 +16,209 @@ Listed below are all the breaking changes you should address when upgrading to a
|
||||
|
||||
Currently, WXT is in pre-release. This means changes to the second digit, `v0.X`, are considered major and have breaking changes. Once v1 is released, only major version bumps will have breaking changes.
|
||||
|
||||
## v0.19.0 → v0.20.0
|
||||
|
||||
v0.20 is a big release! There are lots of breaking changes because this version is intended to be a release candidate for v1.0. If all goes well, v1.0 will be released with no additional breaking changes.
|
||||
|
||||
:::tip
|
||||
Read through all the changes once before making any code changes.
|
||||
:::
|
||||
|
||||
### `webextension-polyfill` Removed
|
||||
|
||||
WXT no longer uses the `webextension-polyfill` internally and `wxt/browser` uses the `chrome`/`browser` globals provided by the browser.
|
||||
|
||||
To upgrade, you have two options:
|
||||
|
||||
1. **Stop using the polyfill**
|
||||
- Replace any manual imports from `wxt/browser/chrome` with `wxt/browser`
|
||||
2. **Continue using the polyfill**
|
||||
- Install the polyfill and WXT's [new polyfill module](https://www.npmjs.com/package/@wxt-dev/webextension-polyfill):
|
||||
```sh
|
||||
pnpm i webextension-polyfill @wxt-dev/webextension-polyfill
|
||||
```
|
||||
- Add the WXT module to your config:
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
export default defineConfig({
|
||||
modules: ['@wxt-dev/webextension-polyfill'],
|
||||
});
|
||||
```
|
||||
|
||||
Regardless of your choice, the `extensionApi` config has been removed. Remove it from your `wxt.config.ts` file if present:
|
||||
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
export default defineConfig({
|
||||
extensionApi: 'chrome', // [!code --]
|
||||
});
|
||||
```
|
||||
|
||||
Additionally, extension API types have changed. `wxt/browser` now uses types from `@types/chrome` instead of `@types/webextension-polyfill`. You will have to migrate any type imports to use `@types/chrome`'s namespace approach:
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```ts
|
||||
import type { Runtime } from 'wxt/browser'; // [!code --]
|
||||
import { browser } from 'wxt/browser'; // [!code ++]
|
||||
|
||||
function getMessageSenderUrl(sender: Runtime.MessageSender): string { // [!code --]
|
||||
function getMessageSenderUrl(sender: browser.runtime.MessageSender): string { // [!code ++]
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
`@types/chrome` are more up-to-date, contain less bugs, and don't have any auto-generated names. So even if you continue to use the polyfill, you will need to update your types to use these types.
|
||||
|
||||
### `public/` and `modules/` Directories Moved
|
||||
|
||||
The default location for the `public/` and `modules/` directories have changed to better align with standards set by other frameworks (Nuxt, Next, Astro, etc). Now, each path is relative to the project's root directory.
|
||||
|
||||
- If you follow the default folder structure, you don't need to make any changes.
|
||||
- If you set a custom `srcDir`, you have two options:
|
||||
1. Keep the folders in the same place and update your project config:
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
export default defineConfig({
|
||||
srcDir: 'src',
|
||||
publicDir: 'src/public', // [!code ++]
|
||||
modulesDir: 'src/modules', // [!code ++]
|
||||
});
|
||||
```
|
||||
2. Move the your `public/` and `modules/` directories to the project root:
|
||||
```diff
|
||||
<root>/
|
||||
+ modules/
|
||||
+ public/
|
||||
src/
|
||||
components/
|
||||
entrypoints/
|
||||
- modules/
|
||||
- public/
|
||||
utils/
|
||||
wxt.config.ts
|
||||
```
|
||||
|
||||
### Import Path Changes
|
||||
|
||||
The APIs exported by `wxt/sandbox`, `wxt/client`, or `wxt/storage` have moved to `wxt/utils/*`.
|
||||
|
||||
To upgrade, replace these imports with the new `#imports` module:
|
||||
|
||||
```ts
|
||||
import { storage } from 'wxt/storage'; // [!code --]
|
||||
import { defineContentScript } from 'wxt/sandbox'; // [!code --]
|
||||
import { ContentScriptContext, useAppConfig } from 'wxt/client'; // [!code --]
|
||||
import { storage } from '#imports'; // [!code ++]
|
||||
import { defineContentScript } from '#imports'; // [!code ++]
|
||||
import { ContentScriptContext, useAppConfig } from '#imports'; // [!code ++]
|
||||
```
|
||||
|
||||
You can combine the imports into a single import statement, but it's easier to just find/replace each statement.
|
||||
|
||||
:::tip
|
||||
Before types will work, you'll need to run `wxt prepare` after installing v0.20 to generate the new TypeScript declarations.
|
||||
:::
|
||||
|
||||
Read more about the new `#imports` module in the [blog post](/blog/2024-12-06-using-imports-module).
|
||||
|
||||
### `createShadowRootUi` CSS Changes
|
||||
|
||||
WXT now resets styles inherited from the webpage (`visibility`, `color`, `font-size`, etc.) by setting `all: initial` inside the shadow root.
|
||||
|
||||
If you use `createShadowRootUi`:
|
||||
|
||||
1. Double check that your UI looks the same as before.
|
||||
2. If you have any manual CSS resets to override a page style, you can remove them:
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```css
|
||||
/* entrypoints/reddit.content/style.css */
|
||||
body { /* [!code --] */
|
||||
/* Override Reddit's default "hidden" visibility on elements */ /* [!code --] */
|
||||
visibility: visible !important; /* [!code --] */
|
||||
} /* [!code --] */
|
||||
```
|
||||
|
||||
:::warning
|
||||
This doesn't effect `rem` units. You should continue using `postcss-rem-to-px` or an equivalent library if the webpage sets the HTML element's `font-size`.
|
||||
:::
|
||||
|
||||
If you run into problems with the new behavior, you can disable it and continue using your current CSS:
|
||||
|
||||
```ts
|
||||
const ui = await createShadowRootUi({
|
||||
inheritStyles: true, // [!code ++]
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
### Default Output Directories Changed
|
||||
|
||||
The default value for [`outDirTemplate`](/api/reference/wxt/interfaces/InlineConfig#outdirtemplate) has changed. Now, different build modes are output to different directories:
|
||||
|
||||
- `--mode production`: `.output/chrome-mv3` (unchanged)
|
||||
- `--mode development`: `.output/chrome-mv3-dev` (`-dev` suffix)
|
||||
- `--mode custom`: `.output/chrome-mv3-custom` (`-[mode]` suffix)
|
||||
|
||||
To revert and use the old behavior, writing all output to the same directory, set `outDirTemplate` option:
|
||||
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
export default defineConfig({
|
||||
outDirTemplate: '{{browser}}-mv{{manifestVersion}}', // [!code ++]
|
||||
});
|
||||
```
|
||||
|
||||
### `runner` APIs Renamed
|
||||
|
||||
To improve consistency with the `web-ext.config.ts` file, the "runner" APIs have been renamed. You can continue using the old names, but they have been deprecated and will be removed in a future version:
|
||||
|
||||
1. The `runner` option has been renamed to `webExt`:
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
export default defineConfig({
|
||||
runner: { // [!code --]
|
||||
webExt: { // [!code ++]
|
||||
startUrls: ["https://wxt.dev"],
|
||||
},
|
||||
});
|
||||
```
|
||||
2. `defineRunnerConfig` has been renamed to `defineWebExtConfig`:
|
||||
```ts
|
||||
// web-ext.config.ts
|
||||
import { defineRunnerConfig } from 'wxt'; // [!code --]
|
||||
import { defineWebExtConfig } from 'wxt'; // [!code ++]
|
||||
```
|
||||
3. The `ExtensionRunnerConfig` type has been renamed to `WebExtConfig`
|
||||
```ts
|
||||
import type { ExtensionRunnerConfig } from 'wxt'; // [!code --]
|
||||
import type { WebExtConfig } from 'wxt'; // [!code ++]
|
||||
```
|
||||
|
||||
### Load Config as ESM
|
||||
|
||||
`wxt.config.ts` and `web-ext.config.ts` are now loaded as ESM modules. Previously, they were loaded as CJS.
|
||||
|
||||
If you're using any CJS APIs, like `__filename` or `__dirname`, replace them with their ESM counterparts, like `import.meta.filename` or `import.meta.dirname`.
|
||||
|
||||
### Deprecated APIs Removed
|
||||
|
||||
- `entrypointLoader` option: WXT now uses `vite-node` for importing entrypoints during the build process.
|
||||
> This was deprecated in v0.19.0, see the [v0.19 section](#v0-18-5-rarr-v0-19-0) for migration steps.
|
||||
- `transformManifest` option: Use the `build:manifestGenerated` hook to transform the manifest instead:
|
||||
<!-- prettier-ignore -->
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
export default defineConfig({
|
||||
transformManifest(manifest) { // [!code --]
|
||||
hooks: { // [!code ++]
|
||||
'build:manifestGenerated': (_, manifest) => { // [!code ++]
|
||||
// ...
|
||||
}, // [!code ++]
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## v0.18.5 → v0.19.0
|
||||
|
||||
### `vite-node` Entrypoint Loader
|
||||
@@ -109,27 +316,6 @@ You have two options:
|
||||
```
|
||||
2. Rename your `modules` directory to something else.
|
||||
|
||||
## 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/essentials/wxt-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`
|
||||
|
||||
+9
-3
@@ -6,7 +6,7 @@ title: Next-gen Web Extension Framework
|
||||
hero:
|
||||
name: WXT
|
||||
text: Next-gen Web Extension Framework
|
||||
tagline: An open source tool that makes Chrome Extension development faster than ever before.
|
||||
tagline: An open source tool that makes web extension development faster than ever before.
|
||||
image:
|
||||
src: /hero-logo.svg
|
||||
alt: WXT
|
||||
@@ -73,9 +73,15 @@ features:
|
||||
linkText: Read docs
|
||||
---
|
||||
|
||||
## Sponsors
|
||||
|
||||
WXT is a [MIT-licensed](https://github.com/wxt-dev/wxt/blob/main/LICENSE) open source project with its ongoing development made possible entirely by the support of these awesome backers. If you'd like to join them, please consider [sponsoring WXT's development](https://github.com/sponsors/wxt-dev).
|
||||
|
||||
<a href="https://github.com/sponsors/wxt-dev"><img alt="WXT Sponsors" src="https://raw.githubusercontent.com/wxt-dev/static/refs/heads/main/sponsorkit/sponsors-wide.svg"></a>
|
||||
|
||||
## Put <span style="color: var(--vp-c-brand-1)">Developer Experience</span> First
|
||||
|
||||
WXT simplifies the chrome extension development process by providing tools for zipping and publishing, the best-in-class dev mode, an opinionated project structure, and more. Iterate faster, develop features not build scripts, and use everything the JS ecosystem has to offer.
|
||||
WXT simplifies the web extension development process by providing tools for zipping and publishing, the best-in-class dev mode, an opinionated project structure, and more. Iterate faster, develop features not build scripts, and use everything the JS ecosystem has to offer.
|
||||
|
||||
<div style="margin: auto; width: 100%; max-width: 900px; text-align: center">
|
||||
<video src="https://github.com/wxt-dev/wxt/assets/10101283/4d678939-1bdb-495c-9c36-3aa281d84c94" controls></video>
|
||||
@@ -87,7 +93,7 @@ WXT simplifies the chrome extension development process by providing tools for z
|
||||
|
||||
## Who's Using WXT?
|
||||
|
||||
Battle tested and ready for production. Explore chrome extensions made with WXT.
|
||||
Battle tested and ready for production. Explore web extensions made with WXT.
|
||||
|
||||
<ClientOnly>
|
||||
<UsingWxtSection />
|
||||
|
||||
+71
-24
@@ -6,23 +6,47 @@ outline: deep
|
||||
|
||||
[Changelog](https://github.com/wxt-dev/wxt/blob/main/packages/wxt/CHANGELOG.md)
|
||||
|
||||
WXT provides a simplified API to replace the `browser.storage.*` APIs. Use the `storage` auto-import from `wxt/storage` or import it manually to get started:
|
||||
A simplified wrapper around the extension storage APIs.
|
||||
|
||||
## Installation
|
||||
|
||||
### With WXT
|
||||
|
||||
This module is built-in to WXT, so you don't need to install anything.
|
||||
|
||||
```ts
|
||||
import { storage } from 'wxt/storage';
|
||||
import { storage } from '#imports';
|
||||
```
|
||||
|
||||
> [!IMPORTANT]
|
||||
> To use the `wxt/storage` API, the `"storage"` permission must be added to the manifest:
|
||||
>
|
||||
> ```ts
|
||||
> // wxt.config.ts
|
||||
> export default defineConfig({
|
||||
> manifest: {
|
||||
> permissions: ['storage'],
|
||||
> },
|
||||
> });
|
||||
> ```
|
||||
If you use auto-imports, `storage` is auto-imported for you, so you don't even need to import it!
|
||||
|
||||
### Without WXT
|
||||
|
||||
Install the NPM package:
|
||||
|
||||
```sh
|
||||
npm i @wxt-dev/storage
|
||||
pnpm add @wxt-dev/storage
|
||||
yarn add @wxt-dev/storage
|
||||
bun add @wxt-dev/storage
|
||||
```
|
||||
|
||||
```ts
|
||||
import { storage } from '@wxt-dev/storage';
|
||||
```
|
||||
|
||||
## Storage Permission
|
||||
|
||||
To use the `@wxt-dev/storage` API, the `"storage"` permission must be added to the manifest:
|
||||
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
export default defineConfig({
|
||||
manifest: {
|
||||
permissions: ['storage'],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Basic Usage
|
||||
|
||||
@@ -51,11 +75,11 @@ await storage.watch<number>(
|
||||
await storage.getMeta<{ v: number }>('local:installDate');
|
||||
```
|
||||
|
||||
For a full list of methods available, see the [API reference](/api/reference/wxt/storage/interfaces/WxtStorage).
|
||||
For a full list of methods available, see the [API reference](/api/reference/wxt/utils/storage/interfaces/WxtStorage).
|
||||
|
||||
## Watchers
|
||||
|
||||
To listen for storage changes, use the `storage.watch` function. It lets you setup a listener for a single key:
|
||||
To listen for storage changes, use the `storage.watch` function. It lets you set up a listener for a single key:
|
||||
|
||||
```ts
|
||||
const unwatch = storage.watch<number>('local:counter', (newCount, oldCount) => {
|
||||
@@ -74,7 +98,7 @@ unwatch();
|
||||
|
||||
## Metadata
|
||||
|
||||
`wxt/storage` also supports setting metadata for keys, stored at `key + "$"`. Metadata is a collection of properties associated with a key. It might be a version number, last modified date, etc.
|
||||
`@wxt-dev/storage` also supports setting metadata for keys, stored at `key + "$"`. Metadata is a collection of properties associated with a key. It might be a version number, last modified date, etc.
|
||||
|
||||
[Other than versioning](#versioning), you are responsible for managing a field's metadata:
|
||||
|
||||
@@ -100,7 +124,7 @@ You can remove all metadata associated with a key, or just specific properties:
|
||||
// Remove all properties
|
||||
await storage.removeMeta('local:preference');
|
||||
|
||||
// Remove one property
|
||||
// Remove only the "lastModified" property
|
||||
await storage.removeMeta('local:preference', 'lastModified');
|
||||
|
||||
// Remove multiple properties
|
||||
@@ -134,7 +158,7 @@ const unwatch = showChangelogOnUpdate.watch((newValue) => {
|
||||
});
|
||||
```
|
||||
|
||||
For a full list of properties and methods available, see the [API reference](/api/reference/wxt/storage/interfaces/WxtStorageItem).
|
||||
For a full list of properties and methods available, see the [API reference](/api/reference/wxt/utils/storage/interfaces/WxtStorageItem).
|
||||
|
||||
### Versioning
|
||||
|
||||
@@ -225,13 +249,13 @@ export const ignoredWebsites = storage.defineItem<IgnoredWebsiteV3[]>( // [!code
|
||||
Internally, this uses a metadata property called `v` to track the value's current version.
|
||||
:::
|
||||
|
||||
In this case, we thought that the ignored website list might change in the future, and were able to setup a versioned storage item from the start.
|
||||
In this case, we thought that the ignored website list might change in the future, and were able to set up a versioned storage item from the start.
|
||||
|
||||
Realistically, you won't know a item needs versioned until you need to change it's schema. Thankfully, it's simple to add versioning to an unversioned storage item.
|
||||
Realistically, you won't know an item needs versioning until you need to change its schema. Thankfully, it's simple to add versioning to an unversioned storage item.
|
||||
|
||||
When a previous version isn't found, WXT assumes the version was `1`. That means you just need to set `version: 2` and add a migration for `2`, and it will just work!
|
||||
|
||||
Lets look at the same ignored websites example from before, but start with an unversioned item this time:
|
||||
Let's look at the same ignored websites example from before, but start with an unversioned item this time:
|
||||
|
||||
:::code-group
|
||||
|
||||
@@ -275,13 +299,13 @@ 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.
|
||||
As soon as `storage.defineItem` is called, WXT checks if migrations need to be run, 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.
|
||||
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:
|
||||
|
||||
@@ -294,7 +318,7 @@ With `storage.defineItem`, there are multiple ways of defining default values:
|
||||
});
|
||||
```
|
||||
|
||||
2. `init` - Initialize and save a value in storage if it is not already saved.
|
||||
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:
|
||||
|
||||
@@ -308,3 +332,26 @@ With `storage.defineItem`, there are multiple ways of defining default values:
|
||||
```
|
||||
|
||||
The value is initialized in storage immediately.
|
||||
|
||||
## Bulk Operations
|
||||
|
||||
When getting or setting multiple values in storage, you can perform bulk operations to improve performance by reducing the number of individual storage calls. The `storage` API provides several methods for performing bulk operations:
|
||||
|
||||
- **`getItems`** - Get multiple values at once.
|
||||
- **`getMetas`** - Get metadata for multiple items at once.
|
||||
- **`setItems`** - Set multiple values at once.
|
||||
- **`setMetas`** - Set metadata for multiple items at once.
|
||||
- **`removeItems`** - Remove multiple values (and optionally metadata) at once.
|
||||
|
||||
All these APIs support both string keys and defined storage items:
|
||||
|
||||
```ts
|
||||
const userId = storage.defineItem('local:userId');
|
||||
|
||||
await storage.setItems([
|
||||
{ key: 'local:installDate', value: Date.now() },
|
||||
{ item: userId, value: generateUserId() },
|
||||
]);
|
||||
```
|
||||
|
||||
Refer to the [API Reference](/api/reference/wxt/utils/storage/interfaces/WxtStorage) for types and examples of how to use all the bulk APIs.
|
||||
|
||||
@@ -20,7 +20,7 @@ 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 Theme {"background": "#161618", "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
|
||||
|
||||
+9
-2
@@ -2,11 +2,18 @@
|
||||
"$schema": "https://typedoc.org/schema.json",
|
||||
"entryPointStrategy": "packages",
|
||||
"entryPoints": ["../packages/wxt"],
|
||||
"plugin": ["typedoc-plugin-markdown", "typedoc-vitepress-theme"],
|
||||
"plugin": [
|
||||
"typedoc-plugin-markdown",
|
||||
"typedoc-vitepress-theme",
|
||||
"typedoc-plugin-frontmatter"
|
||||
],
|
||||
"out": "./api/reference",
|
||||
"githubPages": false,
|
||||
"excludePrivate": true,
|
||||
"excludeProtected": true,
|
||||
"excludeInternal": true,
|
||||
"readme": "none"
|
||||
"readme": "none",
|
||||
"frontmatterGlobals": {
|
||||
"editLink": false
|
||||
}
|
||||
}
|
||||
|
||||
+8
-6
@@ -4,7 +4,7 @@
|
||||
"engines": {
|
||||
"node": ">=18.20.3"
|
||||
},
|
||||
"packageManager": "pnpm@9.12.0",
|
||||
"packageManager": "pnpm@9.13.0",
|
||||
"scripts": {
|
||||
"check": "check && pnpm -r --sequential run check",
|
||||
"test": "pnpm -r --sequential run test run",
|
||||
@@ -20,24 +20,26 @@
|
||||
"@aklinker1/buildc": "^1.1.4",
|
||||
"@aklinker1/check": "^1.4.5",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@vitest/coverage-v8": "^2.1.2",
|
||||
"@vitest/coverage-v8": "^2.1.4",
|
||||
"changelogen": "^0.5.7",
|
||||
"consola": "^3.2.3",
|
||||
"fast-glob": "^3.3.2",
|
||||
"feed": "^4.2.2",
|
||||
"fs-extra": "^11.2.0",
|
||||
"lint-staged": "^15.2.10",
|
||||
"markdown-it-footnote": "^4.0.0",
|
||||
"nano-spawn": "^0.1.0",
|
||||
"nano-spawn": "^0.2.0",
|
||||
"prettier": "^3.3.3",
|
||||
"simple-git-hooks": "^2.11.1",
|
||||
"tsx": "4.15.7",
|
||||
"typedoc": "^0.25.4",
|
||||
"typedoc-plugin-frontmatter": "^1.1.0",
|
||||
"typedoc-plugin-markdown": "4.0.0-next.23",
|
||||
"typedoc-vitepress-theme": "1.0.0-next.3",
|
||||
"typescript": "^5.6.2",
|
||||
"vitepress": "^1.3.4",
|
||||
"typescript": "^5.6.3",
|
||||
"vitepress": "^1.5.0",
|
||||
"vitest-mock-extended": "^2.0.2",
|
||||
"vue": "^3.5.11",
|
||||
"vue": "^3.5.12",
|
||||
"wxt": "workspace:*"
|
||||
},
|
||||
"simple-git-hooks": {
|
||||
|
||||
@@ -46,9 +46,9 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "^1.4.5",
|
||||
"oxlint": "^0.9.9",
|
||||
"publint": "^0.2.11",
|
||||
"typescript": "^5.6.2",
|
||||
"oxlint": "^0.11.1",
|
||||
"publint": "^0.2.12",
|
||||
"typescript": "^5.6.3",
|
||||
"unbuild": "^2.0.0",
|
||||
"wxt": "workspace:*"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
# Changelog
|
||||
|
||||
## v0.2.3
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/i18n-v0.2.2...i18n-v0.2.3)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Prevent app crashes from parse errors due to incomplete file saves ([#1114](https://github.com/wxt-dev/wxt/pull/1114))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Cleanup typos and broken links ([bb5ea34](https://github.com/wxt-dev/wxt/commit/bb5ea34))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Bread Grocery <breadgrocery@gmail.com>
|
||||
- Aaron ([@aklinker1](http://github.com/aklinker1))
|
||||
|
||||
## v0.2.2
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/i18n-v0.2.1...i18n-v0.2.2)
|
||||
|
||||
@@ -49,7 +49,7 @@ However, it does have one major downside:
|
||||
helloWorld: Hello world!
|
||||
```
|
||||
|
||||
> `@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!
|
||||
> `@wxt-dev/i18n` supports the standard messages format, so if you already have localization files at `<rootDir>/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:
|
||||
|
||||
@@ -240,11 +240,11 @@ A key is treated as "verbose" when it is:
|
||||
{
|
||||
"appName": {
|
||||
"message": "GitHub - Better Line Counts",
|
||||
"description": "The app's name, should not be translated",
|
||||
"description": "The app's name, should not be translated"
|
||||
},
|
||||
"ok": "OK",
|
||||
"deleteConfirmation": {
|
||||
"title": "Delete XYZ?"
|
||||
"title": "Delete XYZ?",
|
||||
"message": "You cannot undo this action once taken."
|
||||
}
|
||||
}
|
||||
@@ -304,7 +304,7 @@ const messages = {
|
||||
// ...
|
||||
};
|
||||
|
||||
// Generate JSON files for the browser
|
||||
// Generate JSON files for the extension
|
||||
await generateChromeMessagesFile('dist/_locales/en/messages.json', messages.en);
|
||||
await generateChromeMessagesFile('dist/_locales/de/messages.json', messages.de);
|
||||
// ...
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@wxt-dev/i18n",
|
||||
"description": "Type-safe wrapper around browser.i18n.getMessage with additional features",
|
||||
"version": "0.2.2",
|
||||
"version": "0.2.3",
|
||||
"type": "module",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -26,7 +26,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"chokidar": "^3.6.0",
|
||||
"confbox": "^0.1.7",
|
||||
"confbox": "^0.1.8",
|
||||
"fast-glob": "^3.3.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -39,13 +39,13 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "^1.4.5",
|
||||
"@types/chrome": "^0.0.268",
|
||||
"@types/node": "^20.16.10",
|
||||
"oxlint": "^0.9.9",
|
||||
"publint": "^0.2.11",
|
||||
"typescript": "^5.6.2",
|
||||
"@types/chrome": "^0.0.280",
|
||||
"@types/node": "^20.17.6",
|
||||
"oxlint": "^0.11.1",
|
||||
"publint": "^0.2.12",
|
||||
"typescript": "^5.6.3",
|
||||
"unbuild": "^2.0.0",
|
||||
"vitest": "^2.0.0",
|
||||
"vitest": "^2.1.4",
|
||||
"vitest-plugin-random-seed": "^1.1.0",
|
||||
"wxt": "workspace:*"
|
||||
},
|
||||
|
||||
@@ -158,7 +158,9 @@ export { type GeneratedI18nStructure }
|
||||
if (wxt.config.command === 'serve') {
|
||||
wxt.hooks.hookOnce('build:done', () => {
|
||||
const watcher = watch(localesDir);
|
||||
watcher.on('change', updateLocalizations);
|
||||
watcher.on('change', (path) => {
|
||||
updateLocalizations(path).catch(wxt.logger.error);
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,5 +1,35 @@
|
||||
# Changelog
|
||||
|
||||
## v1.1.3
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/module-react-v1.1.2...module-react-v1.1.3)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Use react 19 in `@wxt-dev/module-react` and `templates/react` ([#1285](https://github.com/wxt-dev/wxt/pull/1285))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron ([@aklinker1](http://github.com/aklinker1))
|
||||
|
||||
## v1.1.2
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/module-react-v1.1.1...module-react-v1.1.2)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Use `config:resolved` hook to update config instead of `ready` ([#1178](https://github.com/wxt-dev/wxt/pull/1178))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Add `oxlint` for linting ([#947](https://github.com/wxt-dev/wxt/pull/947))
|
||||
- Upgrade all non-major dependencies ([#1040](https://github.com/wxt-dev/wxt/pull/1040))
|
||||
- **deps:** Upgrade all non-major dependencies ([#1164](https://github.com/wxt-dev/wxt/pull/1164))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron ([@aklinker1](http://github.com/aklinker1))
|
||||
|
||||
## v1.1.1
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/module-react-v1.1.0...module-react-v1.1.1)
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { defineContentScript } from 'wxt/sandbox';
|
||||
import { ContentScriptContext, createShadowRootUi } from 'wxt/client';
|
||||
import {
|
||||
defineContentScript,
|
||||
ContentScriptContext,
|
||||
createShadowRootUi,
|
||||
} from '#imports';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
|
||||
|
||||
@@ -15,8 +15,9 @@ 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.hook('config:resolved', (wxt) => {
|
||||
// In older versions of WXT, `wxt.config.imports` could be false
|
||||
if (!wxt.config.imports) return;
|
||||
|
||||
wxt.config.imports.dirsScanOptions ??= {};
|
||||
wxt.config.imports.dirsScanOptions.filePatterns = [
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"email": "aaronklinker1+wxt@gmail.com"
|
||||
},
|
||||
"license": "MIT",
|
||||
"version": "1.1.1",
|
||||
"version": "1.1.3",
|
||||
"type": "module",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.mjs",
|
||||
@@ -44,19 +44,19 @@
|
||||
"prepare": "buildc --deps-only -- wxt prepare"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"wxt": ">=0.18.6"
|
||||
"wxt": ">=0.19.16"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vitejs/plugin-react": "^4.3.2"
|
||||
"@vitejs/plugin-react": "^4.3.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "^1.4.5",
|
||||
"@types/react": "^18.3.11",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"publint": "^0.2.11",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"typescript": "^5.6.2",
|
||||
"@types/react": "^19.0.1",
|
||||
"@types/react-dom": "^19.0.2",
|
||||
"publint": "^0.2.12",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"typescript": "^5.6.3",
|
||||
"unbuild": "^2.0.0",
|
||||
"wxt": "workspace:*"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
# Changelog
|
||||
|
||||
## v1.1.3
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/module-solid-v1.1.2...module-solid-v1.1.3)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Use `config:resolved` hook to update config instead of `ready` ([#1178](https://github.com/wxt-dev/wxt/pull/1178))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Add `oxlint` for linting ([#947](https://github.com/wxt-dev/wxt/pull/947))
|
||||
- Upgrade all non-major dependencies ([#1040](https://github.com/wxt-dev/wxt/pull/1040))
|
||||
- **deps:** Upgrade all non-major dependencies ([#1164](https://github.com/wxt-dev/wxt/pull/1164))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron ([@aklinker1](http://github.com/aklinker1))
|
||||
|
||||
## v1.1.2
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/module-solid-v1.1.1...module-solid-v1.1.2)
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { defineContentScript } from 'wxt/sandbox';
|
||||
import { ContentScriptContext, createShadowRootUi } from 'wxt/client';
|
||||
import {
|
||||
defineContentScript,
|
||||
ContentScriptContext,
|
||||
createShadowRootUi,
|
||||
} from '#imports';
|
||||
import { render } from 'solid-js/web';
|
||||
|
||||
export default defineContentScript({
|
||||
|
||||
@@ -18,8 +18,9 @@ export default defineWxtModule<SolidModuleOptions>({
|
||||
addImportPreset(wxt, 'solid-js');
|
||||
|
||||
// Enable auto-imports for JSX files
|
||||
wxt.hooks.hook('ready', (wxt) => {
|
||||
if (wxt.config.imports === false) return;
|
||||
wxt.hook('config:resolved', (wxt) => {
|
||||
// In older versions of WXT, `wxt.config.imports` could be false
|
||||
if (!wxt.config.imports) return;
|
||||
|
||||
wxt.config.imports.dirsScanOptions ??= {};
|
||||
wxt.config.imports.dirsScanOptions.filePatterns = [
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"email": "aaronklinker1+wxt@gmail.com"
|
||||
},
|
||||
"license": "MIT",
|
||||
"version": "1.1.2",
|
||||
"version": "1.1.3",
|
||||
"type": "module",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.mjs",
|
||||
@@ -44,16 +44,16 @@
|
||||
"prepare": "buildc --deps-only -- wxt prepare"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"wxt": ">=0.18.6"
|
||||
"wxt": ">=0.19.16"
|
||||
},
|
||||
"dependencies": {
|
||||
"vite-plugin-solid": "^2.10.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "^1.4.5",
|
||||
"publint": "^0.2.11",
|
||||
"solid-js": "^1.9.1",
|
||||
"typescript": "^5.6.2",
|
||||
"publint": "^0.2.12",
|
||||
"solid-js": "^1.9.3",
|
||||
"typescript": "^5.6.3",
|
||||
"unbuild": "^2.0.0",
|
||||
"wxt": "workspace:*"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,56 @@
|
||||
# Changelog
|
||||
|
||||
## v2.0.2
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/module-svelte-v2.0.1...module-svelte-v2.0.2)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Svelte production errors and warnings on resolve conditions ([#1283](https://github.com/wxt-dev/wxt/pull/1283))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron ([@aklinker1](http://github.com/aklinker1))
|
||||
|
||||
## v2.0.1
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/module-svelte-v2.0.0...module-svelte-v2.0.1)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Modify dev `vite.resolve.conditions` to support Vite 6 + Svelte 5 ([#1230](https://github.com/wxt-dev/wxt/pull/1230))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- **deps:** Upgrade all non-major dependencies ([#1164](https://github.com/wxt-dev/wxt/pull/1164))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron ([@aklinker1](http://github.com/aklinker1))
|
||||
|
||||
## v2.0.0
|
||||
|
||||
[⚠️ breaking changes](https://wxt.dev/guide/upgrade-guide/wxt) • [compare changes](https://github.com/wxt-dev/wxt/compare/module-svelte-v1.0.1...module-svelte-v2.0.0)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- ⚠️ Svelte 5 support ([#1104](https://github.com/wxt-dev/wxt/pull/1104))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Add `oxlint` for linting ([#947](https://github.com/wxt-dev/wxt/pull/947))
|
||||
- Upgrade all non-major dependencies ([#1040](https://github.com/wxt-dev/wxt/pull/1040))
|
||||
|
||||
#### ⚠️ Breaking Changes
|
||||
|
||||
Upgraded `@sveltejs/vite-plugin-svelte` from v3 to v4. This drops support for Svelte 4 and below. To continue using older versions of Svelte, use v1 of this module.
|
||||
|
||||
To upgrade to svelte 5, just install `svelte@5`.
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron ([@aklinker1](http://github.com/aklinker1))
|
||||
|
||||
## v1.0.1
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/module-svelte-v1.0.0...module-svelte-v1.0.1)
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"email": "aaronklinker1+wxt@gmail.com"
|
||||
},
|
||||
"license": "MIT",
|
||||
"version": "1.0.1",
|
||||
"version": "2.0.2",
|
||||
"type": "module",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.mjs",
|
||||
@@ -42,15 +42,16 @@
|
||||
"check": "pnpm build && check"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"wxt": ">=0.18.6"
|
||||
"wxt": ">=0.18.6",
|
||||
"svelte": ">=5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^3.1.2"
|
||||
"@sveltejs/vite-plugin-svelte": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "^1.4.5",
|
||||
"publint": "^0.2.11",
|
||||
"typescript": "^5.6.2",
|
||||
"publint": "^0.2.12",
|
||||
"typescript": "^5.6.3",
|
||||
"unbuild": "^2.0.0",
|
||||
"wxt": "workspace:*"
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ export default defineWxtModule<SvelteModuleOptions>({
|
||||
setup(wxt, options) {
|
||||
const { vite } = options ?? {};
|
||||
|
||||
addViteConfig(wxt, () => ({
|
||||
addViteConfig(wxt, ({ mode }) => ({
|
||||
plugins: [
|
||||
svelte({
|
||||
// Using a svelte.config.js file causes a segmentation fault when importing the file
|
||||
@@ -21,6 +21,9 @@ export default defineWxtModule<SvelteModuleOptions>({
|
||||
...vite,
|
||||
}),
|
||||
],
|
||||
resolve: {
|
||||
conditions: ['browser', mode],
|
||||
},
|
||||
}));
|
||||
|
||||
addImportPreset(wxt, 'svelte');
|
||||
|
||||
@@ -1,5 +1,24 @@
|
||||
# Changelog
|
||||
|
||||
## v1.0.2
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/module-vue-v1.0.1...module-vue-v1.0.2)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Use `config:resolved` hook to update config instead of `ready` ([#1178](https://github.com/wxt-dev/wxt/pull/1178))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Add `oxlint` for linting ([#947](https://github.com/wxt-dev/wxt/pull/947))
|
||||
- **deps:** Bump @vitejs/plugin-vue from 5.1.1 to 5.1.4 ([#1020](https://github.com/wxt-dev/wxt/pull/1020))
|
||||
- Upgrade all non-major dependencies ([#1040](https://github.com/wxt-dev/wxt/pull/1040))
|
||||
- **deps:** Upgrade all non-major dependencies ([#1164](https://github.com/wxt-dev/wxt/pull/1164))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron ([@aklinker1](http://github.com/aklinker1))
|
||||
|
||||
## v1.0.1
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/module-vue-v1.0.0...module-vue-v1.0.1)
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"email": "aaronklinker1+wxt@gmail.com"
|
||||
},
|
||||
"license": "MIT",
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.2",
|
||||
"type": "module",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.mjs",
|
||||
@@ -42,15 +42,15 @@
|
||||
"check": "pnpm build && check"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"wxt": ">=0.18.6"
|
||||
"wxt": ">=0.19.16"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vitejs/plugin-vue": "^5.1.4"
|
||||
"@vitejs/plugin-vue": "^5.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "^1.4.5",
|
||||
"publint": "^0.2.11",
|
||||
"typescript": "^5.6.2",
|
||||
"publint": "^0.2.12",
|
||||
"typescript": "^5.6.3",
|
||||
"unbuild": "^2.0.0",
|
||||
"wxt": "workspace:*"
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ export default defineWxtModule<VueModuleOptions>({
|
||||
}));
|
||||
|
||||
// Enable auto-imports in template files
|
||||
wxt.hooks.hook('ready', (wxt) => {
|
||||
wxt.hook('config:resolved', (wxt) => {
|
||||
if (!wxt.config.imports) return;
|
||||
|
||||
wxt.config.imports.addons ??= {};
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# Changelog
|
||||
|
||||
## v1.0.1
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/storage-v1.0.0...storage-v1.0.1)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Use `browser` for mv2 storage ([#1200](https://github.com/wxt-dev/wxt/pull/1200))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Cleanup changelog ([f5b7f7e](https://github.com/wxt-dev/wxt/commit/f5b7f7e))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Init changelog for storage package ([6fc227b](https://github.com/wxt-dev/wxt/commit/6fc227b))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron ([@aklinker1](http://github.com/aklinker1))
|
||||
|
||||
## v1.0.0
|
||||
|
||||
Extracted `wxt/storage` into it's own package, `@wxt-dev/storage`!
|
||||
|
||||
It's still shipped inside WXT and accessible via `wxt/storage`, but now:
|
||||
|
||||
- Non-WXT projects can use the storage wrapper.
|
||||
- We can make breaking changes to the API separately.
|
||||
|
||||
[Read the docs](https://wxt.dev/storage.html) for more details.
|
||||
|
||||
> This is apart of the v1.0 initiative for WXT.
|
||||
@@ -0,0 +1,36 @@
|
||||
# WXT Storage
|
||||
|
||||
[Changelog](https://github.com/wxt-dev/wxt/blob/main/packages/storage/CHANGELOG.md) • [Docs](https://wxt.dev/storage.html)
|
||||
|
||||
A simplified wrapper around the extension storage APIs.
|
||||
|
||||
## Installation
|
||||
|
||||
### With WXT
|
||||
|
||||
This module is built-in to WXT, so you don't need to install anything.
|
||||
|
||||
```ts
|
||||
import { storage } from 'wxt/storage';
|
||||
```
|
||||
|
||||
If you use auto-imports, `storage` is auto-imported for you, so you don't even need to import it!
|
||||
|
||||
### Without WXT
|
||||
|
||||
Install the NPM package:
|
||||
|
||||
```sh
|
||||
npm i @wxt-dev/storage
|
||||
pnpm add @wxt-dev/storage
|
||||
yarn add @wxt-dev/storage
|
||||
bun add @wxt-dev/storage
|
||||
```
|
||||
|
||||
```ts
|
||||
import { storage } from '@wxt-dev/storage';
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Read full docs on the [documentation website](https://wxt.dev/storage.html).
|
||||
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"name": "@wxt-dev/storage",
|
||||
"description": "Web extension storage API provided by WXT, supports all browsers.",
|
||||
"version": "1.0.1",
|
||||
"type": "module",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/wxt-dev/wxt.git",
|
||||
"directory": "packages/storage"
|
||||
},
|
||||
"homepage": "https://wxt.dev/storage.html",
|
||||
"keywords": [
|
||||
"wxt",
|
||||
"storage",
|
||||
"extension",
|
||||
"addon",
|
||||
"chrome",
|
||||
"firefox",
|
||||
"edge"
|
||||
],
|
||||
"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": {
|
||||
"async-mutex": "^0.5.0",
|
||||
"dequal": "^2.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "^1.4.5",
|
||||
"@types/chrome": "^0.0.268",
|
||||
"@webext-core/fake-browser": "^1.3.1",
|
||||
"oxlint": "^0.9.9",
|
||||
"publint": "^0.2.11",
|
||||
"typescript": "^5.6.2",
|
||||
"unbuild": "^2.0.0",
|
||||
"vitest": "^2.0.0"
|
||||
},
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.mts",
|
||||
"import": "./dist/index.mjs"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
+501
-40
@@ -1,7 +1,6 @@
|
||||
import { fakeBrowser } from '@webext-core/fake-browser';
|
||||
import { describe, it, expect, beforeEach, vi, expectTypeOf } from 'vitest';
|
||||
import { browser } from 'wxt/browser';
|
||||
import { WxtStorageItem, storage } from '../storage';
|
||||
import { MigrationError, type WxtStorageItem, storage } from '../index';
|
||||
|
||||
/**
|
||||
* This works because fakeBrowser is synchronous, and is will finish any number of chained
|
||||
@@ -38,7 +37,7 @@ describe('Storage Utils', () => {
|
||||
expect(actual).toBe(expected);
|
||||
});
|
||||
|
||||
it('should return the value if multiple : are use in the key', async () => {
|
||||
it('should return the value if multiple : are used in the key', async () => {
|
||||
const expected = 'value';
|
||||
await fakeBrowser.storage[storageArea].set({ 'some:key': expected });
|
||||
|
||||
@@ -64,27 +63,97 @@ describe('Storage Utils', () => {
|
||||
});
|
||||
|
||||
describe('getItems', () => {
|
||||
it('should return an array of values', async () => {
|
||||
const expected = [
|
||||
{ key: `${storageArea}:count`, value: 234 },
|
||||
{ key: `${storageArea}:installDate`, value: null },
|
||||
{ key: `${storageArea}:otherValue`, value: 345 },
|
||||
] as const;
|
||||
const params = [
|
||||
expected[0].key,
|
||||
expected[1].key,
|
||||
{
|
||||
key: expected[2].key,
|
||||
options: { defaultValue: expected[2].value },
|
||||
},
|
||||
];
|
||||
it('should get values from multiple storage keys', async () => {
|
||||
const item1 = {
|
||||
key: `${storageArea}:one`,
|
||||
expectedValue: 1,
|
||||
} as const;
|
||||
const item2 = {
|
||||
key: `${storageArea}:two`,
|
||||
expectedValue: null,
|
||||
} as const;
|
||||
|
||||
await fakeBrowser.storage[storageArea].set({
|
||||
count: expected[0].value,
|
||||
one: item1.expectedValue,
|
||||
});
|
||||
|
||||
const actual = await storage.getItems(params);
|
||||
const actual = await storage.getItems([item1.key, item2.key]);
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
expect(actual).toEqual([
|
||||
{ key: item1.key, value: item1.expectedValue },
|
||||
{ key: item2.key, value: item2.expectedValue },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should get values from multiple storage items', async () => {
|
||||
const item1 = storage.defineItem(`${storageArea}:one`);
|
||||
const expectedValue1 = 1;
|
||||
const item2 = storage.defineItem(`${storageArea}:two`);
|
||||
const expectedValue2 = null;
|
||||
|
||||
await fakeBrowser.storage[storageArea].set({
|
||||
one: expectedValue1,
|
||||
});
|
||||
|
||||
const actual = await storage.getItems([item1, item2]);
|
||||
|
||||
expect(actual).toEqual([
|
||||
{ key: item1.key, value: expectedValue1 },
|
||||
{ key: item2.key, value: expectedValue2 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should get values for a combination of different input types', async () => {
|
||||
const key1 = `${storageArea}:one` as const;
|
||||
const expectedValue1 = 1;
|
||||
const item2 = storage.defineItem<number>(`${storageArea}:two`);
|
||||
const expectedValue2 = 2;
|
||||
|
||||
await fakeBrowser.storage[storageArea].set({
|
||||
one: expectedValue1,
|
||||
two: expectedValue2,
|
||||
});
|
||||
|
||||
const actual = await storage.getItems([key1, item2]);
|
||||
|
||||
expect(actual).toEqual([
|
||||
{ key: key1, value: expectedValue1 },
|
||||
{ key: item2.key, value: expectedValue2 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return fallback values for keys when provided', async () => {
|
||||
const key1 = `${storageArea}:one` as const;
|
||||
const expectedValue1 = null;
|
||||
const key2 = `${storageArea}:two` as const;
|
||||
const fallback2 = 2;
|
||||
const expectedValue2 = fallback2;
|
||||
|
||||
const actual = await storage.getItems([
|
||||
key1,
|
||||
{ key: key2, options: { fallback: fallback2 } },
|
||||
]);
|
||||
|
||||
expect(actual).toEqual([
|
||||
{ key: key1, value: expectedValue1 },
|
||||
{ key: key2, value: expectedValue2 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return fallback values for items when provided', async () => {
|
||||
const item1 = storage.defineItem<number>(`${storageArea}:one`);
|
||||
const expectedValue1 = null;
|
||||
const item2 = storage.defineItem(`${storageArea}:two`, {
|
||||
fallback: 2,
|
||||
});
|
||||
const expectedValue2 = item2.fallback;
|
||||
|
||||
const actual = await storage.getItems([item1, item2]);
|
||||
|
||||
expect(actual).toEqual([
|
||||
{ key: item1.key, value: expectedValue1 },
|
||||
{ key: item2.key, value: expectedValue2 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -153,7 +222,7 @@ describe('Storage Utils', () => {
|
||||
describe('setMeta', () => {
|
||||
it('should set metadata at key+$', async () => {
|
||||
const existing = { v: 1 };
|
||||
await browser.storage[storageArea].set({ count$: existing });
|
||||
await chrome.storage[storageArea].set({ count$: existing });
|
||||
const newValues = {
|
||||
date: Date.now(),
|
||||
};
|
||||
@@ -169,7 +238,7 @@ describe('Storage Utils', () => {
|
||||
'should remove any properties set to %s',
|
||||
async (version) => {
|
||||
const existing = { v: 1 };
|
||||
await browser.storage[storageArea].set({ count$: existing });
|
||||
await chrome.storage[storageArea].set({ count$: existing });
|
||||
const expected = {};
|
||||
|
||||
await storage.setMeta(`${storageArea}:count`, { v: version });
|
||||
@@ -180,6 +249,74 @@ describe('Storage Utils', () => {
|
||||
);
|
||||
});
|
||||
|
||||
describe('setMetas', () => {
|
||||
it('should set key metadata correctly', async () => {
|
||||
const key1 = `${storageArea}:one` as const;
|
||||
const initialMeta1 = {};
|
||||
const setMeta1 = { v: 1 };
|
||||
const expectedMeta1 = setMeta1;
|
||||
|
||||
const key2 = `${storageArea}:two` as const;
|
||||
const initialMeta2 = { v: 1 };
|
||||
const setMeta2 = { v: 2 };
|
||||
const expectedMeta2 = setMeta2;
|
||||
|
||||
const key3 = `${storageArea}:three` as const;
|
||||
const initialMeta3 = { v: 1 };
|
||||
const setMeta3 = { d: Date.now() };
|
||||
const expectedMeta3 = { ...initialMeta3, ...setMeta3 };
|
||||
|
||||
await fakeBrowser.storage[storageArea].set({
|
||||
one$: initialMeta1,
|
||||
two$: initialMeta2,
|
||||
three$: initialMeta3,
|
||||
});
|
||||
|
||||
await storage.setMetas([
|
||||
{ key: key1, meta: setMeta1 },
|
||||
{ key: key2, meta: setMeta2 },
|
||||
{ key: key3, meta: setMeta3 },
|
||||
]);
|
||||
|
||||
expect(await storage.getMeta(key1)).toEqual(expectedMeta1);
|
||||
expect(await storage.getMeta(key2)).toEqual(expectedMeta2);
|
||||
expect(await storage.getMeta(key3)).toEqual(expectedMeta3);
|
||||
});
|
||||
|
||||
it('should set item metadata correctly', async () => {
|
||||
const item1 = storage.defineItem(`${storageArea}:one`);
|
||||
const initialMeta1 = {};
|
||||
const setMeta1 = { v: 1 };
|
||||
const expectedMeta1 = setMeta1;
|
||||
|
||||
const item2 = storage.defineItem(`${storageArea}:two`);
|
||||
const initialMeta2 = { v: 1 };
|
||||
const setMeta2 = { v: 2 };
|
||||
const expectedMeta2 = setMeta2;
|
||||
|
||||
const item3 = storage.defineItem(`${storageArea}:three`);
|
||||
const initialMeta3 = { v: 1 };
|
||||
const setMeta3 = { d: Date.now() };
|
||||
const expectedMeta3 = { ...initialMeta3, ...setMeta3 };
|
||||
|
||||
await fakeBrowser.storage[storageArea].set({
|
||||
one$: initialMeta1,
|
||||
two$: initialMeta2,
|
||||
three$: initialMeta3,
|
||||
});
|
||||
|
||||
await storage.setMetas([
|
||||
{ item: item1, meta: setMeta1 },
|
||||
{ item: item2, meta: setMeta2 },
|
||||
{ item: item3, meta: setMeta3 },
|
||||
]);
|
||||
|
||||
expect(await item1.getMeta()).toEqual(expectedMeta1);
|
||||
expect(await item2.getMeta()).toEqual(expectedMeta2);
|
||||
expect(await item3.getMeta()).toEqual(expectedMeta3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeItem', () => {
|
||||
it('should remove the key from storage', async () => {
|
||||
await fakeBrowser.storage[storageArea].set({ count: 456 });
|
||||
@@ -219,29 +356,136 @@ describe('Storage Utils', () => {
|
||||
});
|
||||
|
||||
describe('removeItems', () => {
|
||||
it('should remove multiple items', async () => {
|
||||
it('should remove multiple keys', async () => {
|
||||
const key1 = `${storageArea}:one` as const;
|
||||
const key2 = `${storageArea}:two` as const;
|
||||
const key3 = `${storageArea}:three` as const;
|
||||
await fakeBrowser.storage[storageArea].set({
|
||||
['one']: '1',
|
||||
['two']: null,
|
||||
['two$']: { v: 1 },
|
||||
['three']: '1',
|
||||
['three$']: { v: 1 },
|
||||
one: 1,
|
||||
two: 2,
|
||||
});
|
||||
|
||||
await storage.removeItems([key1, key2]);
|
||||
|
||||
expect(await storage.getItem(key1)).toBeNull();
|
||||
expect(await storage.getItem(key2)).toBeNull();
|
||||
});
|
||||
|
||||
it('should remove multiple keys and metadata when requested', async () => {
|
||||
const key1 = `${storageArea}:one` as const;
|
||||
const key2 = `${storageArea}:two` as const;
|
||||
await fakeBrowser.storage[storageArea].set({
|
||||
one: 1,
|
||||
one$: { v: 1 },
|
||||
two: 2,
|
||||
two$: { v: 1 },
|
||||
});
|
||||
|
||||
await storage.removeItems([
|
||||
key1,
|
||||
key2,
|
||||
{ key: key3, options: { removeMeta: true } },
|
||||
{ key: key2, options: { removeMeta: true } },
|
||||
]);
|
||||
|
||||
expect(await storage.getItem(key1)).toBeNull();
|
||||
expect(await storage.getMeta(key1)).toEqual({ v: 1 });
|
||||
expect(await storage.getItem(key2)).toBeNull();
|
||||
expect(await storage.getMeta(key2)).toEqual({ v: 1 });
|
||||
expect(await storage.getItem(key3)).toBeNull();
|
||||
expect(await storage.getMeta(key3)).toEqual({});
|
||||
expect(await storage.getMeta(key2)).toEqual({});
|
||||
});
|
||||
|
||||
it('should remove multiple items', async () => {
|
||||
const item1 = storage.defineItem(`${storageArea}:one`);
|
||||
const item2 = storage.defineItem(`${storageArea}:two`);
|
||||
await fakeBrowser.storage[storageArea].set({
|
||||
one: 1,
|
||||
two: 2,
|
||||
});
|
||||
|
||||
await storage.removeItems([item1, item2]);
|
||||
|
||||
expect(await item1.getValue()).toBeNull();
|
||||
expect(await item2.getValue()).toBeNull();
|
||||
});
|
||||
|
||||
it('should remove multiple items and metadata when requested', async () => {
|
||||
const item1 = storage.defineItem(`${storageArea}:one`);
|
||||
const item2 = storage.defineItem(`${storageArea}:two`);
|
||||
await fakeBrowser.storage[storageArea].set({
|
||||
one: 1,
|
||||
one$: { v: 1 },
|
||||
two: 2,
|
||||
two$: { v: 1 },
|
||||
});
|
||||
|
||||
await storage.removeItems([
|
||||
item1,
|
||||
{ item: item2, options: { removeMeta: true } },
|
||||
]);
|
||||
|
||||
expect(await item1.getValue()).toBeNull();
|
||||
expect(await item1.getMeta()).toEqual({ v: 1 });
|
||||
expect(await item2.getValue()).toBeNull();
|
||||
expect(await item2.getMeta()).toEqual({});
|
||||
});
|
||||
|
||||
it('should remove multiple items', async () => {
|
||||
const item1 = storage.defineItem(`${storageArea}:one`);
|
||||
const item2 = storage.defineItem(`${storageArea}:two`);
|
||||
|
||||
await fakeBrowser.storage.local.set({
|
||||
one: 1,
|
||||
two: 2,
|
||||
});
|
||||
|
||||
await storage.removeItems([item1, item2]);
|
||||
|
||||
expect(await item1.getValue()).toBeNull();
|
||||
expect(await item2.getValue()).toBeNull();
|
||||
});
|
||||
|
||||
it('should remove items using { item: WxtStorageItem, options?: RemoveItemOptions } objects', async () => {
|
||||
const item1 = storage.defineItem('local:item1');
|
||||
const item2 = storage.defineItem('session:item2');
|
||||
|
||||
await item1.setValue('value1');
|
||||
await item1.setMeta({ v: 1 });
|
||||
await item2.setValue('value2');
|
||||
await item2.setMeta({ v: 1 });
|
||||
|
||||
await storage.removeItems([
|
||||
{ item: item1, options: { removeMeta: true } },
|
||||
{ item: item2, options: { removeMeta: false } },
|
||||
]);
|
||||
|
||||
expect(await item1.getValue()).toBeNull();
|
||||
expect(await item1.getMeta()).toEqual({});
|
||||
expect(await item2.getValue()).toBeNull();
|
||||
expect(await item2.getMeta()).toEqual({ v: 1 });
|
||||
});
|
||||
|
||||
it('should handle a mix of different input types', async () => {
|
||||
const item1 = storage.defineItem('local:item1');
|
||||
const item2 = storage.defineItem('session:item2');
|
||||
const item3 = storage.defineItem('local:item3');
|
||||
|
||||
await item1.setValue('value1');
|
||||
await item2.setValue('value2');
|
||||
await item3.setValue('value3');
|
||||
|
||||
await storage.removeItems([
|
||||
'local:item1',
|
||||
item2,
|
||||
{ key: 'local:item3', options: { removeMeta: true } },
|
||||
]);
|
||||
|
||||
expect(await storage.getItem('local:item1')).toBeNull();
|
||||
expect(await item2.getValue()).toBeNull();
|
||||
expect(await item3.getValue()).toBeNull();
|
||||
expect(await item3.getMeta()).toEqual({});
|
||||
});
|
||||
|
||||
it('should not throw an error when removing non-existent items', async () => {
|
||||
const item = storage.defineItem('local:non_existent');
|
||||
|
||||
await expect(storage.removeItems([item])).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -400,6 +644,22 @@ describe('Storage Utils', () => {
|
||||
},
|
||||
);
|
||||
|
||||
describe('Invalid storage areas', () => {
|
||||
it('should not accept keys without a valid storage area prefix', async () => {
|
||||
// @ts-expect-error
|
||||
await storage.getItem('test').catch(() => {});
|
||||
// @ts-expect-error
|
||||
await storage.getItem('loca:test').catch(() => {});
|
||||
});
|
||||
|
||||
it('should throw an error when using an invalid storage area', async () => {
|
||||
// @ts-expect-error: Test passes if there is a type error here
|
||||
await expect(storage.getItem('invalidArea:key')).rejects.toThrow(
|
||||
'Invalid area',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('defineItem', () => {
|
||||
describe('versioning', () => {
|
||||
it('should migrate values to the latest when a version upgrade is detected', async () => {
|
||||
@@ -553,6 +813,28 @@ describe('Storage Utils', () => {
|
||||
'Version downgrade detected (v2 -> v1) for "local:count"',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error when defining an item with an invalid version', () => {
|
||||
expect(() => storage.defineItem('local:key', { version: 0 })).toThrow(
|
||||
'Storage item version cannot be less than 1',
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle errors in migration functions', async () => {
|
||||
const cause = Error('Some error');
|
||||
const expectedError = new MigrationError('local:key', 2, { cause });
|
||||
const item = storage.defineItem<number>('local:key', {
|
||||
version: 3,
|
||||
migrations: {
|
||||
2: () => {
|
||||
throw cause;
|
||||
},
|
||||
},
|
||||
});
|
||||
await fakeBrowser.storage.local.set({ key: 1, key$: { v: 1 } });
|
||||
|
||||
await expect(item.migrate()).rejects.toThrow(expectedError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getValue', () => {
|
||||
@@ -872,6 +1154,24 @@ describe('Storage Utils', () => {
|
||||
|
||||
await expect(storage.getItem('local:test')).resolves.toBe(expected);
|
||||
});
|
||||
|
||||
it("should re-initialize a value on the next call to getValue if it's been removed", async () => {
|
||||
const init = vi.fn().mockImplementation(Math.random);
|
||||
const item = storage.defineItem<number>('local:key', { init });
|
||||
await waitForInit();
|
||||
|
||||
await item.removeValue();
|
||||
// Make sure it's actually blank before running the test
|
||||
expect(await chrome.storage.local.get()).toEqual({});
|
||||
init.mockClear();
|
||||
|
||||
const [value1, value2] = await Promise.all([
|
||||
item.getValue(),
|
||||
item.getValue(),
|
||||
]);
|
||||
expect(init).toBeCalledTimes(1);
|
||||
expect(value1).toBe(value2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('types', () => {
|
||||
@@ -896,12 +1196,173 @@ describe('Storage Utils', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('types', () => {
|
||||
it('should not accept keys without a valid storage area prefix', async () => {
|
||||
// @ts-expect-error: Test passes if there is a type error here
|
||||
await storage.getItem('test').catch(() => {});
|
||||
// @ts-expect-error
|
||||
await storage.getItem('loca:test').catch(() => {});
|
||||
describe('Multiple Storage Areas', () => {
|
||||
describe('getItems', () => {
|
||||
it('should get the values of multiple storage items efficiently', async () => {
|
||||
const item1 = storage.defineItem<number>('local:item1');
|
||||
const item2 = storage.defineItem<string>('session:item2');
|
||||
const item3 = storage.defineItem<boolean>('local:item3');
|
||||
|
||||
await item1.setValue(42);
|
||||
await item2.setValue('hello');
|
||||
await item3.setValue(false);
|
||||
|
||||
const localGetSpy = vi.spyOn(fakeBrowser.storage.local, 'get');
|
||||
const sessionGetSpy = vi.spyOn(fakeBrowser.storage.session, 'get');
|
||||
|
||||
const values = await storage.getItems([item1, item2, item3]);
|
||||
|
||||
expect(values).toEqual([
|
||||
{
|
||||
key: 'local:item1',
|
||||
value: 42,
|
||||
},
|
||||
{
|
||||
key: 'session:item2',
|
||||
value: 'hello',
|
||||
},
|
||||
{
|
||||
key: 'local:item3',
|
||||
value: false,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(localGetSpy).toBeCalledTimes(1);
|
||||
expect(localGetSpy).toBeCalledWith(['item1', 'item3']);
|
||||
expect(sessionGetSpy).toBeCalledTimes(1);
|
||||
expect(sessionGetSpy).toBeCalledWith(['item2']);
|
||||
});
|
||||
|
||||
it('should return values in the same order as input', async () => {
|
||||
const item1 = storage.defineItem<number>('local:item1');
|
||||
const item2 = storage.defineItem<string>('session:item2');
|
||||
const item3 = storage.defineItem<boolean>('local:item3');
|
||||
|
||||
expect(await storage.getItems([item1, item2, item3])).toEqual([
|
||||
{ key: item1.key, value: null },
|
||||
{ key: item2.key, value: null },
|
||||
{ key: item3.key, value: null },
|
||||
]);
|
||||
expect(await storage.getItems([item3, item2, item1])).toEqual([
|
||||
{ key: item3.key, value: null },
|
||||
{ key: item2.key, value: null },
|
||||
{ key: item1.key, value: null },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMetas', () => {
|
||||
it('should get the metadata of multiple storage items efficiently', async () => {
|
||||
const item1 = storage.defineItem<number, { v: number }>('local:item1');
|
||||
const item2 = storage.defineItem<string, { date: number }>(
|
||||
'session:item2',
|
||||
);
|
||||
const item3 = storage.defineItem<boolean, { flag: boolean }>(
|
||||
'local:item3',
|
||||
);
|
||||
|
||||
await item1.setMeta({ v: 1 });
|
||||
await item2.setMeta({ date: 1234567890 });
|
||||
// item3 has no meta
|
||||
|
||||
const localGetSpy = vi.spyOn(fakeBrowser.storage.local, 'get');
|
||||
const sessionGetSpy = vi.spyOn(fakeBrowser.storage.session, 'get');
|
||||
|
||||
const metas = await storage.getMetas([item1, item2, item3]);
|
||||
|
||||
expect(metas).toEqual([
|
||||
{ key: item1.key, meta: { v: 1 } },
|
||||
{ key: item2.key, meta: { date: 1234567890 } },
|
||||
{ key: item3.key, meta: {} },
|
||||
]);
|
||||
|
||||
expect(localGetSpy).toBeCalledTimes(1);
|
||||
expect(localGetSpy).toBeCalledWith(['item1$', 'item3$']);
|
||||
expect(sessionGetSpy).toBeCalledTimes(1);
|
||||
expect(sessionGetSpy).toBeCalledWith(['item2$']);
|
||||
});
|
||||
|
||||
it('should return the metadata in the same order as input', async () => {
|
||||
const item1 = storage.defineItem<number, { v: number }>('local:item1');
|
||||
const item2 = storage.defineItem<string, { date: number }>(
|
||||
'session:item2',
|
||||
);
|
||||
const item3 = storage.defineItem<boolean, { flag: boolean }>(
|
||||
'local:item3',
|
||||
);
|
||||
|
||||
expect(await storage.getMetas([item1, item2, item3])).toEqual([
|
||||
{ key: item1.key, meta: {} },
|
||||
{ key: item2.key, meta: {} },
|
||||
{ key: item3.key, meta: {} },
|
||||
]);
|
||||
expect(await storage.getMetas([item3, item2, item1])).toEqual([
|
||||
{ key: item3.key, meta: {} },
|
||||
{ key: item2.key, meta: {} },
|
||||
{ key: item1.key, meta: {} },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setItems', () => {
|
||||
it('should set the values of multiple storage items efficiently', async () => {
|
||||
const item1 = storage.defineItem<number>('local:item1');
|
||||
const value1 = 100;
|
||||
const item2 = storage.defineItem<string>('session:item2');
|
||||
const value2 = 'test';
|
||||
const item3 = storage.defineItem<boolean>('local:item3');
|
||||
const value3 = true;
|
||||
|
||||
const localSetSpy = vi.spyOn(fakeBrowser.storage.local, 'set');
|
||||
const sessionSetSpy = vi.spyOn(fakeBrowser.storage.session, 'set');
|
||||
|
||||
await storage.setItems([
|
||||
{ item: item1, value: value1 },
|
||||
{ item: item2, value: value2 },
|
||||
{ item: item3, value: value3 },
|
||||
]);
|
||||
|
||||
expect(localSetSpy).toBeCalledTimes(1);
|
||||
expect(localSetSpy).toBeCalledWith({ item1: value1, item3: value3 });
|
||||
expect(sessionSetSpy).toBeCalledTimes(1);
|
||||
expect(sessionSetSpy).toBeCalledWith({ item2: value2 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('setMetas', () => {
|
||||
it('should set metadata efficiently', async () => {
|
||||
const item1 = storage.defineItem<number, { v: number }>('local:one');
|
||||
const item2 = storage.defineItem<string, { v: number }>('session:two');
|
||||
const item3 = storage.defineItem<boolean, { v: number }>('local:three');
|
||||
await waitForInit();
|
||||
|
||||
const localGetSpy = vi.spyOn(fakeBrowser.storage.local, 'get');
|
||||
const sessionGetSpy = vi.spyOn(fakeBrowser.storage.session, 'get');
|
||||
const localSetSpy = vi.spyOn(fakeBrowser.storage.local, 'set');
|
||||
const sessionSetSpy = vi.spyOn(fakeBrowser.storage.session, 'set');
|
||||
|
||||
await storage.setMetas([
|
||||
{ item: item1, meta: { v: 1 } },
|
||||
{ item: item2, meta: { v: 2 } },
|
||||
{ item: item3, meta: { v: 3 } },
|
||||
]);
|
||||
|
||||
console.log(localGetSpy.mock.calls);
|
||||
expect(localGetSpy).toBeCalledTimes(1);
|
||||
expect(localGetSpy).toBeCalledWith(['one$', 'three$']);
|
||||
expect(sessionGetSpy).toBeCalledTimes(1);
|
||||
expect(sessionGetSpy).toBeCalledWith(['two$']);
|
||||
|
||||
expect(localSetSpy).toBeCalledTimes(1);
|
||||
expect(localSetSpy).toBeCalledWith({
|
||||
one$: { v: 1 },
|
||||
three$: { v: 3 },
|
||||
});
|
||||
expect(sessionSetSpy).toBeCalledTimes(1);
|
||||
expect(sessionSetSpy).toBeCalledWith({
|
||||
two$: { v: 2 },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a nullable type when getItem is called without a fallback', async () => {
|
||||
@@ -1,16 +1,20 @@
|
||||
/// <reference types="chrome" />
|
||||
/**
|
||||
* Simplified storage APIs with support for versioned fields, snapshots, metadata, and item definitions.
|
||||
*
|
||||
* See [the guide](https://wxt.dev/guide/extension-apis/storage.html) for more information.
|
||||
*
|
||||
* @module wxt/storage
|
||||
* See [the guide](https://wxt.dev/storage.html) for more information.
|
||||
* @module @wxt-dev/storage
|
||||
*/
|
||||
import { Storage, browser } from 'wxt/browser';
|
||||
import { dequal } from 'dequal/lite';
|
||||
import { logger } from './sandbox/utils/logger';
|
||||
import { toArray } from './core/utils/arrays';
|
||||
import { Mutex } from 'async-mutex';
|
||||
|
||||
const browser: typeof chrome =
|
||||
// @ts-expect-error
|
||||
globalThis.browser?.runtime?.id == null
|
||||
? globalThis.chrome
|
||||
: // @ts-expect-error
|
||||
globalThis.browser;
|
||||
|
||||
export const storage = createStorage();
|
||||
|
||||
function createStorage(): WxtStorage {
|
||||
@@ -44,6 +48,14 @@ function createStorage(): WxtStorage {
|
||||
};
|
||||
};
|
||||
const getMetaKey = (key: string) => key + '$';
|
||||
const mergeMeta = (oldMeta: any, newMeta: any): any => {
|
||||
const newFields = { ...oldMeta };
|
||||
Object.entries(newMeta).forEach(([key, value]) => {
|
||||
if (value == null) delete newFields[key];
|
||||
else newFields[key] = value;
|
||||
});
|
||||
return newFields;
|
||||
};
|
||||
const getValueOrFallback = (value: any, fallback: any) =>
|
||||
value ?? fallback ?? null;
|
||||
const getMetaValue = (properties: any) =>
|
||||
@@ -78,15 +90,7 @@ function createStorage(): WxtStorage {
|
||||
) => {
|
||||
const metaKey = getMetaKey(driverKey);
|
||||
const existingFields = getMetaValue(await driver.getItem(metaKey));
|
||||
const newFields = { ...existingFields };
|
||||
Object.entries(properties).forEach(([key, value]) => {
|
||||
if (value == null) {
|
||||
delete newFields[key];
|
||||
} else {
|
||||
newFields[key] = value;
|
||||
}
|
||||
});
|
||||
await driver.setItem(metaKey, newFields);
|
||||
await driver.setItem(metaKey, mergeMeta(existingFields, properties));
|
||||
};
|
||||
const removeItem = async (
|
||||
driver: WxtStorageDriver,
|
||||
@@ -109,7 +113,7 @@ function createStorage(): WxtStorage {
|
||||
await driver.removeItem(metaKey);
|
||||
} else {
|
||||
const newFields = getMetaValue(await driver.getItem(metaKey));
|
||||
toArray(properties).forEach((field) => delete newFields[field]);
|
||||
[properties].flat().forEach((field) => delete newFields[field]);
|
||||
await driver.setItem(metaKey, newFields);
|
||||
}
|
||||
};
|
||||
@@ -129,98 +133,195 @@ function createStorage(): WxtStorage {
|
||||
getItems: async (keys) => {
|
||||
const areaToKeyMap = new Map<StorageArea, string[]>();
|
||||
const keyToOptsMap = new Map<string, GetItemOptions<any> | undefined>();
|
||||
const orderedKeys: StorageItemKey[] = [];
|
||||
|
||||
keys.forEach((key) => {
|
||||
let keyStr: StorageItemKey;
|
||||
let opts: GetItemOptions<any> | undefined;
|
||||
if (typeof key === 'string') {
|
||||
// key: string
|
||||
keyStr = key;
|
||||
} else if ('getValue' in key) {
|
||||
// key: WxtStorageItem
|
||||
keyStr = key.key;
|
||||
opts = { fallback: key.fallback };
|
||||
} else {
|
||||
// key: { key, options }
|
||||
keyStr = key.key;
|
||||
opts = key.options;
|
||||
}
|
||||
orderedKeys.push(keyStr);
|
||||
const { driverArea, driverKey } = resolveKey(keyStr);
|
||||
const keys = areaToKeyMap.get(driverArea) ?? [];
|
||||
areaToKeyMap.set(driverArea, keys.concat(driverKey));
|
||||
const areaKeys = areaToKeyMap.get(driverArea) ?? [];
|
||||
areaToKeyMap.set(driverArea, areaKeys.concat(driverKey));
|
||||
keyToOptsMap.set(keyStr, opts);
|
||||
});
|
||||
|
||||
const results = await Promise.all(
|
||||
const resultsMap = new Map<StorageItemKey, any>();
|
||||
await Promise.all(
|
||||
Array.from(areaToKeyMap.entries()).map(async ([driverArea, keys]) => {
|
||||
const driverResults = await drivers[driverArea].getItems(keys);
|
||||
return driverResults.map((driverResult) => {
|
||||
driverResults.forEach((driverResult) => {
|
||||
const key = `${driverArea}:${driverResult.key}` as StorageItemKey;
|
||||
const opts = keyToOptsMap.get(key);
|
||||
const value = getValueOrFallback(
|
||||
driverResult.value,
|
||||
opts?.fallback ?? opts?.defaultValue,
|
||||
);
|
||||
return { key, value };
|
||||
resultsMap.set(key, value);
|
||||
});
|
||||
}),
|
||||
);
|
||||
return results.flat();
|
||||
|
||||
return orderedKeys.map((key) => ({
|
||||
key,
|
||||
value: resultsMap.get(key),
|
||||
}));
|
||||
},
|
||||
getMeta: async (key) => {
|
||||
const { driver, driverKey } = resolveKey(key);
|
||||
return await getMeta(driver, driverKey);
|
||||
},
|
||||
getMetas: async (args) => {
|
||||
const keys = args.map((arg) => {
|
||||
const key = typeof arg === 'string' ? arg : arg.key;
|
||||
const { driverArea, driverKey } = resolveKey(key);
|
||||
return {
|
||||
key,
|
||||
driverArea,
|
||||
driverKey,
|
||||
driverMetaKey: getMetaKey(driverKey),
|
||||
};
|
||||
});
|
||||
const areaToDriverMetaKeysMap = keys.reduce<
|
||||
Partial<Record<StorageArea, (typeof keys)[number][]>>
|
||||
>((map, key) => {
|
||||
map[key.driverArea] ??= [];
|
||||
map[key.driverArea]!.push(key);
|
||||
return map;
|
||||
}, {});
|
||||
|
||||
const resultsMap: Record<string, any> = {};
|
||||
await Promise.all(
|
||||
Object.entries(areaToDriverMetaKeysMap).map(async ([area, keys]) => {
|
||||
const areaRes = await browser.storage[area as StorageArea].get(
|
||||
keys.map((key) => key.driverMetaKey),
|
||||
);
|
||||
keys.forEach((key) => {
|
||||
resultsMap[key.key] = areaRes[key.driverMetaKey] ?? {};
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
return keys.map((key) => ({
|
||||
key: key.key,
|
||||
meta: resultsMap[key.key],
|
||||
}));
|
||||
},
|
||||
setItem: async (key, value) => {
|
||||
const { driver, driverKey } = resolveKey(key);
|
||||
await setItem(driver, driverKey, value);
|
||||
},
|
||||
setItems: async (values) => {
|
||||
const areaToKeyValueMap = new Map<
|
||||
StorageArea,
|
||||
Array<{ key: string; value: any }>
|
||||
>();
|
||||
values.forEach(({ key, value }) => {
|
||||
const { driverArea, driverKey } = resolveKey(key);
|
||||
const values = areaToKeyValueMap.get(driverArea) ?? [];
|
||||
areaToKeyValueMap.set(
|
||||
driverArea,
|
||||
values.concat({ key: driverKey, value }),
|
||||
setItems: async (items) => {
|
||||
const areaToKeyValueMap: Partial<
|
||||
Record<StorageArea, Array<{ key: string; value: any }>>
|
||||
> = {};
|
||||
items.forEach((item) => {
|
||||
const { driverArea, driverKey } = resolveKey(
|
||||
'key' in item ? item.key : item.item.key,
|
||||
);
|
||||
areaToKeyValueMap[driverArea] ??= [];
|
||||
areaToKeyValueMap[driverArea].push({
|
||||
key: driverKey,
|
||||
value: item.value,
|
||||
});
|
||||
});
|
||||
await Promise.all(
|
||||
Array.from(areaToKeyValueMap.entries()).map(
|
||||
async ([driverArea, values]) => {
|
||||
const driver = getDriver(driverArea);
|
||||
await driver.setItems(values);
|
||||
},
|
||||
),
|
||||
Object.entries(areaToKeyValueMap).map(async ([driverArea, values]) => {
|
||||
const driver = getDriver(driverArea as StorageArea);
|
||||
await driver.setItems(values);
|
||||
}),
|
||||
);
|
||||
},
|
||||
setMeta: async (key, properties) => {
|
||||
const { driver, driverKey } = resolveKey(key);
|
||||
await setMeta(driver, driverKey, properties);
|
||||
},
|
||||
setMetas: async (items) => {
|
||||
const areaToMetaUpdatesMap: Partial<
|
||||
Record<StorageArea, { key: string; properties: any }[]>
|
||||
> = {};
|
||||
items.forEach((item) => {
|
||||
const { driverArea, driverKey } = resolveKey(
|
||||
'key' in item ? item.key : item.item.key,
|
||||
);
|
||||
areaToMetaUpdatesMap[driverArea] ??= [];
|
||||
areaToMetaUpdatesMap[driverArea].push({
|
||||
key: driverKey,
|
||||
properties: item.meta,
|
||||
});
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
Object.entries(areaToMetaUpdatesMap).map(
|
||||
async ([storageArea, updates]) => {
|
||||
const driver = getDriver(storageArea as StorageArea);
|
||||
const metaKeys = updates.map(({ key }) => getMetaKey(key));
|
||||
console.log(storageArea, metaKeys);
|
||||
const existingMetas = await driver.getItems(metaKeys);
|
||||
const existingMetaMap = Object.fromEntries(
|
||||
existingMetas.map(({ key, value }) => [key, getMetaValue(value)]),
|
||||
);
|
||||
|
||||
const metaUpdates = updates.map(({ key, properties }) => {
|
||||
const metaKey = getMetaKey(key);
|
||||
return {
|
||||
key: metaKey,
|
||||
value: mergeMeta(existingMetaMap[metaKey] ?? {}, properties),
|
||||
};
|
||||
});
|
||||
|
||||
await driver.setItems(metaUpdates);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
removeItem: async (key, opts) => {
|
||||
const { driver, driverKey } = resolveKey(key);
|
||||
await removeItem(driver, driverKey, opts);
|
||||
},
|
||||
removeItems: async (keys) => {
|
||||
const areaToKeysMap = new Map<StorageArea, string[]>();
|
||||
const areaToKeysMap: Partial<Record<StorageArea, string[]>> = {};
|
||||
|
||||
keys.forEach((key) => {
|
||||
let keyStr: StorageItemKey;
|
||||
let opts: RemoveItemOptions | undefined;
|
||||
if (typeof key === 'string') {
|
||||
// key: string
|
||||
keyStr = key;
|
||||
} else if ('getValue' in key) {
|
||||
// key: WxtStorageItem
|
||||
keyStr = key.key;
|
||||
} else if ('item' in key) {
|
||||
// key: { item, options }
|
||||
keyStr = key.item.key;
|
||||
opts = key.options;
|
||||
} else {
|
||||
// key: { key, options }
|
||||
keyStr = key.key;
|
||||
opts = key.options;
|
||||
}
|
||||
const { driverArea, driverKey } = resolveKey(keyStr);
|
||||
const areaKeys = areaToKeysMap.get(driverArea) ?? [];
|
||||
areaKeys.push(driverKey);
|
||||
areaToKeysMap[driverArea] ??= [];
|
||||
areaToKeysMap[driverArea].push(driverKey);
|
||||
if (opts?.removeMeta) {
|
||||
areaKeys.push(getMetaKey(driverKey));
|
||||
areaToKeysMap[driverArea].push(getMetaKey(driverKey));
|
||||
}
|
||||
areaToKeysMap.set(driverArea, areaKeys);
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
Array.from(areaToKeysMap.entries()).map(async ([driverArea, keys]) => {
|
||||
const driver = getDriver(driverArea);
|
||||
Object.entries(areaToKeysMap).map(async ([driverArea, keys]) => {
|
||||
const driver = getDriver(driverArea as StorageArea);
|
||||
await driver.removeItems(keys);
|
||||
}),
|
||||
);
|
||||
@@ -275,8 +376,8 @@ function createStorage(): WxtStorage {
|
||||
);
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`Running storage migration for ${key}: v${currentVersion} -> v${targetVersion}`,
|
||||
console.debug(
|
||||
`[@wxt-dev/storage] Running storage migration for ${key}: v${currentVersion} -> v${targetVersion}`,
|
||||
);
|
||||
const migrationsToRun = Array.from(
|
||||
{ length: targetVersion - currentVersion },
|
||||
@@ -284,16 +385,22 @@ function createStorage(): WxtStorage {
|
||||
);
|
||||
let migratedValue = value;
|
||||
for (const migrateToVersion of migrationsToRun) {
|
||||
migratedValue =
|
||||
(await migrations?.[migrateToVersion]?.(migratedValue)) ??
|
||||
migratedValue;
|
||||
try {
|
||||
migratedValue =
|
||||
(await migrations?.[migrateToVersion]?.(migratedValue)) ??
|
||||
migratedValue;
|
||||
} catch (err) {
|
||||
throw Error(`v${migrateToVersion} migration failed for "${key}"`, {
|
||||
cause: err,
|
||||
});
|
||||
}
|
||||
}
|
||||
await driver.setItems([
|
||||
{ key: driverKey, value: migratedValue },
|
||||
{ key: driverMetaKey, value: { ...meta, v: targetVersion } },
|
||||
]);
|
||||
logger.debug(
|
||||
`Storage migration completed for ${key} v${targetVersion}`,
|
||||
console.debug(
|
||||
`[@wxt-dev/storage] Storage migration completed for ${key} v${targetVersion}`,
|
||||
{ migratedValue },
|
||||
);
|
||||
};
|
||||
@@ -301,7 +408,10 @@ function createStorage(): WxtStorage {
|
||||
opts?.migrations == null
|
||||
? Promise.resolve()
|
||||
: migrate().catch((err) => {
|
||||
logger.error(`Migration failed for ${key}`, err);
|
||||
console.error(
|
||||
`[@wxt-dev/storage] Migration failed for ${key}`,
|
||||
err,
|
||||
);
|
||||
});
|
||||
|
||||
const initMutex = new Mutex();
|
||||
@@ -323,6 +433,7 @@ function createStorage(): WxtStorage {
|
||||
migrationsDone.then(getOrInitValue);
|
||||
|
||||
return {
|
||||
key,
|
||||
get defaultValue() {
|
||||
return getFallback();
|
||||
},
|
||||
@@ -390,9 +501,7 @@ function createDriver(storageArea: StorageArea): WxtStorageDriver {
|
||||
throw Error(`"browser.storage.${storageArea}" is undefined`);
|
||||
return area;
|
||||
};
|
||||
const watchListeners = new Set<
|
||||
(changes: Storage.StorageAreaOnChangedChangesType) => void
|
||||
>();
|
||||
const watchListeners = new Set<(changes: StorageAreaChanges) => void>();
|
||||
return {
|
||||
getItem: async (key) => {
|
||||
const res = await getStorageArea().get(key);
|
||||
@@ -432,7 +541,7 @@ function createDriver(storageArea: StorageArea): WxtStorageDriver {
|
||||
await getStorageArea().set(data);
|
||||
},
|
||||
watch(key, cb) {
|
||||
const listener = (changes: Storage.StorageAreaOnChangedChangesType) => {
|
||||
const listener = (changes: StorageAreaChanges) => {
|
||||
const change = changes[key];
|
||||
if (change == null) return;
|
||||
if (dequal(change.newValue, change.oldValue)) return;
|
||||
@@ -480,7 +589,9 @@ export interface WxtStorage {
|
||||
*/
|
||||
getItems(
|
||||
keys: Array<
|
||||
StorageItemKey | { key: StorageItemKey; options?: GetItemOptions<any> }
|
||||
| StorageItemKey
|
||||
| WxtStorageItem<any, any>
|
||||
| { key: StorageItemKey; options?: GetItemOptions<any> }
|
||||
>,
|
||||
): Promise<Array<{ key: StorageItemKey; value: any }>>;
|
||||
/**
|
||||
@@ -491,6 +602,15 @@ export interface WxtStorage {
|
||||
* await storage.getMeta("local:installDate");
|
||||
*/
|
||||
getMeta<T extends Record<string, unknown>>(key: StorageItemKey): Promise<T>;
|
||||
/**
|
||||
* Get the metadata of multiple storage items.
|
||||
*
|
||||
* @param items List of keys or items to get the metadata of.
|
||||
* @returns An array containing storage keys and their metadata.
|
||||
*/
|
||||
getMetas(
|
||||
keys: Array<StorageItemKey | WxtStorageItem<any, any>>,
|
||||
): Promise<Array<{ key: StorageItemKey; meta: any }>>;
|
||||
/**
|
||||
* Set a value in storage. Setting a value to `null` or `undefined` is equivalent to calling
|
||||
* `removeItem`.
|
||||
@@ -508,7 +628,12 @@ export interface WxtStorage {
|
||||
* { key: "session:someCounter, value: 5 },
|
||||
* ]);
|
||||
*/
|
||||
setItems(values: Array<{ key: StorageItemKey; value: any }>): Promise<void>;
|
||||
setItems(
|
||||
values: Array<
|
||||
| { key: StorageItemKey; value: any }
|
||||
| { item: WxtStorageItem<any, any>; value: any }
|
||||
>,
|
||||
): Promise<void>;
|
||||
/**
|
||||
* Sets metadata properties. If some properties are already set, but are not included in the
|
||||
* `properties` parameter, they will not be removed.
|
||||
@@ -520,6 +645,17 @@ export interface WxtStorage {
|
||||
key: StorageItemKey,
|
||||
properties: T | null,
|
||||
): Promise<void>;
|
||||
/**
|
||||
* Set the metadata of multiple storage items.
|
||||
*
|
||||
* @param items List of storage keys or items and metadata to set for each.
|
||||
*/
|
||||
setMetas(
|
||||
metas: Array<
|
||||
| { key: StorageItemKey; meta: Record<string, any> }
|
||||
| { item: WxtStorageItem<any, any>; meta: Record<string, any> }
|
||||
>,
|
||||
): Promise<void>;
|
||||
/**
|
||||
* Removes an item from storage.
|
||||
*
|
||||
@@ -532,7 +668,10 @@ export interface WxtStorage {
|
||||
*/
|
||||
removeItems(
|
||||
keys: Array<
|
||||
StorageItemKey | { key: StorageItemKey; options?: RemoveItemOptions }
|
||||
| StorageItemKey
|
||||
| WxtStorageItem<any, any>
|
||||
| { key: StorageItemKey; options?: RemoveItemOptions }
|
||||
| { item: WxtStorageItem<any, any>; options?: RemoveItemOptions }
|
||||
>,
|
||||
): Promise<void>;
|
||||
/**
|
||||
@@ -602,7 +741,11 @@ export interface WxtStorageItem<
|
||||
TMetadata extends Record<string, unknown>,
|
||||
> {
|
||||
/**
|
||||
* @deprecated Renamed to `fallback`, use it instead.
|
||||
* The storage key passed when creating the storage item.
|
||||
*/
|
||||
key: StorageItemKey;
|
||||
/**
|
||||
* @deprecated Renamed to fallback, use it instead.
|
||||
*/
|
||||
defaultValue: TValue;
|
||||
/**
|
||||
@@ -704,11 +847,15 @@ export interface WxtStorageItemOptions<T> {
|
||||
migrations?: Record<number, (oldValue: any) => any>;
|
||||
}
|
||||
|
||||
export type StorageAreaChanges = {
|
||||
[key: string]: chrome.storage.StorageChange;
|
||||
};
|
||||
|
||||
/**
|
||||
* Same as `Partial`, but includes `| null`. It makes all the properties of an object optional and
|
||||
* nullable.
|
||||
*/
|
||||
export type NullablePartial<T> = {
|
||||
type NullablePartial<T> = {
|
||||
[key in keyof T]+?: T[key] | undefined | null;
|
||||
};
|
||||
/**
|
||||
@@ -719,3 +866,13 @@ export type WatchCallback<T> = (newValue: T, oldValue: T) => void;
|
||||
* Call to remove a watch listener
|
||||
*/
|
||||
export type Unwatch = () => void;
|
||||
|
||||
export class MigrationError extends Error {
|
||||
constructor(
|
||||
public key: string,
|
||||
public version: number,
|
||||
options?: ErrorOptions,
|
||||
) {
|
||||
super(`v${version} migration failed for "${key}"`, options);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"verbatimModuleSyntax": true,
|
||||
"types": ["chrome"]
|
||||
},
|
||||
"exclude": ["node_modules/**", "dist/**"]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { defineProject } from 'vitest/config';
|
||||
|
||||
export default defineProject({
|
||||
test: {
|
||||
mockReset: true,
|
||||
restoreMocks: true,
|
||||
setupFiles: ['vitest.setup.ts'],
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
import { fakeBrowser } from '@webext-core/fake-browser';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
vi.stubGlobal('chrome', fakeBrowser);
|
||||
@@ -41,11 +41,11 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "^1.4.5",
|
||||
"oxlint": "^0.9.9",
|
||||
"publint": "^0.2.11",
|
||||
"typescript": "^5.6.2",
|
||||
"oxlint": "^0.11.1",
|
||||
"publint": "^0.2.12",
|
||||
"typescript": "^5.6.3",
|
||||
"unbuild": "^2.0.0",
|
||||
"unocss": "^0.63.3",
|
||||
"unocss": "^0.64.0",
|
||||
"wxt": "workspace:*"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -28,7 +28,7 @@ export default defineWxtModule<UnoCSSOptions>({
|
||||
}
|
||||
|
||||
wxt.hooks.hook('vite:devServer:extendConfig', (config) => {
|
||||
config.plugins?.push(UnoCSS());
|
||||
config.plugins?.push(UnoCSS(resolvedOptions.configOrPath));
|
||||
});
|
||||
|
||||
wxt.hooks.hook('vite:build:extendConfig', async (entries, config) => {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# `@wxt-dev/webextension-polyfill`
|
||||
|
||||
Configures `wxt/browser` to import `browser` from [`webextension-polyfill`](https://github.com/mozilla/webextension-polyfill) instead of using the regular `chrome`/`browser` globals WXT normally provides.
|
||||
|
||||
## Usage
|
||||
|
||||
```sh
|
||||
pnpm i @wxt-dev/webextension-polyfill webextension-polyfill
|
||||
```
|
||||
|
||||
Then add the module to your config:
|
||||
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
export default defineConfig({
|
||||
modules: ['@wxt-dev/webextension-polyfill'],
|
||||
});
|
||||
```
|
||||
@@ -0,0 +1,15 @@
|
||||
import { defineBuildConfig } from 'unbuild';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
export default defineBuildConfig({
|
||||
rootDir: 'modules',
|
||||
outDir: resolve(__dirname, 'dist'),
|
||||
entries: [
|
||||
{ input: 'webextension-polyfill/index.ts', name: 'index' },
|
||||
{ input: 'webextension-polyfill/browser.ts', name: 'browser' },
|
||||
],
|
||||
replace: {
|
||||
'process.env.NPM': 'true',
|
||||
},
|
||||
declaration: true,
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
export default defineContentScript({
|
||||
matches: ['*://*/*'],
|
||||
async main() {
|
||||
console.log(browser.runtime.id);
|
||||
},
|
||||
});
|
||||
@@ -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.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,3 @@
|
||||
const root = document.getElementById('app')!;
|
||||
|
||||
root.textContent = browser.runtime.id;
|
||||
@@ -0,0 +1 @@
|
||||
export { default as browser } from 'webextension-polyfill';
|
||||
@@ -0,0 +1,18 @@
|
||||
import 'wxt';
|
||||
import { addViteConfig, defineWxtModule } from 'wxt/modules';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
export default defineWxtModule({
|
||||
name: '@wxt-dev/webextension-polyfill',
|
||||
setup(wxt) {
|
||||
addViteConfig(wxt, () => ({
|
||||
resolve: {
|
||||
alias: {
|
||||
'wxt/browser': process.env.NPM
|
||||
? '@wxt-dev/webextension-polyfill/browser'
|
||||
: resolve(__dirname, 'browser.ts'),
|
||||
},
|
||||
},
|
||||
}));
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"name": "@wxt-dev/webextension-polyfill",
|
||||
"description": "Use webextension-polyfill with WXT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/wxt-dev/wxt.git",
|
||||
"directory": "packages/webextension-polyfill"
|
||||
},
|
||||
"homepage": "https://github.com/wxt-dev/wxt/blob/main/packages/webextension-polyfill/README.md",
|
||||
"keywords": [
|
||||
"wxt",
|
||||
"module",
|
||||
"webextension-polyfill"
|
||||
],
|
||||
"author": {
|
||||
"name": "Aaron Klinker",
|
||||
"email": "aaronklinker1+wxt@gmail.com"
|
||||
},
|
||||
"license": "MIT",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"main": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.mts",
|
||||
"default": "./dist/index.mjs"
|
||||
},
|
||||
"./browser": {
|
||||
"types": "./dist/browser.d.mts",
|
||||
"default": "./dist/browser.mjs"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "wxt",
|
||||
"check": "pnpm build && check",
|
||||
"build": "buildc -- unbuild",
|
||||
"prepare": "buildc --deps-only -- wxt prepare"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"webextension-polyfill": "*",
|
||||
"wxt": ">=0.20.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "^1.4.5",
|
||||
"@types/webextension-polyfill": "^0.12.1",
|
||||
"publint": "^0.2.12",
|
||||
"typescript": "^5.6.3",
|
||||
"unbuild": "^2.0.0",
|
||||
"webextension-polyfill": "^0.12.0",
|
||||
"wxt": "workspace:*"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": ["../../tsconfig.base.json", "./.wxt/tsconfig.json"],
|
||||
"exclude": ["node_modules/**", "dist/**"]
|
||||
}
|
||||
@@ -18,19 +18,19 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@wxt-dev/i18n": "workspace:*",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/chrome": "^0.0.269",
|
||||
"@types/react": "^18.3.11",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/chrome": "^0.0.280",
|
||||
"@types/react": "^19.0.1",
|
||||
"@types/react-dom": "^19.0.2",
|
||||
"@wxt-dev/auto-icons": "workspace:*",
|
||||
"@wxt-dev/unocss": "workspace:*",
|
||||
"sass": "^1.79.4",
|
||||
"typescript": "^5.6.2",
|
||||
"unocss": "^0.63.3",
|
||||
"vitest": "^2.1.2",
|
||||
"sass": "^1.80.7",
|
||||
"typescript": "^5.6.3",
|
||||
"unocss": "^0.64.0",
|
||||
"vitest": "^2.1.4",
|
||||
"vitest-plugin-random-seed": "^1.1.0",
|
||||
"wxt": "workspace:*"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { defineAppConfig } from 'wxt/sandbox';
|
||||
import { defineAppConfig } from '#imports';
|
||||
|
||||
declare module 'wxt/sandbox' {
|
||||
declare module 'wxt/utils/define-app-config' {
|
||||
export interface WxtAppConfig {
|
||||
example: string;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ describe('Background Entrypoint', () => {
|
||||
fakeBrowser.reset();
|
||||
});
|
||||
|
||||
it("should log the extenion's runtime ID", () => {
|
||||
it("should log the extension's runtime ID", () => {
|
||||
const id = 'some-id';
|
||||
fakeBrowser.runtime.id = id;
|
||||
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import 'uno.css';
|
||||
import './style.css';
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ['https://*.duckduckgo.com/*'],
|
||||
cssInjectionMode: 'manifest',
|
||||
|
||||
async main(ctx) {
|
||||
const dynamicUI = createIntegratedUi(ctx, {
|
||||
position: 'inline',
|
||||
append: 'after',
|
||||
anchor: 'form[role=search]',
|
||||
onMount: (container) => {
|
||||
const app = document.createElement('div');
|
||||
container.id = 'automount-anchor';
|
||||
app.classList.add('m-4', 'text-center', 'text-red-500');
|
||||
app.textContent = i18n.t('prompt_for_name');
|
||||
container.append(app);
|
||||
return { container, app };
|
||||
},
|
||||
onRemove() {
|
||||
console.log('dynamicUI removed');
|
||||
},
|
||||
});
|
||||
|
||||
const autoMountUi = createIntegratedUi(ctx, {
|
||||
position: 'inline',
|
||||
append: 'after',
|
||||
anchor: '#automount-anchor',
|
||||
onMount: (container) => {
|
||||
const app = document.createElement('div');
|
||||
app.id = 'automount-ui';
|
||||
app.classList.add('m-0', 'text-center', 'text-blue-500');
|
||||
app.textContent = `Hello, I'm automount UI.`;
|
||||
container.append(app);
|
||||
},
|
||||
onRemove() {
|
||||
console.log('autoMountUi removed');
|
||||
},
|
||||
});
|
||||
|
||||
autoMountUi.autoMount({
|
||||
onStop: () => {
|
||||
console.log('Auto mount stopped.');
|
||||
},
|
||||
});
|
||||
|
||||
const stopAutoMountButton = createIntegratedUi(ctx, {
|
||||
position: 'inline',
|
||||
append: 'last',
|
||||
anchor: 'form[role=search]',
|
||||
onMount: (container) => {
|
||||
const app = document.createElement('button');
|
||||
container.classList.add('flex', 'flex-justify-center');
|
||||
app.classList.add('mt-4', 'p-2');
|
||||
app.textContent = 'Stop auto-mount';
|
||||
app.onclick = (e) => {
|
||||
e.preventDefault();
|
||||
autoMountUi.remove();
|
||||
};
|
||||
container.append(app);
|
||||
return { container, app };
|
||||
},
|
||||
});
|
||||
|
||||
stopAutoMountButton.mount();
|
||||
|
||||
setInterval(() => {
|
||||
if (dynamicUI.mounted) {
|
||||
dynamicUI.remove();
|
||||
} else {
|
||||
dynamicUI.mount();
|
||||
}
|
||||
}, 2000);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
}
|
||||
@@ -14,12 +14,13 @@ export default defineBackground({
|
||||
|
||||
console.log(useAppConfig());
|
||||
|
||||
// @ts-expect-error: should only accept entrypoints or public assets
|
||||
browser.runtime.getURL('/');
|
||||
browser.runtime.getURL('/background.js');
|
||||
browser.runtime.getURL('/icons/128.png');
|
||||
browser.runtime.getURL('/example.html#hash');
|
||||
browser.runtime.getURL('/example.html?query=param');
|
||||
// @ts-expect-error: should only accept entrypoints or public assets
|
||||
browser.runtime.getURL('/unknown');
|
||||
// @ts-expect-error: should only allow hashes/query params on HTML files
|
||||
browser.runtime.getURL('/icon-128.png?query=param');
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import { presetUno } from 'unocss';
|
||||
|
||||
export default defineConfig({
|
||||
srcDir: 'src',
|
||||
extensionApi: 'chrome',
|
||||
manifest: {
|
||||
permissions: ['storage'],
|
||||
default_locale: 'en',
|
||||
@@ -25,7 +24,7 @@ export default defineConfig({
|
||||
},
|
||||
example: {
|
||||
a: 'a',
|
||||
// @ts-expect-error: c is not defined, this should error out
|
||||
// @ts-expect-error: c is not defined, this should be a type error, but it should show up in the module
|
||||
c: 'c',
|
||||
},
|
||||
unocss: {
|
||||
|
||||
@@ -1,5 +1,202 @@
|
||||
# Changelog
|
||||
|
||||
## v0.19.23
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.22...wxt-v0.19.23)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- **hooks:** Move `entrypoints:resolved` before debug logs and add `entrypoints:found` ([#1292](https://github.com/wxt-dev/wxt/pull/1292))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Allow runtime registered content scripts to not have `matches` ([#1306](https://github.com/wxt-dev/wxt/pull/1306))
|
||||
- Properly close readline instance on close ([#1278](https://github.com/wxt-dev/wxt/pull/1278))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Sync READMEs ([27298b7](https://github.com/wxt-dev/wxt/commit/27298b7))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Simplify keyboard shortcuts ([#1284](https://github.com/wxt-dev/wxt/pull/1284))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron ([@aklinker1](http://github.com/aklinker1))
|
||||
- Abhigyan Trips ([@abhigyantrips](http://github.com/abhigyantrips))
|
||||
|
||||
## v0.19.22
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.21...wxt-v0.19.22)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Exclude entire `import.meta.env` object from content script output ([#1267](https://github.com/wxt-dev/wxt/pull/1267))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron ([@aklinker1](http://github.com/aklinker1))
|
||||
|
||||
## v0.19.21
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.20...wxt-v0.19.21)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Exclude skipped entrypoints from manifest ([#1265](https://github.com/wxt-dev/wxt/pull/1265))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron ([@aklinker1](http://github.com/aklinker1))
|
||||
|
||||
## v0.19.20
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.19...wxt-v0.19.20)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- `autoMount` content script UIs ([#1210](https://github.com/wxt-dev/wxt/pull/1210))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Only show reopen browser shortcut when it can be done ([#1263](https://github.com/wxt-dev/wxt/pull/1263))
|
||||
- Import entrypoint improvements ([#1207](https://github.com/wxt-dev/wxt/pull/1207))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron ([@aklinker1](http://github.com/aklinker1))
|
||||
- 1natsu ([@1natsu172](http://github.com/1natsu172))
|
||||
|
||||
## v0.19.19
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.18...wxt-v0.19.19)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Keyboard shortcut to reopen the browser without restarting the dev command ([#1211](https://github.com/wxt-dev/wxt/pull/1211))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Prevent changing dev server port when reloading config ([#1241](https://github.com/wxt-dev/wxt/pull/1241))
|
||||
- Ensure content scripts are registered immediately in dev mode ([#1253](https://github.com/wxt-dev/wxt/pull/1253))
|
||||
- Exclude skipped entrypoints from Firefox sources zip ([#1238](https://github.com/wxt-dev/wxt/pull/1238))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Nishu ([@nishu-murmu](http://github.com/nishu-murmu))
|
||||
- Aaron ([@aklinker1](http://github.com/aklinker1))
|
||||
|
||||
## v0.19.18
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.17...wxt-v0.19.18)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Correct `dev.reloadCommand` restriction to consider only commands with suggested keys ([#1226](https://github.com/wxt-dev/wxt/pull/1226))
|
||||
|
||||
### 🌊 Types
|
||||
|
||||
- Add overloads to `ContentScriptContext#addEventListener` for different targets ([#1245](https://github.com/wxt-dev/wxt/pull/1245))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Refactor `findEntrypoints` to return all entrypoints with `skipped` set properly ([#1244](https://github.com/wxt-dev/wxt/pull/1244))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron ([@aklinker1](http://github.com/aklinker1))
|
||||
- Jaguar Zhou ([@aiktb](http://github.com/aiktb))
|
||||
|
||||
## v0.19.17
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.16...wxt-v0.19.17)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- New `server:created`, `server:started`, and `server:closed` hooks ([#1179](https://github.com/wxt-dev/wxt/pull/1179))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Re-initialize WXT modules correctly during development ([#1176](https://github.com/wxt-dev/wxt/pull/1176))
|
||||
- ESLint config being generated when ESLint is not installed. ([#1198](https://github.com/wxt-dev/wxt/pull/1198))
|
||||
- Update `vite` dependency range to support v6 ([#1215](https://github.com/wxt-dev/wxt/pull/1215))
|
||||
- Automatically convert MV3 `content_security_policy` to MV2 ([#1168](https://github.com/wxt-dev/wxt/pull/1168))
|
||||
- Correctly remove child elements with integrated UI remove ([#1219](https://github.com/wxt-dev/wxt/pull/1219))
|
||||
- Make content script `matches` optional ([#1220](https://github.com/wxt-dev/wxt/pull/1220))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Fix analyze typo in type ([#1187](https://github.com/wxt-dev/wxt/pull/1187))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron ([@aklinker1](http://github.com/aklinker1))
|
||||
- 1natsu ([@1natsu172](http://github.com/1natsu172))
|
||||
- Nishu ([@nishu-murmu](https://github.com/nishu-murmu))
|
||||
- Florian Metz ([@Timeraa](http://github.com/Timeraa))
|
||||
|
||||
## v0.19.16
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.15...wxt-v0.19.16)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- **hooks:** Add new `config:resolved` hook ([#1177](https://github.com/wxt-dev/wxt/pull/1177))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron ([@aklinker1](http://github.com/aklinker1))
|
||||
|
||||
## v0.19.15
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.14...wxt-v0.19.15)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Extract `wxt/storage` to its own package, `@wxt-dev/storage` ([#1129](https://github.com/wxt-dev/wxt/pull/1129))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Add "/" to `PublicPath` and `browser.runtime.getURL` ([#1171](https://github.com/wxt-dev/wxt/pull/1171))
|
||||
- Add extension ID to event used to invalidate `ContentScriptContext` ([#1175](https://github.com/wxt-dev/wxt/pull/1175))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron ([@aklinker1](http://github.com/aklinker1))
|
||||
- Nishu ([@nishu-murmu](http://github.com/nishu-murmu))
|
||||
|
||||
## v0.19.14
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.13...wxt-v0.19.14)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- **storage:** Support storage items in batch functions ([#990](https://github.com/wxt-dev/wxt/pull/990))
|
||||
- Automatically disable 'Show warning about Self-XSS when pasing code' in new chrome ([#1159](https://github.com/wxt-dev/wxt/pull/1159))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Throw when config file does not exist ([#1156](https://github.com/wxt-dev/wxt/pull/1156))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Cleanup typos and broken links ([bb5ea34](https://github.com/wxt-dev/wxt/commit/bb5ea34))
|
||||
- Fix typo in `popup` and `options` EntrypointOptions ([#1121](https://github.com/wxt-dev/wxt/pull/1121))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- **deps:** Upgrade all non-major dependencies ([#1164](https://github.com/wxt-dev/wxt/pull/1164))
|
||||
- **deps:** Bump dev and non-breaking major dependencies ([#1167](https://github.com/wxt-dev/wxt/pull/1167))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron ([@aklinker1](http://github.com/aklinker1))
|
||||
- Rxliuli ([@rxliuli](http://github.com/rxliuli))
|
||||
- Kongmoumou ([@kongmoumou](http://github.com/kongmoumou))
|
||||
- Florian Metz ([@Timeraa](http://github.com/Timeraa))
|
||||
- Bread Grocery <breadgrocery@gmail.com>
|
||||
|
||||
## v0.19.13
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.12...wxt-v0.19.13)
|
||||
|
||||
@@ -76,6 +76,12 @@ Or see the [installation guide](https://wxt.dev/guide/installation.html) to get
|
||||
- 📏 Bundle analysis
|
||||
- ⬇️ Download and bundle remote URL imports
|
||||
|
||||
## Sponsors
|
||||
|
||||
WXT is a [MIT-licensed](https://github.com/wxt-dev/wxt/blob/main/LICENSE) open source project with its ongoing development made possible entirely by the support of these awesome backers. If you'd like to join them, please consider [sponsoring WXT's development](https://github.com/sponsors/wxt-dev).
|
||||
|
||||
<a href="https://github.com/sponsors/wxt-dev"><img alt="WXT Sponsors" src="https://raw.githubusercontent.com/wxt-dev/static/refs/heads/main/sponsorkit/sponsors.svg"></a>
|
||||
|
||||
## Contributors
|
||||
|
||||
<!-- automd:contributors author="aklinker1" license="MIT" github="wxt-dev/wxt" -->
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { defineBuildConfig } from 'unbuild';
|
||||
import { version } from './package.json';
|
||||
import { version, exports } from './package.json';
|
||||
import { readFile, writeFile } from 'fs/promises';
|
||||
import {
|
||||
virtualEntrypointModuleNames,
|
||||
virtualModuleNames,
|
||||
} from './src/core/utils/virtual-modules';
|
||||
|
||||
const replace = {
|
||||
__vite_ssr_import_meta__: 'undefined',
|
||||
};
|
||||
|
||||
export default defineBuildConfig([
|
||||
// Non-virtual modules can be transpiled with mkdist
|
||||
{
|
||||
@@ -15,8 +19,12 @@ export default defineBuildConfig([
|
||||
input: 'src',
|
||||
pattern: ['**/*', '!**/__tests__', '!**/*.md', '!virtual', '!@types'],
|
||||
declaration: true,
|
||||
esbuild: {
|
||||
define: replace,
|
||||
},
|
||||
},
|
||||
],
|
||||
replace,
|
||||
hooks: {
|
||||
async 'build:done'() {
|
||||
// Replace any template variables in output files
|
||||
@@ -32,10 +40,7 @@ export default defineBuildConfig([
|
||||
...virtualEntrypointModuleNames.map((name) => `virtual:user-${name}`),
|
||||
'virtual:wxt-plugins',
|
||||
'virtual:app-config',
|
||||
'wxt/browser',
|
||||
'wxt/sandbox',
|
||||
'wxt/client',
|
||||
'wxt/testing',
|
||||
...Object.keys(exports).map((path) => 'wxt' + path.slice(1)), // ./utils/storage => wxt/utils/storage
|
||||
],
|
||||
})),
|
||||
]);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user