Compare commits

..

33 Commits

Author SHA1 Message Date
GitHub Actions ba3d8838e3 chore(release): @wxt-dev/module-svelte v2.0.3
vhs / vhs (push) Cancelled after 0s
2025-01-28 21:17:33 +00:00
Eli e9b6f5f97c fix: Upgrade vite-plugin-svelte to support Vite 6 (#1375)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2025-01-28 15:09:01 -06:00
Aaron 5b6c82d38f chore: Fix formatting 2025-01-28 14:43:38 -06:00
Aaron 5b41b70abe docs: Add SECURITY.md 2025-01-28 14:41:56 -06:00
Okinea Dev 93f624610f chore: add pull request template (#1372)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2025-01-28 08:03:26 -06:00
Okinea Dev 73bdd49471 chore: simplify FUNDING.yml (#1371) 2025-01-28 07:32:59 -06:00
GitHub Actions 635f3f0898 chore(release): wxt v0.19.26
vhs / vhs (push) Cancelled after 0s
2025-01-28 06:46:49 +00:00
Deniz Uğur a53ee6d590 fix(context): Deduplicate wxt:content-script-started (#1364)
Co-authored-by: DenizUgur <DenizUgur@users.noreply.github.com>
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2025-01-28 00:41:12 -06:00
GitHub Actions 665b9194cf chore(release): @wxt-dev/storage v1.1.0
vhs / vhs (push) Cancelled after 0s
2025-01-28 06:40:20 +00:00
Gabriel Roldán 4690418c94 Fix: Remove duplicated css rule from vue template (#1369) 2025-01-27 16:00:09 -06:00
Chengxi 20eea2a7d1 feat: Add storage.clear (#1368)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2025-01-27 07:22:19 -06:00
rxliuli ac7b6760f2 docs: Add missing bracket in example (#1367) 2025-01-25 01:14:48 -06:00
GitHub Actions a593cfbbb6 chore(release): wxt v0.19.25
vhs / vhs (push) Cancelled after 0s
2025-01-22 05:08:46 +00:00
Aaron a205f2a0a8 chore: Update lockfile 2025-01-21 23:03:09 -06:00
Aaron 8e2badc92b fix: Drop support for vite 6.0.9+ until websocket fix is finished 2025-01-21 23:02:22 -06:00
redwoodlid 7c18d9c0b8 docs: Add react language ID to README (#1347) 2025-01-15 10:25:40 -06:00
Aaron 55a1f39450 Update changelog 2025-01-15 10:23:36 -06:00
GitHub Actions d708eca78e chore(release): wxt v0.19.24
vhs / vhs (push) Cancelled after 0s
2025-01-12 22:48:50 +00:00
1natsu 5d7e2516b6 fix: autoMount not working if anchor is already present (#1350) 2025-01-12 16:43:07 -06:00
Eric fb6a29872b docs: Add "HTML to Markdown" to the homepage (#1345) 2025-01-10 11:28:27 -06:00
rxliuli 187206062e docs: Update content script UI examples to use Svelte v5 (#1342) 2025-01-07 23:51:46 -06:00
Aaron 2615e29e52 docs: Add section to FAQ about teleports/portals 2025-01-07 23:37:53 -06:00
Aaron 8ca176d37e docs: Update i18n.md 2025-01-06 17:40:57 -06:00
Aaron cb2bf43f30 docs: Update content-scripts.md 2025-01-06 17:36:43 -06:00
Aaron 1719d17cff docs: Fix typo 2025-01-06 17:28:31 -06:00
Aaron 32afb8d97c docs: Improve manifest docs, mention entrypoint-specific options 2025-01-06 17:28:09 -06:00
Aaron 72b8a29d76 docs: Improve entrypoint docs 2025-01-06 17:09:20 -06:00
Aaron af0a32d478 docs: Add missing tabs in project structure 2025-01-06 16:59:23 -06:00
Nikolay Ponomarev 060e85f90e docs: Add missing web_accessible_resources to injectScript example (#1337) 2025-01-06 16:23:41 -06:00
Aaron 654a54ac79 docs: Update link 2025-01-05 10:14:49 -06:00
ntnyq ef24358942 docs: Fix unocss readme (#1329)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2025-01-04 17:01:43 -06:00
Aaron e5e67e39bd docs: Add paragraph on browser binary auto-discovery 2025-01-01 17:23:04 -06:00
Ahmed Dinar 228304bef9 docs: Added "Blync" to the homepage (#1319) 2025-01-01 08:56:07 -06:00
165 changed files with 2009 additions and 2441 deletions
+2 -14
View File
@@ -1,15 +1,3 @@
# These are supported funding model platforms
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/displaying-a-sponsor-button-in-your-repository#about-funding-files
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']
github: wxt-dev
+13
View File
@@ -0,0 +1,13 @@
### Overview
<!-- Describe your changes and why you made them -->
### Manual Testing
<!-- Describe how to test your changes to make sure the PR works as intended -->
### Related Issue
<!-- If this PR is related to an issue, please link it here -->
This PR closes #<issue_number>
-1
View File
@@ -15,7 +15,6 @@ on:
- module-vue
- storage
- unocss
- webextension-polyfill
- wxt
jobs:
-1
View File
@@ -14,7 +14,6 @@ on:
- module-svelte
- module-vue
- storage
- webextension-polyfill
- wxt
jobs:
-10
View File
@@ -166,13 +166,3 @@ 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.
+15
View File
@@ -0,0 +1,15 @@
# Security Policy
While WXT is in prerelease, only the latest version will recieve security updates. The latest version is:
<img alt="npm version" src="https://img.shields.io/npm/v/wxt?labelColor=black&color=%234fa048">
<!--
## Reporting a Vulnerability
Use this section to tell people how to report a vulnerability.
Tell them where to go, how often they can expect to get an update on a
reported vulnerability, what to expect if the vulnerability is accepted or
declined, etc.
-->
-70
View File
@@ -1,70 +0,0 @@
<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>
-76
View File
@@ -1,76 +0,0 @@
<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>&bull;</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>
@@ -1,72 +0,0 @@
<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(', ') }}
&bull;
{{ 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>
@@ -62,6 +62,8 @@ const chromeExtensionIds = [
'hjjkgbibknbahijglkffklflidncplkn', // Show IP Live View of Website IPs for Developers
'ilbikcehnpkmldojkcmlldkoelofnbde', // Strong Password Generator
'ocllfkhcdopiafndigclebelbecaiocp', // ZenGram: Mindful Instagram, Your Way
'odffpjnpocjfcaclnenaaaddghkgijdb', // Blync: Preview Links, Selection Search, AI Assistant
'kofbbilhmnkcmibjbioafflgmpkbnmme', // HTML to Markdown - Convert webpages to markdown
];
const { data, err, isLoading } = useListExtensionDetails(chromeExtensionIds);
@@ -1,15 +0,0 @@
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()}`;
});
}
+3 -32
View File
@@ -14,19 +14,14 @@ 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 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 = origin;
const ogImage = `${origin}/social-preview.png`;
const ogUrl = 'https://wxt.dev';
const ogImage = 'https://wxt.dev/social-preview.png';
// https://vitepress.dev/reference/site-config
export default defineConfig({
@@ -38,30 +33,7 @@ export default defineConfig({
},
lastUpdated: true,
sitemap: {
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');
hostname: 'https://wxt.dev',
},
head: [
@@ -117,7 +89,6 @@ 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}`, '/'),
-3
View File
@@ -1,3 +0,0 @@
import { createContentLoader } from 'vitepress';
export default createContentLoader('blog/*.md');
+4 -7
View File
@@ -3,17 +3,14 @@ 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)
.component('EntrypointPatterns', EntrypointPatterns)
.component('UsingWxtSection', UsingWxtSection)
.component('ExampleSearch', ExampleSearch)
.component('blog', BlogLayout);
ctx.app.component('Icon', Icon);
ctx.app.component('EntrypointPatterns', EntrypointPatterns);
ctx.app.component('UsingWxtSection', UsingWxtSection);
ctx.app.component('ExampleSearch', ExampleSearch);
},
};
Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 132 KiB

-9
View File
@@ -1,9 +0,0 @@
---
layout: page
---
<script lang="ts" setup>
import BlogHome from './.vitepress/components/BlogHome.vue';
</script>
<BlogHome />
@@ -1,12 +0,0 @@
---
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_
@@ -1,72 +0,0 @@
---
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).
+1 -5
View File
@@ -29,7 +29,7 @@ img.src = imageUrl;
## `/public` Directory
Files inside `<rootDir>/public/` are copied into the output folder as-is, without being processed by WXT's bundler.
Files inside `<srcDir>/public/` are copied into the output folder as-is, without being processed by WXT's bundler.
Here's how you access them:
@@ -54,10 +54,6 @@ 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.
+13 -19
View File
@@ -11,7 +11,19 @@ export default defineConfig({
});
```
By default, WXT automatically sets up auto-imports for all of it's own APIs and some of your project directories:
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:
- `<srcDir>/components/*`
- `<srcDir>/composables/*`
@@ -20,8 +32,6 @@ By default, WXT automatically sets up auto-imports for all of it's own APIs and
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).
@@ -100,19 +110,3 @@ 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/WebExtConfig) for a full list of config.
> See the [API Reference](/api/reference/wxt/interfaces/ExtensionRunnerConfig) 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 { defineWebExtConfig } from 'wxt';
import { defineRunnerConfig } from 'wxt';
export default defineWebExtConfig({
export default defineRunnerConfig({
// ...
});
```
@@ -32,7 +32,7 @@ You can configure browser startup in 3 places:
To set or customize the browser opened during development:
```ts
export default defineWebExtConfig({
export default defineRunnerConfig({
binaries: {
chrome: '/path/to/chrome-beta', // Use Chrome Beta instead of regular Chrome
firefox: 'firefoxdeveloperedition', // Use Firefox Developer Edition instead of regular Firefox
@@ -41,6 +41,8 @@ export default defineWebExtConfig({
});
```
By default, WXT will try to automatically discover where Chrome/Firefox are installed. However, if you have chrome installed in a non-standard location, you need to set it manually as shown above.
### Persist Data
By default, to keep from modifying your browser's existing profiles, `web-ext` creates a brand new profile every time you run the `dev` script.
@@ -52,7 +54,7 @@ To persist data, set the `--user-data-dir` flag:
:::code-group
```ts [Mac/Linux]
export default defineWebExtConfig({
export default defineRunnerConfig({
chromiumArgs: ['--user-data-dir=./.wxt/chrome-data'],
});
```
@@ -60,7 +62,7 @@ export default defineWebExtConfig({
```ts [Windows]
import { resolve } from 'node:path';
export default defineWebExtConfig({
export default defineRunnerConfig({
// On Windows, the path must be absolute
chromiumProfile: resolve('.wxt/chrome-data'),
keepProfileChanges: true,
@@ -80,7 +82,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 defineWebExtConfig({
export default defineRunnerConfig({
disabled: true,
});
```
@@ -17,3 +17,63 @@ 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() {
// ...
},
});
```
+12 -20
View File
@@ -1,10 +1,17 @@
# Manifest
In WXT, there is no `manifest.json` file in your source code. Instead, WXT generates it during the build process based off files in your project.
In WXT, there is no `manifest.json` file in your source code. Instead, WXT generates the manifest from multiple sources:
## Manifest Config
- Global options [defined in your `wxt.config.ts` file](#global-options)
- Entrypoint-specific options [defined in your entrypoints](/guide/essentials/entrypoints#defining-manifest-options)
- [WXT Modules](/guide/essentials/wxt-modules) added to your project can modify your manifest
- [Hooks](/guide/essentials/config/hooks) defined in your project can modify your manifest
To manually add a property to the `manifest.json` output during builds, use the `manifest` config inside `wxt.config.ts`:
Your extension's `manifest.json` will be output to `.output/{target}/manifest.json` when running `wxt build`.
## Global Options
To add a property to your manifest, use the `manifest` config inside your `wxt.config.ts`:
```ts
export default defineConfig({
@@ -130,7 +137,7 @@ public/
└─ icon-128.png
```
Specifically, if an icon must match one of these regex to be discovered:
Specifically, an icon must match one of these regex to be discovered:
<<< @/../packages/wxt/src/core/utils/manifest.ts#snippet
@@ -216,22 +223,7 @@ By default, whenever an `action` is generated, WXT falls back to `browser_action
### Action With Popup
To generate a manifest where a UI appears after clicking the icon, just create a [Popup entrypoint](/guide/essentials/entrypoints#popup).
```ts
export default defineConfig({
hooks: {
build: {
manifestGenerated(manifest) {
// Update the manifest variable by reference
manifest.name = 'Overriden name';
},
},
},
});
```
If you want to use a `page_action` for MV2, add the following meta tag to the HTML document's head:
To generate a manifest where a UI appears after clicking the icon, just create a [Popup entrypoint](/guide/essentials/entrypoints#popup). If you want to use a `page_action` for MV2, add the following meta tag to the HTML document's head:
```html
<meta name="manifest.type" content="page_action" />
+4 -4
View File
@@ -5,10 +5,10 @@
Define runtime configuration in a single place, `<srcDir>/app.config.ts`:
```ts
import { defineAppConfig } from '#imports';
import { defineAppConfig } from 'wxt/sandbox';
// Define types for your config
declare module 'wxt/utils/define-app-config' {
declare module 'wxt/sandbox' {
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 '#imports';
import { useAppConfig } from 'wxt/sandbox';
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/utils/define-app-config' {
declare module 'wxt/sandbox' {
export interface WxtAppConfig {
apiKey?: string;
skipWelcome: boolean;
+38 -22
View File
@@ -4,12 +4,14 @@ outline: deep
# Content Scripts
> To create a content script, see [Entrypoint Types](/guide/essentials/entrypoints#content-scripts).
## Context
The first argument to a content script's `main` function is it's "context".
```ts
// entrypoints/content.ts
// entrypoints/example.content.ts
export default defineContentScript({
main(ctx) {},
});
@@ -62,7 +64,7 @@ In regular web extensions, CSS for content scripts is usually a separate CSS fil
In WXT, to add CSS to a content script, simply import the CSS file into your JS entrypoint, and WXT will automatically add the bundled CSS output to the `css` array.
```ts
// entrypoints/content/index.ts
// entrypoints/example.content/index.ts
import './style.css';
export default defineContentScript({
@@ -78,16 +80,16 @@ To create a standalone content script that only includes a CSS file:
// wxt.config.ts
export default defineConfig({
hooks: {
"build:manifestGenerated": (wxt, manifest) => {
'build:manifestGenerated': (wxt, manifest) => {
manifest.content_scripts ??= [];
manifest.content_scripts.push({
// Build extension once to see where your CSS get's written to
css: ["content-scripts/example.css"],
matches: ["*://*/*"]
)
}
}
})
css: ['content-scripts/example.css'],
matches: ['*://*/*'],
});
},
},
});
```
## UI
@@ -198,6 +200,7 @@ export default defineContentScript({
```ts [Svelte]
// entrypoints/example-ui.content/index.ts
import App from './App.svelte';
import { mount, unmount } from 'svelte';
export default defineContentScript({
matches: ['<all_urls>'],
@@ -208,14 +211,13 @@ export default defineContentScript({
anchor: 'body',
onMount: (container) => {
// Create the Svelte app inside the UI container
const app = new App({
mount(App, {
target: container,
});
return app;
},
onRemove: (app) => {
// Destroy the app when the UI is removed
app.$destroy();
unmount(app);
},
});
@@ -255,13 +257,13 @@ export default defineContentScript({
:::
See the [API Reference](/api/reference/wxt/utils/content-script-ui/integrated/functions/createIntegratedUi) for the complete list of options.
See the [API Reference](/api/reference/wxt/client/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/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.
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.
To use `createShadowRootUi`, follow these steps:
@@ -379,6 +381,7 @@ export default defineContentScript({
// 1. Import the style
import './style.css';
import App from './App.svelte';
import { mount, unmount } from 'svelte';
export default defineContentScript({
matches: ['<all_urls>'],
@@ -393,14 +396,13 @@ export default defineContentScript({
anchor: 'body',
onMount: (container) => {
// Create the Svelte app inside the UI container
const app = new App({
mount(App, {
target: container,
});
return app;
},
onRemove: (app) => {
onRemove: () => {
// Destroy the app when the UI is removed
app?.$destroy();
unmount(app);
},
});
@@ -444,7 +446,7 @@ export default defineContentScript({
:::
See the [API Reference](/api/reference/wxt/utils/content-script-ui/shadow-root/functions/createShadowRootUi) for the complete list of options.
See the [API Reference](/api/reference/wxt/client/functions/createShadowRootUi) for the complete list of options.
Full examples:
@@ -455,7 +457,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/utils/content-script-ui/iframe/functions/createIframeUi), which simplifies setting up the IFrame.
WXT provides a helper function, [`createIframeUi`](/api/reference/wxt/client/functions/createIframeUi), which simplifies setting up the IFrame.
1. Create an HTML page that will be loaded into your IFrame:
```html
@@ -510,7 +512,7 @@ WXT provides a helper function, [`createIframeUi`](/api/reference/wxt/utils/cont
});
```
See the [API Reference](/api/reference/wxt/utils/content-script-ui/iframe/functions/createIframeUi) for the complete list of options.
See the [API Reference](/api/reference/wxt/client/functions/createIframeUi) for the complete list of options.
## Isolated World vs Main World
@@ -566,6 +568,20 @@ export default defineContentScript({
});
```
```json
export default defineConfig({
manifest: {
// ...
web_accessible_resources: [
{
resources: ["example-main-world.js"],
matches: ["*://*/*"],
}
]
}
});
```
`injectScript` works by creating a `script` element on the page pointing to your script. This loads the script into the page's context so it runs in the main world.
`injectScript` returns a promise, that when resolved, means the script has been evaluated by the browser and you can start communicating with it.
@@ -607,7 +623,7 @@ export default defineContentScript({
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.
See the [API Reference](/api/reference/wxt/client/interfaces/ContentScriptUi.html#automount) for the complete list of options.
## Dealing with SPAs
+14 -3
View File
@@ -28,7 +28,7 @@ For web extensions, there are two types of entrypoints:
- **Listed**: Referenced in the `manifest.json`
- **Unlisted**: Not referenced in the `manifest.json`
Throughout the rest of WXT's documentation, listed files are referred to by name. For example:
Throughout the rest of WXT's documentation, listed entrypoints are referred to by name. For example:
- Popup
- Options
@@ -42,7 +42,7 @@ Some examples of "unlisted" entrypoints:
- JS files injected by content scripts into the page's main world
:::tip
Regardless of whether a entrypoint is listed or unlisted, it will still be bundled into your extension and be available at runtime.
Regardless of whether an entrypoint is listed or unlisted, it will still be bundled into your extension and be available at runtime.
:::
## Adding Entrypoints
@@ -72,7 +72,7 @@ Refer to the [Entrypoint Types](#entrypoint-types) section for the full list of
## Defining Manifest Options
Most listed entrypoints have options that need to be added to the `manifest.json`. With WXT however, instead of defining the options in a separate file, _you define these options inside the entrypoint file itself_.
Most listed entrypoints have options that need to be added to the `manifest.json`. However with WXT, instead of defining the options in a separate file, _you define these options inside the entrypoint file itself_.
For example, here's how to define `matches` for content scripts:
@@ -86,6 +86,17 @@ export default defineContentScript({
});
```
For HTML entrypoints, options are configured as `<meta>` tags. For example, to use a `page_action` for your MV2 popup:
```html
<!doctype html>
<html lang="en">
<head>
<meta name="manifest.type" content="page_action" />
</head>
</html>
```
> Refer to the [Entrypoint Types](#entrypoint-types) sections for a list of options configurable inside each entrypoint, and how to define them.
When building your extension, WXT will look at the options defined in your entrypoints, and generate the manifest accordingly.
+35 -12
View File
@@ -4,29 +4,52 @@
Different browsers provide different global variables for accessing the extension APIs (chrome provides `chrome`, firefox provides `browser`, etc).
WXT merges these two into a unified API accessed through the `browser` variable.
WXT simplifies this - always use `browser`:
```ts
import { browser } from 'wxt/browser';
browser.action.onClicked.addListener(() => {
// ...
});
```
:::tip
With auto-imports enabled, you don't even need to import this variable from `wxt/browser`!
:::
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`.
The `browser` variable WXT provides is a simple export of the `browser` or `chrome` globals provided by the browser at runtime:
## Webextension Polyfill
<<< @/../packages/wxt/src/browser.ts#snippet
> Since `v0.1.0`
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).
By default, WXT uses the [`webextension-polyfill` by Mozilla](https://www.npmjs.com/package/webextension-polyfill) to make the extension API consistent between browsers.
## Accessing Types
To access types, you should import the relevant namespace from `wxt/browser`:
All types can be accessed via WXT's `browser` object:
```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:
```ts
function handleMessage(message: any, sender: browser.runtime.Sender) {
@@ -36,7 +59,7 @@ function handleMessage(message: any, sender: browser.runtime.Sender) {
## Feature Detection
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`.
Depending on the manifest version and browser, 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.
+4
View File
@@ -81,6 +81,10 @@ However, it is recommended you stick with the vanilla API (or a package based on
- Translations are not bundled multiple times, keeping your extension small
- Zero configuration
However, there is one major downside to the vanilla API and any packages built on top of it:
- Language cannot be changed without changing your browser/system language
Here are some examples of how to setup a third party i18n library:
- [vue-i18n](https://github.com/wxt-dev/wxt-examples/tree/main/examples/vue-i18n)
+18 -18
View File
@@ -59,24 +59,24 @@ After enabling it, your project structure should look like this:
<!-- prettier-ignore -->
```html
📂 {rootDir}/
📁 .output/
📁 .wxt/
📁 modules/
📁 public/
📂 src/
📁 assets/
📁 components/
📁 composables/
📁 entrypoints/
📁 hooks/
📁 utils/
📄 app.config.ts
📄 .env
📄 .env.publish
📄 package.json
📄 tsconfig.json
📄 web-ext.config.ts
📄 wxt.config.ts
📁 .output/
📁 .wxt/
📂 src/
📁 assets/
📁 components/
📁 composables/
📁 entrypoints/
📁 hooks/
📁 modules/
📁 public/
📁 utils/
📄 app.config.ts
📄 .env
📄 .env.publish
📄 package.json
📄 tsconfig.json
📄 web-ext.config.ts
📄 wxt.config.ts
```
## Customizing Other Directories
+1 -1
View File
@@ -6,7 +6,7 @@ You can use the vanilla APIs (see docs above), use [WXT's built-in storage API](
## Alternatives
1. [`wxt/utils/storage`](/storage) (recommended): WXT ships with its own wrapper around the vanilla storage APIs that simplifies common use cases
1. [`wxt/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.
+1 -29
View File
@@ -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/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!
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!
```ts
import { describe, it, expect } from 'vitest';
@@ -71,34 +71,6 @@ 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.
+2 -2
View File
@@ -112,12 +112,12 @@ export default defineWxtModule<AnalyticModuleOptions>({
```ts
import { defineWxtModule } from 'wxt/modules';
import 'wxt/utils/define-app-config';
import 'wxt/sandbox';
export interface MyModuleRuntimeOptions {
// Add your runtime options here...
}
declare module 'wxt/utils/define-app-config' {
declare module 'wxt/sandbox' {
export interface WxtAppConfig {
myModule: MyModuleOptions;
}
+126 -36
View File
@@ -28,47 +28,137 @@ 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.
This is usually caused by one of two things (or both) when using `createShadowRootUi`:
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:
1. Styles are added outside the `ShadowRoot`
- 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/)
:::details
Some component libraries manually add CSS to the page by adding a `<style>` or `<link>` element. They place this element in the document's `<head>` by default. This causes your styles to be placed outside the `ShadowRoot` and it's isolation blocks the styles from being applied to your UI.
> If your library isn't listed above, try searching it's docs/issues for "shadow root", "shadow dom", or "css container".
When a library does this, **you need to tell the library where to put its styles**. Here's the documentation for a few popular component libraries:
`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:
- 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/)
```tsx
import { StyleProvider } from '@ant-design/cssinjs'; // [!code ++]
import ReactDOM from 'react-dom/client';
import App from './App.tsx';
> If your library isn't listed above, try searching it's docs/issues for "shadow root", "shadow dom", or "css container". Not all libraries support shadow DOMs, you may have to open an issue to request this feature.
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();
},
});
```
Here's an example of configuring Antd's styles:
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:
```tsx
import { StyleProvider } from '@ant-design/cssinjs';
import ReactDOM from 'react-dom/client';
import App from './App.tsx';
```ts
import 'vuetify/styles'; // <-- This line imports the CSS, just like importing a .css file
import { createVuetify } from 'vuetify';
```
const ui = await create`ShadowRoot`Ui(ctx, {
// ...
onMount: (container, shadow) => {
const cssContainer = shadow.querySelector('head')!;
const root = ReactDOM.createRoot(container);
root.render(
<StyleProvider container={cssContainer}>
<App />
</StyleProvider>,
);
return root;
},
});
```
:::
2. UI elements are added outside the `ShadowRoot`
::::::details
This is mostly caused by `Teleport` or `Portal` components that render an element somewhere else in the DOM, usually in the document's `<body>`. This is usually done for dialogs or popover components. This renders the element is outside the `ShadowRoot`, so styles are not applied to it.
To fix this, **you need to both provide a target to your app AND pass the target to the `Teleport`/`Portal`**.
First, store the reference to the `ShadowRoot`'s `<body>` element (not the document's `<body>`):
:::code-group
```ts [Vue]
import { createApp } from 'vue';
import App from './App.vue';
const ui = await create`ShadowRoot`Ui(ctx, {
// ...
onMount: (container, shadow) => {
const teleportTarget = shadow.querySelector('body')!;
const app = createApp(App)
.provide('TeleportTarget', teleportTarget)
.mount(container);
return app;
},
});
ui.mount();
```
```tsx [React]
// hooks/PortalTargetContext.ts
import { createContext } from 'react';
export const PortalTargetContext = createContext<HTMLElement>();
// entrypoints/example.content.ts
import ReactDOM from 'react-dom/client';
import App from './App.tsx';
import PortalTargetContext from '~/hooks/PortalTargetContext';
const ui = await create`ShadowRoot`Ui(ctx, {
// ...
onMount: (container, shadow) => {
const portalTarget = shadow.querySelector('body')!;
const root = ReactDOM.createRoot(container);
root.render(
<PortalTargetContext.Provider value={portalTarget}>
<App />
</PortalTargetContext.Provider>,
);
return root;
},
});
ui.mount();
```
:::
Then use the reference when teleporting/portaling part of your UI to a different place in the DOM:
:::code-group
```vue [Vue]
<script lang="ts" setup>
import { Teleport } from 'vue';
const teleportTarget = inject('TeleportTarget');
</script>
<template>
<div>
<Teleport :to="teleportTarget">
<dialog>My dialog</dialog>
</Teleport>
</div>
</template>
```
```tsx [React]
import { useContext } from 'react';
import { createPortal } from 'react-dom';
import PortalTargetContext from '~/hooks/PortalTargetContext';
const MyComponent = () => {
const portalTarget = useContext(PortalTargetContext);
return <div>{createPortal(<dialog>My dialog</dialog>, portalTarget)}</div>;
};
```
:::
::::::
Both issues have the same cause: the library puts something outside the `ShadowRoot`, and the `ShadowRoot`'s isolation prevents CSS from being applied to your UI.
Both issues have the same fix: tell the library to put elements inside the `ShadowRoot`, not outside it. See the details above for more information and example fixes for each problem.
-225
View File
@@ -1,7 +1,3 @@
---
outline: deep
---
# Upgrading WXT
## Overview
@@ -16,227 +12,6 @@ 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 &rarr; 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 ++]
});
```
If you've loaded the extension into your browser manually for development, uninstall and re-install it from the new dev output directory.
### Internal Auto-import Options Changed
Only relevant if you refer to WXT's built-in `preset`s in the `imports` config in a module or hooks.
Instead of using `package` to auto-detect APIs to auto-import, WXT now uses `from` and `imports` to manually list APIs that are imported.
```ts
presets: [
{ package: "wxt/browser" }, // [!code --]
{ from: "wxt/browser": imports: ["browser"] }, // [!code --]
// ...
]
```
See [PR #1315 `packages/wxt/src/core/resolve-config.ts` changes](https://github.com/wxt-dev/wxt/pull/1315/files#diff-ff0465c3a486d3ba187204149a25fc8f632c44d65da356dc04c0f2b268a71506) for exact changes made.
### `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 &rarr; v0.19.0
### `vite-node` Entrypoint Loader
+6 -6
View File
@@ -15,7 +15,7 @@ A simplified wrapper around the extension storage APIs.
This module is built-in to WXT, so you don't need to install anything.
```ts
import { storage } from '#imports';
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!
@@ -37,7 +37,7 @@ import { storage } from '@wxt-dev/storage';
## Storage Permission
To use the `@wxt-dev/storage` API, the `"storage"` permission must be added to the manifest:
To use the `wxt/storage` API, the `"storage"` permission must be added to the manifest:
```ts
// wxt.config.ts
@@ -75,7 +75,7 @@ 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/utils/storage/interfaces/WxtStorage).
For a full list of methods available, see the [API reference](/api/reference/@wxt-dev/storage/interfaces/WxtStorage).
## Watchers
@@ -98,7 +98,7 @@ unwatch();
## Metadata
`@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.
`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.
[Other than versioning](#versioning), you are responsible for managing a field's metadata:
@@ -158,7 +158,7 @@ const unwatch = showChangelogOnUpdate.watch((newValue) => {
});
```
For a full list of properties and methods available, see the [API reference](/api/reference/wxt/utils/storage/interfaces/WxtStorageItem).
For a full list of properties and methods available, see the [API reference](/api/reference/@wxt-dev/storage/interfaces/WxtStorageItem).
### Versioning
@@ -354,4 +354,4 @@ await storage.setItems([
]);
```
Refer to the [API Reference](/api/reference/wxt/utils/storage/interfaces/WxtStorage) for types and examples of how to use all the bulk APIs.
Refer to the [API Reference](/api/reference/@wxt-dev/storage/interfaces/WxtStorage) for types and examples of how to use all the bulk APIs.
-1
View File
@@ -24,7 +24,6 @@
"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",
+2 -1
View File
@@ -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 `<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!
> `@wxt-dev/i18n` supports the standard messages format, so if you already have localization files at `<srcDir>/public/_locale/<lang>/messages.json`, you don't need to convert them to YAML or refactor them - just move them to `<srcDir>/locales/<lang>.json` and they'll just work out of the box!
4. To get a translation, use the auto-imported `i18n` object or import it manually:
@@ -354,6 +354,7 @@ You'll need to configure it the extension so it knows where your localization fi
# You can check available language ids here: https://code.visualstudio.com/docs/languages/identifiers
languageIds:
- typescript
- typescriptreact
# Look for t("...")
usageMatchRegex:
@@ -1,8 +1,5 @@
import {
defineContentScript,
ContentScriptContext,
createShadowRootUi,
} from '#imports';
import { defineContentScript } from 'wxt/sandbox';
import { ContentScriptContext, createShadowRootUi } from 'wxt/client';
import React from 'react';
import ReactDOM from 'react-dom/client';
+1 -2
View File
@@ -16,8 +16,7 @@ export default defineWxtModule<ReactModuleOptions>({
// Enable auto-imports for JSX files
wxt.hook('config:resolved', (wxt) => {
// In older versions of WXT, `wxt.config.imports` could be false
if (!wxt.config.imports) return;
if (wxt.config.imports === false) return;
wxt.config.imports.dirsScanOptions ??= {};
wxt.config.imports.dirsScanOptions.filePatterns = [
@@ -1,8 +1,5 @@
import {
defineContentScript,
ContentScriptContext,
createShadowRootUi,
} from '#imports';
import { defineContentScript } from 'wxt/sandbox';
import { ContentScriptContext, createShadowRootUi } from 'wxt/client';
import { render } from 'solid-js/web';
export default defineContentScript({
+1 -2
View File
@@ -19,8 +19,7 @@ export default defineWxtModule<SolidModuleOptions>({
// Enable auto-imports for JSX files
wxt.hook('config:resolved', (wxt) => {
// In older versions of WXT, `wxt.config.imports` could be false
if (!wxt.config.imports) return;
if (wxt.config.imports === false) return;
wxt.config.imports.dirsScanOptions ??= {};
wxt.config.imports.dirsScanOptions.filePatterns = [
+12
View File
@@ -1,5 +1,17 @@
# Changelog
## v2.0.3
[compare changes](https://github.com/wxt-dev/wxt/compare/module-svelte-v2.0.2...module-svelte-v2.0.3)
### 🩹 Fixes
- Upgrade `vite-plugin-svelte` to support Vite 6 ([#1375](https://github.com/wxt-dev/wxt/pull/1375))
### ❤️ Contributors
- Eli ([@lishaduck](http://github.com/lishaduck))
## v2.0.2
[compare changes](https://github.com/wxt-dev/wxt/compare/module-svelte-v2.0.1...module-svelte-v2.0.2)
+2 -2
View File
@@ -17,7 +17,7 @@
"email": "aaronklinker1+wxt@gmail.com"
},
"license": "MIT",
"version": "2.0.2",
"version": "2.0.3",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
@@ -46,7 +46,7 @@
"svelte": ">=5"
},
"dependencies": {
"@sveltejs/vite-plugin-svelte": "^4.0.0"
"@sveltejs/vite-plugin-svelte": "^4.0.0 || ^5.0.0"
},
"devDependencies": {
"@aklinker1/check": "^1.4.5",
+17
View File
@@ -1,5 +1,22 @@
# Changelog
## v1.1.0
[compare changes](https://github.com/wxt-dev/wxt/compare/storage-v1.0.1...storage-v1.1.0)
### 🚀 Enhancements
- Add `storage.clear` ([#1368](https://github.com/wxt-dev/wxt/pull/1368))
### 📖 Documentation
- Update link ([654a54a](https://github.com/wxt-dev/wxt/commit/654a54a))
### ❤️ Contributors
- Chengxi ([@chengxilo](http://github.com/chengxilo))
- Aaron ([@aklinker1](http://github.com/aklinker1))
## v1.0.1
[compare changes](https://github.com/wxt-dev/wxt/compare/storage-v1.0.0...storage-v1.0.1)
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@wxt-dev/storage",
"description": "Web extension storage API provided by WXT, supports all browsers.",
"version": "1.0.1",
"version": "1.1.0",
"type": "module",
"repository": {
"type": "git",
@@ -489,6 +489,18 @@ describe('Storage Utils', () => {
});
});
describe('clear', () => {
it('should remove all items', async () => {
await fakeBrowser.storage[storageArea].set({
one: 1,
two: 2,
});
await storage.clear(storageArea);
expect(await fakeBrowser.storage[storageArea].get()).toEqual({});
});
});
describe('removeMeta', () => {
it('should remove all metadata', async () => {
await fakeBrowser.storage[storageArea].set({ count$: { v: 4 } });
+15 -1
View File
@@ -326,6 +326,10 @@ function createStorage(): WxtStorage {
}),
);
},
clear: async (base) => {
const driver = getDriver(base);
await driver.clear();
},
removeMeta: async (key, properties) => {
const { driver, driverKey } = resolveKey(key);
await removeMeta(driver, driverKey, properties);
@@ -534,6 +538,9 @@ function createDriver(storageArea: StorageArea): WxtStorageDriver {
removeItems: async (keys) => {
await getStorageArea().remove(keys);
},
clear: async () => {
await getStorageArea().clear();
},
snapshot: async () => {
return await getStorageArea().get();
},
@@ -674,6 +681,12 @@ export interface WxtStorage {
| { item: WxtStorageItem<any, any>; options?: RemoveItemOptions }
>,
): Promise<void>;
/**
* Removes all items from the provided storage area.
*/
clear(base: StorageArea): Promise<void>;
/**
* Remove the entire metadata for a key, or specific properties by name.
*
@@ -712,7 +725,7 @@ export interface WxtStorage {
/**
* Define a storage item with a default value, type, or versioning.
*
* Read full docs: https://wxt.dev/guide/extension-apis/storage.html#defining-storage-items
* Read full docs: https://wxt.dev/storage.html#defining-storage-items
*/
defineItem<TValue, TMetadata extends Record<string, unknown> = {}>(
key: StorageItemKey,
@@ -730,6 +743,7 @@ interface WxtStorageDriver {
setItems(values: Array<{ key: string; value: any }>): Promise<void>;
removeItem(key: string): Promise<void>;
removeItems(keys: string[]): Promise<void>;
clear(): Promise<void>;
snapshot(): Promise<Record<string, unknown>>;
restoreSnapshot(data: Record<string, unknown>): Promise<void>;
watch<T>(key: string, cb: WatchCallback<T | null>): Unwatch;
+3 -3
View File
@@ -38,10 +38,10 @@ The module can be configured via the `unocss` config:
export default defineConfig({
modules: ['@wxt-dev/unocss'],
unocss: {
// Will only apply unocss for popup/main.ts
entrypoints: ['popup/main.ts'],
// Exclude unocss from running for the background
excludeEntrypoints: ['background'],
},
});
```
Options have JSDocs available in your editor, or you can read them in the source code: [`UnoCSSOptions`](https://github.com/wxt-dev/wxt/blob/main/packages/auto-icons/src/index.ts).
Options have JSDocs available in your editor, or you can read them in the source code: [`UnoCSSOptions`](https://github.com/wxt-dev/wxt/blob/main/packages/unocss/src/index.ts).
-18
View File
@@ -1,18 +0,0 @@
# `@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'],
});
```
@@ -1,15 +0,0 @@
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,
});
@@ -1,6 +0,0 @@
export default defineContentScript({
matches: ['*://*/*'],
async main() {
console.log(browser.runtime.id);
},
});
@@ -1,12 +0,0 @@
<!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>
@@ -1,3 +0,0 @@
const root = document.getElementById('app')!;
root.textContent = browser.runtime.id;
@@ -1 +0,0 @@
export { default as browser } from 'webextension-polyfill';
@@ -1,18 +0,0 @@
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'),
},
},
}));
},
});
@@ -1,56 +0,0 @@
{
"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:*"
}
}
@@ -1,4 +0,0 @@
{
"extends": ["../../tsconfig.base.json", "./.wxt/tsconfig.json"],
"exclude": ["node_modules/**", "dist/**"]
}
+2 -2
View File
@@ -1,6 +1,6 @@
import { defineAppConfig } from '#imports';
import { defineAppConfig } from 'wxt/sandbox';
declare module 'wxt/utils/define-app-config' {
declare module 'wxt/sandbox' {
export interface WxtAppConfig {
example: string;
}
@@ -11,7 +11,7 @@ describe('Background Entrypoint', () => {
fakeBrowser.reset();
});
it("should log the extension's runtime ID", () => {
it("should log the extenion's runtime ID", () => {
const id = 'some-id';
fakeBrowser.runtime.id = id;
+1
View File
@@ -3,6 +3,7 @@ import { presetUno } from 'unocss';
export default defineConfig({
srcDir: 'src',
extensionApi: 'chrome',
manifest: {
permissions: ['storage'],
default_locale: 'en',
+45 -1
View File
@@ -1,5 +1,49 @@
# Changelog
## v0.19.26
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.25...wxt-v0.19.26)
### 🩹 Fixes
- **context:** Deduplicate `wxt:content-script-started` ([#1364](https://github.com/wxt-dev/wxt/pull/1364))
### ❤️ Contributors
- Deniz Uğur ([@DenizUgur](http://github.com/DenizUgur))
## v0.19.25
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.24...wxt-v0.19.25)
### 🩹 Fixes
- Drop support for vite 6.0.9+ until websocket fix is finished ([8e2badc](https://github.com/wxt-dev/wxt/commit/8e2badc))
### ❤️ Contributors
- Aaron ([@aklinker1](http://github.com/aklinker1))
## v0.19.24
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.23...wxt-v0.19.24)
### 🩹 Fixes
- **init:** Remove "experimental" from bun package manager ([b150a52](https://github.com/wxt-dev/wxt/commit/b150a52))
- **web-ext:** Correctly pass `browserConsole` option to open devtools in Firefox ([#1308](https://github.com/wxt-dev/wxt/pull/1308))
- `autoMount` not working if anchor is already present ([#1350](https://github.com/wxt-dev/wxt/pull/1350))
### 🏡 Chore
- Fix typo in CLI docs ([028c601](https://github.com/wxt-dev/wxt/commit/028c601))
### ❤️ Contributors
- 1natsu ([@1natsu172](http://github.com/1natsu172))
- Paulo Pinto ([@psrpinto](https://github.com/psrpinto))
- Aaron ([@aklinker1](http://github.com/aklinker1))
## v0.19.23
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.22...wxt-v0.19.23)
@@ -2775,4 +2819,4 @@ Initial release of WXT. Full support for production builds and initial toolkit f
### 🤖 CI
- Create validation workflow ([#12](https://github.com/wxt-dev/wxt/pull/12))
- Create release workflow ([#13](https://github.com/wxt-dev/wxt/pull/13))
- Create release workflow ([#13](https://github.com/wxt-dev/wxt/pull/13))
+5 -10
View File
@@ -1,15 +1,11 @@
import { defineBuildConfig } from 'unbuild';
import { version, exports } from './package.json';
import { version } 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
{
@@ -19,12 +15,8 @@ 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
@@ -40,7 +32,10 @@ export default defineBuildConfig([
...virtualEntrypointModuleNames.map((name) => `virtual:user-${name}`),
'virtual:wxt-plugins',
'virtual:app-config',
...Object.keys(exports).map((path) => 'wxt' + path.slice(1)), // ./utils/storage => wxt/utils/storage
'wxt/browser',
'wxt/sandbox',
'wxt/client',
'wxt/testing',
],
})),
]);
@@ -5,43 +5,17 @@ exports[`Auto Imports > eslintrc > "enabled: 8" should output a JSON config file
----------------------------------------
{
"globals": {
"AutoMount": true,
"AutoMountOptions": true,
"ContentScriptAnchoredOptions": true,
"ContentScriptAppendMode": true,
"ContentScriptContext": true,
"ContentScriptInlinePositioningOptions": true,
"ContentScriptModalPositioningOptions": true,
"ContentScriptOverlayAlignment": true,
"ContentScriptOverlayPositioningOptions": true,
"ContentScriptPositioningOptions": true,
"ContentScriptUi": true,
"ContentScriptUiOptions": true,
"IframeContentScriptUi": true,
"IframeContentScriptUiOptions": true,
"InjectScriptOptions": true,
"IntegratedContentScriptUi": true,
"IntegratedContentScriptUiOptions": true,
"InvalidMatchPattern": true,
"MatchPattern": true,
"MigrationError": true,
"ScriptPublicPath": true,
"ShadowRootContentScriptUi": true,
"ShadowRootContentScriptUiOptions": true,
"StopAutoMount": true,
"StorageArea": true,
"StorageAreaChanges": true,
"StorageItemKey": true,
"WxtAppConfig": true,
"WxtStorage": true,
"WxtStorageItem": true,
"WxtWindowEventMap": true,
"browser": true,
"createIframeUi": true,
"createIntegratedUi": true,
"createShadowRootUi": true,
"defineAppConfig": true,
"defineBackground": true,
"defineConfig": true,
"defineContentScript": true,
"defineUnlistedScript": true,
"defineWxtPlugin": true,
@@ -58,43 +32,17 @@ exports[`Auto Imports > eslintrc > "enabled: 9" should output a flat config file
".wxt/eslint-auto-imports.mjs
----------------------------------------
const globals = {
"AutoMount": true,
"AutoMountOptions": true,
"ContentScriptAnchoredOptions": true,
"ContentScriptAppendMode": true,
"ContentScriptContext": true,
"ContentScriptInlinePositioningOptions": true,
"ContentScriptModalPositioningOptions": true,
"ContentScriptOverlayAlignment": true,
"ContentScriptOverlayPositioningOptions": true,
"ContentScriptPositioningOptions": true,
"ContentScriptUi": true,
"ContentScriptUiOptions": true,
"IframeContentScriptUi": true,
"IframeContentScriptUiOptions": true,
"InjectScriptOptions": true,
"IntegratedContentScriptUi": true,
"IntegratedContentScriptUiOptions": true,
"InvalidMatchPattern": true,
"MatchPattern": true,
"MigrationError": true,
"ScriptPublicPath": true,
"ShadowRootContentScriptUi": true,
"ShadowRootContentScriptUiOptions": true,
"StopAutoMount": true,
"StorageArea": true,
"StorageAreaChanges": true,
"StorageItemKey": true,
"WxtAppConfig": true,
"WxtStorage": true,
"WxtStorageItem": true,
"WxtWindowEventMap": true,
"browser": true,
"createIframeUi": true,
"createIntegratedUi": true,
"createShadowRootUi": true,
"defineAppConfig": true,
"defineBackground": true,
"defineConfig": true,
"defineContentScript": true,
"defineUnlistedScript": true,
"defineWxtPlugin": true,
@@ -119,43 +67,17 @@ exports[`Auto Imports > eslintrc > "enabled: true" should output a JSON config f
----------------------------------------
{
"globals": {
"AutoMount": true,
"AutoMountOptions": true,
"ContentScriptAnchoredOptions": true,
"ContentScriptAppendMode": true,
"ContentScriptContext": true,
"ContentScriptInlinePositioningOptions": true,
"ContentScriptModalPositioningOptions": true,
"ContentScriptOverlayAlignment": true,
"ContentScriptOverlayPositioningOptions": true,
"ContentScriptPositioningOptions": true,
"ContentScriptUi": true,
"ContentScriptUiOptions": true,
"IframeContentScriptUi": true,
"IframeContentScriptUiOptions": true,
"InjectScriptOptions": true,
"IntegratedContentScriptUi": true,
"IntegratedContentScriptUiOptions": true,
"InvalidMatchPattern": true,
"MatchPattern": true,
"MigrationError": true,
"ScriptPublicPath": true,
"ShadowRootContentScriptUi": true,
"ShadowRootContentScriptUiOptions": true,
"StopAutoMount": true,
"StorageArea": true,
"StorageAreaChanges": true,
"StorageItemKey": true,
"WxtAppConfig": true,
"WxtStorage": true,
"WxtStorageItem": true,
"WxtWindowEventMap": true,
"browser": true,
"createIframeUi": true,
"createIntegratedUi": true,
"createShadowRootUi": true,
"defineAppConfig": true,
"defineBackground": true,
"defineConfig": true,
"defineContentScript": true,
"defineUnlistedScript": true,
"defineWxtPlugin": true,
@@ -173,43 +95,17 @@ exports[`Auto Imports > eslintrc > should allow customizing the output 1`] = `
----------------------------------------
{
"globals": {
"AutoMount": "readonly",
"AutoMountOptions": "readonly",
"ContentScriptAnchoredOptions": "readonly",
"ContentScriptAppendMode": "readonly",
"ContentScriptContext": "readonly",
"ContentScriptInlinePositioningOptions": "readonly",
"ContentScriptModalPositioningOptions": "readonly",
"ContentScriptOverlayAlignment": "readonly",
"ContentScriptOverlayPositioningOptions": "readonly",
"ContentScriptPositioningOptions": "readonly",
"ContentScriptUi": "readonly",
"ContentScriptUiOptions": "readonly",
"IframeContentScriptUi": "readonly",
"IframeContentScriptUiOptions": "readonly",
"InjectScriptOptions": "readonly",
"IntegratedContentScriptUi": "readonly",
"IntegratedContentScriptUiOptions": "readonly",
"InvalidMatchPattern": "readonly",
"MatchPattern": "readonly",
"MigrationError": "readonly",
"ScriptPublicPath": "readonly",
"ShadowRootContentScriptUi": "readonly",
"ShadowRootContentScriptUiOptions": "readonly",
"StopAutoMount": "readonly",
"StorageArea": "readonly",
"StorageAreaChanges": "readonly",
"StorageItemKey": "readonly",
"WxtAppConfig": "readonly",
"WxtStorage": "readonly",
"WxtStorageItem": "readonly",
"WxtWindowEventMap": "readonly",
"browser": "readonly",
"createIframeUi": "readonly",
"createIntegratedUi": "readonly",
"createShadowRootUi": "readonly",
"defineAppConfig": "readonly",
"defineBackground": "readonly",
"defineConfig": "readonly",
"defineContentScript": "readonly",
"defineUnlistedScript": "readonly",
"defineWxtPlugin": "readonly",
+17 -132
View File
@@ -17,49 +17,24 @@ describe('Auto Imports', () => {
// Generated by wxt
export {}
declare global {
const ContentScriptContext: typeof import('wxt/utils/content-script-context')['ContentScriptContext']
const InvalidMatchPattern: typeof import('wxt/utils/match-patterns')['InvalidMatchPattern']
const MatchPattern: typeof import('wxt/utils/match-patterns')['MatchPattern']
const ContentScriptContext: typeof import('wxt/client')['ContentScriptContext']
const InvalidMatchPattern: typeof import('wxt/sandbox')['InvalidMatchPattern']
const MatchPattern: typeof import('wxt/sandbox')['MatchPattern']
const MigrationError: typeof import('wxt/storage')['MigrationError']
const browser: typeof import('wxt/browser')['browser']
const createIframeUi: typeof import('wxt/utils/content-script-ui/iframe')['createIframeUi']
const createIntegratedUi: typeof import('wxt/utils/content-script-ui/integrated')['createIntegratedUi']
const createShadowRootUi: typeof import('wxt/utils/content-script-ui/shadow-root')['createShadowRootUi']
const defineAppConfig: typeof import('wxt/utils/define-app-config')['defineAppConfig']
const defineBackground: typeof import('wxt/utils/define-background')['defineBackground']
const defineContentScript: typeof import('wxt/utils/define-content-script')['defineContentScript']
const defineUnlistedScript: typeof import('wxt/utils/define-unlisted-script')['defineUnlistedScript']
const defineWxtPlugin: typeof import('wxt/utils/define-wxt-plugin')['defineWxtPlugin']
const createIframeUi: typeof import('wxt/client')['createIframeUi']
const createIntegratedUi: typeof import('wxt/client')['createIntegratedUi']
const createShadowRootUi: typeof import('wxt/client')['createShadowRootUi']
const defineAppConfig: typeof import('wxt/sandbox')['defineAppConfig']
const defineBackground: typeof import('wxt/sandbox')['defineBackground']
const defineConfig: typeof import('wxt')['defineConfig']
const defineContentScript: typeof import('wxt/sandbox')['defineContentScript']
const defineUnlistedScript: typeof import('wxt/sandbox')['defineUnlistedScript']
const defineWxtPlugin: typeof import('wxt/sandbox')['defineWxtPlugin']
const fakeBrowser: typeof import('wxt/testing')['fakeBrowser']
const injectScript: typeof import('wxt/utils/inject-script')['injectScript']
const storage: typeof import('wxt/utils/storage')['storage']
const useAppConfig: typeof import('wxt/utils/app-config')['useAppConfig']
}
// for type re-export
declare global {
// @ts-ignore
export type { StorageArea, WxtStorage, WxtStorageItem, StorageItemKey, StorageAreaChanges, MigrationError } from 'wxt/utils/storage'
import('wxt/utils/storage')
// @ts-ignore
export type { WxtWindowEventMap } from 'wxt/utils/content-script-context'
import('wxt/utils/content-script-context')
// @ts-ignore
export type { IframeContentScriptUi, IframeContentScriptUiOptions } from 'wxt/utils/content-script-ui/iframe'
import('wxt/utils/content-script-ui/iframe')
// @ts-ignore
export type { IntegratedContentScriptUi, IntegratedContentScriptUiOptions } from 'wxt/utils/content-script-ui/integrated'
import('wxt/utils/content-script-ui/integrated')
// @ts-ignore
export type { ShadowRootContentScriptUi, ShadowRootContentScriptUiOptions } from 'wxt/utils/content-script-ui/shadow-root'
import('wxt/utils/content-script-ui/shadow-root')
// @ts-ignore
export type { ContentScriptUi, ContentScriptUiOptions, ContentScriptOverlayAlignment, ContentScriptAppendMode, ContentScriptInlinePositioningOptions, ContentScriptOverlayPositioningOptions, ContentScriptModalPositioningOptions, ContentScriptPositioningOptions, ContentScriptAnchoredOptions, AutoMountOptions, StopAutoMount, AutoMount } from 'wxt/utils/content-script-ui/types'
import('wxt/utils/content-script-ui/types')
// @ts-ignore
export type { WxtAppConfig } from 'wxt/utils/define-app-config'
import('wxt/utils/define-app-config')
// @ts-ignore
export type { ScriptPublicPath, InjectScriptOptions } from 'wxt/utils/inject-script'
import('wxt/utils/inject-script')
const injectScript: typeof import('wxt/client')['injectScript']
const storage: typeof import('wxt/storage')['storage']
const useAppConfig: typeof import('wxt/client')['useAppConfig']
}
"
`);
@@ -80,54 +55,10 @@ describe('Auto Imports', () => {
/// <reference types="./types/paths.d.ts" />
/// <reference types="./types/i18n.d.ts" />
/// <reference types="./types/globals.d.ts" />
/// <reference types="@types/chrome" />
/// <reference types="./types/imports-module.d.ts" />
/// <reference types="./types/imports.d.ts" />
"
`);
});
it('should generate the #imports module', async () => {
const project = new TestProject();
project.addFile('entrypoints/popup.html', `<html></html>`);
// Project auto-imports should also be present
project.addFile(
'utils/time.ts',
`export function startOfDay(date: Date): Date {
throw Error("TODO")
}`,
);
await project.prepare();
expect(await project.serializeFile('.wxt/types/imports-module.d.ts'))
.toMatchInlineSnapshot(`
".wxt/types/imports-module.d.ts
----------------------------------------
// Generated by wxt
// Types for the #import virtual module
declare module '#imports' {
export { browser } from 'wxt/browser';
export { storage, StorageArea, WxtStorage, WxtStorageItem, StorageItemKey, StorageAreaChanges, MigrationError } from 'wxt/utils/storage';
export { useAppConfig } from 'wxt/utils/app-config';
export { ContentScriptContext, WxtWindowEventMap } from 'wxt/utils/content-script-context';
export { createIframeUi, IframeContentScriptUi, IframeContentScriptUiOptions } from 'wxt/utils/content-script-ui/iframe';
export { createIntegratedUi, IntegratedContentScriptUi, IntegratedContentScriptUiOptions } from 'wxt/utils/content-script-ui/integrated';
export { createShadowRootUi, ShadowRootContentScriptUi, ShadowRootContentScriptUiOptions } from 'wxt/utils/content-script-ui/shadow-root';
export { ContentScriptUi, ContentScriptUiOptions, ContentScriptOverlayAlignment, ContentScriptAppendMode, ContentScriptInlinePositioningOptions, ContentScriptOverlayPositioningOptions, ContentScriptModalPositioningOptions, ContentScriptPositioningOptions, ContentScriptAnchoredOptions, AutoMountOptions, StopAutoMount, AutoMount } from 'wxt/utils/content-script-ui/types';
export { defineAppConfig, WxtAppConfig } from 'wxt/utils/define-app-config';
export { defineBackground } from 'wxt/utils/define-background';
export { defineContentScript } from 'wxt/utils/define-content-script';
export { defineUnlistedScript } from 'wxt/utils/define-unlisted-script';
export { defineWxtPlugin } from 'wxt/utils/define-wxt-plugin';
export { injectScript, ScriptPublicPath, InjectScriptOptions } from 'wxt/utils/inject-script';
export { InvalidMatchPattern, MatchPattern } from 'wxt/utils/match-patterns';
export { fakeBrowser } from 'wxt/testing';
export { startOfDay } from '../utils/time';
}
"
`);
});
});
describe('imports: false', () => {
@@ -143,7 +74,7 @@ describe('Auto Imports', () => {
expect(await project.fileExists('.wxt/types/imports.d.ts')).toBe(false);
});
it('should only include imports-module.d.ts in the the project', async () => {
it('should not include imports.d.ts in the type references', async () => {
const project = new TestProject();
project.setConfigFileConfig({
imports: false,
@@ -163,56 +94,10 @@ describe('Auto Imports', () => {
/// <reference types="./types/paths.d.ts" />
/// <reference types="./types/i18n.d.ts" />
/// <reference types="./types/globals.d.ts" />
/// <reference types="@types/chrome" />
/// <reference types="./types/imports-module.d.ts" />
"
`,
);
});
it('should only generate the #imports module', async () => {
const project = new TestProject();
project.setConfigFileConfig({
imports: false,
});
project.addFile('entrypoints/popup.html', `<html></html>`);
// Project auto-imports should also be present
project.addFile(
'utils/time.ts',
`export function startOfDay(date: Date): Date {
throw Error("TODO")
}`,
);
await project.prepare();
expect(await project.serializeFile('.wxt/types/imports-module.d.ts'))
.toMatchInlineSnapshot(`
".wxt/types/imports-module.d.ts
----------------------------------------
// Generated by wxt
// Types for the #import virtual module
declare module '#imports' {
export { browser } from 'wxt/browser';
export { storage, StorageArea, WxtStorage, WxtStorageItem, StorageItemKey, StorageAreaChanges, MigrationError } from 'wxt/utils/storage';
export { useAppConfig } from 'wxt/utils/app-config';
export { ContentScriptContext, WxtWindowEventMap } from 'wxt/utils/content-script-context';
export { createIframeUi, IframeContentScriptUi, IframeContentScriptUiOptions } from 'wxt/utils/content-script-ui/iframe';
export { createIntegratedUi, IntegratedContentScriptUi, IntegratedContentScriptUiOptions } from 'wxt/utils/content-script-ui/integrated';
export { createShadowRootUi, ShadowRootContentScriptUi, ShadowRootContentScriptUiOptions } from 'wxt/utils/content-script-ui/shadow-root';
export { ContentScriptUi, ContentScriptUiOptions, ContentScriptOverlayAlignment, ContentScriptAppendMode, ContentScriptInlinePositioningOptions, ContentScriptOverlayPositioningOptions, ContentScriptModalPositioningOptions, ContentScriptPositioningOptions, ContentScriptAnchoredOptions, AutoMountOptions, StopAutoMount, AutoMount } from 'wxt/utils/content-script-ui/types';
export { defineAppConfig, WxtAppConfig } from 'wxt/utils/define-app-config';
export { defineBackground } from 'wxt/utils/define-background';
export { defineContentScript } from 'wxt/utils/define-content-script';
export { defineUnlistedScript } from 'wxt/utils/define-unlisted-script';
export { defineWxtPlugin } from 'wxt/utils/define-wxt-plugin';
export { injectScript, ScriptPublicPath, InjectScriptOptions } from 'wxt/utils/inject-script';
export { InvalidMatchPattern, MatchPattern } from 'wxt/utils/match-patterns';
export { fakeBrowser } from 'wxt/testing';
}
"
`);
});
});
describe('eslintrc', () => {
+1 -1
View File
@@ -180,7 +180,7 @@ describe('Hooks', () => {
const server = await project.startServer({
hooks,
webExt: {
runner: {
disabled: true,
},
});
+25 -22
View File
@@ -1,33 +1,36 @@
import { describe, it, expect } from 'vitest';
import { TestProject } from '../utils';
describe('Manifest Content', () => {
it.each([
{ browser: undefined, outDir: 'chrome-mv3', expected: undefined },
{ browser: 'chrome', outDir: 'chrome-mv3', expected: undefined },
{ browser: 'firefox', outDir: 'firefox-mv2', expected: true },
{ browser: 'safari', outDir: 'safari-mv2', expected: false },
])(
'should respect the per-browser entrypoint option with %j',
async ({ browser, expected, outDir }) => {
const project = new TestProject();
describe.each(['vite-node', 'jiti'] as const)(
'Manifest Content (Vite runtime? %s)',
(entrypointImporter) => {
it.each([
{ browser: undefined, outDir: 'chrome-mv3', expected: undefined },
{ browser: 'chrome', outDir: 'chrome-mv3', expected: undefined },
{ browser: 'firefox', outDir: 'firefox-mv2', expected: true },
{ browser: 'safari', outDir: 'safari-mv2', expected: false },
])(
'should respect the per-browser entrypoint option with %j',
async ({ browser, expected, outDir }) => {
const project = new TestProject();
project.addFile(
'entrypoints/background.ts',
`export default defineBackground({
project.addFile(
'entrypoints/background.ts',
`export default defineBackground({
persistent: {
firefox: true,
safari: false,
},
main: () => {},
})`,
);
await project.build({ browser });
);
await project.build({ browser, experimental: { entrypointImporter } });
const safariManifest = await project.getOutputManifest(
`.output/${outDir}/manifest.json`,
);
expect(safariManifest.background.persistent).toBe(expected);
},
);
});
const safariManifest = await project.getOutputManifest(
`.output/${outDir}/manifest.json`,
);
expect(safariManifest.background.persistent).toBe(expected);
},
);
},
);
+16 -4
View File
@@ -191,7 +191,10 @@ describe('Module Helpers', () => {
);
const expectedText = addPluginModule(project);
await project.build();
await project.build({
// reduce build output when comparing test failures
extensionApi: 'chrome',
});
await expect(project.serializeOutput()).resolves.toContain(expectedText);
});
@@ -208,7 +211,10 @@ describe('Module Helpers', () => {
);
const expectedText = addPluginModule(project);
await project.build();
await project.build({
// reduce build output when comparing test failures
extensionApi: 'chrome',
});
await expect(project.serializeOutput()).resolves.toContain(expectedText);
});
@@ -226,7 +232,10 @@ describe('Module Helpers', () => {
);
const expectedText = addPluginModule(project);
await project.build();
await project.build({
// reduce build output when comparing test failures
extensionApi: 'chrome',
});
await expect(project.serializeOutput()).resolves.toContain(expectedText);
});
@@ -239,7 +248,10 @@ describe('Module Helpers', () => {
);
const expectedText = addPluginModule(project);
await project.build();
await project.build({
// reduce build output when comparing test failures
extensionApi: 'chrome',
});
await expect(project.serializeOutput()).resolves.toContain(expectedText);
});
@@ -262,6 +262,9 @@ describe('Output Directory Structure', () => {
project.addFile('entrypoints/popup/main.ts', `logHello('popup')`);
await project.build({
// Simplify the build output for comparison
extensionApi: 'chrome',
vite: () => ({
build: {
// Make output for snapshot readible
@@ -344,6 +347,9 @@ describe('Output Directory Structure', () => {
project.addFile('entrypoints/popup/main.ts', `logHello('popup')`);
await project.build({
// Simplify the build output for comparison
extensionApi: 'chrome',
vite: () => ({
build: {
// Make output for snapshot readible
@@ -239,8 +239,6 @@ describe('TypeScript Project', () => {
/// <reference types="./types/paths.d.ts" />
/// <reference types="./types/i18n.d.ts" />
/// <reference types="./types/globals.d.ts" />
/// <reference types="@types/chrome" />
/// <reference types="./types/imports-module.d.ts" />
/// <reference types="./types/imports.d.ts" />
"
`);
@@ -1,5 +1,6 @@
import { describe, it, expect } from 'vitest';
import { TestProject } from '../utils';
import { InlineConfig } from '../../src/types';
describe('User Config', () => {
// Root directory is tested with all tests.
@@ -87,6 +88,24 @@ describe('User Config', () => {
`);
});
it('should exclude the polyfill when extensionApi="chrome"', async () => {
const buildBackground = async (config?: InlineConfig) => {
const background = `export default defineBackground(() => console.log(browser.runtime.id));`;
const projectWithPolyfill = new TestProject();
projectWithPolyfill.addFile('entrypoints/background.ts', background);
await projectWithPolyfill.build(config);
return await projectWithPolyfill.serializeFile(
'.output/chrome-mv3/background.js',
);
};
const withPolyfill = await buildBackground();
const withoutPolyfill = await buildBackground({
extensionApi: 'chrome',
});
expect(withoutPolyfill).not.toBe(withPolyfill);
});
it('should respect changing config files', async () => {
const project = new TestProject();
project.addFile(
-1
View File
@@ -63,7 +63,6 @@ describe('Zipping', () => {
"name": "test",
"description": "Example description",
"version": "1.0.0",
"type": "module",
"dependencies": {
"wxt": "../../../../..",
"flatten": "1.0.3"
-1
View File
@@ -39,7 +39,6 @@ export class TestProject {
name: 'E2E Extension',
description: 'Example description',
version: '0.0.0',
type: 'module',
dependencies: {
wxt: '../../..',
},
+72 -109
View File
@@ -1,9 +1,74 @@
{
"name": "wxt",
"type": "module",
"version": "0.20.0-beta1",
"version": "0.19.26",
"description": "Next gen framework for developing web extensions",
"repository": {
"type": "git",
"url": "git+https://github.com/wxt-dev/wxt.git"
},
"homepage": "https://wxt.dev",
"keywords": [
"vite",
"chrome",
"web",
"extension",
"browser",
"bundler",
"framework"
],
"author": {
"name": "Aaron Klinker",
"email": "aaronklinker1+wxt@gmail.com"
},
"license": "MIT",
"files": [
"bin",
"dist"
],
"bin": {
"wxt": "./bin/wxt.mjs",
"wxt-publish-extension": "./bin/wxt-publish-extension.cjs"
},
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.mjs"
},
"./client": {
"types": "./dist/client/index.d.ts",
"default": "./dist/client/index.mjs"
},
"./sandbox": {
"types": "./dist/sandbox/index.d.ts",
"default": "./dist/sandbox/index.mjs"
},
"./browser": {
"types": "./dist/browser/index.d.ts",
"default": "./dist/browser/index.mjs"
},
"./browser/chrome": {
"types": "./dist/browser/chrome.d.ts",
"import": "./dist/browser/chrome.mjs"
},
"./testing": {
"types": "./dist/testing/index.d.ts",
"default": "./dist/testing/index.mjs"
},
"./storage": {
"types": "./dist/storage.d.ts",
"default": "./dist/storage.mjs"
},
"./vite-builder-env": {
"types": "./dist/vite-builder-env.d.ts"
},
"./modules": {
"types": "./dist/modules.d.ts",
"default": "./dist/modules.mjs"
}
},
"scripts": {
"wxt": "tsx src/cli/index.ts",
"build": "buildc -- unbuild",
@@ -19,12 +84,13 @@
"@1natsu/wait-element": "^4.1.2",
"@aklinker1/rollup-plugin-visualizer": "5.12.0",
"@types/chrome": "^0.0.280",
"@types/webextension-polyfill": "^0.12.1",
"@webext-core/fake-browser": "^1.3.1",
"@webext-core/isolated-element": "^1.1.2",
"@webext-core/match-patterns": "^1.0.3",
"@wxt-dev/storage": "workspace:^1.0.0",
"async-mutex": "^0.5.0",
"c12": "^2.0.1",
"c12": "^1.11.2",
"cac": "^6.7.14",
"chokidar": "^3.6.0",
"ci-info": "^4.1.0",
@@ -39,6 +105,7 @@
"giget": "^1.2.3",
"hookable": "^5.5.3",
"is-wsl": "^3.1.0",
"jiti": "^1.21.6",
"json5": "^2.2.3",
"jszip": "^3.10.1",
"linkedom": "^0.18.5",
@@ -56,9 +123,10 @@
"publish-browser-extension": "^2.2.2",
"scule": "^1.3.0",
"unimport": "^3.13.1",
"vite": "^5.0.0 || ^6.0.0",
"vite": "^5.0.0 || <=6.0.8",
"vite-node": "^2.1.4",
"web-ext-run": "^0.2.1"
"web-ext-run": "^0.2.1",
"webextension-polyfill": "^0.12.0"
},
"devDependencies": {
"@aklinker1/check": "^1.4.5",
@@ -83,110 +151,5 @@
"@types/chrome": {
"optional": true
}
},
"repository": {
"type": "git",
"url": "git+https://github.com/wxt-dev/wxt.git"
},
"homepage": "https://wxt.dev",
"keywords": [
"vite",
"chrome",
"web",
"extension",
"browser",
"bundler",
"framework"
],
"author": {
"name": "Aaron Klinker",
"email": "aaronklinker1+wxt@gmail.com"
},
"files": [
"bin",
"dist"
],
"bin": {
"wxt": "./bin/wxt.mjs",
"wxt-publish-extension": "./bin/wxt-publish-extension.cjs"
},
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.mjs"
},
"./utils/app-config": {
"types": "./dist/utils/app-config.d.ts",
"default": "./dist/utils/app-config.mjs"
},
"./utils/inject-script": {
"types": "./dist/utils/inject-script.d.ts",
"default": "./dist/utils/inject-script.mjs"
},
"./utils/content-script-context": {
"types": "./dist/utils/content-script-context.d.ts",
"default": "./dist/utils/content-script-context.mjs"
},
"./utils/content-script-ui/types": {
"types": "./dist/utils/content-script-ui/types.d.ts",
"default": "./dist/utils/content-script-ui/types.mjs"
},
"./utils/content-script-ui/integrated": {
"types": "./dist/utils/content-script-ui/integrated.d.ts",
"default": "./dist/utils/content-script-ui/integrated.mjs"
},
"./utils/content-script-ui/shadow-root": {
"types": "./dist/utils/content-script-ui/shadow-root.d.ts",
"default": "./dist/utils/content-script-ui/shadow-root.mjs"
},
"./utils/content-script-ui/iframe": {
"types": "./dist/utils/content-script-ui/iframe.d.ts",
"default": "./dist/utils/content-script-ui/iframe.mjs"
},
"./utils/define-app-config": {
"types": "./dist/utils/define-app-config.d.ts",
"default": "./dist/utils/define-app-config.mjs"
},
"./utils/define-background": {
"types": "./dist/utils/define-background.d.ts",
"default": "./dist/utils/define-background.mjs"
},
"./utils/define-content-script": {
"types": "./dist/utils/define-content-script.d.ts",
"default": "./dist/utils/define-content-script.mjs"
},
"./utils/define-unlisted-script": {
"types": "./dist/utils/define-unlisted-script.d.ts",
"default": "./dist/utils/define-unlisted-script.mjs"
},
"./utils/define-wxt-plugin": {
"types": "./dist/utils/define-wxt-plugin.d.ts",
"default": "./dist/utils/define-wxt-plugin.mjs"
},
"./utils/match-patterns": {
"types": "./dist/utils/match-patterns.d.ts",
"default": "./dist/utils/match-patterns.mjs"
},
"./utils/storage": {
"types": "./dist/utils/storage.d.ts",
"default": "./dist/utils/storage.mjs"
},
"./browser": {
"types": "./dist/browser.d.ts",
"default": "./dist/browser.mjs"
},
"./testing": {
"types": "./dist/testing/index.d.ts",
"default": "./dist/testing/index.mjs"
},
"./vite-builder-env": {
"types": "./dist/vite-builder-env.d.ts"
},
"./modules": {
"types": "./dist/modules.d.ts",
"default": "./dist/modules.mjs"
}
}
}
@@ -67,5 +67,20 @@ describe('Module Utilities', () => {
expect(wxt.config.imports && wxt.config.imports.presets).toHaveLength(2);
});
it("should not enable imports if they've been disabled", async () => {
const preset = 'vue';
const wxt = fakeWxt({
hooks: createHooks(),
config: {
imports: false,
},
});
addImportPreset(wxt, preset);
await wxt.hooks.callHook('config:resolved', wxt);
expect(wxt.config.imports).toBe(false);
});
});
});
-38
View File
@@ -1,38 +0,0 @@
/// <reference types="chrome" />
/**
* Contains the `browser` export which you should use to access the extension APIs in your project:
* ```ts
* import { browser } from 'wxt/browser';
*
* browser.runtime.onInstalled.addListener(() => {
* // ...
* })
* ```
* @module wxt/browser
*/
/**
* This interface is empty because it is generated per-project when running `wxt prepare`. See:
* - `.wxt/types/paths.d.ts`
*/
export interface WxtRuntime {}
/**
* This interface is empty because it is generated per-project when running `wxt prepare`. See:
* - `.wxt/types/i18n.d.ts`
*/
export interface WxtI18n {}
export type WxtBrowser = Omit<typeof chrome, 'runtime' | 'i18n'> & {
runtime: WxtRuntime & Omit<(typeof chrome)['runtime'], 'getURL'>;
i18n: WxtI18n & Omit<(typeof chrome)['i18n'], 'getMessage'>;
};
// #region snippet
export const browser: WxtBrowser =
// @ts-expect-error
globalThis.browser?.runtime?.id == null
? globalThis.chrome
: // @ts-expect-error
globalThis.browser;
// #endregion snippet
+23
View File
@@ -0,0 +1,23 @@
/// <reference types="chrome" />
/**
* EXPERIMENTAL
*
* Includes the `chrome` API and types when using `extensionApi: 'chrome'`.
*
* @module wxt/browser/chrome
*/
import type { WxtRuntime, WxtI18n } from './index';
export type WxtBrowser = Omit<typeof chrome, 'runtime' | 'i18n'> & {
runtime: WxtRuntime & Omit<(typeof chrome)['runtime'], 'getURL'>;
i18n: WxtI18n & Omit<(typeof chrome)['i18n'], 'getMessage'>;
};
// #region snippet
export const browser: WxtBrowser =
// @ts-expect-error
globalThis.browser?.runtime?.id == null
? globalThis.chrome
: // @ts-expect-error
globalThis.browser;
// #endregion snippet
+87
View File
@@ -0,0 +1,87 @@
/**
* Includes the `browser` API and types when using `extensionApi: 'webextension-polyfill'` (the default).
*
* @module wxt/browser
*/
import originalBrowser, { Browser } from 'webextension-polyfill';
export type AugmentedBrowser = Omit<Browser, 'runtime' | 'i18n'> & {
runtime: WxtRuntime & Omit<Browser['runtime'], 'getURL'>;
i18n: WxtI18n & Omit<Browser['i18n'], 'getMessage'>;
};
/**
* This interface is empty because it is generated per-project when running `wxt prepare`. See:
* - `.wxt/types/paths.d.ts`
*/
export interface WxtRuntime {}
/**
* This interface is empty because it is generated per-project when running `wxt prepare`. See:
* - `.wxt/types/i18n.d.ts`
*/
export interface WxtI18n {}
export const browser: AugmentedBrowser = originalBrowser;
// re-export all the types from webextension-polyfill
// Because webextension-polyfill uses a weird namespace with "import export", there isn't a good way
// to get these types without re-listing them.
/** @ignore */
export type {
ActivityLog,
Alarms,
Bookmarks,
Action,
BrowserAction,
BrowserSettings,
BrowsingData,
CaptivePortal,
Clipboard,
Commands,
ContentScripts,
ContextualIdentities,
Cookies,
DeclarativeNetRequest,
Devtools,
Dns,
Downloads,
Events,
Experiments,
Extension,
ExtensionTypes,
Find,
GeckoProfiler,
History,
I18n,
Identity,
Idle,
Management,
Manifest, // TODO: Export custom manifest types that are valid for both Chrome and Firefox.
ContextMenus,
Menus,
NetworkStatus,
NormandyAddonStudy,
Notifications,
Omnibox,
PageAction,
Permissions,
Pkcs11,
Privacy,
Proxy,
Runtime,
Scripting,
Search,
Sessions,
SidebarAction,
Storage,
Tabs,
Theme,
TopSites,
Types,
UserScripts,
WebNavigation,
WebRequest,
Windows,
} from 'webextension-polyfill';
+15 -50
View File
@@ -1,28 +1,29 @@
import { addViteConfig, defineWxtModule } from '../modules';
import type {
EslintGlobalsPropValue,
Wxt,
WxtDirFileEntry,
WxtModule,
WxtResolvedUnimportOptions,
} from '../types';
import { type Unimport, createUnimport, toExports } from 'unimport';
import { type Unimport, createUnimport } from 'unimport';
import { Plugin } from 'vite';
import { extname } from 'node:path';
export default defineWxtModule({
name: 'wxt:built-in:unimport',
setup(wxt) {
const options = wxt.config.imports;
if (options === false) return;
let unimport: Unimport;
const isEnabled = () => !wxt.config.imports.disabled;
// Add user module imports to config
wxt.hooks.hook('config:resolved', () => {
const addModuleImports = (module: WxtModule<any>) => {
if (!module.imports) return;
wxt.config.imports.imports ??= [];
wxt.config.imports.imports.push(...module.imports);
options.imports ??= [];
options.imports.push(...module.imports);
};
wxt.config.builtinModules.forEach(addModuleImports);
@@ -33,7 +34,7 @@ export default defineWxtModule({
// config inside "config:resolved" are applied.
wxt.hooks.afterEach((event) => {
if (event.name === 'config:resolved') {
unimport = createUnimport(wxt.config.imports);
unimport = createUnimport(options);
}
});
@@ -42,37 +43,22 @@ export default defineWxtModule({
// Update cache before each rebuild
await unimport.init();
// Always generate the #import module types
entries.push(await getImportsModuleEntry(wxt, unimport));
if (!isEnabled()) return;
// Only create global types when user has enabled auto-imports
entries.push(await getImportsDeclarationEntry(unimport));
if (wxt.config.imports.eslintrc.enabled === false) return;
// Only generate ESLint config if that feature is enabled
if (options.eslintrc.enabled === false) return;
entries.push(
await getEslintConfigEntry(
unimport,
wxt.config.imports.eslintrc.enabled,
wxt.config.imports,
),
await getEslintConfigEntry(unimport, options.eslintrc.enabled, options),
);
});
// Add vite plugin
addViteConfig(wxt, () => ({
plugins: [vitePlugin(unimport, isEnabled())],
plugins: [vitePlugin(unimport)],
}));
},
});
export function vitePlugin(
unimport: Unimport,
autoImportsEnabled: boolean,
): Plugin {
export function vitePlugin(unimport: Unimport): Plugin {
const ENABLED_EXTENSIONS = new Set([
'.js',
'.jsx',
@@ -90,12 +76,7 @@ export function vitePlugin(
// Don't transform non-js files
if (!ENABLED_EXTENSIONS.has(extname(id))) return;
const injected = await unimport.injectImports(code, id, {
// Only auto-import modules if enabled
autoImport: autoImportsEnabled,
// Always resolve the #imports module, even when auto-imports are disabled
transformVirtualImports: true,
});
const injected = await unimport.injectImports(code, id);
return {
code: injected.code,
map: injected.s.generateMap({ hires: 'boundary', source: id }),
@@ -107,6 +88,9 @@ export function vitePlugin(
async function getImportsDeclarationEntry(
unimport: Unimport,
): Promise<WxtDirFileEntry> {
// Load project imports into unimport memory so they are output via generateTypeDeclarations
await unimport.init();
return {
path: 'types/imports.d.ts',
text: [
@@ -118,25 +102,6 @@ async function getImportsDeclarationEntry(
};
}
async function getImportsModuleEntry(
wxt: Wxt,
unimport: Unimport,
): Promise<WxtDirFileEntry> {
const imports = await unimport.getImports();
return {
path: 'types/imports-module.d.ts',
text: [
'// Generated by wxt',
'// Types for the #import virtual module',
"declare module '#imports' {",
` ${toExports(imports, wxt.config.wxtDir, true).replaceAll('\n', '\n ')}`,
'}',
'',
].join('\n'),
tsReference: true,
};
}
async function getEslintConfigEntry(
unimport: Unimport,
version: 8 | 9,
+3
View File
@@ -2,6 +2,9 @@ import cli from './commands';
import { version } from '../version';
import { isAliasedCommand } from './cli-utils';
// TODO: Remove. See https://github.com/wxt-dev/wxt/issues/277
process.env.VITE_CJS_IGNORE_WARNING = 'true';
// Grab the command that we're trying to run
cli.parse(process.argv, { run: false });
@@ -1,7 +1,6 @@
/** @module wxt/utils/app-config */
// @ts-expect-error: Untyped virtual module
import appConfig from 'virtual:app-config';
import type { WxtAppConfig } from '../utils/define-app-config';
import type { WxtAppConfig } from '../sandbox/define-app-config';
export function useAppConfig(): WxtAppConfig {
return appConfig;
@@ -1,7 +1,7 @@
/** @vitest-environment happy-dom */
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ContentScriptContext } from '../content-script-context';
import { ContentScriptContext } from '..';
import { fakeBrowser } from '@webext-core/fake-browser';
/**
@@ -24,7 +24,7 @@ describe('Content Script Context', () => {
const onInvalidated = vi.fn();
ctx.onInvalidated(onInvalidated);
// @ts-ignore
// @ts-expect-error
delete fakeBrowser.runtime.id;
const isValid = ctx.isValid;
@@ -1,12 +1,8 @@
/** @module wxt/utils/content-script-context */
import { ContentScriptDefinition } from '../types';
import { ContentScriptDefinition } from '../../types';
import { browser } from 'wxt/browser';
import { logger } from '../utils/internal/logger';
import {
WxtLocationChangeEvent,
getUniqueEventName,
} from './internal/custom-events';
import { createLocationWatcher } from './internal/location-watcher';
import { logger } from '../../sandbox/utils/logger';
import { WxtLocationChangeEvent, getUniqueEventName } from './custom-events';
import { createLocationWatcher } from './location-watcher';
/**
* Implements [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController).
@@ -18,7 +14,7 @@ import { createLocationWatcher } from './internal/location-watcher';
* To create context for testing, you can use the class's constructor:
*
* ```ts
* import { ContentScriptContext } from 'wxt/utils/content-scripts-context';
* import { ContentScriptContext } from 'wxt/client';
*
* test("storage listener should be removed when context is invalidated", () => {
* const ctx = new ContentScriptContext('test');
@@ -46,6 +42,7 @@ export class ContentScriptContext implements AbortController {
private isTopFrame = window.self === window.top;
private abortController: AbortController;
private locationWatcher = createLocationWatcher(this);
private receivedMessageIds = new Set<string>();
constructor(
private readonly contentScriptName: string,
@@ -237,19 +234,28 @@ export class ContentScriptContext implements AbortController {
{
type: ContentScriptContext.SCRIPT_STARTED_MESSAGE_TYPE,
contentScriptName: this.contentScriptName,
messageId: Math.random().toString(36).slice(2),
},
'*',
);
}
verifyScriptStartedEvent(event: MessageEvent) {
const isScriptStartedEvent =
event.data?.type === ContentScriptContext.SCRIPT_STARTED_MESSAGE_TYPE;
const isSameContentScript =
event.data?.contentScriptName === this.contentScriptName;
const isNotDuplicate = !this.receivedMessageIds.has(event.data?.messageId);
return isScriptStartedEvent && isSameContentScript && isNotDuplicate;
}
listenForNewerScripts(options?: { ignoreFirstEvent?: boolean }) {
let isFirst = true;
const cb = (event: MessageEvent) => {
if (
event.data?.type === ContentScriptContext.SCRIPT_STARTED_MESSAGE_TYPE &&
event.data?.contentScriptName === this.contentScriptName
) {
if (this.verifyScriptStartedEvent(event)) {
this.receivedMessageIds.add(event.data.messageId);
const wasFirst = isFirst;
isFirst = false;
if (wasFirst && options?.ignoreFirstEvent) return;
@@ -0,0 +1,2 @@
export * from './content-script-context';
export * from './ui';
@@ -1,4 +1,4 @@
import { ContentScriptContext } from '../content-script-context';
import { ContentScriptContext } from '.';
import { WxtLocationChangeEvent } from './custom-events';
/**
@@ -1,10 +1,12 @@
/** @vitest-environment happy-dom */
import { describe, it, beforeEach, vi, expect, afterEach } from 'vitest';
import { createIntegratedUi } from '../integrated';
import { createIframeUi } from '../iframe';
import { createShadowRootUi } from '../shadow-root';
import { describe, it, beforeEach, afterEach, vi, expect } from 'vitest';
import {
createIntegratedUi,
createIframeUi,
createShadowRootUi,
ContentScriptUi,
} from '..';
import { ContentScriptContext } from '../../content-script-context';
import { ContentScriptUi } from '../types';
/**
* Util for floating promise.
@@ -544,6 +546,25 @@ describe('Content Script UIs', () => {
] as const)(
'built-in UI type: $name',
({ name, createUiFunction, uiSelector }) => {
it('should mount if an anchor already exists at the initialization', async () => {
const onMount = vi.fn(appendTestApp);
ui = await createUiFunction(ctx, {
position: 'inline',
onMount,
anchor: `#parent > #${DYNAMIC_CHILD_ID}`,
page: name === 'iframe' ? '/page.html' : undefined,
name: 'test-component',
});
appendTestElement({ id: DYNAMIC_CHILD_ID });
ui.autoMount();
await runMicrotasks();
await expect
.poll(() => document.querySelector(uiSelector))
.not.toBeNull();
});
it('should mount when an anchor is dynamically added and unmount when an anchor is removed', async () => {
const onMount = vi.fn(appendTestApp);
const onRemove = vi.fn();
@@ -1,20 +1,197 @@
import type {
ContentScriptAnchoredOptions,
ContentScriptPositioningOptions,
AutoMount,
AutoMountOptions,
BaseMountFunctions,
ContentScriptUiOptions,
MountFunctions,
} from './types';
import { browser } from 'wxt/browser';
import { waitElement } from '@1natsu/wait-element';
import {
isExist as mountDetector,
isNotExist as removeDetector,
} from '@1natsu/wait-element/detectors';
import { logger } from '../../utils/internal/logger';
import { ContentScriptContext } from '..';
import {
AutoMount,
AutoMountOptions,
BaseMountFunctions,
ContentScriptAnchoredOptions,
ContentScriptPositioningOptions,
ContentScriptUiOptions,
IframeContentScriptUi,
IframeContentScriptUiOptions,
IntegratedContentScriptUi,
IntegratedContentScriptUiOptions,
MountFunctions,
ShadowRootContentScriptUi,
ShadowRootContentScriptUiOptions,
} from './types';
import { logger } from '../../../sandbox/utils/logger';
import { createIsolatedElement } from '@webext-core/isolated-element';
export * from './types';
export function applyPosition(
/**
* Create a content script UI without any isolation.
*
* @see https://wxt.dev/guide/essentials/content-scripts.html#integrated
*/
export function createIntegratedUi<TMounted>(
ctx: ContentScriptContext,
options: IntegratedContentScriptUiOptions<TMounted>,
): IntegratedContentScriptUi<TMounted> {
const wrapper = document.createElement(options.tag || 'div');
wrapper.setAttribute('data-wxt-integrated', '');
let mounted: TMounted | undefined = undefined;
const mount = () => {
applyPosition(wrapper, undefined, options);
mountUi(wrapper, options);
mounted = options.onMount?.(wrapper);
};
const remove = () => {
options.onRemove?.(mounted);
wrapper.replaceChildren();
wrapper.remove();
mounted = undefined;
};
const mountFunctions = createMountFunctions(
{
mount,
remove,
},
options,
);
ctx.onInvalidated(remove);
return {
get mounted() {
return mounted;
},
wrapper,
...mountFunctions,
};
}
/**
* Create a content script UI using an iframe.
*
* @see https://wxt.dev/guide/essentials/content-scripts.html#iframe
*/
export function createIframeUi<TMounted>(
ctx: ContentScriptContext,
options: IframeContentScriptUiOptions<TMounted>,
): IframeContentScriptUi<TMounted> {
const wrapper = document.createElement('div');
wrapper.setAttribute('data-wxt-iframe', '');
const iframe = document.createElement('iframe');
// @ts-expect-error: getURL is defined per-project, but not inside the package
iframe.src = browser.runtime.getURL(options.page);
wrapper.appendChild(iframe);
let mounted: TMounted | undefined = undefined;
const mount = () => {
applyPosition(wrapper, iframe, options);
mountUi(wrapper, options);
mounted = options.onMount?.(wrapper, iframe);
};
const remove = () => {
options.onRemove?.(mounted);
wrapper.remove();
mounted = undefined;
};
const mountFunctions = createMountFunctions(
{
mount,
remove,
},
options,
);
ctx.onInvalidated(remove);
return {
get mounted() {
return mounted;
},
iframe,
wrapper,
...mountFunctions,
};
}
/**
* Create a content script UI inside a [`ShadowRoot`](https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot).
*
* > This function is async because it has to load the CSS via a network call.
*
* @see https://wxt.dev/guide/essentials/content-scripts.html#shadow-root
*/
export async function createShadowRootUi<TMounted>(
ctx: ContentScriptContext,
options: ShadowRootContentScriptUiOptions<TMounted>,
): Promise<ShadowRootContentScriptUi<TMounted>> {
const css = [options.css ?? ''];
if (ctx.options?.cssInjectionMode === 'ui') {
const entryCss = await loadCss();
// Replace :root selectors with :host since we're in a shadow root
css.push(entryCss.replaceAll(':root', ':host'));
}
const {
isolatedElement: uiContainer,
parentElement: shadowHost,
shadow,
} = await createIsolatedElement({
name: options.name,
css: {
textContent: css.join('\n').trim(),
},
mode: options.mode ?? 'open',
isolateEvents: options.isolateEvents,
});
shadowHost.setAttribute('data-wxt-shadow-root', '');
let mounted: TMounted | undefined;
const mount = () => {
// Add shadow root element to DOM
mountUi(shadowHost, options);
applyPosition(shadowHost, shadow.querySelector('html'), options);
// Mount UI inside shadow root
mounted = options.onMount(uiContainer, shadow, shadowHost);
};
const remove = () => {
// Cleanup mounted state
options.onRemove?.(mounted);
// Detach shadow root from DOM
shadowHost.remove();
// Remove children from uiContainer
while (uiContainer.lastChild)
uiContainer.removeChild(uiContainer.lastChild);
// Clear mounted value
mounted = undefined;
};
const mountFunctions = createMountFunctions(
{
mount,
remove,
},
options,
);
ctx.onInvalidated(remove);
return {
shadow,
shadowHost,
uiContainer,
...mountFunctions,
get mounted() {
return mounted;
},
};
}
function applyPosition(
root: HTMLElement,
positionedElement: HTMLElement | undefined | null,
options: ContentScriptPositioningOptions,
@@ -50,9 +227,7 @@ export function applyPosition(
}
}
export function getAnchor(
options: ContentScriptAnchoredOptions,
): Element | undefined {
function getAnchor(options: ContentScriptAnchoredOptions): Element | undefined {
if (options.anchor == null) return document.body;
let resolved =
@@ -79,7 +254,7 @@ export function getAnchor(
return resolved ?? undefined;
}
export function mountUi(
function mountUi(
root: HTMLElement,
options: ContentScriptAnchoredOptions,
): void {
@@ -112,7 +287,7 @@ export function mountUi(
}
}
export function createMountFunctions<TMounted>(
function createMountFunctions<TMounted>(
baseFunctions: BaseMountFunctions,
options: ContentScriptUiOptions<TMounted>,
): MountFunctions {
@@ -180,6 +355,11 @@ function autoMountUi(
async function observeElement(selector: string | null | undefined) {
let isAnchorExist = !!getAnchor(options);
// Mount if anchor exists at initialization.
if (isAnchorExist) {
uiCallbacks.mount();
}
while (!abortController.signal.aborted) {
try {
const changedAnchor = await waitElement(selector ?? 'body', {
@@ -212,3 +392,23 @@ function autoMountUi(
return { stopAutoMount: _stopAutoMount };
}
/**
* Load the CSS for the current entrypoint.
*/
async function loadCss(): Promise<string> {
// @ts-expect-error: getURL is defined per-project, but not inside the package
const url = browser.runtime.getURL(
`/content-scripts/${import.meta.env.ENTRYPOINT}.css`,
);
try {
const res = await fetch(url);
return await res.text();
} catch (err) {
logger.warn(
`Failed to load styles @ ${url}. Did you forget to import the stylesheet in your entrypoint?`,
err,
);
return '';
}
}
@@ -1,3 +1,41 @@
export interface IntegratedContentScriptUi<TMounted>
extends ContentScriptUi<TMounted> {
/**
* A wrapper div that assists in positioning.
*/
wrapper: HTMLElement;
}
export interface IframeContentScriptUi<TMounted>
extends ContentScriptUi<TMounted> {
/**
* The iframe added to the DOM.
*/
iframe: HTMLIFrameElement;
/**
* A wrapper div that assists in positioning.
*/
wrapper: HTMLDivElement;
}
export interface ShadowRootContentScriptUi<TMounted>
extends ContentScriptUi<TMounted> {
/**
* The `HTMLElement` hosting the shadow root used to isolate the UI's styles. This is the element
* that get's added to the DOM. This element's style is not isolated from the webpage.
*/
shadowHost: HTMLElement;
/**
* The container element inside the `ShadowRoot` whose styles are isolated. The UI is mounted
* inside this `HTMLElement`.
*/
uiContainer: HTMLElement;
/**
* The shadow root performing the isolation.
*/
shadow: ShadowRoot;
}
export interface ContentScriptUi<TMounted> extends MountFunctions {
mounted: TMounted | undefined;
}
@@ -11,6 +49,81 @@ export type ContentScriptUiOptions<TMounted> = ContentScriptPositioningOptions &
onRemove?: (mounted: TMounted | undefined) => void;
};
export type IntegratedContentScriptUiOptions<TMounted> =
ContentScriptUiOptions<TMounted> & {
/**
* Tag used to create the wrapper element.
*
* @default "div"
*/
tag?: string;
/**
* Callback executed when mounting the UI. This function should create and append the UI to the
* `wrapper` element. It is called every time `ui.mount()` is called.
*
* Optionally return a value that can be accessed at `ui.mounted` or in the `onRemove` callback.
*/
onMount: (wrapper: HTMLElement) => TMounted;
};
export type IframeContentScriptUiOptions<TMounted> =
ContentScriptUiOptions<TMounted> & {
/**
* The path to the HTML page that will be shown in the iframe. This string is passed into
* `browser.runtime.getURL`.
*/
// @ts-expect-error: HtmlPublicPath is generated per-project
page: import('wxt/browser').HtmlPublicPath;
/**
* Callback executed when mounting the UI. Use this function to customize the iframe or wrapper
* element's appearance. It is called every time `ui.mount()` is called.
*
* Optionally return a value that can be accessed at `ui.mounted` or in the `onRemove` callback.
*/
onMount?: (wrapper: HTMLElement, iframe: HTMLIFrameElement) => TMounted;
};
export type ShadowRootContentScriptUiOptions<TMounted> =
ContentScriptUiOptions<TMounted> & {
/**
* The name of the custom component used to host the ShadowRoot. Must be kebab-case.
*/
name: string;
/**
* Custom CSS text to apply to the UI. If your content script imports/generates CSS and you've
* set `cssInjectionMode: "ui"`, the imported CSS will be included automatically. You do not need
* to pass those styles in here. This is for any additional styles not in the imported CSS.
*/
css?: string;
/**
* ShadowRoot's mode.
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot/mode
* @default "open"
*/
mode?: 'open' | 'closed';
/**
* When enabled, `event.stopPropagation` will be called on events trying to bubble out of the
* shadow root.
*
* - Set to `true` to stop the propagation of a default set of events,
* `["keyup", "keydown", "keypress"]`
* - Set to an array of event names to stop the propagation of a custom list of events
*/
isolateEvents?: boolean | string[];
/**
* Callback executed when mounting the UI. This function should create and append the UI to the
* `uiContainer` element. It is called every time `ui.mount()` is called.
*
* Optionally return a value that can be accessed at `ui.mounted` or in the `onRemove` callback.
*/
onMount: (
uiContainer: HTMLElement,
shadow: ShadowRoot,
shadowHost: HTMLElement,
) => TMounted;
};
export type ContentScriptOverlayAlignment =
| 'top-left'
| 'top-right'
+8
View File
@@ -0,0 +1,8 @@
/**
* Any runtime APIs that use the web extension APIs.
*
* @module wxt/client
*/
export * from './content-scripts';
export * from './app-config';
export * from './inject-script';
@@ -1,4 +1,3 @@
/** @module wxt/utils/inject-script */
import { browser } from 'wxt/browser';
export type ScriptPublicPath = Extract<
+37 -17
View File
@@ -20,6 +20,7 @@ import {
import { Hookable } from 'hookable';
import { toArray } from '../../utils/arrays';
import { safeVarName } from '../../utils/strings';
import { importEntrypointFile } from '../../utils/building';
import { ViteNodeServer } from 'vite-node/server';
import { ViteNodeRunner } from 'vite-node/client';
import { installSourcemapsSupport } from 'vite-node/source-map';
@@ -77,6 +78,7 @@ export async function createViteBuilder(
wxtPlugins.tsconfigPaths(wxtConfig),
wxtPlugins.noopBackground(),
wxtPlugins.globals(wxtConfig),
wxtPlugins.resolveExtensionApi(wxtConfig),
wxtPlugins.defineImportMeta(),
wxtPlugins.wxtPluginLoader(wxtConfig),
wxtPlugins.resolveAppConfig(wxtConfig),
@@ -279,26 +281,44 @@ export async function createViteBuilder(
version: vite.version,
async importEntrypoint(path) {
const env = createExtensionEnvironment();
const { runner, server } = await createViteNodeImporter([path]);
const res = await env.run(() => runner.executeFile(path));
await server.close();
requireDefaultExport(path, res);
return res.default;
switch (wxtConfig.entrypointLoader) {
default:
case 'jiti': {
return await env.run(() => importEntrypointFile(path));
}
case 'vite-node': {
const { runner, server } = await createViteNodeImporter([path]);
const res = await env.run(() => runner.executeFile(path));
await server.close();
requireDefaultExport(path, res);
return res.default;
}
}
},
async importEntrypoints(paths) {
const env = createExtensionEnvironment();
const { runner, server } = await createViteNodeImporter(paths);
const res = await env.run(() =>
Promise.all(
paths.map(async (path) => {
const mod = await runner.executeFile(path);
requireDefaultExport(path, mod);
return mod.default;
}),
),
);
await server.close();
return res;
switch (wxtConfig.entrypointLoader) {
default:
case 'jiti': {
return await env.run(() =>
Promise.all(paths.map(importEntrypointFile)),
);
}
case 'vite-node': {
const { runner, server } = await createViteNodeImporter(paths);
const res = await env.run(() =>
Promise.all(
paths.map(async (path) => {
const mod = await runner.executeFile(path);
requireDefaultExport(path, mod);
return mod.default;
}),
),
);
await server.close();
return res;
}
}
},
async build(group) {
let entryConfig;
@@ -3,7 +3,12 @@ import type * as vite from 'vite';
import { ResolvedConfig } from '../../../../types';
/**
* Mock `wxt/browser` and stub the global `browser`/`chrome` types with a fake version of the extension APIs
* Mock `webextension-polyfill`, `wxt/browser`, and `wxt/browser/*` by inlining
* all dependencies that import them and adding a custom alias so that Vite
* resolves to a mocked version of the module.
*
* TODO: Detect non-wxt dependencies (like `@webext-core/*`) that import `webextension-polyfill` via
* `npm list` and inline them automatically.
*/
export function extensionApiMock(config: ResolvedConfig): vite.PluginOption {
const virtualSetupModule = 'virtual:wxt-setup';
@@ -22,12 +27,14 @@ export function extensionApiMock(config: ResolvedConfig): vite.PluginOption {
},
resolve: {
alias: [
{ find: 'webextension-polyfill', replacement },
// wxt/browser, wxt/browser/...
{ find: 'wxt/browser', replacement },
{ find: /^wxt\/browser.*/, replacement },
],
},
ssr: {
// Inline all WXT modules subdependencies can be mocked
// Inline all WXT modules so vite processes them so the aliases can
// be resolved
noExternal: ['wxt'],
},
};
@@ -9,6 +9,7 @@ export * from './cssEntrypoints';
export * from './bundleAnalysis';
export * from './globals';
export * from './extensionApiMock';
export * from './resolveExtensionApi';
export * from './entrypointGroupGlobals';
export * from './defineImportMeta';
export * from './removeEntrypointMainFunction';
@@ -15,7 +15,7 @@ export function noopBackground(): Plugin {
},
load(id) {
if (id === resolvedVirtualModuleId) {
return `import { defineBackground } from 'wxt/utils/define-background';\nexport default defineBackground(() => void 0)`;
return `import { defineBackground } from 'wxt/sandbox';\nexport default defineBackground(() => void 0)`;
}
},
};
@@ -0,0 +1,27 @@
import { ResolvedConfig } from '../../../../types';
import type * as vite from 'vite';
/**
* Apply the experimental config for which extension API is used. This only
* effects the extension API included at RUNTIME - during development, types
* depend on the import.
*
* NOTE: this only works if we import `wxt/browser` instead of using the relative path.
*/
export function resolveExtensionApi(config: ResolvedConfig): vite.Plugin {
return {
name: 'wxt:resolve-extension-api',
config() {
// Only apply the config if we're disabling the polyfill
if (config.extensionApi === 'webextension-polyfill') return;
return {
resolve: {
alias: [
{ find: /^wxt\/browser$/, replacement: 'wxt/browser/chrome' },
],
},
};
},
};
}

Some files were not shown because too many files have changed in this diff Show More