Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 32500a78be | |||
| 6c9beee71a | |||
| b6a0cbbbed | |||
| 406766b37f | |||
| 6f26eb5072 | |||
| 900d3bbb18 | |||
| 148bc4a6eb | |||
| ee3cbe6925 | |||
| 11ecd3f818 | |||
| 72d4f7de3a | |||
| 6be319f379 | |||
| 84083bd9b0 | |||
| d718f7fb50 | |||
| 7530628eb8 | |||
| 4b3ffc90c8 | |||
| 01e4c1975a | |||
| 65dfded682 | |||
| c712175445 | |||
| 0f71d570ca | |||
| b150a52471 | |||
| 028c601515 | |||
| af382ef065 | |||
| 91ef7f2ab5 | |||
| 0bf7279c8f | |||
| 62525d33c6 | |||
| 88ff40e301 | |||
| 1a72a0cdc6 | |||
| 2b07b4bc12 | |||
| 06c09e7c87 | |||
| 30b96ac87c | |||
| 2e9b5cffe8 | |||
| 3e1641a6ec |
@@ -15,6 +15,7 @@ on:
|
||||
- module-vue
|
||||
- storage
|
||||
- unocss
|
||||
- webextension-polyfill
|
||||
- wxt
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -14,6 +14,7 @@ on:
|
||||
- module-svelte
|
||||
- module-vue
|
||||
- storage
|
||||
- webextension-polyfill
|
||||
- wxt
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -166,3 +166,13 @@ 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.
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
// @ts-expect-error: Vitepress data-loader magic, this import is correct
|
||||
import { data } from '../loaders/blog.data';
|
||||
import BlogPostPreview from './BlogPostPreview.vue';
|
||||
|
||||
const posts = computed(() =>
|
||||
data
|
||||
.map((post) => ({
|
||||
...post,
|
||||
...post.frontmatter,
|
||||
date: new Date(post.frontmatter.date),
|
||||
}))
|
||||
.sort((a, b) => b.date.getTime() - a.date.getTime()),
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container">
|
||||
<div>
|
||||
<div class="vp-doc">
|
||||
<h1>Blog</h1>
|
||||
</div>
|
||||
|
||||
<ul>
|
||||
<BlogPostPreview v-for="post of posts" :key="post.url" :post />
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
.container > div {
|
||||
padding: 32px;
|
||||
max-width: 900px;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
ul {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
list-style: none;
|
||||
}
|
||||
ul,
|
||||
li {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
ul li {
|
||||
padding-top: 16px;
|
||||
margin-top: 16px;
|
||||
border-top: 1px solid var(--vp-c-default);
|
||||
}
|
||||
ul li:last-child {
|
||||
padding-bottom: 16px;
|
||||
margin-bottom: 16px;
|
||||
border-bottom: 1px solid var(--vp-c-default);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,76 @@
|
||||
<script lang="ts" setup>
|
||||
import useBlogDate from '../composables/useBlogDate';
|
||||
import { useData } from 'vitepress';
|
||||
|
||||
const { frontmatter } = useData();
|
||||
const date = useBlogDate(() => frontmatter.value.date);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="vp-doc">
|
||||
<main class="container-content">
|
||||
<h1 v-html="$frontmatter.title" />
|
||||
<p class="meta-row">
|
||||
<a
|
||||
class="author"
|
||||
v-for="author of $frontmatter.authors"
|
||||
:key="author.github"
|
||||
:href="`https://github.com/${author.github}`"
|
||||
>
|
||||
<img :src="`https://github.com/${author.github}.png?size=96`" />
|
||||
<span>{{ author.name }}</span>
|
||||
</a>
|
||||
<span>•</span>
|
||||
<span>{{ date }}</span>
|
||||
</p>
|
||||
<Content />
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
vp-doc {
|
||||
display: flex;
|
||||
}
|
||||
main {
|
||||
max-width: 1080px;
|
||||
padding: 32px;
|
||||
margin: auto;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
main {
|
||||
padding: 64px;
|
||||
}
|
||||
}
|
||||
.meta-row {
|
||||
display: flex;
|
||||
color: var(--vp-c-text-2);
|
||||
gap: 16px;
|
||||
overflow: hidden;
|
||||
padding-bottom: 32px;
|
||||
}
|
||||
.meta-row > * {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.author {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
color: var(--vp-c-text-2);
|
||||
font-weight: normal;
|
||||
text-decoration: none;
|
||||
}
|
||||
.author img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 100%;
|
||||
}
|
||||
.author span {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.author:hover {
|
||||
text-decoration: underline;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,72 @@
|
||||
<script lang="ts" setup>
|
||||
import useBlogDate from '../composables/useBlogDate';
|
||||
|
||||
const props = defineProps<{
|
||||
post: {
|
||||
title: string;
|
||||
description?: string;
|
||||
date: Date;
|
||||
url: string;
|
||||
authors: Array<{ name: string; github: string }>;
|
||||
};
|
||||
}>();
|
||||
|
||||
const date = useBlogDate(() => props.post.date);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li class="blog-list-item">
|
||||
<a :href="post.url">
|
||||
<div class="vp-doc">
|
||||
<h3 class="title" v-html="post.title" />
|
||||
<p class="description" v-html="post.description" />
|
||||
<p class="meta">
|
||||
{{ post.authors.map((author) => author.name).join(', ') }}
|
||||
•
|
||||
{{ date }}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
li {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
h3 {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
li > a > div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0 -16px;
|
||||
padding: 16px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
li > a > div:hover {
|
||||
background: var(--vp-c-default);
|
||||
}
|
||||
li .title {
|
||||
color: var(--vp-c-text);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
li .description {
|
||||
font-size: 16px;
|
||||
color: var(--vp-c-text-2);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
li .meta {
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
color: var(--vp-c-text-2);
|
||||
}
|
||||
</style>
|
||||
@@ -59,6 +59,8 @@ const chromeExtensionIds = [
|
||||
'npgghjedpchajflknnbngajkjkdhncdo', // aesthetic Notion, styled
|
||||
'hmdcmlfkchdmnmnmheododdhjedfccka', // Eye Dropper
|
||||
'eihpmapodnppeemkhkbhikmggfojdkjd', // Cursorful - Screen Recorder with Auto Zoom
|
||||
'hjjkgbibknbahijglkffklflidncplkn', // Show IP – Live View of Website IPs for Developers
|
||||
'ilbikcehnpkmldojkcmlldkoelofnbde', // Strong Password Generator
|
||||
];
|
||||
|
||||
const { data, err, isLoading } = useListExtensionDetails(chromeExtensionIds);
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { computed, toValue, MaybeRefOrGetter } from 'vue';
|
||||
|
||||
const MONTH_FORMATTER = new Intl.DateTimeFormat(
|
||||
globalThis?.navigator?.language,
|
||||
{
|
||||
month: 'long',
|
||||
},
|
||||
);
|
||||
|
||||
export default function (date: MaybeRefOrGetter<Date | string>) {
|
||||
return computed(() => {
|
||||
const d = new Date(toValue(date));
|
||||
return `${MONTH_FORMATTER.format(d)} ${d.getDate()}, ${d.getFullYear()}`;
|
||||
});
|
||||
}
|
||||
@@ -14,14 +14,19 @@ 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 = 'https://wxt.dev';
|
||||
const ogImage = 'https://wxt.dev/social-preview.png';
|
||||
const ogUrl = origin;
|
||||
const ogImage = `${origin}/social-preview.png`;
|
||||
|
||||
// https://vitepress.dev/reference/site-config
|
||||
export default defineConfig({
|
||||
@@ -33,7 +38,30 @@ export default defineConfig({
|
||||
},
|
||||
lastUpdated: true,
|
||||
sitemap: {
|
||||
hostname: 'https://wxt.dev',
|
||||
hostname: origin,
|
||||
},
|
||||
|
||||
async buildEnd(site) {
|
||||
// Only construct the RSS document for production builds
|
||||
const { default: blogDataLoader } = await import('./loaders/blog.data');
|
||||
const posts = await blogDataLoader.load();
|
||||
const feed = new Feed({
|
||||
copyright: 'MIT',
|
||||
id: 'wxt',
|
||||
title: 'WXT Blog',
|
||||
link: `${origin}/blog`,
|
||||
});
|
||||
posts.forEach((post) => {
|
||||
feed.addItem({
|
||||
date: post.frontmatter.date,
|
||||
link: new URL(post.url, origin).href,
|
||||
title: post.frontmatter.title,
|
||||
description: post.frontmatter.description,
|
||||
});
|
||||
});
|
||||
console.log('rss.xml:');
|
||||
console.log(feed.rss2());
|
||||
await writeFile(join(site.outDir, 'rss.xml'), feed.rss2(), 'utf8');
|
||||
},
|
||||
|
||||
head: [
|
||||
@@ -89,6 +117,7 @@ export default defineConfig({
|
||||
navItem('Guide', '/guide/installation'),
|
||||
navItem('Examples', '/examples'),
|
||||
navItem('API', '/api/reference/wxt'),
|
||||
navItem('Blog', '/blog'),
|
||||
navItem(`v${wxtVersion}`, [
|
||||
navItem('wxt', [
|
||||
navItem(`v${wxtVersion}`, '/'),
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { createContentLoader } from 'vitepress';
|
||||
|
||||
export default createContentLoader('blog/*.md');
|
||||
@@ -3,14 +3,14 @@
|
||||
--wxt-c-green-1: #0b8a00;
|
||||
--wxt-c-green-2: #096600;
|
||||
--wxt-c-green-3: #096600;
|
||||
--wxt-c-green-soft: rgba(#0b8a00 / 0.14);
|
||||
--wxt-c-green-soft: rgba(11, 138, 0, 0.14);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--wxt-c-green-1: #67d45e;
|
||||
--wxt-c-green-2: #329929;
|
||||
--wxt-c-green-3: #21651b;
|
||||
--wxt-c-green-soft: rgba(#67d45e / 0.14);
|
||||
--wxt-c-green-soft: rgba(103, 212, 94, 0.14);
|
||||
}
|
||||
|
||||
/* https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css */
|
||||
|
||||
@@ -3,14 +3,17 @@ import Icon from '../components/Icon.vue';
|
||||
import EntrypointPatterns from '../components/EntrypointPatterns.vue';
|
||||
import UsingWxtSection from '../components/UsingWxtSection.vue';
|
||||
import ExampleSearch from '../components/ExampleSearch.vue';
|
||||
import BlogLayout from '../components/BlogLayout.vue';
|
||||
import './custom.css';
|
||||
|
||||
export default {
|
||||
extends: DefaultTheme,
|
||||
enhanceApp(ctx) {
|
||||
ctx.app.component('Icon', Icon);
|
||||
ctx.app.component('EntrypointPatterns', EntrypointPatterns);
|
||||
ctx.app.component('UsingWxtSection', UsingWxtSection);
|
||||
ctx.app.component('ExampleSearch', ExampleSearch);
|
||||
ctx.app
|
||||
.component('Icon', Icon)
|
||||
.component('EntrypointPatterns', EntrypointPatterns)
|
||||
.component('UsingWxtSection', UsingWxtSection)
|
||||
.component('ExampleSearch', ExampleSearch)
|
||||
.component('blog', BlogLayout);
|
||||
},
|
||||
};
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 132 KiB |
@@ -0,0 +1,9 @@
|
||||
---
|
||||
layout: page
|
||||
---
|
||||
|
||||
<script lang="ts" setup>
|
||||
import BlogHome from './.vitepress/components/BlogHome.vue';
|
||||
</script>
|
||||
|
||||
<BlogHome />
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
layout: blog
|
||||
title: Real World Messaging
|
||||
description: |
|
||||
The extension messaging APIs are difficult to learn. Let's go beyond the simple examples from Chrome and Firefox's documentation to build our own simple messaging system from scratch.
|
||||
authors:
|
||||
- name: Aaron Klinker
|
||||
github: aklinker1
|
||||
date: 2024-10-20T04:54:23.601Z
|
||||
---
|
||||
|
||||
Test content **bold** _italic_
|
||||
@@ -0,0 +1,72 @@
|
||||
---
|
||||
layout: blog
|
||||
title: Introducing <code>#imports</code>
|
||||
description: Learn how WXT's new <code>#imports</code> module works and how to use it.
|
||||
authors:
|
||||
- name: Aaron Klinker
|
||||
github: aklinker1
|
||||
date: 2024-12-06T14:39:00.000Z
|
||||
---
|
||||
|
||||
WXT v0.20 introduced a new way of manually importing its APIs: **the `#imports` module**. This module was introduced to simplify import statements and provide more visibility into all the APIs WXT provides.
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```ts
|
||||
import { browser } from 'wxt/browser'; // [!code --]
|
||||
import { createShadowRootUi } from 'wxt/utils/content-script-ui/shadow-root'; // [!code --]
|
||||
import { defineContentScript } from 'wxt/utils/define-content-script'; // [!code --]
|
||||
import { injectScript } from 'wxt/utils/inject-script'; // [!code --]
|
||||
import { // [!code ++]
|
||||
browser, createShadowRootUi, defineContentScript, injectScript // [!code ++]
|
||||
} from '#imports'; // [!code ++]
|
||||
```
|
||||
|
||||
The `#imports` module is considered a "virtual module", because the file doesn't actually exist. At build-time, imports are split into individual statements for each API:
|
||||
|
||||
:::code-group
|
||||
|
||||
```ts [What you write]
|
||||
import { defineContentScript, injectScript } from '#imports';
|
||||
```
|
||||
|
||||
```ts [What the bundler sees]
|
||||
import { defineContentScript } from 'wxt/utils/define-content-script';
|
||||
import { injectScript } from 'wxt/utils/inject-script';
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
Think of `#imports` as a convenient way to access all of WXT's APIs from one place, without impacting performance or bundle size.
|
||||
|
||||
This enables better tree-shaking compared to v0.19 and below.
|
||||
|
||||
:::tip Need to lookup the full import path of an API?
|
||||
Open up your project's `.wxt/types/imports-module.d.ts` file.
|
||||
:::
|
||||
|
||||
## Mocking
|
||||
|
||||
When writing tests, you might need to mock APIs from the `#imports` module. While mocking these APIs is very easy, it may not be immediately clear how to accomplish it.
|
||||
|
||||
Let's look at an example using Vitest. When [configured with `wxt/testing`](/guide/essentials/unit-testing#vitest), Vitest sees the same transformed code as the bundler. That means to mock an API from `#imports`, you need to call `vi.mock` with the real import path, not `#imports`:
|
||||
|
||||
```ts
|
||||
import { injectScript } from '#imports';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
vi.mock('wxt/utils/inject-script')
|
||||
const injectScriptMock = vi.mocked(injectScript);
|
||||
|
||||
injectScriptMock.mockReturnValueOnce(...);
|
||||
```
|
||||
|
||||
## Conclusion
|
||||
|
||||
You don't have to use `#imports` if you don't like - you can continue importing APIs from their submodules. However, using `#imports` is the recommended approach moving forwards.
|
||||
|
||||
- As more APIs are added, you won't have to memorize additional import paths.
|
||||
- If breaking changes are made to import paths in future major versions, `#imports` won't break.
|
||||
|
||||
Happy Coding 😄
|
||||
|
||||
> P.S. Yes, this is exactly how [Nuxt's `#imports`](https://nuxt.com/docs/guide/concepts/auto-imports#explicit-imports) works! We use the exact same library, [`unimport`](https://github.com/unjs/unimport).
|
||||
@@ -29,7 +29,7 @@ img.src = imageUrl;
|
||||
|
||||
## `/public` Directory
|
||||
|
||||
Files inside `<srcDir>/public/` are copied into the output folder as-is, without being processed by WXT's bundler.
|
||||
Files inside `<rootDir>/public/` are copied into the output folder as-is, without being processed by WXT's bundler.
|
||||
|
||||
Here's how you access them:
|
||||
|
||||
@@ -54,6 +54,10 @@ img.src = imageUrl;
|
||||
|
||||
:::
|
||||
|
||||
:::warning
|
||||
Assets in the `public/` directory are **_not_** accessible in content scripts by default. To use a public asset in a content script, you must add it to your manifest's [`web_accessible_resources` array](/api/reference/wxt/type-aliases/UserManifest#web-accessible-resources).
|
||||
:::
|
||||
|
||||
## Inside Content Scripts
|
||||
|
||||
Assets inside content scripts are a little different. By default, when you import an asset, it returns just the path to the asset. This is because Vite assumes you're loading assets from the same hostname.
|
||||
|
||||
@@ -11,19 +11,7 @@ export default defineConfig({
|
||||
});
|
||||
```
|
||||
|
||||
By default, WXT automatically setups up auto-imports for all of it's own APIs:
|
||||
|
||||
- [`browser`](/api/reference/wxt/browser/variables/browser) from `wxt/browser`
|
||||
- [`defineContentScript`](/api/reference/wxt/sandbox/functions/defineContentScript) from `wxt/sandbox`
|
||||
- [`defineBackground`](/api/reference/wxt/sandbox/functions/defineBackground) from `wxt/sandbox`
|
||||
- [`defineUnlistedScript`](/api/reference/wxt/sandbox/functions/defineUnlistedScript) from `wxt/sandbox`
|
||||
- [`createIntegratedUi`](/api/reference/wxt/client/functions/createIntegratedUi) from `wxt/client`
|
||||
- [`createShadowRootUi`](/api/reference/wxt/client/functions/createShadowRootUi) from `wxt/client`
|
||||
- [`createIframeUi`](/api/reference/wxt/client/functions/createIframeUi) from `wxt/client`
|
||||
- [`fakeBrowser`](/api/reference/wxt/testing/variables/fakeBrowser) from `wxt/testing`
|
||||
- And more!
|
||||
|
||||
WXT also adds some project directories as auto-import sources automatically:
|
||||
By default, WXT automatically sets up auto-imports for all of it's own APIs and some of your project directories:
|
||||
|
||||
- `<srcDir>/components/*`
|
||||
- `<srcDir>/composables/*`
|
||||
@@ -32,6 +20,8 @@ WXT also adds some project directories as auto-import sources automatically:
|
||||
|
||||
All named and default exports from files in these directories are available everywhere else in your project without having to import them.
|
||||
|
||||
To see the complete list of auto-imported APIs, run [`wxt prepare`](/api/cli/wxt-prepare) and look at your project's `.wxt/types/imports-module.d.ts` file.
|
||||
|
||||
## TypeScript
|
||||
|
||||
For TypeScript and your editor to recognize auto-imported variables, you need to run the [`wxt prepare` command](/api/cli/wxt-prepare).
|
||||
@@ -110,3 +100,19 @@ export default defineConfig({
|
||||
imports: false, // [!code ++]
|
||||
});
|
||||
```
|
||||
|
||||
## Explicit Imports (`#imports`)
|
||||
|
||||
You can manually import all of WXT's APIs via the `#imports` module:
|
||||
|
||||
```ts
|
||||
import {
|
||||
createShadowRootUi,
|
||||
ContentScriptContext,
|
||||
MatchPattern,
|
||||
} from '#imports';
|
||||
```
|
||||
|
||||
To learn more about how the `#imports` module works, read the [related blog post](/blog/2024-12-06-using-imports-module).
|
||||
|
||||
If you've disabled auto-imports, you should still use `#imports` to import all of WXT's APIs from a single place.
|
||||
|
||||
@@ -4,7 +4,7 @@ outline: deep
|
||||
|
||||
# Browser Startup
|
||||
|
||||
> See the [API Reference](/api/reference/wxt/interfaces/ExtensionRunnerConfig) for a full list of config.
|
||||
> See the [API Reference](/api/reference/wxt/interfaces/WebExtConfig) for a full list of config.
|
||||
|
||||
During development WXT uses [`web-ext` by Mozilla](https://www.npmjs.com/package/web-ext) to automatically open a browser window with your extension installed.
|
||||
|
||||
@@ -15,9 +15,9 @@ You can configure browser startup in 3 places:
|
||||
1. `<rootDir>/web-ext.config.ts`: Ignored from version control, this file lets you configure your own options for a specific project without affecting other developers
|
||||
|
||||
```ts
|
||||
import { defineRunnerConfig } from 'wxt';
|
||||
import { defineWebExtConfig } from 'wxt';
|
||||
|
||||
export default defineRunnerConfig({
|
||||
export default defineWebExtConfig({
|
||||
// ...
|
||||
});
|
||||
```
|
||||
@@ -32,7 +32,7 @@ You can configure browser startup in 3 places:
|
||||
To set or customize the browser opened during development:
|
||||
|
||||
```ts
|
||||
export default defineRunnerConfig({
|
||||
export default defineWebExtConfig({
|
||||
binaries: {
|
||||
chrome: '/path/to/chrome-beta', // Use Chrome Beta instead of regular Chrome
|
||||
firefox: 'firefoxdeveloperedition', // Use Firefox Developer Edition instead of regular Firefox
|
||||
@@ -52,7 +52,7 @@ To persist data, set the `--user-data-dir` flag:
|
||||
:::code-group
|
||||
|
||||
```ts [Mac/Linux]
|
||||
export default defineRunnerConfig({
|
||||
export default defineWebExtConfig({
|
||||
chromiumArgs: ['--user-data-dir=./.wxt/chrome-data'],
|
||||
});
|
||||
```
|
||||
@@ -60,7 +60,7 @@ export default defineRunnerConfig({
|
||||
```ts [Windows]
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
export default defineRunnerConfig({
|
||||
export default defineWebExtConfig({
|
||||
// On Windows, the path must be absolute
|
||||
chromiumProfile: resolve('.wxt/chrome-data'),
|
||||
keepProfileChanges: true,
|
||||
@@ -80,7 +80,7 @@ You can use any directory you'd like for `--user-data-dir`, the examples above c
|
||||
If you prefer to load the extension into your browser manually, you can disable the auto-open behavior:
|
||||
|
||||
```ts
|
||||
export default defineRunnerConfig({
|
||||
export default defineWebExtConfig({
|
||||
disabled: true,
|
||||
});
|
||||
```
|
||||
|
||||
@@ -17,63 +17,3 @@ If you're running into errors while importing entrypoints, run `wxt prepare --de
|
||||
:::
|
||||
|
||||
Once the environment has been polyfilled and your code pre-processed, it's up the entrypoint loader to import your code, extracting the options from the default export.
|
||||
|
||||
There are two options for loading your entrypoints:
|
||||
|
||||
1. `vite-node` - default as of `v0.19.0`
|
||||
2. `jiti` (**DEPRECATED, will be removed in `v0.20.0`**) - Default before `v0.19.0`
|
||||
|
||||
## vite-node
|
||||
|
||||
Since 0.19.0, WXT uses `vite-node`, the same tool that powers Vitest and Nuxt, to import your entrypoint files. It re-uses the same vite config used when building your extension, making it the most stable entrypoint loader.
|
||||
|
||||
## jiti
|
||||
|
||||
To enable `jiti`:
|
||||
|
||||
```ts
|
||||
export default defineConfig({
|
||||
entrypointLoader: 'jiti',
|
||||
});
|
||||
```
|
||||
|
||||
This is the original method WXT used to import TS files. However, because it doesn't support vite plugins like `vite-node`, it does one additional pre-processing step: It removes **_ALL_** imports from your code.
|
||||
|
||||
That means you cannot use imported variables outside the `main` function in JS entrypoints, like for content script `matches` or other options:
|
||||
|
||||
```ts
|
||||
// entrypoints/content.ts
|
||||
import { GOOGLE_MATCHES } from '~/utils/match-patterns';
|
||||
|
||||
export default defineContentScript({
|
||||
matches: GOOGLE_MATCHES,
|
||||
main() {
|
||||
// ...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```
|
||||
$ wxt build
|
||||
wxt build
|
||||
|
||||
WXT 0.14.1
|
||||
ℹ Building chrome-mv3 for production with Vite 5.0.5
|
||||
✖ Command failed after 360 ms
|
||||
|
||||
[8:55:54 AM] ERROR entrypoints/content.ts: Cannot use imported variable "GOOGLE_MATCHES" before main function.
|
||||
```
|
||||
|
||||
Usually, this error occurs when you try to extract options into a shared file or when running code outside the `main` function. To fix the example from above, use literal values when defining an entrypoint instead of importing them:
|
||||
|
||||
```ts
|
||||
import { GOOGLE_MATCHES } from '~/utils/match-patterns'; // [!code --]
|
||||
|
||||
export default defineContentScript({
|
||||
matches: GOOGLE_MATCHES, // [!code --]
|
||||
matches: ['*//*.google.com/*'], // [!code ++]
|
||||
main() {
|
||||
// ...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
@@ -156,6 +156,11 @@ Alternatively, you can use [`@wxt-dev/auto-icons`](https://www.npmjs.com/package
|
||||
|
||||
> [Chrome docs](https://developer.chrome.com/docs/extensions/reference/permissions/)
|
||||
|
||||
Most of the time, you need to manually add permissions to your manifest. Only in a few specific situations are permissions added automatically:
|
||||
|
||||
- During development: the `tabs` and `scripting` permissions will be added to enable hot reloading.
|
||||
- When a `sidepanel` entrypoint is present: The `sidepanel` permission is added.
|
||||
|
||||
```ts
|
||||
export default defineConfig({
|
||||
manifest: {
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
Define runtime configuration in a single place, `<srcDir>/app.config.ts`:
|
||||
|
||||
```ts
|
||||
import { defineAppConfig } from 'wxt/sandbox';
|
||||
import { defineAppConfig } from '#imports';
|
||||
|
||||
// Define types for your config
|
||||
declare module 'wxt/sandbox' {
|
||||
declare module 'wxt/utils/define-app-config' {
|
||||
export interface WxtAppConfig {
|
||||
theme?: 'light' | 'dark';
|
||||
}
|
||||
@@ -26,7 +26,7 @@ This file is committed to the repo, so don't put any secrets here. Instead, use
|
||||
To access runtime config, WXT provides the `useAppConfig` function:
|
||||
|
||||
```ts
|
||||
import { useAppConfig } from 'wxt/sandbox';
|
||||
import { useAppConfig } from '#imports';
|
||||
|
||||
console.log(useAppConfig()); // { theme: "dark" }
|
||||
```
|
||||
@@ -36,7 +36,7 @@ console.log(useAppConfig()); // { theme: "dark" }
|
||||
You can use environment variables in the `app.config.ts` file.
|
||||
|
||||
```ts
|
||||
declare module 'wxt/sandbox' {
|
||||
declare module 'wxt/utils/define-app-config' {
|
||||
export interface WxtAppConfig {
|
||||
apiKey?: string;
|
||||
skipWelcome: boolean;
|
||||
|
||||
@@ -255,13 +255,13 @@ export default defineContentScript({
|
||||
|
||||
:::
|
||||
|
||||
See the [API Reference](/api/reference/wxt/client/functions/createIntegratedUi) for the complete list of options.
|
||||
See the [API Reference](/api/reference/wxt/utils/content-script-ui/integrated/functions/createIntegratedUi) for the complete list of options.
|
||||
|
||||
### Shadow Root
|
||||
|
||||
Often in web extensions, you don't want your content script's CSS affecting the page, or vise-versa. The [`ShadowRoot`](https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot) API is ideal for this.
|
||||
|
||||
WXT's [`createShadowRootUi`](/api/reference/wxt/client/functions/createShadowRootUi) abstracts all the `ShadowRoot` setup away, making it easy to create UIs whose styles are isolated from the page. It also supports an optional `isolateEvents` parameter to further isolate user interactions.
|
||||
WXT's [`createShadowRootUi`](/api/reference/wxt/utils/content-script-ui/shadow-root/functions/createShadowRootUi) abstracts all the `ShadowRoot` setup away, making it easy to create UIs whose styles are isolated from the page. It also supports an optional `isolateEvents` parameter to further isolate user interactions.
|
||||
|
||||
To use `createShadowRootUi`, follow these steps:
|
||||
|
||||
@@ -444,7 +444,7 @@ export default defineContentScript({
|
||||
|
||||
:::
|
||||
|
||||
See the [API Reference](/api/reference/wxt/client/functions/createShadowRootUi) for the complete list of options.
|
||||
See the [API Reference](/api/reference/wxt/utils/content-script-ui/shadow-root/functions/createShadowRootUi) for the complete list of options.
|
||||
|
||||
Full examples:
|
||||
|
||||
@@ -455,7 +455,7 @@ Full examples:
|
||||
|
||||
If you don't need to run your UI in the same frame as the content script, you can use an IFrame to host your UI instead. Since an IFrame just hosts an HTML page, **_HMR is supported_**.
|
||||
|
||||
WXT provides a helper function, [`createIframeUi`](/api/reference/wxt/client/functions/createIframeUi), which simplifies setting up the IFrame.
|
||||
WXT provides a helper function, [`createIframeUi`](/api/reference/wxt/utils/content-script-ui/iframe/functions/createIframeUi), which simplifies setting up the IFrame.
|
||||
|
||||
1. Create an HTML page that will be loaded into your IFrame:
|
||||
```html
|
||||
@@ -510,7 +510,7 @@ WXT provides a helper function, [`createIframeUi`](/api/reference/wxt/client/fun
|
||||
});
|
||||
```
|
||||
|
||||
See the [API Reference](/api/reference/wxt/client/functions/createIframeUi) for the complete list of options.
|
||||
See the [API Reference](/api/reference/wxt/utils/content-script-ui/iframe/functions/createIframeUi) for the complete list of options.
|
||||
|
||||
## Isolated World vs Main World
|
||||
|
||||
@@ -607,7 +607,7 @@ export default defineContentScript({
|
||||
When the `ui.remove` is called, `autoMount` also stops.
|
||||
:::
|
||||
|
||||
See the [API Reference](/api/reference/wxt/client/interfaces/ContentScriptUi.html#automount) for the complete list of options.
|
||||
See the [API Reference](/api/reference/wxt/utils/content-script-ui/types/interfaces/ContentScriptUi#automount) for the complete list of options.
|
||||
|
||||
## Dealing with SPAs
|
||||
|
||||
|
||||
@@ -306,9 +306,12 @@ Follow the [Devtools Example](https://github.com/wxt-dev/examples/tree/main/exam
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Options Title</title>
|
||||
|
||||
<!-- Customize the manifest options -->
|
||||
<meta name="manifest.open_in_tab" content="true|false" />
|
||||
<meta name="manifest.chrome_style" content="true|false" />
|
||||
<meta name="manifest.browser_style" content="true|false" />
|
||||
|
||||
<!-- Set include/exclude if the page should be removed from some builds -->
|
||||
<meta name="manifest.include" content="['chrome', ...]" />
|
||||
<meta name="manifest.exclude" content="['chrome', ...]" />
|
||||
@@ -336,7 +339,11 @@ Follow the [Devtools Example](https://github.com/wxt-dev/examples/tree/main/exam
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
<!-- Set the `action.default_title` in the manifest -->
|
||||
<title>Default Popup Title</title>
|
||||
|
||||
<!-- Customize the manifest options -->
|
||||
<meta
|
||||
name="manifest.default_icon"
|
||||
content="{
|
||||
@@ -347,6 +354,7 @@ Follow the [Devtools Example](https://github.com/wxt-dev/examples/tree/main/exam
|
||||
/>
|
||||
<meta name="manifest.type" content="page_action|browser_action" />
|
||||
<meta name="manifest.browser_style" content="true|false" />
|
||||
|
||||
<!-- Set include/exclude if the page should be removed from some builds -->
|
||||
<meta name="manifest.include" content="['chrome', ...]" />
|
||||
<meta name="manifest.exclude" content="['chrome', ...]" />
|
||||
@@ -381,6 +389,7 @@ Firefox does not support sandboxed pages.
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Title</title>
|
||||
|
||||
<!-- Set include/exclude if the page should be removed from some builds -->
|
||||
<meta name="manifest.include" content="['chrome', ...]" />
|
||||
<meta name="manifest.exclude" content="['chrome', ...]" />
|
||||
@@ -413,6 +422,8 @@ In Chrome, side panels use the `side_panel` API, while Firefox uses the `sidebar
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Default Side Panel Title</title>
|
||||
|
||||
<!-- Customize the manifest options -->
|
||||
<meta
|
||||
name="manifest.default_icon"
|
||||
content="{
|
||||
@@ -423,6 +434,7 @@ In Chrome, side panels use the `side_panel` API, while Firefox uses the `sidebar
|
||||
/>
|
||||
<meta name="manifest.open_at_install" content="true|false" />
|
||||
<meta name="manifest.browser_style" content="true|false" />
|
||||
|
||||
<!-- Set include/exclude if the page should be removed from some builds -->
|
||||
<meta name="manifest.include" content="['chrome', ...]" />
|
||||
<meta name="manifest.exclude" content="['chrome', ...]" />
|
||||
@@ -472,6 +484,7 @@ body {
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Title</title>
|
||||
|
||||
<!-- Set include/exclude if the page should be removed from some builds -->
|
||||
<meta name="manifest.include" content="['chrome', ...]" />
|
||||
<meta name="manifest.exclude" content="['chrome', ...]" />
|
||||
|
||||
@@ -4,52 +4,29 @@
|
||||
|
||||
Different browsers provide different global variables for accessing the extension APIs (chrome provides `chrome`, firefox provides `browser`, etc).
|
||||
|
||||
WXT simplifies this - always use `browser`:
|
||||
WXT merges these two into a unified API accessed through the `browser` variable.
|
||||
|
||||
```ts
|
||||
import { browser } from 'wxt/browser';
|
||||
|
||||
browser.action.onClicked.addListener(() => {
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
Other than that, refer to Chrome and Mozilla's documentation for how to use specific APIs. Everything a normal extension can do, WXT can do as well, just via `browser` instead of `chrome`.
|
||||
:::tip
|
||||
With auto-imports enabled, you don't even need to import this variable from `wxt/browser`!
|
||||
:::
|
||||
|
||||
## Webextension Polyfill
|
||||
The `browser` variable WXT provides is a simple export of the `browser` or `chrome` globals provided by the browser at runtime:
|
||||
|
||||
> Since `v0.1.0`
|
||||
<<< @/../packages/wxt/src/browser.ts#snippet
|
||||
|
||||
By default, WXT uses the [`webextension-polyfill` by Mozilla](https://www.npmjs.com/package/webextension-polyfill) to make the extension API consistent between browsers.
|
||||
This means you can use the promise-style API for both MV2 and MV3, and it will work across all browsers (Chromium, Firefox, Safari, etc).
|
||||
|
||||
To access types, you should import the relevant namespace from `wxt/browser`:
|
||||
## Accessing Types
|
||||
|
||||
```ts
|
||||
import { Runtime } from 'wxt/browser';
|
||||
|
||||
function handleMessage(message: any, sender: Runtime.Sender) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Disabling the polyfill
|
||||
|
||||
> Since `v0.19.0`
|
||||
|
||||
After the release of MV3 and Chrome's official deprecation of MV2 in June 2024, the polyfill isn't really doing anything useful anymore.
|
||||
|
||||
You can disable it with a single line:
|
||||
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
export default defineConfig({
|
||||
extensionApi: 'chrome',
|
||||
});
|
||||
```
|
||||
|
||||
This will change `wxt/browser` to simply export the `browser` or `chrome` globals based on browser at runtime:
|
||||
|
||||
<<< @/../packages/wxt/src/browser/chrome.ts#snippet
|
||||
|
||||
Accessing types is a little different with the polyfill disabled. They do not need to be imported; they're available on the `browser` object itself:
|
||||
All types can be accessed via WXT's `browser` object:
|
||||
|
||||
```ts
|
||||
function handleMessage(message: any, sender: browser.runtime.Sender) {
|
||||
@@ -59,7 +36,7 @@ function handleMessage(message: any, sender: browser.runtime.Sender) {
|
||||
|
||||
## Feature Detection
|
||||
|
||||
Depending on the manifest version and browser, some APIs are not available at runtime. If an API is not available, it will be `undefined`.
|
||||
Depending on the manifest version, browser, and permissions, some APIs are not available at runtime. If an API is not available, it will be `undefined`.
|
||||
|
||||
:::warning
|
||||
Types will not help you here. The types WXT provides for `browser` assume all APIs exist. You are responsible for knowing whether an API is available or not.
|
||||
|
||||
@@ -61,14 +61,14 @@ After enabling it, your project structure should look like this:
|
||||
📂 {rootDir}/
|
||||
📁 .output/
|
||||
📁 .wxt/
|
||||
📁 modules/
|
||||
📁 public/
|
||||
📂 src/
|
||||
📁 assets/
|
||||
📁 components/
|
||||
📁 composables/
|
||||
📁 entrypoints/
|
||||
📁 hooks/
|
||||
📁 modules/
|
||||
📁 public/
|
||||
📁 utils/
|
||||
📄 app.config.ts
|
||||
📄 .env
|
||||
|
||||
@@ -6,7 +6,7 @@ You can use the vanilla APIs (see docs above), use [WXT's built-in storage API](
|
||||
|
||||
## Alternatives
|
||||
|
||||
1. [`wxt/storage`](/storage) (recommended): WXT ships with its own wrapper around the vanilla storage APIs that simplifies common use cases
|
||||
1. [`wxt/utils/storage`](/storage) (recommended): WXT ships with its own wrapper around the vanilla storage APIs that simplifies common use cases
|
||||
|
||||
2. DIY: If you're migrating to WXT and already have a storage wrapper, keep using it. In the future, if you want to delete that code, you can use one of these alternatives, but there's no reason to replace working code during a migration.
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ Here are real projects with unit testing setup. Look at the code and tests to se
|
||||
|
||||
### Example Tests
|
||||
|
||||
This example demonstrates that you don't have to mock `browser.storage` (used by `wxt/storage`) in tests - [`@webext-core/fake-browser`](https://webext-core.aklinker1.io/fake-browser/installation) implements storage in-memory so it behaves like it would in a real extension!
|
||||
This example demonstrates that you don't have to mock `browser.storage` (used by `wxt/utils/storage`) in tests - [`@webext-core/fake-browser`](https://webext-core.aklinker1.io/fake-browser/installation) implements storage in-memory so it behaves like it would in a real extension!
|
||||
|
||||
```ts
|
||||
import { describe, it, expect } from 'vitest';
|
||||
@@ -71,6 +71,34 @@ describe('isLoggedIn', () => {
|
||||
});
|
||||
```
|
||||
|
||||
### Mocking WXT APIs
|
||||
|
||||
First, you need to understand how the `#imports` module works. When WXT (and vitest) sees this import during a preprocessing step, the import is replaced with multiple imports pointing to their "real" import path.
|
||||
|
||||
For example, this is what your write in your source code:
|
||||
|
||||
```ts
|
||||
// What you write
|
||||
import { injectScript, createShadowRootUi } from '#imports';
|
||||
```
|
||||
|
||||
But Vitest sees this:
|
||||
|
||||
```ts
|
||||
import { injectScript } from 'wxt/browser';
|
||||
import { createShadowRootUi } from 'wxt/utils/content-script-ui/shadow-root';
|
||||
```
|
||||
|
||||
So in this case, if you wanted to mock `injectScript`, you need to pass in `"wxt/utils/inject-script"`, not `"#imports"`.
|
||||
|
||||
```ts
|
||||
vi.mock("wxt/utils/inject-script", () => ({
|
||||
injectScript: ...
|
||||
}))
|
||||
```
|
||||
|
||||
Refer to your project's `.wxt/types/imports-module.d.ts` file to lookup real import paths for `#imports`. If the file doesn't exist, run [`wxt prepare`](/guide/essentials/config/typescript).
|
||||
|
||||
## Other Testing Frameworks
|
||||
|
||||
To use a different framework, you will likely have to disable auto-imports, setup import aliases, manually mock the extension APIs, and setup the test environment to support all of WXT's features that you use.
|
||||
|
||||
@@ -112,12 +112,12 @@ export default defineWxtModule<AnalyticModuleOptions>({
|
||||
|
||||
```ts
|
||||
import { defineWxtModule } from 'wxt/modules';
|
||||
import 'wxt/sandbox';
|
||||
import 'wxt/utils/define-app-config';
|
||||
|
||||
export interface MyModuleRuntimeOptions {
|
||||
// Add your runtime options here...
|
||||
}
|
||||
declare module 'wxt/sandbox' {
|
||||
declare module 'wxt/utils/define-app-config' {
|
||||
export interface WxtAppConfig {
|
||||
myModule: MyModuleOptions;
|
||||
}
|
||||
@@ -167,6 +167,31 @@ This file could then be loaded at runtime:
|
||||
const res = await fetch(browser.runtime.getURL('/some-text.txt'));
|
||||
```
|
||||
|
||||
#### Add custom entrypoints
|
||||
|
||||
Once the existing files under the `entrypoints/` directory have been discovered, the `entrypoints:found` hook can be used to add custom entrypoints.
|
||||
|
||||
:::info
|
||||
The `entrypoints:found` hook is triggered before validation is carried out on the list of entrypoints. Thus, any custom entrypoints will still be checked for duplicate names and logged during debugging.
|
||||
:::
|
||||
|
||||
```ts
|
||||
import { defineWxtModule } from 'wxt/modules';
|
||||
|
||||
export default defineWxtModule({
|
||||
setup(wxt) {
|
||||
wxt.hook('entrypoints:found', (_, entrypointInfos) => {
|
||||
// Add your new entrypoint
|
||||
entrypointInfos.push({
|
||||
name: 'my-custom-script',
|
||||
inputPath: 'path/to/custom-script.js',
|
||||
type: 'content-script',
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
#### Generate runtime module
|
||||
|
||||
Create a file in `.wxt`, add an alias to import it, and add auto-imports for exported variables.
|
||||
|
||||
@@ -28,7 +28,7 @@ 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 `createShadowRoot`, your UI is isolated from the document's styles because it's inside a ShadowRoot.
|
||||
Component libraries place their CSS in the document's `<head>` by default. When using `createShadowRootUi`, your UI is isolated from the document's styles because it's inside a ShadowRoot.
|
||||
|
||||
To fix this, you need to tell your component library to insert it's CSS inside the shadow root. Here's the docs for a couple of popular libraries:
|
||||
|
||||
@@ -38,7 +38,7 @@ To fix this, you need to tell your component library to insert it's CSS inside t
|
||||
|
||||
> If your library isn't listed above, try searching it's docs/issues for "shadow root", "shadow dom", or "css container".
|
||||
|
||||
`createShadowRoot` 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:
|
||||
`createShadowRootUi` provides it's own `<head>` element inside the shadow root, so that were you should tell the library to add the CSS. Here's an example with Ant Design:
|
||||
|
||||
```tsx
|
||||
import { StyleProvider } from '@ant-design/cssinjs'; // [!code ++]
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
outline: deep
|
||||
---
|
||||
|
||||
# Upgrading WXT
|
||||
|
||||
## Overview
|
||||
@@ -12,6 +16,209 @@ Listed below are all the breaking changes you should address when upgrading to a
|
||||
|
||||
Currently, WXT is in pre-release. This means changes to the second digit, `v0.X`, are considered major and have breaking changes. Once v1 is released, only major version bumps will have breaking changes.
|
||||
|
||||
## v0.19.0 → v0.20.0
|
||||
|
||||
v0.20 is a big release! There are lots of breaking changes because this version is intended to be a release candidate for v1.0. If all goes well, v1.0 will be released with no additional breaking changes.
|
||||
|
||||
:::tip
|
||||
Read through all the changes once before making any code changes.
|
||||
:::
|
||||
|
||||
### `webextension-polyfill` Removed
|
||||
|
||||
WXT no longer uses the `webextension-polyfill` internally and `wxt/browser` uses the `chrome`/`browser` globals provided by the browser.
|
||||
|
||||
To upgrade, you have two options:
|
||||
|
||||
1. **Stop using the polyfill**
|
||||
- Replace any manual imports from `wxt/browser/chrome` with `wxt/browser`
|
||||
2. **Continue using the polyfill**
|
||||
- Install the polyfill and WXT's [new polyfill module](https://www.npmjs.com/package/@wxt-dev/webextension-polyfill):
|
||||
```sh
|
||||
pnpm i webextension-polyfill @wxt-dev/webextension-polyfill
|
||||
```
|
||||
- Add the WXT module to your config:
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
export default defineConfig({
|
||||
modules: ['@wxt-dev/webextension-polyfill'],
|
||||
});
|
||||
```
|
||||
|
||||
Regardless of your choice, the `extensionApi` config has been removed. Remove it from your `wxt.config.ts` file if present:
|
||||
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
export default defineConfig({
|
||||
extensionApi: 'chrome', // [!code --]
|
||||
});
|
||||
```
|
||||
|
||||
Additionally, extension API types have changed. `wxt/browser` now uses types from `@types/chrome` instead of `@types/webextension-polyfill`. You will have to migrate any type imports to use `@types/chrome`'s namespace approach:
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```ts
|
||||
import type { Runtime } from 'wxt/browser'; // [!code --]
|
||||
import { browser } from 'wxt/browser'; // [!code ++]
|
||||
|
||||
function getMessageSenderUrl(sender: Runtime.MessageSender): string { // [!code --]
|
||||
function getMessageSenderUrl(sender: browser.runtime.MessageSender): string { // [!code ++]
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
`@types/chrome` are more up-to-date, contain less bugs, and don't have any auto-generated names. So even if you continue to use the polyfill, you will need to update your types to use these types.
|
||||
|
||||
### `public/` and `modules/` Directories Moved
|
||||
|
||||
The default location for the `public/` and `modules/` directories have changed to better align with standards set by other frameworks (Nuxt, Next, Astro, etc). Now, each path is relative to the project's root directory.
|
||||
|
||||
- If you follow the default folder structure, you don't need to make any changes.
|
||||
- If you set a custom `srcDir`, you have two options:
|
||||
1. Keep the folders in the same place and update your project config:
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
export default defineConfig({
|
||||
srcDir: 'src',
|
||||
publicDir: 'src/public', // [!code ++]
|
||||
modulesDir: 'src/modules', // [!code ++]
|
||||
});
|
||||
```
|
||||
2. Move the your `public/` and `modules/` directories to the project root:
|
||||
```diff
|
||||
<root>/
|
||||
+ modules/
|
||||
+ public/
|
||||
src/
|
||||
components/
|
||||
entrypoints/
|
||||
- modules/
|
||||
- public/
|
||||
utils/
|
||||
wxt.config.ts
|
||||
```
|
||||
|
||||
### Import Path Changes
|
||||
|
||||
The APIs exported by `wxt/sandbox`, `wxt/client`, or `wxt/storage` have moved to `wxt/utils/*`.
|
||||
|
||||
To upgrade, replace these imports with the new `#imports` module:
|
||||
|
||||
```ts
|
||||
import { storage } from 'wxt/storage'; // [!code --]
|
||||
import { defineContentScript } from 'wxt/sandbox'; // [!code --]
|
||||
import { ContentScriptContext, useAppConfig } from 'wxt/client'; // [!code --]
|
||||
import { storage } from '#imports'; // [!code ++]
|
||||
import { defineContentScript } from '#imports'; // [!code ++]
|
||||
import { ContentScriptContext, useAppConfig } from '#imports'; // [!code ++]
|
||||
```
|
||||
|
||||
You can combine the imports into a single import statement, but it's easier to just find/replace each statement.
|
||||
|
||||
:::tip
|
||||
Before types will work, you'll need to run `wxt prepare` after installing v0.20 to generate the new TypeScript declarations.
|
||||
:::
|
||||
|
||||
Read more about the new `#imports` module in the [blog post](/blog/2024-12-06-using-imports-module).
|
||||
|
||||
### `createShadowRootUi` CSS Changes
|
||||
|
||||
WXT now resets styles inherited from the webpage (`visibility`, `color`, `font-size`, etc.) by setting `all: initial` inside the shadow root.
|
||||
|
||||
If you use `createShadowRootUi`:
|
||||
|
||||
1. Double check that your UI looks the same as before.
|
||||
2. If you have any manual CSS resets to override a page style, you can remove them:
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```css
|
||||
/* entrypoints/reddit.content/style.css */
|
||||
body { /* [!code --] */
|
||||
/* Override Reddit's default "hidden" visibility on elements */ /* [!code --] */
|
||||
visibility: visible !important; /* [!code --] */
|
||||
} /* [!code --] */
|
||||
```
|
||||
|
||||
:::warning
|
||||
This doesn't effect `rem` units. You should continue using `postcss-rem-to-px` or an equivalent library if the webpage sets the HTML element's `font-size`.
|
||||
:::
|
||||
|
||||
If you run into problems with the new behavior, you can disable it and continue using your current CSS:
|
||||
|
||||
```ts
|
||||
const ui = await createShadowRootUi({
|
||||
inheritStyles: true, // [!code ++]
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
### Default Output Directories Changed
|
||||
|
||||
The default value for [`outDirTemplate`](/api/reference/wxt/interfaces/InlineConfig#outdirtemplate) has changed. Now, different build modes are output to different directories:
|
||||
|
||||
- `--mode production`: `.output/chrome-mv3` (unchanged)
|
||||
- `--mode development`: `.output/chrome-mv3-dev` (`-dev` suffix)
|
||||
- `--mode custom`: `.output/chrome-mv3-custom` (`-[mode]` suffix)
|
||||
|
||||
To revert and use the old behavior, writing all output to the same directory, set `outDirTemplate` option:
|
||||
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
export default defineConfig({
|
||||
outDirTemplate: '{{browser}}-mv{{manifestVersion}}', // [!code ++]
|
||||
});
|
||||
```
|
||||
|
||||
### `runner` APIs Renamed
|
||||
|
||||
To improve consistency with the `web-ext.config.ts` file, the "runner" APIs have been renamed. You can continue using the old names, but they have been deprecated and will be removed in a future version:
|
||||
|
||||
1. The `runner` option has been renamed to `webExt`:
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
export default defineConfig({
|
||||
runner: { // [!code --]
|
||||
webExt: { // [!code ++]
|
||||
startUrls: ["https://wxt.dev"],
|
||||
},
|
||||
});
|
||||
```
|
||||
2. `defineRunnerConfig` has been renamed to `defineWebExtConfig`:
|
||||
```ts
|
||||
// web-ext.config.ts
|
||||
import { defineRunnerConfig } from 'wxt'; // [!code --]
|
||||
import { defineWebExtConfig } from 'wxt'; // [!code ++]
|
||||
```
|
||||
3. The `ExtensionRunnerConfig` type has been renamed to `WebExtConfig`
|
||||
```ts
|
||||
import type { ExtensionRunnerConfig } from 'wxt'; // [!code --]
|
||||
import type { WebExtConfig } from 'wxt'; // [!code ++]
|
||||
```
|
||||
|
||||
### Load Config as ESM
|
||||
|
||||
`wxt.config.ts` and `web-ext.config.ts` are now loaded as ESM modules. Previously, they were loaded as CJS.
|
||||
|
||||
If you're using any CJS APIs, like `__filename` or `__dirname`, replace them with their ESM counterparts, like `import.meta.filename` or `import.meta.dirname`.
|
||||
|
||||
### Deprecated APIs Removed
|
||||
|
||||
- `entrypointLoader` option: WXT now uses `vite-node` for importing entrypoints during the build process.
|
||||
> This was deprecated in v0.19.0, see the [v0.19 section](#v0-18-5-rarr-v0-19-0) for migration steps.
|
||||
- `transformManifest` option: Use the `build:manifestGenerated` hook to transform the manifest instead:
|
||||
<!-- prettier-ignore -->
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
export default defineConfig({
|
||||
transformManifest(manifest) { // [!code --]
|
||||
hooks: { // [!code ++]
|
||||
'build:manifestGenerated': (_, manifest) => { // [!code ++]
|
||||
// ...
|
||||
}, // [!code ++]
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## v0.18.5 → v0.19.0
|
||||
|
||||
### `vite-node` Entrypoint Loader
|
||||
|
||||
+6
-6
@@ -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 'wxt/storage';
|
||||
import { storage } from '#imports';
|
||||
```
|
||||
|
||||
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/storage` API, the `"storage"` permission must be added to the manifest:
|
||||
To use the `@wxt-dev/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-dev/storage/interfaces/WxtStorage).
|
||||
For a full list of methods available, see the [API reference](/api/reference/wxt/utils/storage/interfaces/WxtStorage).
|
||||
|
||||
## Watchers
|
||||
|
||||
@@ -98,7 +98,7 @@ unwatch();
|
||||
|
||||
## Metadata
|
||||
|
||||
`wxt/storage` also supports setting metadata for keys, stored at `key + "$"`. Metadata is a collection of properties associated with a key. It might be a version number, last modified date, etc.
|
||||
`@wxt-dev/storage` also supports setting metadata for keys, stored at `key + "$"`. Metadata is a collection of properties associated with a key. It might be a version number, last modified date, etc.
|
||||
|
||||
[Other than versioning](#versioning), you are responsible for managing a field's metadata:
|
||||
|
||||
@@ -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-dev/storage/interfaces/WxtStorageItem).
|
||||
For a full list of properties and methods available, see the [API reference](/api/reference/wxt/utils/storage/interfaces/WxtStorageItem).
|
||||
|
||||
### Versioning
|
||||
|
||||
@@ -354,4 +354,4 @@ await storage.setItems([
|
||||
]);
|
||||
```
|
||||
|
||||
Refer to the [API Reference](/api/reference/@wxt-dev/storage/interfaces/WxtStorage) for types and examples of how to use all the bulk APIs.
|
||||
Refer to the [API Reference](/api/reference/wxt/utils/storage/interfaces/WxtStorage) for types and examples of how to use all the bulk APIs.
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"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",
|
||||
|
||||
@@ -49,7 +49,7 @@ However, it does have one major downside:
|
||||
helloWorld: Hello world!
|
||||
```
|
||||
|
||||
> `@wxt-dev/i18n` supports the standard messages format, so if you already have localization files at `<srcDir>/public/_locale/<lang>/messages.json`, you don't need to convert them to YAML or refactor them - just move them to `<srcDir>/locales/<lang>.json` and they'll just work out of the box!
|
||||
> `@wxt-dev/i18n` supports the standard messages format, so if you already have localization files at `<rootDir>/public/_locale/<lang>/messages.json`, you don't need to convert them to YAML or refactor them - just move them to `<srcDir>/locales/<lang>.json` and they'll just work out of the box!
|
||||
|
||||
4. To get a translation, use the auto-imported `i18n` object or import it manually:
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { defineContentScript } from 'wxt/sandbox';
|
||||
import { ContentScriptContext, createShadowRootUi } from 'wxt/client';
|
||||
import {
|
||||
defineContentScript,
|
||||
ContentScriptContext,
|
||||
createShadowRootUi,
|
||||
} from '#imports';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
|
||||
|
||||
@@ -16,7 +16,8 @@ export default defineWxtModule<ReactModuleOptions>({
|
||||
|
||||
// Enable auto-imports for JSX files
|
||||
wxt.hook('config:resolved', (wxt) => {
|
||||
if (wxt.config.imports === false) return;
|
||||
// In older versions of WXT, `wxt.config.imports` could be false
|
||||
if (!wxt.config.imports) return;
|
||||
|
||||
wxt.config.imports.dirsScanOptions ??= {};
|
||||
wxt.config.imports.dirsScanOptions.filePatterns = [
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { defineContentScript } from 'wxt/sandbox';
|
||||
import { ContentScriptContext, createShadowRootUi } from 'wxt/client';
|
||||
import {
|
||||
defineContentScript,
|
||||
ContentScriptContext,
|
||||
createShadowRootUi,
|
||||
} from '#imports';
|
||||
import { render } from 'solid-js/web';
|
||||
|
||||
export default defineContentScript({
|
||||
|
||||
@@ -19,7 +19,8 @@ export default defineWxtModule<SolidModuleOptions>({
|
||||
|
||||
// Enable auto-imports for JSX files
|
||||
wxt.hook('config:resolved', (wxt) => {
|
||||
if (wxt.config.imports === false) return;
|
||||
// In older versions of WXT, `wxt.config.imports` could be false
|
||||
if (!wxt.config.imports) return;
|
||||
|
||||
wxt.config.imports.dirsScanOptions ??= {};
|
||||
wxt.config.imports.dirsScanOptions.filePatterns = [
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# `@wxt-dev/webextension-polyfill`
|
||||
|
||||
Configures `wxt/browser` to import `browser` from [`webextension-polyfill`](https://github.com/mozilla/webextension-polyfill) instead of using the regular `chrome`/`browser` globals WXT normally provides.
|
||||
|
||||
## Usage
|
||||
|
||||
```sh
|
||||
pnpm i @wxt-dev/webextension-polyfill webextension-polyfill
|
||||
```
|
||||
|
||||
Then add the module to your config:
|
||||
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
export default defineConfig({
|
||||
modules: ['@wxt-dev/webextension-polyfill'],
|
||||
});
|
||||
```
|
||||
@@ -0,0 +1,15 @@
|
||||
import { defineBuildConfig } from 'unbuild';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
export default defineBuildConfig({
|
||||
rootDir: 'modules',
|
||||
outDir: resolve(__dirname, 'dist'),
|
||||
entries: [
|
||||
{ input: 'webextension-polyfill/index.ts', name: 'index' },
|
||||
{ input: 'webextension-polyfill/browser.ts', name: 'browser' },
|
||||
],
|
||||
replace: {
|
||||
'process.env.NPM': 'true',
|
||||
},
|
||||
declaration: true,
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
export default defineContentScript({
|
||||
matches: ['*://*/*'],
|
||||
async main() {
|
||||
console.log(browser.runtime.id);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Document</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="./main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,3 @@
|
||||
const root = document.getElementById('app')!;
|
||||
|
||||
root.textContent = browser.runtime.id;
|
||||
@@ -0,0 +1 @@
|
||||
export { default as browser } from 'webextension-polyfill';
|
||||
@@ -0,0 +1,18 @@
|
||||
import 'wxt';
|
||||
import { addViteConfig, defineWxtModule } from 'wxt/modules';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
export default defineWxtModule({
|
||||
name: '@wxt-dev/webextension-polyfill',
|
||||
setup(wxt) {
|
||||
addViteConfig(wxt, () => ({
|
||||
resolve: {
|
||||
alias: {
|
||||
'wxt/browser': process.env.NPM
|
||||
? '@wxt-dev/webextension-polyfill/browser'
|
||||
: resolve(__dirname, 'browser.ts'),
|
||||
},
|
||||
},
|
||||
}));
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"name": "@wxt-dev/webextension-polyfill",
|
||||
"description": "Use webextension-polyfill with WXT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/wxt-dev/wxt.git",
|
||||
"directory": "packages/webextension-polyfill"
|
||||
},
|
||||
"homepage": "https://github.com/wxt-dev/wxt/blob/main/packages/webextension-polyfill/README.md",
|
||||
"keywords": [
|
||||
"wxt",
|
||||
"module",
|
||||
"webextension-polyfill"
|
||||
],
|
||||
"author": {
|
||||
"name": "Aaron Klinker",
|
||||
"email": "aaronklinker1+wxt@gmail.com"
|
||||
},
|
||||
"license": "MIT",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"main": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.mts",
|
||||
"default": "./dist/index.mjs"
|
||||
},
|
||||
"./browser": {
|
||||
"types": "./dist/browser.d.mts",
|
||||
"default": "./dist/browser.mjs"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "wxt",
|
||||
"check": "pnpm build && check",
|
||||
"build": "buildc -- unbuild",
|
||||
"prepare": "buildc --deps-only -- wxt prepare"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"webextension-polyfill": "*",
|
||||
"wxt": ">=0.20.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "^1.4.5",
|
||||
"@types/webextension-polyfill": "^0.12.1",
|
||||
"publint": "^0.2.12",
|
||||
"typescript": "^5.6.3",
|
||||
"unbuild": "^2.0.0",
|
||||
"webextension-polyfill": "^0.12.0",
|
||||
"wxt": "workspace:*"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": ["../../tsconfig.base.json", "./.wxt/tsconfig.json"],
|
||||
"exclude": ["node_modules/**", "dist/**"]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { defineAppConfig } from 'wxt/sandbox';
|
||||
import { defineAppConfig } from '#imports';
|
||||
|
||||
declare module 'wxt/sandbox' {
|
||||
declare module 'wxt/utils/define-app-config' {
|
||||
export interface WxtAppConfig {
|
||||
example: string;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ describe('Background Entrypoint', () => {
|
||||
fakeBrowser.reset();
|
||||
});
|
||||
|
||||
it("should log the extenion's runtime ID", () => {
|
||||
it("should log the extension's runtime ID", () => {
|
||||
const id = 'some-id';
|
||||
fakeBrowser.runtime.id = id;
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import { presetUno } from 'unocss';
|
||||
|
||||
export default defineConfig({
|
||||
srcDir: 'src',
|
||||
extensionApi: 'chrome',
|
||||
manifest: {
|
||||
permissions: ['storage'],
|
||||
default_locale: 'en',
|
||||
|
||||
@@ -1,5 +1,31 @@
|
||||
# Changelog
|
||||
|
||||
## v0.19.23
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.22...wxt-v0.19.23)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- **hooks:** Move `entrypoints:resolved` before debug logs and add `entrypoints:found` ([#1292](https://github.com/wxt-dev/wxt/pull/1292))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Allow runtime registered content scripts to not have `matches` ([#1306](https://github.com/wxt-dev/wxt/pull/1306))
|
||||
- Properly close readline instance on close ([#1278](https://github.com/wxt-dev/wxt/pull/1278))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Sync READMEs ([27298b7](https://github.com/wxt-dev/wxt/commit/27298b7))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Simplify keyboard shortcuts ([#1284](https://github.com/wxt-dev/wxt/pull/1284))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron ([@aklinker1](http://github.com/aklinker1))
|
||||
- Abhigyan Trips ([@abhigyantrips](http://github.com/abhigyantrips))
|
||||
|
||||
## v0.19.22
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.19.21...wxt-v0.19.22)
|
||||
@@ -2749,4 +2775,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))
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { defineBuildConfig } from 'unbuild';
|
||||
import { version } from './package.json';
|
||||
import { version, exports } from './package.json';
|
||||
import { readFile, writeFile } from 'fs/promises';
|
||||
import {
|
||||
virtualEntrypointModuleNames,
|
||||
virtualModuleNames,
|
||||
} from './src/core/utils/virtual-modules';
|
||||
|
||||
const replace = {
|
||||
__vite_ssr_import_meta__: 'undefined',
|
||||
};
|
||||
|
||||
export default defineBuildConfig([
|
||||
// Non-virtual modules can be transpiled with mkdist
|
||||
{
|
||||
@@ -15,8 +19,12 @@ export default defineBuildConfig([
|
||||
input: 'src',
|
||||
pattern: ['**/*', '!**/__tests__', '!**/*.md', '!virtual', '!@types'],
|
||||
declaration: true,
|
||||
esbuild: {
|
||||
define: replace,
|
||||
},
|
||||
},
|
||||
],
|
||||
replace,
|
||||
hooks: {
|
||||
async 'build:done'() {
|
||||
// Replace any template variables in output files
|
||||
@@ -32,10 +40,7 @@ export default defineBuildConfig([
|
||||
...virtualEntrypointModuleNames.map((name) => `virtual:user-${name}`),
|
||||
'virtual:wxt-plugins',
|
||||
'virtual:app-config',
|
||||
'wxt/browser',
|
||||
'wxt/sandbox',
|
||||
'wxt/client',
|
||||
'wxt/testing',
|
||||
...Object.keys(exports).map((path) => 'wxt' + path.slice(1)), // ./utils/storage => wxt/utils/storage
|
||||
],
|
||||
})),
|
||||
]);
|
||||
|
||||
@@ -15,7 +15,6 @@ exports[`Auto Imports > eslintrc > "enabled: 8" should output a JSON config file
|
||||
"createShadowRootUi": true,
|
||||
"defineAppConfig": true,
|
||||
"defineBackground": true,
|
||||
"defineConfig": true,
|
||||
"defineContentScript": true,
|
||||
"defineUnlistedScript": true,
|
||||
"defineWxtPlugin": true,
|
||||
@@ -42,7 +41,6 @@ const globals = {
|
||||
"createShadowRootUi": true,
|
||||
"defineAppConfig": true,
|
||||
"defineBackground": true,
|
||||
"defineConfig": true,
|
||||
"defineContentScript": true,
|
||||
"defineUnlistedScript": true,
|
||||
"defineWxtPlugin": true,
|
||||
@@ -77,7 +75,6 @@ exports[`Auto Imports > eslintrc > "enabled: true" should output a JSON config f
|
||||
"createShadowRootUi": true,
|
||||
"defineAppConfig": true,
|
||||
"defineBackground": true,
|
||||
"defineConfig": true,
|
||||
"defineContentScript": true,
|
||||
"defineUnlistedScript": true,
|
||||
"defineWxtPlugin": true,
|
||||
@@ -105,7 +102,6 @@ exports[`Auto Imports > eslintrc > should allow customizing the output 1`] = `
|
||||
"createShadowRootUi": "readonly",
|
||||
"defineAppConfig": "readonly",
|
||||
"defineBackground": "readonly",
|
||||
"defineConfig": "readonly",
|
||||
"defineContentScript": "readonly",
|
||||
"defineUnlistedScript": "readonly",
|
||||
"defineWxtPlugin": "readonly",
|
||||
|
||||
@@ -17,24 +17,23 @@ describe('Auto Imports', () => {
|
||||
// Generated by wxt
|
||||
export {}
|
||||
declare global {
|
||||
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 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 MigrationError: typeof import('wxt/utils/storage')['MigrationError']
|
||||
const browser: typeof import('wxt/browser')['browser']
|
||||
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 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 fakeBrowser: typeof import('wxt/testing')['fakeBrowser']
|
||||
const injectScript: typeof import('wxt/client')['injectScript']
|
||||
const storage: typeof import('wxt/storage')['storage']
|
||||
const useAppConfig: typeof import('wxt/client')['useAppConfig']
|
||||
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']
|
||||
}
|
||||
"
|
||||
`);
|
||||
@@ -55,10 +54,53 @@ 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 { MigrationError, storage } from 'wxt/utils/storage';
|
||||
export { useAppConfig } from 'wxt/utils/app-config';
|
||||
export { ContentScriptContext } from 'wxt/utils/content-script-context';
|
||||
export { createIframeUi } from 'wxt/utils/content-script-ui/iframe';
|
||||
export { createIntegratedUi } from 'wxt/utils/content-script-ui/integrated';
|
||||
export { createShadowRootUi } from 'wxt/utils/content-script-ui/shadow-root';
|
||||
export { defineAppConfig } 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 } 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', () => {
|
||||
@@ -74,7 +116,7 @@ describe('Auto Imports', () => {
|
||||
expect(await project.fileExists('.wxt/types/imports.d.ts')).toBe(false);
|
||||
});
|
||||
|
||||
it('should not include imports.d.ts in the type references', async () => {
|
||||
it('should only include imports-module.d.ts in the the project', async () => {
|
||||
const project = new TestProject();
|
||||
project.setConfigFileConfig({
|
||||
imports: false,
|
||||
@@ -94,10 +136,55 @@ 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 { MigrationError, storage } from 'wxt/utils/storage';
|
||||
export { useAppConfig } from 'wxt/utils/app-config';
|
||||
export { ContentScriptContext } from 'wxt/utils/content-script-context';
|
||||
export { createIframeUi } from 'wxt/utils/content-script-ui/iframe';
|
||||
export { createIntegratedUi } from 'wxt/utils/content-script-ui/integrated';
|
||||
export { createShadowRootUi } from 'wxt/utils/content-script-ui/shadow-root';
|
||||
export { defineAppConfig } 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 } from 'wxt/utils/inject-script';
|
||||
export { InvalidMatchPattern, MatchPattern } from 'wxt/utils/match-patterns';
|
||||
export { fakeBrowser } from 'wxt/testing';
|
||||
}
|
||||
"
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('eslintrc', () => {
|
||||
|
||||
@@ -11,6 +11,7 @@ const hooks: WxtHooks = {
|
||||
'build:done': vi.fn(),
|
||||
'build:manifestGenerated': vi.fn(),
|
||||
'build:publicAssets': vi.fn(),
|
||||
'entrypoints:found': vi.fn(),
|
||||
'entrypoints:resolved': vi.fn(),
|
||||
'entrypoints:grouped': vi.fn(),
|
||||
'vite:build:extendConfig': vi.fn(),
|
||||
@@ -60,6 +61,7 @@ describe('Hooks', () => {
|
||||
'build:done': false,
|
||||
'build:publicAssets': false,
|
||||
'build:manifestGenerated': false,
|
||||
'entrypoints:found': true,
|
||||
'entrypoints:grouped': false,
|
||||
'entrypoints:resolved': true,
|
||||
'vite:build:extendConfig': false,
|
||||
@@ -91,6 +93,7 @@ describe('Hooks', () => {
|
||||
'build:done': true,
|
||||
'build:publicAssets': true,
|
||||
'build:manifestGenerated': true,
|
||||
'entrypoints:found': true,
|
||||
'entrypoints:grouped': true,
|
||||
'entrypoints:resolved': true,
|
||||
'vite:build:extendConfig': 1,
|
||||
@@ -122,6 +125,7 @@ describe('Hooks', () => {
|
||||
'build:done': true,
|
||||
'build:publicAssets': true,
|
||||
'build:manifestGenerated': true,
|
||||
'entrypoints:found': true,
|
||||
'entrypoints:grouped': true,
|
||||
'entrypoints:resolved': true,
|
||||
'vite:build:extendConfig': 1,
|
||||
@@ -153,6 +157,7 @@ describe('Hooks', () => {
|
||||
'build:done': true,
|
||||
'build:publicAssets': true,
|
||||
'build:manifestGenerated': true,
|
||||
'entrypoints:found': 2,
|
||||
'entrypoints:grouped': true,
|
||||
'entrypoints:resolved': 2,
|
||||
'vite:build:extendConfig': 1,
|
||||
@@ -175,7 +180,7 @@ describe('Hooks', () => {
|
||||
|
||||
const server = await project.startServer({
|
||||
hooks,
|
||||
runner: {
|
||||
webExt: {
|
||||
disabled: true,
|
||||
},
|
||||
});
|
||||
@@ -191,6 +196,7 @@ describe('Hooks', () => {
|
||||
'build:done': true,
|
||||
'build:publicAssets': true,
|
||||
'build:manifestGenerated': true,
|
||||
'entrypoints:found': true,
|
||||
'entrypoints:grouped': true,
|
||||
'entrypoints:resolved': true,
|
||||
'vite:build:extendConfig': 2,
|
||||
|
||||
@@ -1,36 +1,33 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { TestProject } from '../utils';
|
||||
|
||||
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();
|
||||
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();
|
||||
|
||||
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, experimental: { entrypointImporter } });
|
||||
);
|
||||
await project.build({ browser });
|
||||
|
||||
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);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -191,10 +191,7 @@ describe('Module Helpers', () => {
|
||||
);
|
||||
const expectedText = addPluginModule(project);
|
||||
|
||||
await project.build({
|
||||
// reduce build output when comparing test failures
|
||||
extensionApi: 'chrome',
|
||||
});
|
||||
await project.build();
|
||||
|
||||
await expect(project.serializeOutput()).resolves.toContain(expectedText);
|
||||
});
|
||||
@@ -211,10 +208,7 @@ describe('Module Helpers', () => {
|
||||
);
|
||||
const expectedText = addPluginModule(project);
|
||||
|
||||
await project.build({
|
||||
// reduce build output when comparing test failures
|
||||
extensionApi: 'chrome',
|
||||
});
|
||||
await project.build();
|
||||
|
||||
await expect(project.serializeOutput()).resolves.toContain(expectedText);
|
||||
});
|
||||
@@ -232,10 +226,7 @@ describe('Module Helpers', () => {
|
||||
);
|
||||
const expectedText = addPluginModule(project);
|
||||
|
||||
await project.build({
|
||||
// reduce build output when comparing test failures
|
||||
extensionApi: 'chrome',
|
||||
});
|
||||
await project.build();
|
||||
|
||||
await expect(project.serializeOutput()).resolves.toContain(expectedText);
|
||||
});
|
||||
@@ -248,10 +239,7 @@ describe('Module Helpers', () => {
|
||||
);
|
||||
const expectedText = addPluginModule(project);
|
||||
|
||||
await project.build({
|
||||
// reduce build output when comparing test failures
|
||||
extensionApi: 'chrome',
|
||||
});
|
||||
await project.build();
|
||||
|
||||
await expect(project.serializeOutput()).resolves.toContain(expectedText);
|
||||
});
|
||||
|
||||
@@ -262,9 +262,6 @@ 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
|
||||
@@ -347,9 +344,6 @@ 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,6 +239,8 @@ 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,6 +1,5 @@
|
||||
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.
|
||||
@@ -88,24 +87,6 @@ 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(
|
||||
|
||||
@@ -63,6 +63,7 @@ describe('Zipping', () => {
|
||||
"name": "test",
|
||||
"description": "Example description",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"wxt": "../../../../..",
|
||||
"flatten": "1.0.3"
|
||||
|
||||
@@ -39,6 +39,7 @@ export class TestProject {
|
||||
name: 'E2E Extension',
|
||||
description: 'Example description',
|
||||
version: '0.0.0',
|
||||
type: 'module',
|
||||
dependencies: {
|
||||
wxt: '../../..',
|
||||
},
|
||||
|
||||
+108
-71
@@ -1,74 +1,9 @@
|
||||
{
|
||||
"name": "wxt",
|
||||
"type": "module",
|
||||
"version": "0.19.22",
|
||||
"version": "0.19.23",
|
||||
"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",
|
||||
@@ -84,13 +19,12 @@
|
||||
"@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": "^1.11.2",
|
||||
"c12": "^2.0.1",
|
||||
"cac": "^6.7.14",
|
||||
"chokidar": "^3.6.0",
|
||||
"ci-info": "^4.1.0",
|
||||
@@ -105,7 +39,6 @@
|
||||
"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",
|
||||
@@ -125,8 +58,7 @@
|
||||
"unimport": "^3.13.1",
|
||||
"vite": "^5.0.0 || ^6.0.0",
|
||||
"vite-node": "^2.1.4",
|
||||
"web-ext-run": "^0.2.1",
|
||||
"webextension-polyfill": "^0.12.0"
|
||||
"web-ext-run": "^0.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "^1.4.5",
|
||||
@@ -151,5 +83,110 @@
|
||||
"@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,20 +67,5 @@ 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/// <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
|
||||
@@ -1,23 +0,0 @@
|
||||
/// <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
|
||||
@@ -1,87 +0,0 @@
|
||||
/**
|
||||
* 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';
|
||||
@@ -1,29 +1,28 @@
|
||||
import { addViteConfig, defineWxtModule } from '../modules';
|
||||
import type {
|
||||
EslintGlobalsPropValue,
|
||||
Wxt,
|
||||
WxtDirFileEntry,
|
||||
WxtModule,
|
||||
WxtResolvedUnimportOptions,
|
||||
} from '../types';
|
||||
import { type Unimport, createUnimport } from 'unimport';
|
||||
import { type Unimport, createUnimport, toExports } 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;
|
||||
|
||||
options.imports ??= [];
|
||||
options.imports.push(...module.imports);
|
||||
wxt.config.imports.imports ??= [];
|
||||
wxt.config.imports.imports.push(...module.imports);
|
||||
};
|
||||
|
||||
wxt.config.builtinModules.forEach(addModuleImports);
|
||||
@@ -34,7 +33,7 @@ export default defineWxtModule({
|
||||
// config inside "config:resolved" are applied.
|
||||
wxt.hooks.afterEach((event) => {
|
||||
if (event.name === 'config:resolved') {
|
||||
unimport = createUnimport(options);
|
||||
unimport = createUnimport(wxt.config.imports);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -43,22 +42,37 @@ 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 (options.eslintrc.enabled === false) return;
|
||||
if (wxt.config.imports.eslintrc.enabled === false) return;
|
||||
|
||||
// Only generate ESLint config if that feature is enabled
|
||||
entries.push(
|
||||
await getEslintConfigEntry(unimport, options.eslintrc.enabled, options),
|
||||
await getEslintConfigEntry(
|
||||
unimport,
|
||||
wxt.config.imports.eslintrc.enabled,
|
||||
wxt.config.imports,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
// Add vite plugin
|
||||
addViteConfig(wxt, () => ({
|
||||
plugins: [vitePlugin(unimport)],
|
||||
plugins: [vitePlugin(unimport, isEnabled())],
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
export function vitePlugin(unimport: Unimport): Plugin {
|
||||
export function vitePlugin(
|
||||
unimport: Unimport,
|
||||
autoImportsEnabled: boolean,
|
||||
): Plugin {
|
||||
const ENABLED_EXTENSIONS = new Set([
|
||||
'.js',
|
||||
'.jsx',
|
||||
@@ -76,7 +90,12 @@ export function vitePlugin(unimport: Unimport): Plugin {
|
||||
// Don't transform non-js files
|
||||
if (!ENABLED_EXTENSIONS.has(extname(id))) return;
|
||||
|
||||
const injected = await unimport.injectImports(code, id);
|
||||
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,
|
||||
});
|
||||
return {
|
||||
code: injected.code,
|
||||
map: injected.s.generateMap({ hires: 'boundary', source: id }),
|
||||
@@ -88,9 +107,6 @@ export function vitePlugin(unimport: Unimport): Plugin {
|
||||
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: [
|
||||
@@ -102,6 +118,25 @@ 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).replaceAll('\n', '\n '),
|
||||
'}',
|
||||
'',
|
||||
].join('\n'),
|
||||
tsReference: true,
|
||||
};
|
||||
}
|
||||
|
||||
async function getEslintConfigEntry(
|
||||
unimport: Unimport,
|
||||
version: 8 | 9,
|
||||
|
||||
@@ -2,9 +2,6 @@ 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,2 +0,0 @@
|
||||
export * from './content-script-context';
|
||||
export * from './ui';
|
||||
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* Any runtime APIs that use the web extension APIs.
|
||||
*
|
||||
* @module wxt/client
|
||||
*/
|
||||
export * from './content-scripts';
|
||||
export * from './app-config';
|
||||
export * from './inject-script';
|
||||
@@ -20,7 +20,6 @@ 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';
|
||||
@@ -78,7 +77,6 @@ export async function createViteBuilder(
|
||||
wxtPlugins.tsconfigPaths(wxtConfig),
|
||||
wxtPlugins.noopBackground(),
|
||||
wxtPlugins.globals(wxtConfig),
|
||||
wxtPlugins.resolveExtensionApi(wxtConfig),
|
||||
wxtPlugins.defineImportMeta(),
|
||||
wxtPlugins.wxtPluginLoader(wxtConfig),
|
||||
wxtPlugins.resolveAppConfig(wxtConfig),
|
||||
@@ -281,44 +279,26 @@ export async function createViteBuilder(
|
||||
version: vite.version,
|
||||
async importEntrypoint(path) {
|
||||
const env = createExtensionEnvironment();
|
||||
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;
|
||||
}
|
||||
}
|
||||
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();
|
||||
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;
|
||||
}
|
||||
}
|
||||
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;
|
||||
@@ -372,6 +352,9 @@ export async function createViteBuilder(
|
||||
},
|
||||
},
|
||||
watcher: viteServer.watcher,
|
||||
on(event, cb) {
|
||||
viteServer.httpServer?.on(event, cb);
|
||||
},
|
||||
};
|
||||
|
||||
return server;
|
||||
|
||||
@@ -3,12 +3,7 @@ import type * as vite from 'vite';
|
||||
import { ResolvedConfig } from '../../../../types';
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Mock `wxt/browser` and stub the global `browser`/`chrome` types with a fake version of the extension APIs
|
||||
*/
|
||||
export function extensionApiMock(config: ResolvedConfig): vite.PluginOption {
|
||||
const virtualSetupModule = 'virtual:wxt-setup';
|
||||
@@ -27,14 +22,12 @@ 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 so vite processes them so the aliases can
|
||||
// be resolved
|
||||
// Inline all WXT modules subdependencies can be mocked
|
||||
noExternal: ['wxt'],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -9,7 +9,6 @@ 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/sandbox';\nexport default defineBackground(() => void 0)`;
|
||||
return `import { defineBackground } from 'wxt/utils/define-background';\nexport default defineBackground(() => void 0)`;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
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' },
|
||||
],
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -171,6 +171,8 @@ async function createServerInternal(): Promise<WxtDevServer> {
|
||||
await runner.openBrowser();
|
||||
};
|
||||
|
||||
builderServer.on?.('close', () => keyboardShortcuts.stop());
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { ExtensionRunnerConfig } from '../types';
|
||||
|
||||
export function defineRunnerConfig(
|
||||
config: ExtensionRunnerConfig,
|
||||
): ExtensionRunnerConfig {
|
||||
return config;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import consola from 'consola';
|
||||
import { WebExtConfig } from '../types';
|
||||
|
||||
/**
|
||||
* @deprecated Use `defineWebExtConfig` instead. Same function, different name.
|
||||
*/
|
||||
export function defineRunnerConfig(config: WebExtConfig): WebExtConfig {
|
||||
consola.warn(
|
||||
'`defineRunnerConfig` is deprecated, use `defineWebExtConfig` instead. See https://wxt.dev/guide/resources/upgrading.html#v0-19-0-rarr-v0-20-0',
|
||||
);
|
||||
return defineWebExtConfig(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure how [`web-ext`](https://github.com/mozilla/web-ext) starts the browser during development.
|
||||
*/
|
||||
export function defineWebExtConfig(config: WebExtConfig): WebExtConfig {
|
||||
return config;
|
||||
}
|
||||
@@ -37,10 +37,8 @@ export async function generateWxtDir(entrypoints: Entrypoint[]): Promise<void> {
|
||||
// import.meta.env.*
|
||||
entries.push(await getGlobalsDeclarationEntry());
|
||||
|
||||
// @types/chrome
|
||||
if (wxt.config.extensionApi === 'chrome') {
|
||||
entries.push({ module: '@types/chrome' });
|
||||
}
|
||||
// wxt/browser types
|
||||
entries.push({ module: '@types/chrome' });
|
||||
|
||||
// tsconfig.json
|
||||
entries.push(await getTsConfigEntry());
|
||||
@@ -237,10 +235,11 @@ async function getTsConfigEntry(): Promise<WxtDirFileEntry> {
|
||||
.flatMap(([alias, absolutePath]) => {
|
||||
const aliasPath = getTsconfigPath(absolutePath);
|
||||
return [
|
||||
` "${alias}": ["${aliasPath}"]`,
|
||||
` "${alias}/*": ["${aliasPath}/*"]`,
|
||||
`"${alias}": ["${aliasPath}"]`,
|
||||
`"${alias}/*": ["${aliasPath}/*"]`,
|
||||
];
|
||||
})
|
||||
.map((line) => ` ${line}`)
|
||||
.join(',\n');
|
||||
|
||||
const text = `{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export * from './build';
|
||||
export * from './clean';
|
||||
export * from './define-config';
|
||||
export * from './define-runner-config';
|
||||
export * from './define-web-ext-config';
|
||||
export * from './create-server';
|
||||
export * from './initialize';
|
||||
export * from './prepare';
|
||||
|
||||
@@ -11,7 +11,7 @@ export async function initialize(options: {
|
||||
template: string;
|
||||
packageManager: string;
|
||||
}) {
|
||||
consola.info('Initalizing new project');
|
||||
consola.info('Initializing new project');
|
||||
|
||||
const templates = await listTemplates();
|
||||
const defaultTemplate = templates.find(
|
||||
@@ -44,10 +44,7 @@ export async function initialize(options: {
|
||||
{ title: pc.red('npm'), value: 'npm' },
|
||||
{ title: pc.yellow('pnpm'), value: 'pnpm' },
|
||||
{ title: pc.cyan('yarn'), value: 'yarn' },
|
||||
{
|
||||
title: `${pc.magenta('bun')}${pc.gray(' (experimental)')}`,
|
||||
value: 'bun',
|
||||
},
|
||||
{ title: pc.magenta('bun'), value: 'bun' },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -6,14 +6,13 @@ import {
|
||||
ConfigEnv,
|
||||
UserManifestFn,
|
||||
UserManifest,
|
||||
ExtensionRunnerConfig,
|
||||
WebExtConfig,
|
||||
WxtResolvedUnimportOptions,
|
||||
Logger,
|
||||
WxtCommand,
|
||||
WxtModule,
|
||||
WxtModuleWithMetadata,
|
||||
ResolvedEslintrc,
|
||||
Eslintrc,
|
||||
} from '../types';
|
||||
import path from 'node:path';
|
||||
import { createFsCache } from './utils/cache';
|
||||
@@ -28,6 +27,7 @@ import { getEslintVersion } from './utils/eslint';
|
||||
import { safeStringToNumber } from './utils/number';
|
||||
import { loadEnv } from './utils/env';
|
||||
import { getPort } from 'get-port-please';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
/**
|
||||
* Given an inline config, discover the config file if necessary, merge the results, resolve any
|
||||
@@ -50,9 +50,6 @@ export async function resolveConfig(
|
||||
name: 'wxt',
|
||||
cwd: inlineConfig.root ?? process.cwd(),
|
||||
rcFile: false,
|
||||
jitiOptions: {
|
||||
esmResolve: true,
|
||||
},
|
||||
});
|
||||
if (inlineConfig.configFile && metadata.layers?.length === 0) {
|
||||
throw Error(`Config file "${inlineConfig.configFile}" not found`);
|
||||
@@ -90,40 +87,47 @@ export async function resolveConfig(
|
||||
srcDir,
|
||||
mergedConfig.entrypointsDir ?? 'entrypoints',
|
||||
);
|
||||
const modulesDir = path.resolve(srcDir, mergedConfig.modulesDir ?? 'modules');
|
||||
if (await isDirMissing(entrypointsDir)) {
|
||||
logMissingDir(logger, 'Entrypoints', entrypointsDir);
|
||||
}
|
||||
const modulesDir = path.resolve(root, mergedConfig.modulesDir ?? 'modules');
|
||||
const filterEntrypoints = mergedConfig.filterEntrypoints?.length
|
||||
? new Set(mergedConfig.filterEntrypoints)
|
||||
: undefined;
|
||||
const publicDir = path.resolve(srcDir, mergedConfig.publicDir ?? 'public');
|
||||
const publicDir = path.resolve(root, mergedConfig.publicDir ?? 'public');
|
||||
const typesDir = path.resolve(wxtDir, 'types');
|
||||
const outBaseDir = path.resolve(root, mergedConfig.outDir ?? '.output');
|
||||
const modeSuffixes: Record<string, string | undefined> = {
|
||||
production: '',
|
||||
development: '-dev',
|
||||
};
|
||||
const modeSuffix = modeSuffixes[mode] ?? `-${mode}`;
|
||||
const outDirTemplate = (
|
||||
mergedConfig.outDirTemplate ?? `${browser}-mv${manifestVersion}`
|
||||
mergedConfig.outDirTemplate ??
|
||||
`${browser}-mv${manifestVersion}${modeSuffix}`
|
||||
)
|
||||
// Resolve all variables in the template
|
||||
.replaceAll('{{browser}}', browser)
|
||||
.replaceAll('{{manifestVersion}}', manifestVersion.toString())
|
||||
.replaceAll('{{modeSuffix}}', modeSuffixes[mode] ?? `-${mode}`)
|
||||
.replaceAll('{{modeSuffix}}', modeSuffix)
|
||||
.replaceAll('{{mode}}', mode)
|
||||
.replaceAll('{{command}}', command);
|
||||
|
||||
const outDir = path.resolve(outBaseDir, outDirTemplate);
|
||||
const reloadCommand = mergedConfig.dev?.reloadCommand ?? 'Alt+R';
|
||||
|
||||
const runnerConfig = await loadConfig<ExtensionRunnerConfig>({
|
||||
if (inlineConfig.runner != null || userConfig.runner != null) {
|
||||
logger.warn(
|
||||
'`InlineConfig#runner` is deprecated, use `InlineConfig#webExt` instead. See https://wxt.dev/guide/resources/upgrading.html#v0-19-0-rarr-v0-20-0',
|
||||
);
|
||||
}
|
||||
const runnerConfig = await loadConfig<WebExtConfig>({
|
||||
name: 'web-ext',
|
||||
cwd: root,
|
||||
globalRc: true,
|
||||
rcFile: '.webextrc',
|
||||
overrides: inlineConfig.runner,
|
||||
defaults: userConfig.runner,
|
||||
overrides: inlineConfig.webExt ?? inlineConfig.runner,
|
||||
defaults: userConfig.webExt ?? userConfig.runner,
|
||||
});
|
||||
// Make sure alias are absolute
|
||||
const alias = Object.fromEntries(
|
||||
@@ -171,8 +175,6 @@ export async function resolveConfig(
|
||||
{},
|
||||
);
|
||||
|
||||
const extensionApi = mergedConfig.extensionApi ?? 'webextension-polyfill';
|
||||
|
||||
return {
|
||||
browser,
|
||||
command,
|
||||
@@ -182,13 +184,7 @@ export async function resolveConfig(
|
||||
filterEntrypoints,
|
||||
env,
|
||||
fsCache: createFsCache(wxtDir),
|
||||
imports: await getUnimportOptions(
|
||||
wxtDir,
|
||||
srcDir,
|
||||
logger,
|
||||
extensionApi,
|
||||
mergedConfig,
|
||||
),
|
||||
imports: await getUnimportOptions(wxtDir, srcDir, logger, mergedConfig),
|
||||
logger,
|
||||
manifest: await resolveManifestConfig(env, mergedConfig.manifest),
|
||||
manifestVersion,
|
||||
@@ -203,12 +199,9 @@ export async function resolveConfig(
|
||||
typesDir,
|
||||
wxtDir,
|
||||
zip: resolveZipConfig(root, browser, outBaseDir, mergedConfig),
|
||||
transformManifest: mergedConfig.transformManifest,
|
||||
analysis: resolveAnalysisConfig(root, mergedConfig),
|
||||
userConfigMetadata: userConfigMetadata ?? {},
|
||||
alias,
|
||||
extensionApi,
|
||||
entrypointLoader: mergedConfig.entrypointLoader ?? 'vite-node',
|
||||
experimental: defu(mergedConfig.experimental, {}),
|
||||
dev: {
|
||||
server: devServerConfig,
|
||||
@@ -254,12 +247,6 @@ async function mergeInlineConfig(
|
||||
return defu(inline, user);
|
||||
};
|
||||
|
||||
// Merge transformManifest option
|
||||
const transformManifest: InlineConfig['transformManifest'] = (manifest) => {
|
||||
userConfig.transformManifest?.(manifest);
|
||||
inlineConfig.transformManifest?.(manifest);
|
||||
};
|
||||
|
||||
const merged = defu(inlineConfig, userConfig);
|
||||
|
||||
// Builders
|
||||
@@ -272,7 +259,6 @@ async function mergeInlineConfig(
|
||||
return {
|
||||
...merged,
|
||||
// Custom merge values
|
||||
transformManifest,
|
||||
imports,
|
||||
manifest,
|
||||
...builderConfig,
|
||||
@@ -342,38 +328,41 @@ async function getUnimportOptions(
|
||||
wxtDir: string,
|
||||
srcDir: string,
|
||||
logger: Logger,
|
||||
extensionApi: ResolvedConfig['extensionApi'],
|
||||
config: InlineConfig,
|
||||
): Promise<WxtResolvedUnimportOptions | false> {
|
||||
if (config.imports === false) return false;
|
||||
|
||||
): Promise<WxtResolvedUnimportOptions> {
|
||||
const disabled = config.imports === false;
|
||||
const eslintrc = await getUnimportEslintOptions(wxtDir, config.imports);
|
||||
// mlly sometimes picks up things as exports that aren't. That's what this array contains.
|
||||
const invalidExports = ['options'];
|
||||
const defaultOptions: WxtResolvedUnimportOptions = {
|
||||
debugLog: logger.debug,
|
||||
imports: [
|
||||
{ name: 'defineConfig', from: 'wxt' },
|
||||
{ name: 'fakeBrowser', from: 'wxt/testing' },
|
||||
],
|
||||
imports: [{ name: 'fakeBrowser', from: 'wxt/testing' }],
|
||||
// prettier-ignore
|
||||
presets: [
|
||||
{
|
||||
package: 'wxt/client',
|
||||
// There seems to be a bug in unimport that thinks "options" is an
|
||||
// export from wxt/client, but it doesn't actually exist... so it's
|
||||
// ignored.
|
||||
ignore: ['options'],
|
||||
},
|
||||
{
|
||||
package:
|
||||
extensionApi === 'chrome' ? 'wxt/browser/chrome' : 'wxt/browser',
|
||||
},
|
||||
{ package: 'wxt/sandbox' },
|
||||
{ package: 'wxt/storage' },
|
||||
{ package: 'wxt/browser' },
|
||||
{ package: 'wxt/utils/storage' },
|
||||
{ package: 'wxt/utils/app-config' },
|
||||
{ package: 'wxt/utils/content-script-context' },
|
||||
{ package: 'wxt/utils/content-script-ui/iframe', ignore: invalidExports },
|
||||
{ package: 'wxt/utils/content-script-ui/integrated', ignore: invalidExports },
|
||||
{ package: 'wxt/utils/content-script-ui/shadow-root', ignore: invalidExports },
|
||||
{ package: 'wxt/utils/content-script-ui/types' },
|
||||
{ package: 'wxt/utils/define-app-config' },
|
||||
{ package: 'wxt/utils/define-background' },
|
||||
{ package: 'wxt/utils/define-content-script' },
|
||||
{ package: 'wxt/utils/define-unlisted-script' },
|
||||
{ package: 'wxt/utils/define-wxt-plugin' },
|
||||
{ package: 'wxt/utils/inject-script', ignore: invalidExports },
|
||||
{ package: 'wxt/utils/match-patterns' },
|
||||
],
|
||||
virtualImports: ['#imports'],
|
||||
debugLog: logger.debug,
|
||||
warn: logger.warn,
|
||||
dirs: ['components', 'composables', 'hooks', 'utils'],
|
||||
dirsScanOptions: {
|
||||
cwd: srcDir,
|
||||
},
|
||||
eslintrc: await getUnimportEslintOptions(wxtDir, config.imports?.eslintrc),
|
||||
eslintrc,
|
||||
dirs: disabled ? [] : ['components', 'composables', 'hooks', 'utils'],
|
||||
disabled,
|
||||
};
|
||||
|
||||
return defu<WxtResolvedUnimportOptions, [WxtResolvedUnimportOptions]>(
|
||||
@@ -384,34 +373,34 @@ async function getUnimportOptions(
|
||||
|
||||
async function getUnimportEslintOptions(
|
||||
wxtDir: string,
|
||||
options: Eslintrc | undefined,
|
||||
options: InlineConfig['imports'],
|
||||
): Promise<ResolvedEslintrc> {
|
||||
const rawEslintEnabled = options?.enabled ?? 'auto';
|
||||
let eslintEnabled: ResolvedEslintrc['enabled'];
|
||||
switch (rawEslintEnabled) {
|
||||
const inlineEnabled =
|
||||
options === false ? false : (options?.eslintrc?.enabled ?? 'auto');
|
||||
|
||||
let enabled: ResolvedEslintrc['enabled'];
|
||||
switch (inlineEnabled) {
|
||||
case 'auto':
|
||||
const version = await getEslintVersion();
|
||||
let major = parseInt(version[0]);
|
||||
if (isNaN(major)) eslintEnabled = false;
|
||||
if (major <= 8) eslintEnabled = 8;
|
||||
else if (major >= 9) eslintEnabled = 9;
|
||||
if (isNaN(major)) enabled = false;
|
||||
if (major <= 8) enabled = 8;
|
||||
else if (major >= 9) enabled = 9;
|
||||
// NaN
|
||||
else eslintEnabled = false;
|
||||
else enabled = false;
|
||||
break;
|
||||
case true:
|
||||
eslintEnabled = 8;
|
||||
enabled = 8;
|
||||
break;
|
||||
default:
|
||||
eslintEnabled = rawEslintEnabled;
|
||||
enabled = inlineEnabled;
|
||||
}
|
||||
|
||||
return {
|
||||
enabled: eslintEnabled,
|
||||
enabled,
|
||||
filePath: path.resolve(
|
||||
wxtDir,
|
||||
eslintEnabled === 9
|
||||
? 'eslint-auto-imports.mjs'
|
||||
: 'eslintrc-auto-import.json',
|
||||
enabled === 9 ? 'eslint-auto-imports.mjs' : 'eslintrc-auto-import.json',
|
||||
),
|
||||
globalsPropValue: true,
|
||||
};
|
||||
@@ -421,20 +410,10 @@ async function getUnimportEslintOptions(
|
||||
* Returns the path to `node_modules/wxt`.
|
||||
*/
|
||||
async function resolveWxtModuleDir() {
|
||||
// TODO: Use this once we're fully running in ESM, see https://github.com/wxt-dev/wxt/issues/277
|
||||
// const url = import.meta.resolve('wxt', import.meta.url);
|
||||
const url = import.meta.resolve('wxt', import.meta.url);
|
||||
// resolve() returns the "wxt/dist/index.mjs" file, not the package's root
|
||||
// directory, which we want to return from this function.
|
||||
// return path.resolve(fileURLToPath(url), '../..');
|
||||
|
||||
const requireResolve =
|
||||
globalThis.require?.resolve ??
|
||||
(await import('node:module')).default.createRequire(import.meta.url)
|
||||
.resolve;
|
||||
|
||||
// resolve() returns the "wxt/dist/index.mjs" file, not the package's root
|
||||
// directory, which we want to return from this function.
|
||||
return path.resolve(requireResolve('wxt'), '../..');
|
||||
return path.resolve(fileURLToPath(url), '../..');
|
||||
}
|
||||
|
||||
async function isDirMissing(dir: string) {
|
||||
@@ -532,3 +511,12 @@ export async function resolveWxtUserModules(
|
||||
);
|
||||
return [...npmModules, ...localModules];
|
||||
}
|
||||
|
||||
// Mock `import.meta.resolve` in tests
|
||||
// @ts-expect-error
|
||||
if (typeof __vite_ssr_import_meta__ !== 'undefined') {
|
||||
// @ts-expect-error: Untyped global defined by Vitest
|
||||
__vite_ssr_import_meta__.resolve = (path: string) =>
|
||||
// @ts-expect-error: vitestCreateRequire defined in vitest.setup.ts
|
||||
'file://' + vitestCreateRequire(import.meta.url).resolve(path);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
fakeWxtDevServer,
|
||||
setFakeWxt,
|
||||
} from '../testing/fake-objects';
|
||||
import { Manifest } from 'webextension-polyfill';
|
||||
import {
|
||||
BuildOutput,
|
||||
ContentScriptEntrypoint,
|
||||
@@ -57,7 +56,7 @@ describe('Manifest Utils', () => {
|
||||
outDir,
|
||||
},
|
||||
});
|
||||
const expected: Partial<Manifest.WebExtensionManifest> = {
|
||||
const expected: Partial<chrome.runtime.Manifest> = {
|
||||
action: {
|
||||
default_icon: popup.options.defaultIcon,
|
||||
default_title: popup.options.defaultTitle,
|
||||
@@ -1114,31 +1113,6 @@ describe('Manifest Utils', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('transformManifest option', () => {
|
||||
it("should call the transformManifest option after the manifest is generated, but before it's returned", async () => {
|
||||
const entrypoints: Entrypoint[] = [];
|
||||
const buildOutput = fakeBuildOutput();
|
||||
const newAuthor = 'Custom Author';
|
||||
setFakeWxt({
|
||||
config: {
|
||||
transformManifest(manifest: any) {
|
||||
manifest.author = newAuthor;
|
||||
},
|
||||
},
|
||||
});
|
||||
const expected = {
|
||||
author: newAuthor,
|
||||
};
|
||||
|
||||
const { manifest: actual } = await generateManifest(
|
||||
entrypoints,
|
||||
buildOutput,
|
||||
);
|
||||
|
||||
expect(actual).toMatchObject(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('version', () => {
|
||||
it.each(['chrome', 'safari', 'edge'] as const)(
|
||||
'should include version and version_name as is on %s',
|
||||
|
||||
@@ -57,13 +57,13 @@ describe('Transform Utils', () => {
|
||||
|
||||
it('should remove unused imports', () => {
|
||||
const input = `
|
||||
import { defineBackground } from "wxt/sandbox"
|
||||
import { defineBackground } from "#imports"
|
||||
import { test1 } from "somewhere1"
|
||||
import test2 from "somewhere2"
|
||||
|
||||
export default defineBackground(() => {})
|
||||
`;
|
||||
const expected = `import { defineBackground } from "wxt/sandbox"
|
||||
const expected = `import { defineBackground } from "#imports"
|
||||
|
||||
export default defineBackground();`;
|
||||
|
||||
@@ -74,13 +74,13 @@ export default defineBackground();`;
|
||||
|
||||
it('should remove explict side-effect imports', () => {
|
||||
const input = `
|
||||
import { defineBackground } from "wxt/sandbox"
|
||||
import { defineBackground } from "#imports"
|
||||
import "my-polyfill"
|
||||
import "./style.css"
|
||||
|
||||
export default defineBackground(() => {})
|
||||
`;
|
||||
const expected = `import { defineBackground } from "wxt/sandbox"
|
||||
const expected = `import { defineBackground } from "#imports"
|
||||
|
||||
export default defineBackground();`;
|
||||
|
||||
|
||||
@@ -72,9 +72,10 @@ describe('Validation Utils', () => {
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
it("should return an error when content scripts don't have a matches", () => {
|
||||
it('should return an error when "registration: manifest" content scripts don\'t have matches', () => {
|
||||
const entrypoint = fakeContentScriptEntrypoint({
|
||||
options: {
|
||||
registration: 'manifest',
|
||||
// @ts-expect-error
|
||||
matches: null,
|
||||
},
|
||||
@@ -83,7 +84,8 @@ describe('Validation Utils', () => {
|
||||
errors: [
|
||||
{
|
||||
type: 'error',
|
||||
message: '`matches` is required',
|
||||
message:
|
||||
'`matches` is required for manifest registered content scripts',
|
||||
value: null,
|
||||
entrypoint,
|
||||
},
|
||||
@@ -96,5 +98,24 @@ describe('Validation Utils', () => {
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should allow "registration: runtime" content scripts to not have matches', () => {
|
||||
const entrypoint = fakeContentScriptEntrypoint({
|
||||
options: {
|
||||
registration: 'runtime',
|
||||
// @ts-expect-error
|
||||
matches: null,
|
||||
},
|
||||
});
|
||||
const expected = {
|
||||
errors: [],
|
||||
errorCount: 0,
|
||||
warningCount: 0,
|
||||
};
|
||||
|
||||
const actual = validateEntrypoints([entrypoint]);
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { importEntrypointFile } from '../../../utils/building';
|
||||
import { resolve } from 'node:path';
|
||||
import { setFakeWxt } from '../../../utils/testing/fake-objects';
|
||||
|
||||
const entrypointPath = (filename: string) =>
|
||||
resolve(__dirname, 'test-entrypoints', filename);
|
||||
|
||||
describe('importEntrypointFile', () => {
|
||||
beforeEach(() => {
|
||||
setFakeWxt({
|
||||
config: {
|
||||
imports: false,
|
||||
debug: false,
|
||||
// Run inside the demo folder so that wxt is in the node_modules
|
||||
// WXT must also be built for these tests to pass
|
||||
root: 'demo',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
['background.ts', { main: expect.any(Function) }],
|
||||
['content.ts', { main: expect.any(Function), matches: ['<all_urls>'] }],
|
||||
['unlisted.ts', { main: expect.any(Function) }],
|
||||
['react.tsx', { main: expect.any(Function) }],
|
||||
['with-named.ts', { main: expect.any(Function) }],
|
||||
])(
|
||||
'should return the default export of test-entrypoints/%s',
|
||||
async (file, expected) => {
|
||||
const actual = await importEntrypointFile(entrypointPath(file));
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
},
|
||||
);
|
||||
|
||||
it('should return undefined when there is no default export', async () => {
|
||||
const actual = await importEntrypointFile(
|
||||
entrypointPath('no-default-export.ts'),
|
||||
);
|
||||
|
||||
expect(actual).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should throw a custom error message when an imported variable is used before main', async () => {
|
||||
await expect(() =>
|
||||
importEntrypointFile(entrypointPath('imported-option.ts')),
|
||||
).rejects.toThrowError(
|
||||
`imported-option.ts: Cannot use imported variable "faker" outside the main function.`,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defineBackground } from '../../../../../sandbox';
|
||||
import { defineBackground } from '../../../../../utils/define-background';
|
||||
|
||||
export default defineBackground({
|
||||
main() {},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defineContentScript } from '../../../../../sandbox';
|
||||
import { defineContentScript } from '../../../../../utils/define-content-script';
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ['<all_urls>'],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defineContentScript } from '../../../../../sandbox';
|
||||
import { defineContentScript } from '../../../../../utils/define-content-script';
|
||||
import { faker } from '@faker-js/faker';
|
||||
|
||||
export default defineContentScript({
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
import { defineUnlistedScript } from '../../../../../sandbox';
|
||||
import { defineUnlistedScript } from '../../../../../utils/define-unlisted-script';
|
||||
|
||||
export default defineUnlistedScript(() => {});
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
import { defineUnlistedScript } from '../../../../../sandbox';
|
||||
import { defineUnlistedScript } from '../../../../../utils/define-unlisted-script';
|
||||
|
||||
export default defineUnlistedScript(() => {});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defineBackground } from '../../../../../sandbox';
|
||||
import { defineBackground } from '../../../../../utils/define-background';
|
||||
|
||||
export const a = {};
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
BackgroundEntrypoint,
|
||||
ContentScriptEntrypoint,
|
||||
Entrypoint,
|
||||
EntrypointInfo,
|
||||
GenericEntrypoint,
|
||||
OptionsEntrypoint,
|
||||
PopupEntrypoint,
|
||||
@@ -67,6 +68,8 @@ export async function findEntrypoints(): Promise<Entrypoint[]> {
|
||||
return results;
|
||||
}, []);
|
||||
|
||||
await wxt.hooks.callHook('entrypoints:found', wxt, entrypointInfos);
|
||||
|
||||
// Validation
|
||||
preventNoEntrypoints(entrypointInfos);
|
||||
preventDuplicateEntrypointNames(entrypointInfos);
|
||||
@@ -137,6 +140,8 @@ export async function findEntrypoints(): Promise<Entrypoint[]> {
|
||||
skipped: isEntrypointSkipped(entry),
|
||||
}));
|
||||
|
||||
await wxt.hooks.callHook('entrypoints:resolved', wxt, entrypoints);
|
||||
|
||||
wxt.logger.debug('All entrypoints:', entrypoints);
|
||||
const skippedEntrypointNames = entrypoints
|
||||
.filter((item) => item.skipped)
|
||||
@@ -151,17 +156,10 @@ export async function findEntrypoints(): Promise<Entrypoint[]> {
|
||||
].join('\n'),
|
||||
);
|
||||
}
|
||||
await wxt.hooks.callHook('entrypoints:resolved', wxt, entrypoints);
|
||||
|
||||
return entrypoints;
|
||||
}
|
||||
|
||||
interface EntrypointInfo {
|
||||
name: string;
|
||||
inputPath: string;
|
||||
type: Entrypoint['type'];
|
||||
}
|
||||
|
||||
/** Returns a map of input paths to the file's options. */
|
||||
async function importEntrypoints(infos: EntrypointInfo[]) {
|
||||
const resMap: Record<string, Record<string, any> | undefined> = {};
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
import createJITI, { TransformOptions as JitiTransformOptions } from 'jiti';
|
||||
import { createUnimport } from 'unimport';
|
||||
import fs from 'fs-extra';
|
||||
import { relative, resolve } from 'node:path';
|
||||
import { removeProjectImportStatements } from '../../utils/strings';
|
||||
import { normalizePath } from '../../utils/paths';
|
||||
import { TransformOptions, transformSync } from 'esbuild';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { wxt } from '../../wxt';
|
||||
|
||||
/**
|
||||
* Get the value from the default export of a `path`.
|
||||
*
|
||||
* It works by:
|
||||
*
|
||||
* 1. Reading the file text
|
||||
* 2. Stripping all imports from it via regex
|
||||
* 3. Auto-import only the client helper functions
|
||||
*
|
||||
* This prevents resolving imports of imports, speeding things up and preventing "xxx is not
|
||||
* defined" errors.
|
||||
*
|
||||
* Downside is that code cannot be executed outside of the main fucntion for the entrypoint,
|
||||
* otherwise you will see "xxx is not defined" errors for any imports used outside of main function.
|
||||
*/
|
||||
export async function importEntrypointFile<T>(path: string): Promise<T> {
|
||||
wxt.logger.debug('Loading file metadata:', path);
|
||||
// JITI & Babel uses normalized paths.
|
||||
const normalPath = normalizePath(path);
|
||||
|
||||
const unimport = createUnimport({
|
||||
...wxt.config.imports,
|
||||
// Only allow specific imports, not all from the project
|
||||
dirs: [],
|
||||
});
|
||||
await unimport.init();
|
||||
|
||||
const text = await fs.readFile(path, 'utf-8');
|
||||
const textNoImports = removeProjectImportStatements(text);
|
||||
const { code } = await unimport.injectImports(textNoImports);
|
||||
wxt.logger.debug(
|
||||
['Text:', text, 'No imports:', textNoImports, 'Code:', code].join('\n'),
|
||||
);
|
||||
|
||||
const jiti = createJITI(
|
||||
typeof __filename !== 'undefined'
|
||||
? __filename
|
||||
: fileURLToPath(import.meta.url),
|
||||
{
|
||||
cache: false,
|
||||
debug: wxt.config.debug,
|
||||
esmResolve: true,
|
||||
alias: {
|
||||
'webextension-polyfill': resolve(
|
||||
wxt.config.wxtModuleDir,
|
||||
'dist/virtual/mock-browser.mjs',
|
||||
),
|
||||
// TODO: Resolve this virtual module to some file with
|
||||
// `export default {}` instead of this hack of using another file with
|
||||
// a default export.
|
||||
'virtual:app-config': resolve(
|
||||
wxt.config.wxtModuleDir,
|
||||
'dist/virtual/mock-browser.mjs',
|
||||
),
|
||||
},
|
||||
// Continue using node to load TS files even if `bun run --bun` is detected. Jiti does not
|
||||
// respect the custom transform function when using it's native bun option.
|
||||
experimentalBun: false,
|
||||
// List of extensions to transform with esbuild
|
||||
extensions: [
|
||||
'.ts',
|
||||
'.cts',
|
||||
'.mts',
|
||||
'.tsx',
|
||||
'.js',
|
||||
'.cjs',
|
||||
'.mjs',
|
||||
'.jsx',
|
||||
],
|
||||
transform(opts) {
|
||||
const isEntrypoint = opts.filename === normalPath;
|
||||
return transformSync(
|
||||
// Use modified source code for entrypoints
|
||||
isEntrypoint ? code : opts.source,
|
||||
getEsbuildOptions(opts),
|
||||
);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
try {
|
||||
const res = await jiti(path);
|
||||
return res.default;
|
||||
} catch (err) {
|
||||
const filePath = relative(wxt.config.root, path);
|
||||
if (err instanceof ReferenceError) {
|
||||
// "XXX is not defined" - usually due to WXT removing imports
|
||||
const variableName = err.message.replace(' is not defined', '');
|
||||
throw Error(
|
||||
`${filePath}: Cannot use imported variable "${variableName}" outside the main function. See https://wxt.dev/guide/go-further/entrypoint-side-effects.html`,
|
||||
{ cause: err },
|
||||
);
|
||||
} else {
|
||||
wxt.logger.error(err);
|
||||
throw Error(`Failed to load entrypoint: ${filePath}`, { cause: err });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getEsbuildOptions(opts: JitiTransformOptions): TransformOptions {
|
||||
const isJsx = opts.filename?.endsWith('x');
|
||||
return {
|
||||
format: 'cjs',
|
||||
loader: isJsx ? 'tsx' : 'ts',
|
||||
define: {
|
||||
'import.meta.env.ENTRYPOINT': '"build"',
|
||||
},
|
||||
...(isJsx
|
||||
? {
|
||||
// `h` and `Fragment` are undefined, but that's OK because JSX is never evaluated while
|
||||
// grabbing the entrypoint's options.
|
||||
jsxFactory: 'h',
|
||||
jsxFragment: 'Fragment',
|
||||
}
|
||||
: undefined),
|
||||
};
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user