Compare commits

...

43 Commits

Author SHA1 Message Date
GitHub Actions 5e361c0085 chore(release): @wxt-dev/module-solid v1.1.1 2024-07-02 16:23:54 +00:00
Aaron ccfc207e92 fix: Use prepare instead of postinstall for local dev setup (#788) 2024-07-02 11:18:44 -05:00
GitHub Actions 36be4ce771 chore(release): @wxt-dev/module-solid v1.1.0 2024-07-02 15:37:46 +00:00
KnightYoshi 18f8a387b0 feat: add eslint 9 config support (#762)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2024-07-02 10:32:32 -05:00
C.C 48c2c07557 docs: Fix manifest typo (#787) 2024-07-02 07:09:07 -05:00
Aaron 61548a9478 fix(dev): Catch error when attempting to reload a tab in a saved tab group (#786) 2024-07-01 16:11:00 -05:00
dependabot[bot] fd678e9572 chore(deps): bump oven-sh/setup-bun from 1 to 2 (#785)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-01 15:40:10 -05:00
Aaron 63fb913f76 docs: New example search (#751) 2024-06-30 09:01:42 -05:00
Aaron faffe20a62 chore(deps): Upgrade non-major deps (#778) 2024-06-29 15:00:15 -05:00
Aaron 3ed8bd10fd chore: Upgrade buildc to v1.0.10 (#780) 2024-06-29 14:09:56 -05:00
Asakura Mizu 6f3f270573 fix: Respect custom outDir when cleaning and zipping (#774) 2024-06-28 09:05:40 -05:00
Aaron 5347cb4626 chore: Replace consola with wxt.logger (#776) 2024-06-28 09:04:21 -05:00
Aaron d6b4d2d1f6 chore(dev-deps): Upgrade buildc to v1.0.9 2024-06-27 16:47:35 -05:00
Aaron 0d7ae287da chore(dev-deps): Upgrade buildc to v1.0.8 2024-06-27 16:38:40 -05:00
Aaron 90cb3bd3bf feat: Enable auto-imports for JSX/TSX files (#773) 2024-06-27 00:05:45 -05:00
Prakhar 🔱 Man of Creativity 60eb97c4b2 docs: Update examples URL (#768)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2024-06-26 13:52:10 -05:00
Aaron 0ffc6eae3e Fix comment typo 2024-06-25 15:12:59 -05:00
GitHub Actions 06a8c5fb0e chore(release): wxt v0.18.10 2024-06-25 20:03:59 +00:00
Aaron eb86413f35 chore: Consolidate unimport code into a built-in module (#771) 2024-06-25 14:59:28 -05:00
Aaron b29d49cbf1 fix: Await prepare:types hook 2024-06-24 10:04:44 -05:00
Aaron c0a0edb30c feat(modules): Allow adding generated public files (#769) 2024-06-24 08:46:45 -05:00
Aaron b15a933c75 feat: Add prepare:types hook to extend .wxt/ directory generation (#767) 2024-06-23 15:12:28 -05:00
Malix 1851be22a7 docs: Update CRXJS Firefox Compatibility (#764)
Co-authored-by: Aaron <aaronklinker1@gmail.com>
2024-06-23 10:32:09 -05:00
Aaron 39f6c29e4f chore: Refactor package manager test fixtures 2024-06-21 09:10:22 -05:00
GitHub Actions 936e89fbfb chore(release): wxt v0.18.9 2024-06-20 15:34:16 +00:00
Aaron ad365ecde4 feat(experimental): Replace viteRuntime option with entrypointImporter option, and implement vite-node importer (#761) 2024-06-20 10:27:19 -05:00
GitHub Actions b4283e37bd chore(release): wxt v0.18.8 2024-06-20 14:03:16 +00:00
Aaron b467ad760a fix(modules): addViteConfig ignored user vite config (#760) 2024-06-20 08:59:46 -05:00
Aaron 9fc6408ef9 chore: Add E2E test for addImportPreset 2024-06-20 08:52:45 -05:00
Aaron 394cb1a520 docs: Add 'WorkFlowy MultiFlow' to homepage 2024-06-19 14:16:58 -05:00
Aaron ffec88549b fix: Ignore .wxt file changes in dev (#755) 2024-06-19 08:33:52 -05:00
Ziheng Xun 4758bea187 docs: update sandbox.md (#754) 2024-06-19 08:06:08 -05:00
Aaron 6671471f75 feat(dev): Reload extension when public files change (#752) 2024-06-19 08:05:56 -05:00
Aaron ae78dc7fb3 chore: Refactor client web socket util (#753) 2024-06-18 22:47:30 -05:00
Aaron b62af3d007 chore(dev-deps): Upgrade @aklinker1/buildc to 1.0.7 (#748) 2024-06-18 00:11:14 -05:00
Aaron 43ae44ecd5 fix: Don't load plugins twice in HTML pages (#746) 2024-06-17 23:41:39 -05:00
Aaron 5bab9d2f59 chore: Fix build script flags (#745) 2024-06-17 23:41:10 -05:00
Aaron 7edf1c8103 fix: Upgrade wxt peer to >= 0.18.6
This closes https://github.com/wxt-dev/wxt/issues/743
2024-06-17 14:42:24 -05:00
Aaron 53b4eac955 docs: Update wxt init command for bun 2024-06-17 14:39:41 -05:00
GitHub Actions f9a10c2c52 chore(release): wxt v0.18.7 2024-06-17 16:17:42 +00:00
Aaron 8206ca9164 fix(zip): Revert dotfile changes from #674 (#742) 2024-06-17 11:10:06 -05:00
Aaron 1a823cc008 chore: Extract build cache script to NPM package (#737) 2024-06-17 11:09:18 -05:00
Aaron efc011eb81 ci: Fix release 422 failure 2024-06-14 00:36:03 -05:00
107 changed files with 2169 additions and 1015 deletions
+3 -2
View File
@@ -18,7 +18,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: ./.github/actions/setup - uses: ./.github/actions/setup
- run: pnpm -r --sequential build - run: pnpm buildc all
build-demo: build-demo:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
@@ -33,7 +33,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: ./.github/actions/setup - uses: ./.github/actions/setup
- uses: oven-sh/setup-bun@v1 - uses: oven-sh/setup-bun@v2
- name: pnpm test:coverage - name: pnpm test:coverage
run: pnpm test:coverage --reporter=default --reporter=hanging-process run: pnpm test:coverage --reporter=default --reporter=hanging-process
- uses: codecov/codecov-action@v4 - uses: codecov/codecov-action@v4
@@ -48,6 +48,7 @@ jobs:
template: template:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
strategy: strategy:
fail-fast: false
matrix: matrix:
template: template:
- react - react
@@ -1,34 +0,0 @@
<script lang="ts" setup>
import { ref, onMounted, computed } from 'vue';
const props = defineProps<{
tag?: string;
}>();
const examples = ref();
onMounted(async () => {
const res = await fetch(
'https://raw.githubusercontent.com/wxt-dev/wxt-examples/main/examples.json',
);
examples.value = await res.json();
});
const filteredExamples = computed(() => {
if (props.tag == null) return examples.value;
return examples.value.filter((example) => {
return example.tags?.includes(props.tag);
});
});
</script>
<template>
<ul>
<li v-if="examples == null">Loading...</li>
<template v-else>
<li v-for="example of filteredExamples">
<a :href="example.url" target="_blank">{{ example.name }}</a>
</li>
</template>
</ul>
</template>
@@ -0,0 +1,226 @@
<script lang="ts" setup>
import { ref, onMounted, computed, toRaw, Ref } from 'vue';
import ExampleSearchFilterByItem from './ExampleSearchFilterByItem.vue';
import ExampleSearchResult from './ExampleSearchResult.vue';
import { ExamplesMetadata, KeySelectedObject } from '../utils/types';
const props = defineProps<{
tag?: string;
}>();
const exampleMetadata = ref<ExamplesMetadata>();
onMounted(async () => {
const res = await fetch(
'https://raw.githubusercontent.com/wxt-dev/examples/main/metadata.json',
);
exampleMetadata.value = await res.json();
});
const searchText = ref('');
const selectedApis = ref<KeySelectedObject>({});
const selectedPermissions = ref<KeySelectedObject>({});
const selectedPackages = ref<KeySelectedObject>({});
function useRequiredItems(selectedItems: Ref<KeySelectedObject>) {
return computed(() =>
Array.from(
Object.entries(toRaw(selectedItems.value)).reduce(
(set, [pkg, checked]) => {
if (checked) set.add(pkg);
return set;
},
new Set<string>(),
),
),
);
}
const requiredApis = useRequiredItems(selectedApis);
const requiredPermissions = useRequiredItems(selectedPermissions);
const requiredPackages = useRequiredItems(selectedPackages);
function doesExampleMatchSelected(
exampleItems: string[],
requiredItems: Ref<string[]>,
) {
const exampleItemsSet = new Set(exampleItems);
return !requiredItems.value.find((item) => !exampleItemsSet.has(item));
}
const filteredExamples = computed(() => {
const text = searchText.value.toLowerCase();
return exampleMetadata.value.examples.filter((example) => {
const matchesText = example.searchText.toLowerCase().includes(text);
const matchesApis = doesExampleMatchSelected(example.apis, requiredApis);
const matchesPermissions = doesExampleMatchSelected(
example.permissions,
requiredPermissions,
);
const matchesPackages = doesExampleMatchSelected(
example.packages,
requiredPackages,
);
return matchesText && matchesApis && matchesPermissions && matchesPackages;
});
});
</script>
<template>
<div class="example-layout">
<div class="search">
<input v-model="searchText" placeholder="Search for an example..." />
</div>
<div class="filters">
<ExampleSearchFilterByItem
label="APIs"
:items="exampleMetadata?.allApis"
v-model="selectedApis"
/>
<ExampleSearchFilterByItem
label="Permissions"
:items="exampleMetadata?.allPermissions"
v-model="selectedPermissions"
/>
<ExampleSearchFilterByItem
label="Packages"
:items="exampleMetadata?.allPackages"
v-model="selectedPackages"
/>
</div>
<div class="results">
<p v-if="exampleMetadata == null">Loading examples...</p>
<template v-else>
<ul class="search-results">
<ExampleSearchResult
v-for="example of filteredExamples"
:key="example.name"
:example
/>
</ul>
<p v-if="filteredExamples.length === 0">No matching examples</p>
</template>
</div>
</div>
</template>
<style scoped>
.example-layout {
display: grid;
grid-template-columns: 1fr;
grid-template-rows: 1fr;
grid-template-areas:
'search'
'results';
gap: 16px;
}
@media only screen and (min-width: 720px) {
.example-layout {
grid-template-columns: 256px 1fr;
grid-template-rows: auto 1fr;
grid-template-areas:
'filters search'
'filters results';
}
}
.search {
grid-area: search;
background: var(--vp-c-bg-soft);
padding: 20px;
width: 100%;
display: flex;
border-radius: 16px;
}
.filters {
display: none;
grid-area: filters;
}
@media only screen and (min-width: 720px) {
.filters {
display: flex;
flex-direction: column;
gap: 2px;
border-radius: 16px;
overflow: hidden;
align-self: flex-start;
}
}
.results {
grid-area: results;
}
.box {
border-radius: 16px;
overflow: hidden;
}
.search input {
min-width: 0;
flex: 1;
font-size: 16px;
}
.checkbox-col {
flex: 1;
padding: 16px;
display: flex;
flex-direction: column;
overflow-y: auto;
max-height: 200px;
font-size: 14px;
gap: 4px;
}
.filter-btn {
color: var(--vp-c-brand-1);
}
.checkbox-col .header {
font-size: 12px;
font-weight: bold;
opacity: 50%;
}
.checkbox-col p {
display: flex;
gap: 4px;
align-items: flex-start;
text-wrap: wrap;
overflow-wrap: anywhere;
line-height: 140%;
}
span {
padding-top: 1px;
}
.checkbox-col input[type='checkbox'] {
width: 16px;
height: 16px;
flex-shrink: 0;
}
.checkbox-col-container {
display: flex;
}
.search-results {
display: grid;
grid-template-columns: repeat(1, 1fr);
gap: 16px;
}
@media only screen and (min-width: 800px) {
.search-results {
grid-template-columns: repeat(2, 1fr);
}
}
@media only screen and (min-width: 1024px) {
.search-results {
grid-template-columns: repeat(3, 1fr);
}
}
a {
background-color: red;
}
</style>
@@ -0,0 +1,100 @@
<script lang="ts" setup>
import { computed, toRaw } from 'vue';
import { KeySelectedObject } from '../utils/types';
const props = defineProps<{
label: string;
items?: string[];
}>();
const selectedItems = defineModel<KeySelectedObject>({
required: true,
});
const count = computed(() => {
return Object.values(toRaw(selectedItems.value)).filter(Boolean).length;
});
function toggleItem(pkg: string) {
selectedItems.value = {
...toRaw(selectedItems.value),
[pkg]: !selectedItems.value[pkg],
};
}
</script>
<template>
<div class="filter-container">
<p class="header">
<span>Filter by {{ label }}</span> <span v-if="count">({{ count }})</span>
</p>
<div class="scroll-container">
<ul>
<li v-for="item in items">
<label :title="item">
<input
type="checkbox"
:checked="selectedItems[item]"
@input="toggleItem(item)"
/>
<span>{{ item }}</span>
</label>
</li>
</ul>
</div>
</div>
</template>
<style scoped>
.filter-container {
height: 300px;
display: flex;
flex-direction: column;
background: var(--vp-c-bg-soft);
}
.scroll-container {
flex: 1;
overflow: hidden;
position: relative;
}
.scroll-container ul {
position: absolute;
overflow-y: auto;
left: 0;
top: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
gap: 4px;
font-size: small;
padding: 8px 16px 16px 16px;
}
.header {
padding: 8px 16px;
font-size: 12px;
font-weight: bold;
opacity: 50%;
}
label {
display: flex;
gap: 4px;
align-items: flex-start;
text-wrap: wrap;
overflow-wrap: anywhere;
line-height: 140%;
cursor: pointer;
text-wrap: nowrap;
}
span {
padding-top: 1px;
}
input[type='checkbox'] {
width: 16px;
height: 16px;
flex-shrink: 0;
}
</style>
@@ -0,0 +1,72 @@
<script lang="ts" setup>
import { Example } from '../utils/types';
const props = defineProps<{
example: Example;
}>();
</script>
<template>
<li>
<a :href="example.url" target="_blank">
<p class="name">{{ example.name }}</p>
<p class="description">{{ example.description }}</p>
<p class="link">Open &rarr;</p>
</a>
</li>
</template>
<style scoped>
* {
min-width: 0;
}
a {
padding: 16px;
display: flex;
flex-direction: column;
border: 2px solid var(--vp-c-bg-soft);
border-radius: 16px;
color: var(--vp-c-text-1) !important;
gap: 8px;
}
a:hover {
outline: 2px solid var(--vp-c-brand-2);
}
.name {
font-size: 16px;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex-shrink: 0;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.description {
height: 53px;
opacity: 70%;
font-size: 14px;
font-weight: normal;
line-height: 120%;
min-height: 0;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.link {
opacity: 0;
transition: 250ms;
color: var(--vp-c-brand-2);
font-weight: bold;
text-align: right;
}
a:hover .link {
opacity: 100%;
}
</style>
@@ -29,6 +29,7 @@ const chromeExtensionIds = [
'nedcanggplmbbgmlpcjiafgjcpdimpea', // YTBlock - Block any content from YouTube™ 'nedcanggplmbbgmlpcjiafgjcpdimpea', // YTBlock - Block any content from YouTube™
'oadbjpccljkplmhnjekgjamejnbadlne', // demo.fun - Interactive product demos that convert 'oadbjpccljkplmhnjekgjamejnbadlne', // demo.fun - Interactive product demos that convert
'iopdafdcollfgaoffingmahpffckmjni', // SmartEReply: Elevate Your LinkedIn™ Engagement with AI 🚀📈 'iopdafdcollfgaoffingmahpffckmjni', // SmartEReply: Elevate Your LinkedIn™ Engagement with AI 🚀📈
'khjdmjcmpolknpccmaaipmidphjokhdf', // WorkFlowy MultiFlow
]; ];
const { data, err, isLoading } = useListExtensionDetails(chromeExtensionIds); const { data, err, isLoading } = useListExtensionDetails(chromeExtensionIds);
-1
View File
@@ -78,7 +78,6 @@ export default defineConfig({
menuItem('Configuration', 'configuration'), menuItem('Configuration', 'configuration'),
menuItem('Entrypoints', 'entrypoints'), menuItem('Entrypoints', 'entrypoints'),
menuItem('Assets', 'assets'), menuItem('Assets', 'assets'),
menuItem('Testing', 'testing'),
menuItem('Publishing', 'publishing'), menuItem('Publishing', 'publishing'),
menuItem('Migrate to WXT', 'migrate-to-wxt'), menuItem('Migrate to WXT', 'migrate-to-wxt'),
menuItem('Compare', 'compare'), menuItem('Compare', 'compare'),
+2 -2
View File
@@ -2,7 +2,7 @@ import DefaultTheme from 'vitepress/theme';
import Icon from '../components/Icon.vue'; import Icon from '../components/Icon.vue';
import EntrypointPatterns from '../components/EntrypointPatterns.vue'; import EntrypointPatterns from '../components/EntrypointPatterns.vue';
import UsingWxtSection from '../components/UsingWxtSection.vue'; import UsingWxtSection from '../components/UsingWxtSection.vue';
import ExampleList from '../components/ExampleList.vue'; import ExampleSearch from '../components/ExampleSearch.vue';
import './custom.css'; import './custom.css';
export default { export default {
@@ -11,6 +11,6 @@ export default {
ctx.app.component('Icon', Icon); ctx.app.component('Icon', Icon);
ctx.app.component('EntrypointPatterns', EntrypointPatterns); ctx.app.component('EntrypointPatterns', EntrypointPatterns);
ctx.app.component('UsingWxtSection', UsingWxtSection); ctx.app.component('UsingWxtSection', UsingWxtSection);
ctx.app.component('ExampleList', ExampleList); ctx.app.component('ExampleSearch', ExampleSearch);
}, },
}; };
+18
View File
@@ -0,0 +1,18 @@
export interface Example {
name: string;
description?: string;
url: string;
searchText: string;
apis: string[];
permissions: string[];
packages: string[];
}
export type ExamplesMetadata = {
examples: Example[];
allApis: string[];
allPermissions: string[];
allPackages: string[];
};
export type KeySelectedObject = Record<string, boolean | undefined>;
+16 -4
View File
@@ -1,7 +1,19 @@
# Examples ---
layout: page
---
Simple walkthroughs to accomplish common tasks or patterns with WXT. <style>
.examples-container {
padding: 32px;
}
</style>
<ExampleList /> <div class="examples-container">
<div class="vp-doc">
<h1>Examples</h1>
</div>
> Full code available at [`wxt-dev/wxt-examples`](https://github.com/wxt-dev/wxt-examples) <br />
<ExampleSearch />
</div>
+2 -2
View File
@@ -5,8 +5,8 @@ Lets compare the features of WXT vs [Plasmo](https://docs.plasmo.com/framework)
## Overview ## Overview
| Features | WXT | Plasmo | CRXJS | | Features | WXT | Plasmo | CRXJS |
| ---------------------------------------------------- | :-------------------------: | :--------------------------------------: | :---------------------------------------------------------------------: | | ---------------------------------------------------- | :-------------------------: | :--------------------------------------: | :-------------------------------------: |
| Supports all browsers | ✅ | ✅ | ❌ See [#56](https://github.com/crxjs/chrome-extension-tools/issues/56) | | Supports all browsers | ✅ | ✅ | ✅ As of `v2.0.0-beta.23` |
| MV2 Support | ✅ | ✅ | 🟡 Either MV2 or MV3 | | MV2 Support | ✅ | ✅ | 🟡 Either MV2 or MV3 |
| MV3 Support | ✅ | ✅ | 🟡 Either MV2 or MV3 | | MV3 Support | ✅ | ✅ | 🟡 Either MV2 or MV3 |
| Create Extension ZIPs | ✅ | ✅ | ❌ | | Create Extension ZIPs | ✅ | ✅ | ❌ |
+9 -3
View File
@@ -15,7 +15,14 @@ npx wxt@latest init <project-name>
``` ```
```sh [bun] ```sh [bun]
bunx wxt@latest init <project-name> # The "wxt init" command currently fails when ran with bunx.
# Use NPX as a workaround, and select "bun" as your package
# manager. To stay up to date with this issue, follow
# https://github.com/wxt-dev/wxt/issues/707
#
# bunx wxt@latest init <project-name>
npx wxt@latest init <project-name>
``` ```
::: :::
@@ -96,7 +103,6 @@ pnpm dev
``` ```
:::tip 🎉&ensp;Well done! :::tip 🎉&ensp;Well done!
The dev command will build the extension for development, open the browser, and reload the different parts of the extension when you save changes. The dev command will build the extension for development, open the browser, and reload the different parts of the extension when you save changes.
::: :::
@@ -107,4 +113,4 @@ You're ready to build your web extension!
- Read the rest of the "Get Started" pages for a high-overview of what WXT can do - Read the rest of the "Get Started" pages for a high-overview of what WXT can do
- Read the [Guide](/guide/key-concepts/manifest) to learn in-depth about each feature WXT supports - Read the [Guide](/guide/key-concepts/manifest) to learn in-depth about each feature WXT supports
- [Configure WXT](./configuration) by creating a `wxt.config.ts` file - [Configure WXT](./configuration) by creating a `wxt.config.ts` file
- Checkout [example projects](https://github.com/wxt-dev/wxt-examples) to see how to perform common tasks with WXT - Checkout [example projects](https://github.com/wxt-dev/examples) to see how to perform common tasks with WXT
-25
View File
@@ -1,25 +0,0 @@
# Testing
## Official Frameworks
WXT officially supports [Vitest](https://vitest.dev/) for unit tests and either [Playwright](https://playwright.dev/) or [Puppeteer](https://pptr.dev/) for E2E tests against Chromium browsers.
For details setting up each testing framework, see the official examples:
<ExampleList tag="testing" />
## Unofficial Frameworks
Puppeteer and Playwright are the only E2E test runners that support Chrome Extensions. There are no other options at the time of writing.
There are other options for unit tests however, like [Jest](https://jestjs.io/), [Mocha](https://mochajs.org/), or [`node:test`](https://nodejs.org/api/test.html). **_WXT does not claim to support any of them_** because none of them support all of WXT's features, like TypeScript or auto-imports.
If you want to try to use a different framework for unit tests, you will need to configure the environment manually:
- **Auto-imports**: Add `unimport` to your test environment or disable them by setting `imports: false` in your `wxt.config.ts` file
- **`browser` mock**: Mock the `webextension-polyfill` module globally with `wxt/dist/virtual/mock-browser.js`
- **[Remote Code Bundling](/guide/go-further/remote-code)**: If you use it, configure your environment to handle the `url:` module prefix
- **Global Variables**: If you consume them, manually define globals provided by WXT (like `import.meta.env.BROWSER`) by adding them to the global scope before accessing them (`import.meta.env.BROWSER = "chrome"`)
- **Import paths**: If you use the `@/` or `~/` path aliases, add them to your test environment
[Here's how Vitest is configured](https://github.com/wxt-dev/wxt/blob/main/packages/wxt/src/testing/wxt-vitest-plugin.ts) for reference.
@@ -37,4 +37,4 @@ Chrome extensions allow you to add panels and side panes to the devtools window.
See the WXT's examples for a full walkthrough of extending the devtools window: See the WXT's examples for a full walkthrough of extending the devtools window:
<ExampleList tag="devtools" /> - [Devtools Setup](https://github.com/wxt-dev/wxt-examples/tree/main/examples/vanilla-devtools#readme)
@@ -12,8 +12,8 @@ Firefox does not support sandboxed pages.
:patterns="[ :patterns="[
['sandbox.html', 'sandbox.html'], ['sandbox.html', 'sandbox.html'],
['sandbox/index.html', 'sandbox.html'], ['sandbox/index.html', 'sandbox.html'],
['<name>.sandbox.html', '<name>.html` '], ['<name>.sandbox.html', '<name>.html'],
['<name>.sandbox/index.html', '<name>.html` '], ['<name>.sandbox/index.html', '<name>.html'],
]" ]"
/> />
+3 -1
View File
@@ -6,7 +6,9 @@ WXT officially supports [Vitest](https://vitest.dev/) for unit tests and either
For details setting up each testing framework, see the official examples: For details setting up each testing framework, see the official examples:
<ExampleList tag="testing" /> - [Vitest](https://github.com/wxt-dev/wxt-examples/tree/main/examples/vanilla-vitest#readme)
- [Playwright](https://github.com/wxt-dev/wxt-examples/tree/main/examples/vanilla-playwright#readme)
- [Puppeteer](https://github.com/wxt-dev/wxt-examples/tree/main/examples/vanilla-puppeteer#readme)
### Unofficial Frameworks ### Unofficial Frameworks
+27 -10
View File
@@ -79,24 +79,41 @@ export default defineConfig({
## ESLint ## ESLint
ESLint doesn't understand auto-imports; it thinks all auto-imported variables are undeclared globals and will report lint errors for each. To fix this, extend the ESLint file generated inside the `.wxt` directory: By default, ESLint thinks auto-imported variables are not defined. You have to add them to the `globals` config so it knows these variables exist at runtime.
```js WXT generates a config file containing the globals you can extend! By default, WXT will attempt to detect if ESLint is installed in your project and generate the config file if so. If the config file isn't being generated automatically, you can manually tell WXT to generate it:
// .eslintrc.js
module.exports = {
extends: ['./.wxt/eslintrc-auto-import.json'],
};
```
By default, this file will be generated when ESLint is a direct dependency. If ESLint is a subdependency or your project is a monorepo, it may not be generated automatically. In this case, you can tell WXT to generate it:
```ts ```ts
// wxt.config.ts // wxt.config.ts
export default defineConfig({ export default defineConfig({
imports: { imports: {
eslintrc: { eslintrc: {
enabled: true, enabled: 8, // Generate ESLint v8 compatible config
// or
enabled: 9, // Generate ESLint v9 compatible config
}, },
}, },
}); });
``` ```
### ESLint 9 and above
WXT supports the new "flat config" file format. Just import the generated file and add it to the array of config to extend.
```js
// eslint.config.mjs
import autoImports from './.wxt/eslint-auto-imports.mjs';
export default [autoImports];
```
### ESLint 8 and below
Just extend the generated file:
```js
// .eslintrc.mjs
export default {
extends: ['./.wxt/eslintrc-auto-import.json'],
};
```
+3 -2
View File
@@ -25,7 +25,7 @@ Here's an example `wxt.config.ts` file:
import { defineConfig } from 'wxt'; import { defineConfig } from 'wxt';
export default defineConfig({ export default defineConfig({
mainfest: { manifest: {
action: { action: {
default_title: 'Some Title', default_title: 'Some Title',
}, },
@@ -176,7 +176,8 @@ export default defineConfig({
See the official localization examples for more details: See the official localization examples for more details:
<ExampleList tag="i18n" /> - [I18n](https://github.com/wxt-dev/wxt-examples/tree/main/examples/vanilla-i18n#readme)
- [Vue I18n](https://github.com/wxt-dev/wxt-examples/tree/main/examples/vue-i18n#readme)
## Actions ## Actions
+6 -6
View File
@@ -15,17 +15,17 @@
"docs:gen": "typedoc --options docs/typedoc.json", "docs:gen": "typedoc --options docs/typedoc.json",
"docs:dev": "pnpm -s docs:gen && vitepress dev docs", "docs:dev": "pnpm -s docs:gen && vitepress dev docs",
"docs:build": "pnpm -s docs:gen && vitepress build docs", "docs:build": "pnpm -s docs:gen && vitepress build docs",
"docs:preview": "pnpm -s docs:gen && vitepress preview docs", "docs:preview": "pnpm -s docs:gen && vitepress preview docs"
"build-deps": "pnpm tsx scripts/build-deps.ts"
}, },
"devDependencies": { "devDependencies": {
"@aklinker1/buildc": "^1.0.10",
"@aklinker1/check": "^1.3.1", "@aklinker1/check": "^1.3.1",
"@types/fs-extra": "^11.0.4", "@types/fs-extra": "^11.0.4",
"@vitest/coverage-v8": "^1.6.0", "@vitest/coverage-v8": "^1.6.0",
"changelogen": "^0.5.5", "changelogen": "^0.5.5",
"consola": "^3.2.3", "consola": "^3.2.3",
"dependency-graph": "^1.0.0", "dependency-graph": "^1.0.0",
"execa": "^9.2.0", "execa": "^9.3.0",
"fast-glob": "^3.3.2", "fast-glob": "^3.3.2",
"fs-extra": "^11.2.0", "fs-extra": "^11.2.0",
"hasha": "^6.0.0", "hasha": "^6.0.0",
@@ -33,16 +33,16 @@
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"prettier": "^3.3.2", "prettier": "^3.3.2",
"simple-git-hooks": "^2.11.1", "simple-git-hooks": "^2.11.1",
"tsx": "^4.15.4", "tsx": "4.15.7",
"typedoc": "^0.25.4", "typedoc": "^0.25.4",
"typedoc-plugin-markdown": "4.0.0-next.23", "typedoc-plugin-markdown": "4.0.0-next.23",
"typedoc-vitepress-theme": "1.0.0-next.3", "typedoc-vitepress-theme": "1.0.0-next.3",
"typescript": "^5.4.5", "typescript": "^5.5.2",
"vitepress": "^1.2.3", "vitepress": "^1.2.3",
"vitest": "^1.6.0", "vitest": "^1.6.0",
"vitest-mock-extended": "^1.3.1", "vitest-mock-extended": "^1.3.1",
"vitest-plugin-random-seed": "^1.1.0", "vitest-plugin-random-seed": "^1.1.0",
"vue": "^3.4.27", "vue": "^3.4.31",
"wxt": "workspace:*", "wxt": "workspace:*",
"yaml": "^2.4.5" "yaml": "^2.4.5"
}, },
+12
View File
@@ -0,0 +1,12 @@
import { defineBuildConfig } from 'unbuild';
import { resolve } from 'node:path';
export default defineBuildConfig({
rootDir: 'modules',
outDir: resolve(__dirname, 'dist'),
entries: [{ input: 'react.ts', name: 'index' }],
rollup: {
emitCJS: true,
},
declaration: true,
});
+5
View File
@@ -0,0 +1,5 @@
export default function () {
const [count, setCount] = useState(0);
const increment = () => setCount((count) => count + 1);
return <button onClick={increment}>Count: {count}</button>;
}
@@ -0,0 +1,33 @@
import { defineContentScript } from 'wxt/sandbox';
import { ContentScriptContext, createShadowRootUi } from 'wxt/client';
import React from 'react';
import ReactDOM from 'react-dom/client';
export default defineContentScript({
matches: ['*://*/*'],
async main(ctx) {
const ui = await createUi(ctx);
ui.mount();
},
});
function createUi(ctx: ContentScriptContext) {
return createShadowRootUi(ctx, {
name: 'react-ui',
position: 'inline',
append: 'first',
onMount(container) {
const root = ReactDOM.createRoot(container);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
return root;
},
onRemove(root) {
root?.unmount();
},
});
}
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="./main.tsx"></script>
</body>
</html>
@@ -0,0 +1,10 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
const root = document.getElementById('app')!;
ReactDOM.createRoot(root).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
@@ -13,6 +13,17 @@ export default defineWxtModule<ReactModuleOptions>({
})); }));
addImportPreset(wxt, 'react'); addImportPreset(wxt, 'react');
// Enable auto-imports for JSX files
wxt.hooks.hook('ready', (wxt) => {
if (wxt.config.imports === false) return;
wxt.config.imports.dirsScanOptions ??= {};
wxt.config.imports.dirsScanOptions.filePatterns = [
// Default plus JSX/TSX
'*.{ts,js,mjs,cjs,mts,cts,jsx,tsx}',
];
});
}, },
}); });
+10 -5
View File
@@ -21,20 +21,25 @@
"dist" "dist"
], ],
"scripts": { "scripts": {
"build": "pnpm -s build-deps && unbuild", "dev": "wxt",
"check": "pnpm -s build-deps && check", "check": "buildc --deps-only -- check",
"build-deps": "pnpm -ws build-deps @wxt-dev/module-react" "build": "buildc -- unbuild",
"prepare": "buildc --deps-only -- wxt prepare"
}, },
"peerDependencies": { "peerDependencies": {
"wxt": ">=0.18.5" "wxt": ">=0.18.6"
}, },
"dependencies": { "dependencies": {
"@vitejs/plugin-react": "^4.3.1" "@vitejs/plugin-react": "^4.3.1"
}, },
"devDependencies": { "devDependencies": {
"@aklinker1/check": "^1.3.1", "@aklinker1/check": "^1.3.1",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"publint": "^0.2.8", "publint": "^0.2.8",
"typescript": "^5.4.5", "react": "^18.3.1",
"react-dom": "^18.3.1",
"typescript": "^5.5.2",
"unbuild": "^2.0.0", "unbuild": "^2.0.0",
"wxt": "workspace:*" "wxt": "workspace:*"
} }
View File
+5 -1
View File
@@ -1,4 +1,8 @@
{ {
"extends": "../../tsconfig.base.json", "extends": ["../../tsconfig.base.json", "./.wxt/tsconfig.json"],
"compilerOptions": {
"allowImportingTsExtensions": true,
"jsx": "react-jsx"
},
"exclude": ["node_modules/**", "dist/**"] "exclude": ["node_modules/**", "dist/**"]
} }
+25
View File
@@ -1,5 +1,30 @@
# Changelog # Changelog
## v1.1.1
[compare changes](https://github.com/wxt-dev/wxt/compare/module-solid-v1.1.0...module-solid-v1.1.1)
### 🩹 Fixes
- Use `prepare` instead of `postinstall` for local dev setup ([#788](https://github.com/wxt-dev/wxt/pull/788))
## v1.1.0
[compare changes](https://github.com/wxt-dev/wxt/compare/module-solid-v1.0.1...module-solid-v1.1.0)
### 🚀 Enhancements
- Enable auto-imports for JSX/TSX files ([#773](https://github.com/wxt-dev/wxt/pull/773))
### 🩹 Fixes
- Upgrade wxt peer to >= 0.18.6 ([7edf1c8](https://github.com/wxt-dev/wxt/commit/7edf1c8))
### 🏡 Chore
- Extract build cache script to NPM package ([#737](https://github.com/wxt-dev/wxt/pull/737))
- **deps:** Upgrade non-major deps ([#778](https://github.com/wxt-dev/wxt/pull/778))
## v1.0.1 ## v1.0.1
[compare changes](https://github.com/wxt-dev/wxt/compare/module-solid-v1.0.0...module-solid-v1.0.1) [compare changes](https://github.com/wxt-dev/wxt/compare/module-solid-v1.0.0...module-solid-v1.0.1)
+12
View File
@@ -0,0 +1,12 @@
import { defineBuildConfig } from 'unbuild';
import { resolve } from 'node:path';
export default defineBuildConfig({
rootDir: 'modules',
outDir: resolve(__dirname, 'dist'),
entries: [{ input: 'solid.ts', name: 'index' }],
rollup: {
emitCJS: true,
},
declaration: true,
});
+7
View File
@@ -0,0 +1,7 @@
import { Component } from 'solid-js';
export const App: Component = () => {
const [count, setCount] = createSignal(0);
const increment = () => setCount((count) => count + 1);
return <button onClick={increment}>Count: {count()}</button>;
};
@@ -0,0 +1,26 @@
import { defineContentScript } from 'wxt/sandbox';
import { ContentScriptContext, createShadowRootUi } from 'wxt/client';
import { render } from 'solid-js/web';
export default defineContentScript({
matches: ['*://*/*'],
async main(ctx) {
const ui = await createUi(ctx);
ui.mount();
},
});
function createUi(ctx: ContentScriptContext) {
return createShadowRootUi(ctx, {
name: 'solid-ui',
position: 'inline',
append: 'first',
onMount(container) {
return render(() => <App />, container);
},
onRemove(unmount) {
unmount?.();
},
});
}
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="./main.tsx"></script>
</body>
</html>
@@ -0,0 +1,5 @@
import { render } from 'solid-js/web';
const root = document.getElementById('app')!;
render(() => <App />, root);
@@ -16,6 +16,17 @@ export default defineWxtModule<SolidModuleOptions>({
})); }));
addImportPreset(wxt, 'solid-js'); addImportPreset(wxt, 'solid-js');
// Enable auto-imports for JSX files
wxt.hooks.hook('ready', (wxt) => {
if (wxt.config.imports === false) return;
wxt.config.imports.dirsScanOptions ??= {};
wxt.config.imports.dirsScanOptions.filePatterns = [
// Default plus JSX/TSX
'*.{ts,js,mjs,cjs,mts,cts,jsx,tsx}',
];
});
}, },
}); });
+8 -6
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wxt-dev/module-solid", "name": "@wxt-dev/module-solid",
"version": "1.0.1", "version": "1.1.1",
"type": "module", "type": "module",
"main": "./dist/index.cjs", "main": "./dist/index.cjs",
"module": "./dist/index.mjs", "module": "./dist/index.mjs",
@@ -21,12 +21,13 @@
"dist" "dist"
], ],
"scripts": { "scripts": {
"build": "pnpm -s build-deps && unbuild", "dev": "wxt",
"check": "pnpm -s build-deps && check", "check": "buildc --deps-only -- check",
"build-deps": "pnpm -ws build-deps @wxt-dev/module-solid" "build": "buildc -- unbuild",
"prepare": "buildc --deps-only -- wxt prepare"
}, },
"peerDependencies": { "peerDependencies": {
"wxt": ">=0.18.5" "wxt": ">=0.18.6"
}, },
"dependencies": { "dependencies": {
"vite-plugin-solid": "^2.10.2" "vite-plugin-solid": "^2.10.2"
@@ -34,7 +35,8 @@
"devDependencies": { "devDependencies": {
"@aklinker1/check": "^1.3.1", "@aklinker1/check": "^1.3.1",
"publint": "^0.2.8", "publint": "^0.2.8",
"typescript": "^5.4.5", "solid-js": "^1.8.17",
"typescript": "^5.5.2",
"unbuild": "^2.0.0", "unbuild": "^2.0.0",
"wxt": "workspace:*" "wxt": "workspace:*"
} }
View File
+5 -1
View File
@@ -1,4 +1,8 @@
{ {
"extends": "../../tsconfig.base.json", "extends": ["../../tsconfig.base.json", "./.wxt/tsconfig.json"],
"compilerOptions": {
"jsx": "preserve",
"jsxImportSource": "solid-js"
},
"exclude": ["node_modules/**", "dist/**"] "exclude": ["node_modules/**", "dist/**"]
} }
+4 -5
View File
@@ -21,12 +21,11 @@
"dist" "dist"
], ],
"scripts": { "scripts": {
"build": "pnpm -s build-deps && unbuild", "build": "buildc -- unbuild",
"check": "pnpm -s build-deps && check", "check": "buildc --deps-only -- check"
"build-deps": "pnpm -ws build-deps @wxt-dev/module-svelte"
}, },
"peerDependencies": { "peerDependencies": {
"wxt": ">=0.18.5" "wxt": ">=0.18.6"
}, },
"dependencies": { "dependencies": {
"@sveltejs/vite-plugin-svelte": "^3.1.1" "@sveltejs/vite-plugin-svelte": "^3.1.1"
@@ -34,7 +33,7 @@
"devDependencies": { "devDependencies": {
"@aklinker1/check": "^1.3.1", "@aklinker1/check": "^1.3.1",
"publint": "^0.2.8", "publint": "^0.2.8",
"typescript": "^5.4.5", "typescript": "^5.5.2",
"unbuild": "^2.0.0", "unbuild": "^2.0.0",
"wxt": "workspace:*" "wxt": "workspace:*"
} }
+4 -5
View File
@@ -21,12 +21,11 @@
"dist" "dist"
], ],
"scripts": { "scripts": {
"build": "pnpm -s build-deps && unbuild", "build": "buildc -- unbuild",
"check": "pnpm -s build-deps && check", "check": "buildc --deps-only -- check"
"build-deps": "pnpm -ws build-deps @wxt-dev/module-vue"
}, },
"peerDependencies": { "peerDependencies": {
"wxt": ">=0.18.5" "wxt": ">=0.18.6"
}, },
"dependencies": { "dependencies": {
"@vitejs/plugin-vue": "^5.0.5" "@vitejs/plugin-vue": "^5.0.5"
@@ -34,7 +33,7 @@
"devDependencies": { "devDependencies": {
"@aklinker1/check": "^1.3.1", "@aklinker1/check": "^1.3.1",
"publint": "^0.2.8", "publint": "^0.2.8",
"typescript": "^5.4.5", "typescript": "^5.5.2",
"unbuild": "^2.0.0", "unbuild": "^2.0.0",
"wxt": "workspace:*" "wxt": "workspace:*"
} }
+12
View File
@@ -0,0 +1,12 @@
import autoImports from './.wxt/eslintrc-auto-import.js';
export default [
{
languageOptions: {
globals: {
...autoImports.globals,
},
sourceType: 'module',
},
},
];
+12 -10
View File
@@ -4,18 +4,17 @@
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "pnpm -s build-deps && wxt", "dev": "buildc --deps-only -- wxt",
"build": "pnpm -s build-deps && wxt build", "build": "buildc -- wxt build",
"build:all": "pnpm -s build-deps && run-s -s 'build:all:*'", "build:all": "buildc --deps-only -- run-s -s 'build:all:*'",
"build:all:chrome-mv3": "wxt build", "build:all:chrome-mv3": "wxt build",
"build:all:chrome-mv2": "wxt build --mv2", "build:all:chrome-mv2": "wxt build --mv2",
"build:all:firefox-mv3": "wxt build -b firefox --mv3", "build:all:firefox-mv3": "wxt build -b firefox --mv3",
"build:all:firefox-mv2": "wxt build -b firefox", "build:all:firefox-mv2": "wxt build -b firefox",
"test": "pnpm -s build-deps && vitest", "test": "buildc --deps-only -- vitest",
"zip": "pnpm -s build-deps && wxt zip", "zip": "buildc --deps-only -- wxt zip",
"check": "pnpm -s build-deps && check", "check": "buildc --deps-only -- check",
"postinstall": "pnpm -s build-deps && wxt prepare", "postinstall": "buildc --deps-only -- wxt prepare"
"build-deps": "pnpm -sw build-deps wxt-demo"
}, },
"dependencies": { "dependencies": {
"react": "^18.3.1", "react": "^18.3.1",
@@ -24,8 +23,11 @@
"devDependencies": { "devDependencies": {
"@types/react": "^18.3.3", "@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0", "@types/react-dom": "^18.3.0",
"sass": "^1.77.5", "sass": "^1.77.6",
"typescript": "^5.4.5", "typescript": "^5.5.2",
"wxt": "workspace:*" "wxt": "workspace:*"
},
"buildc": {
"outDir": ".output/chrome-mv3"
} }
} }
@@ -13,7 +13,7 @@ export default defineContentScript({
anchor: 'form[role=search]', anchor: 'form[role=search]',
onMount: (container) => { onMount: (container) => {
const app = document.createElement('div'); const app = document.createElement('div');
app.textContent = 'Custom content script UI'; app.textContent = browser.i18n.getMessage('prompt_for_name');
container.append(app); container.append(app);
}, },
}); });
+4 -1
View File
@@ -19,7 +19,10 @@ export default defineConfig({
open: true, open: true,
}, },
experimental: { experimental: {
viteRuntime: true, entrypointImporter: 'vite-node',
},
runner: {
startUrls: ['https://duckduckgo.com'],
}, },
example: { example: {
a: 'a', a: 'a',
+60
View File
@@ -1,5 +1,65 @@
# Changelog # Changelog
## v0.18.10
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.18.9...wxt-v0.18.10)
### 🚀 Enhancements
- Add `prepare:types` hook to extend `.wxt/` directory generation ([#767](https://github.com/wxt-dev/wxt/pull/767))
- **modules:** Allow adding generated public files ([#769](https://github.com/wxt-dev/wxt/pull/769))
### 🩹 Fixes
- Await `prepare:types` hook ([b29d49c](https://github.com/wxt-dev/wxt/commit/b29d49c))
### 🏡 Chore
- Refactor package manager test fixtures ([39f6c29](https://github.com/wxt-dev/wxt/commit/39f6c29))
- Consolidate `unimport` code into a built-in module ([#771](https://github.com/wxt-dev/wxt/pull/771))
## v0.18.9
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.18.8...wxt-v0.18.9)
### 🚀 Enhancements
- **experimental:** Replace `viteRuntime` option with `entrypointImporter` option, and implement `vite-node` importer ([#761](https://github.com/wxt-dev/wxt/pull/761))
## v0.18.8
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.18.7...wxt-v0.18.8)
### 🚀 Enhancements
- **dev:** Reload extension when public files change ([#752](https://github.com/wxt-dev/wxt/pull/752))
### 🩹 Fixes
- Don't load plugins twice in HTML pages ([#746](https://github.com/wxt-dev/wxt/pull/746))
- Ignore .wxt file changes in dev ([#755](https://github.com/wxt-dev/wxt/pull/755))
- **modules:** `addViteConfig` ignored user vite config ([#760](https://github.com/wxt-dev/wxt/pull/760))
### 🏡 Chore
- Refactor client web socket util ([#753](https://github.com/wxt-dev/wxt/pull/753))
- Add E2E test for `addImportPreset` ([9fc6408](https://github.com/wxt-dev/wxt/commit/9fc6408))
## v0.18.7
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.18.6...wxt-v0.18.7)
### 🩹 Fixes
- **dev:** Fix CSP error loading HTML plugins ([#723](https://github.com/wxt-dev/wxt/pull/723))
- Generalize react-refresh preamble logic to virtualize all inline scripts ([#728](https://github.com/wxt-dev/wxt/pull/728))
- **zip:** Revert dotfile changes from #674 ([#742](https://github.com/wxt-dev/wxt/pull/742), [#674](https://github.com/wxt-dev/wxt/issues/674))
### 🏡 Chore
- **deps:** Upgrade and sync all dependencies ([#725](https://github.com/wxt-dev/wxt/pull/725))
- Extract build cache script to NPM package ([#737](https://github.com/wxt-dev/wxt/pull/737))
## v0.18.6 ## v0.18.6
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.18.5...wxt-v0.18.6) [compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.18.5...wxt-v0.18.6)
@@ -0,0 +1,102 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Auto Imports > eslintrc > "enabled: 8" should output a JSON config file compatible with ESlint 8 1`] = `
".wxt/eslintrc-auto-import.json
----------------------------------------
{
"globals": {
"ContentScriptContext": true,
"InvalidMatchPattern": true,
"MatchPattern": true,
"browser": true,
"createIframeUi": true,
"createIntegratedUi": true,
"createShadowRootUi": true,
"defineBackground": true,
"defineConfig": true,
"defineContentScript": true,
"defineUnlistedScript": true,
"defineWxtPlugin": true,
"fakeBrowser": true,
"storage": true
}
}
"
`;
exports[`Auto Imports > eslintrc > "enabled: 9" should output a flat config file compatible with ESlint 9 1`] = `
".wxt/eslint-auto-imports.mjs
----------------------------------------
const globals = {
"ContentScriptContext": true,
"InvalidMatchPattern": true,
"MatchPattern": true,
"browser": true,
"createIframeUi": true,
"createIntegratedUi": true,
"createShadowRootUi": true,
"defineBackground": true,
"defineConfig": true,
"defineContentScript": true,
"defineUnlistedScript": true,
"defineWxtPlugin": true,
"fakeBrowser": true,
"storage": true
}
export default {
languageOptions: {
globals,
sourceType: "module",
},
};
"
`;
exports[`Auto Imports > eslintrc > "enabled: true" should output a JSON config file compatible with ESlint 8 1`] = `
".wxt/eslintrc-auto-import.json
----------------------------------------
{
"globals": {
"ContentScriptContext": true,
"InvalidMatchPattern": true,
"MatchPattern": true,
"browser": true,
"createIframeUi": true,
"createIntegratedUi": true,
"createShadowRootUi": true,
"defineBackground": true,
"defineConfig": true,
"defineContentScript": true,
"defineUnlistedScript": true,
"defineWxtPlugin": true,
"fakeBrowser": true,
"storage": true
}
}
"
`;
exports[`Auto Imports > eslintrc > should allow customizing the output 1`] = `
"example.json
----------------------------------------
{
"globals": {
"ContentScriptContext": "readonly",
"InvalidMatchPattern": "readonly",
"MatchPattern": "readonly",
"browser": "readonly",
"createIframeUi": "readonly",
"createIntegratedUi": "readonly",
"createShadowRootUi": "readonly",
"defineBackground": "readonly",
"defineConfig": "readonly",
"defineContentScript": "readonly",
"defineUnlistedScript": "readonly",
"defineWxtPlugin": "readonly",
"fakeBrowser": "readonly",
"storage": "readonly"
}
}
"
`;
+155 -49
View File
@@ -1,5 +1,6 @@
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { TestProject } from '../utils'; import { TestProject } from '../utils';
import { execaCommand } from 'execa';
describe('Auto Imports', () => { describe('Auto Imports', () => {
describe('imports: { ... }', () => { describe('imports: { ... }', () => {
@@ -47,10 +48,10 @@ describe('Auto Imports', () => {
---------------------------------------- ----------------------------------------
// Generated by wxt // Generated by wxt
/// <reference types="wxt/vite-builder-env" /> /// <reference types="wxt/vite-builder-env" />
/// <reference types="./types/imports.d.ts" />
/// <reference types="./types/paths.d.ts" /> /// <reference types="./types/paths.d.ts" />
/// <reference types="./types/i18n.d.ts" /> /// <reference types="./types/i18n.d.ts" />
/// <reference types="./types/globals.d.ts" /> /// <reference types="./types/globals.d.ts" />
/// <reference types="./types/imports.d.ts" />
" "
`); `);
}); });
@@ -96,7 +97,7 @@ describe('Auto Imports', () => {
}); });
describe('eslintrc', () => { describe('eslintrc', () => {
it('should output the globals list for ESLint to consume', async () => { it('"enabled: true" should output a JSON config file compatible with ESlint 8', async () => {
const project = new TestProject(); const project = new TestProject();
project.addFile('entrypoints/popup.html', `<html></html>`); project.addFile('entrypoints/popup.html', `<html></html>`);
@@ -108,30 +109,43 @@ describe('Auto Imports', () => {
}, },
}); });
expect(await project.serializeFile('.wxt/eslintrc-auto-import.json')) expect(
.toMatchInlineSnapshot(` await project.serializeFile('.wxt/eslintrc-auto-import.json'),
".wxt/eslintrc-auto-import.json ).toMatchSnapshot();
---------------------------------------- });
{
"globals": { it('"enabled: 8" should output a JSON config file compatible with ESlint 8', async () => {
"ContentScriptContext": true, const project = new TestProject();
"InvalidMatchPattern": true, project.addFile('entrypoints/popup.html', `<html></html>`);
"MatchPattern": true,
"browser": true, await project.prepare({
"createIframeUi": true, imports: {
"createIntegratedUi": true, eslintrc: {
"createShadowRootUi": true, enabled: 8,
"defineBackground": true, },
"defineConfig": true, },
"defineContentScript": true, });
"defineUnlistedScript": true,
"defineWxtPlugin": true, expect(
"fakeBrowser": true, await project.serializeFile('.wxt/eslintrc-auto-import.json'),
"storage": true ).toMatchSnapshot();
} });
}
" it('"enabled: 9" should output a flat config file compatible with ESlint 9', async () => {
`); const project = new TestProject();
project.addFile('entrypoints/popup.html', `<html></html>`);
await project.prepare({
imports: {
eslintrc: {
enabled: 9,
},
},
});
expect(
await project.serializeFile('.wxt/eslint-auto-imports.mjs'),
).toMatchSnapshot();
}); });
it('should allow customizing the output', async () => { it('should allow customizing the output', async () => {
@@ -148,30 +162,122 @@ describe('Auto Imports', () => {
}, },
}); });
expect(await project.serializeFile('example.json')) expect(await project.serializeFile('example.json')).toMatchSnapshot();
.toMatchInlineSnapshot(` });
"example.json
---------------------------------------- describe('Actual linting results', () => {
{ async function runEslint(
"globals": { project: TestProject,
"ContentScriptContext": "readonly", version: boolean | 'auto' | 8 | 9,
"InvalidMatchPattern": "readonly", ) {
"MatchPattern": "readonly", project.addFile(
"browser": "readonly", 'entrypoints/background.js',
"createIframeUi": "readonly", `export default defineBackground(() => {})`,
"createIntegratedUi": "readonly", );
"createShadowRootUi": "readonly", await project.prepare({
"defineBackground": "readonly", imports: { eslintrc: { enabled: version } },
"defineConfig": "readonly", });
"defineContentScript": "readonly", return await execaCommand('pnpm eslint entrypoints/background.js', {
"defineUnlistedScript": "readonly", cwd: project.root,
"defineWxtPlugin": "readonly", });
"fakeBrowser": "readonly",
"storage": "readonly"
} }
}
" describe('ESLint 9', () => {
`); it('should have lint errors when not extending generated config', async () => {
const project = new TestProject({
devDependencies: {
'@eslint/js': '9.5.0',
eslint: '9.5.0',
},
});
project.addFile(
'eslint.config.mjs',
`
import eslint from "@eslint/js";
export default [
eslint.configs.recommended,
];
`,
);
await expect(runEslint(project, 9)).rejects.toMatchObject({
message: expect.stringContaining(
"'defineBackground' is not defined",
),
});
});
it('should not have any lint errors when configured', async () => {
const project = new TestProject({
devDependencies: {
'@eslint/js': '9.5.0',
eslint: '9.5.0',
},
});
project.addFile(
'eslint.config.mjs',
`
import eslint from "@eslint/js";
import autoImports from "./.wxt/eslint-auto-imports.mjs";
export default [
eslint.configs.recommended,
autoImports,
];
`,
);
const res = await runEslint(project, 9);
expect(res).toBeDefined();
});
});
describe('ESLint 8', () => {
it('should have lint errors when not extending generated config', async () => {
const project = new TestProject({
devDependencies: {
eslint: '8.57.0',
},
});
project.addFile(
'.eslintrc',
JSON.stringify({
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
env: { es6: true },
extends: ['eslint:recommended'],
}),
);
await expect(runEslint(project, 8)).rejects.toMatchObject({
message: expect.stringContaining(
"'defineBackground' is not defined",
),
});
});
it('should not have any lint errors when configured', async () => {
const project = new TestProject({
devDependencies: {
eslint: '8.57.0',
},
});
project.addFile(
'.eslintrc',
JSON.stringify({
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
env: { es6: true },
extends: [
'eslint:recommended',
'./.wxt/eslintrc-auto-import.json',
],
}),
);
const res = await runEslint(project, 8);
expect(res).toBeDefined();
});
});
}); });
}); });
}); });
+5
View File
@@ -4,6 +4,7 @@ import { WxtHooks } from '~/types';
const hooks: WxtHooks = { const hooks: WxtHooks = {
ready: vi.fn(), ready: vi.fn(),
'prepare:types': vi.fn(),
'build:before': vi.fn(), 'build:before': vi.fn(),
'build:done': vi.fn(), 'build:done': vi.fn(),
'build:manifestGenerated': vi.fn(), 'build:manifestGenerated': vi.fn(),
@@ -41,6 +42,7 @@ describe('Hooks', () => {
expectHooksToBeCalled({ expectHooksToBeCalled({
ready: true, ready: true,
'prepare:types': true,
'build:before': false, 'build:before': false,
'build:done': false, 'build:done': false,
'build:publicAssets': false, 'build:publicAssets': false,
@@ -60,6 +62,7 @@ describe('Hooks', () => {
expectHooksToBeCalled({ expectHooksToBeCalled({
ready: true, ready: true,
'prepare:types': true,
'build:before': true, 'build:before': true,
'build:done': true, 'build:done': true,
'build:publicAssets': true, 'build:publicAssets': true,
@@ -79,6 +82,7 @@ describe('Hooks', () => {
expectHooksToBeCalled({ expectHooksToBeCalled({
ready: true, ready: true,
'prepare:types': true,
'build:before': true, 'build:before': true,
'build:done': true, 'build:done': true,
'build:publicAssets': true, 'build:publicAssets': true,
@@ -104,6 +108,7 @@ describe('Hooks', () => {
expectHooksToBeCalled({ expectHooksToBeCalled({
ready: true, ready: true,
'prepare:types': true,
'build:before': true, 'build:before': true,
'build:done': true, 'build:done': true,
'build:publicAssets': true, 'build:publicAssets': true,
@@ -1,9 +1,9 @@
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { TestProject } from '../utils'; import { TestProject } from '../utils';
describe.each([true, false])( describe.each(['jiti', 'vite-runtime', 'vite-node'] as const)(
'Manifest Content (Vite runtime? %s)', 'Manifest Content (Vite runtime? %s)',
(viteRuntime) => { (entrypointImporter) => {
it.each([ it.each([
{ browser: undefined, outDir: 'chrome-mv3', expected: undefined }, { browser: undefined, outDir: 'chrome-mv3', expected: undefined },
{ browser: 'chrome', outDir: 'chrome-mv3', expected: undefined }, { browser: 'chrome', outDir: 'chrome-mv3', expected: undefined },
@@ -24,7 +24,7 @@ describe.each([true, false])(
main: () => {}, main: () => {},
})`, })`,
); );
await project.build({ browser, experimental: { viteRuntime } }); await project.build({ browser, experimental: { entrypointImporter } });
const safariManifest = await project.getOutputManifest( const safariManifest = await project.getOutputManifest(
`.output/${outDir}/manifest.json`, `.output/${outDir}/manifest.json`,
+34 -1
View File
@@ -101,7 +101,13 @@ describe('Module Helpers', () => {
export default defineWxtModule((wxt) => { export default defineWxtModule((wxt) => {
addPublicAssets(wxt, "${normalizePath(dir)}") addPublicAssets(wxt, "${normalizePath(dir)}")
}) wxt.hooks.hook("build:publicAssets", (_, assets) => {
assets.push({
relativeDest: "example/generated.txt",
contents: "",
});
});
});
`, `,
); );
@@ -114,6 +120,9 @@ describe('Module Helpers', () => {
await expect( await expect(
project.fileExists('.output/chrome-mv3/module.txt'), project.fileExists('.output/chrome-mv3/module.txt'),
).resolves.toBe(true); ).resolves.toBe(true);
await expect(
project.fileExists('.output/chrome-mv3/example/generated.txt'),
).resolves.toBe(true);
}); });
it("should not overwrite the user's public files", async () => { it("should not overwrite the user's public files", async () => {
@@ -287,5 +296,29 @@ describe('Module Helpers', () => {
await expect(project.serializeOutput()).resolves.toContain(expectedText); await expect(project.serializeOutput()).resolves.toContain(expectedText);
}); });
it('should add preset', async () => {
const project = new TestProject();
project.addFile(
'entrypoints/background.ts',
`export default defineBackground(() => {
customImport();
});`,
);
project.addFile(
'modules/test.ts',
`import { defineWxtModule, addImportPreset } from 'wxt/modules';
export default defineWxtModule((wxt) => {
addImportPreset(wxt, "vue");
})`,
);
await project.build();
await expect(
project.serializeFile('.wxt/types/imports.d.ts'),
).resolves.toContain("const ref: typeof import('vue')['ref']");
});
}); });
}); });
@@ -233,10 +233,10 @@ describe('TypeScript Project', () => {
---------------------------------------- ----------------------------------------
// Generated by wxt // Generated by wxt
/// <reference types="wxt/vite-builder-env" /> /// <reference types="wxt/vite-builder-env" />
/// <reference types="./types/imports.d.ts" />
/// <reference types="./types/paths.d.ts" /> /// <reference types="./types/paths.d.ts" />
/// <reference types="./types/i18n.d.ts" /> /// <reference types="./types/i18n.d.ts" />
/// <reference types="./types/globals.d.ts" /> /// <reference types="./types/globals.d.ts" />
/// <reference types="./types/imports.d.ts" />
" "
`); `);
}); });
+4 -2
View File
@@ -97,7 +97,8 @@ describe('Zipping', () => {
expect(await project.fileExists(sourcesZip)).toBe(true); expect(await project.fileExists(sourcesZip)).toBe(true);
}); });
it('should not zip hidden files into sources by default', async () => { // TODO: Fix #738 and re-enable
it.skip('should not zip hidden files into sources by default', async () => {
const project = new TestProject({ const project = new TestProject({
name: 'test', name: 'test',
version: '1.0.0', version: '1.0.0',
@@ -117,7 +118,8 @@ describe('Zipping', () => {
expect(await project.fileExists(unzipDir, '.env')).toBe(false); expect(await project.fileExists(unzipDir, '.env')).toBe(false);
}); });
it('should allow zipping hidden files into sources when explicitly listed', async () => { // TODO: Fix #738 and re-enable
it.skip('should allow zipping hidden files into sources when explicitly listed', async () => {
const project = new TestProject({ const project = new TestProject({
name: 'test', name: 'test',
version: '1.0.0', version: '1.0.0',
+14 -13
View File
@@ -1,7 +1,7 @@
{ {
"name": "wxt", "name": "wxt",
"type": "module", "type": "module",
"version": "0.18.6", "version": "0.18.10",
"description": "Next gen framework for developing web extensions", "description": "Next gen framework for developing web extensions",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -92,12 +92,12 @@
}, },
"scripts": { "scripts": {
"wxt": "tsx src/cli/index.ts", "wxt": "tsx src/cli/index.ts",
"build": "tsx scripts/build.ts", "build": "buildc -- tsx scripts/build.ts",
"check": "run-s -c check:*", "check": "buildc --deps-only -- run-s -c check:*",
"check:default": "check", "check:default": "check",
"check:tsc-virtual": "tsc --noEmit -p src/virtual", "check:tsc-virtual": "tsc --noEmit -p src/virtual",
"test": "vitest", "test": "buildc --deps-only -- vitest",
"test:e2e": "vitest -r e2e", "test:e2e": "buildc --deps-only -- vitest -r e2e",
"sync-releases": "pnpx changelogen@latest gh release" "sync-releases": "pnpx changelogen@latest gh release"
}, },
"dependencies": { "dependencies": {
@@ -124,12 +124,12 @@
"jiti": "^1.21.6", "jiti": "^1.21.6",
"json5": "^2.2.3", "json5": "^2.2.3",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"linkedom": "^0.18.3", "linkedom": "^0.18.4",
"magicast": "^0.3.4", "magicast": "^0.3.4",
"minimatch": "^9.0.4", "minimatch": "^9.0.5",
"natural-compare": "^1.4.0", "natural-compare": "^1.4.0",
"normalize-path": "^3.0.0", "normalize-path": "^3.0.0",
"nypm": "^0.3.8", "nypm": "^0.3.9",
"ohash": "^1.1.3", "ohash": "^1.1.3",
"open": "^10.1.0", "open": "^10.1.0",
"ora": "^7.0.1", "ora": "^7.0.1",
@@ -137,7 +137,8 @@
"prompts": "^2.4.2", "prompts": "^2.4.2",
"publish-browser-extension": "^2.1.3", "publish-browser-extension": "^2.1.3",
"unimport": "^3.7.2", "unimport": "^3.7.2",
"vite": "^5.3.0", "vite": "^5.3.2",
"vite-node": "^1.6.0",
"web-ext-run": "^0.2.0", "web-ext-run": "^0.2.0",
"webextension-polyfill": "^0.10.0" "webextension-polyfill": "^0.10.0"
}, },
@@ -147,17 +148,17 @@
"@types/fs-extra": "^11.0.4", "@types/fs-extra": "^11.0.4",
"@types/lodash.merge": "^4.6.9", "@types/lodash.merge": "^4.6.9",
"@types/natural-compare": "^1.4.3", "@types/natural-compare": "^1.4.3",
"@types/node": "^20.14.2", "@types/node": "^20.14.9",
"@types/normalize-path": "^3.0.2", "@types/normalize-path": "^3.0.2",
"@types/prompts": "^2.4.9", "@types/prompts": "^2.4.9",
"execa": "^9.2.0", "execa": "^9.3.0",
"extract-zip": "^2.0.1", "extract-zip": "^2.0.1",
"happy-dom": "^13.10.1", "happy-dom": "^13.10.1",
"lodash.merge": "^4.6.2", "lodash.merge": "^4.6.2",
"p-map": "^7.0.2", "p-map": "^7.0.2",
"publint": "^0.2.8", "publint": "^0.2.8",
"tsup": "^8.1.0", "tsup": "^8.1.0",
"tsx": "^4.15.4", "tsx": "4.15.7",
"typescript": "^5.4.5" "typescript": "^5.5.2"
} }
} }
+3 -3
View File
@@ -13,10 +13,10 @@ describe('Module Utilities', () => {
const userConfig = {}; const userConfig = {};
const moduleConfig = { build: { sourcemap: true } }; const moduleConfig = { build: { sourcemap: true } };
wxt.config.vite = () => userConfig; wxt.config.vite = () => Promise.resolve(userConfig);
addViteConfig(wxt, () => moduleConfig); addViteConfig(wxt, () => moduleConfig);
await wxt.hooks.callHook('ready', wxt); await wxt.hooks.callHook('ready', wxt);
const actual: any = wxt.config.vite(wxt.config.env); const actual = await wxt.config.vite(wxt.config.env);
expect(actual).toEqual(expected); expect(actual).toEqual(expected);
}); });
@@ -32,7 +32,7 @@ describe('Module Utilities', () => {
wxt.config.vite = () => userConfig; wxt.config.vite = () => userConfig;
addViteConfig(wxt, () => moduleConfig); addViteConfig(wxt, () => moduleConfig);
await wxt.hooks.callHook('ready', wxt); await wxt.hooks.callHook('ready', wxt);
const actual: any = wxt.config.vite(wxt.config.env); const actual = await wxt.config.vite(wxt.config.env);
expect(actual).toEqual(expected); expect(actual).toEqual(expected);
}); });
@@ -0,0 +1,4 @@
import { WxtModule } from '~/types';
import unimport from './unimport';
export const builtinModules: WxtModule<any>[] = [unimport];
@@ -0,0 +1,149 @@
import { addViteConfig, defineWxtModule } from '~/modules';
import type {
EslintGlobalsPropValue,
WxtDirFileEntry,
WxtModule,
WxtResolvedUnimportOptions,
} from '~/types';
import { type Unimport, createUnimport } from 'unimport';
import { Plugin } from 'vite';
import { extname } from 'node:path';
export default defineWxtModule({
name: 'wxt:built-in:unimport',
setup(wxt) {
const options = wxt.config.imports;
if (options === false) return;
let unimport: Unimport;
// Add user module imports to config
wxt.hooks.hook('ready', () => {
const addModuleImports = (module: WxtModule<any>) => {
if (!module.imports) return;
options.imports ??= [];
options.imports.push(...module.imports);
};
wxt.config.builtinModules.forEach(addModuleImports);
wxt.config.userModules.forEach(addModuleImports);
});
// Create unimport instance AFTER "ready" so any modifications to the
// config inside "ready" are applied.
wxt.hooks.afterEach((event) => {
if (event.name === 'ready') {
unimport = createUnimport(options);
}
});
// Generate types
wxt.hooks.hook('prepare:types', async (_, entries) => {
// Update cache before each rebuild
await unimport.init();
entries.push(await getImportsDeclarationEntry(unimport));
if (options.eslintrc.enabled === false) return;
entries.push(
await getEslintConfigEntry(unimport, options.eslintrc.enabled, options),
);
});
// Add vite plugin
addViteConfig(wxt, () => ({
plugins: [vitePlugin(unimport)],
}));
},
});
export function vitePlugin(unimport: Unimport): Plugin {
const ENABLED_EXTENSIONS = new Set([
'.js',
'.jsx',
'.ts',
'.tsx',
'.vue',
'.svelte',
]);
return {
name: 'wxt:unimport',
async transform(code, id) {
// Don't transform dependencies
if (id.includes('node_modules')) return;
// Don't transform non-js files
if (!ENABLED_EXTENSIONS.has(extname(id))) return;
const injected = await unimport.injectImports(code, id);
return {
code: injected.code,
map: injected.s.generateMap({ hires: 'boundary', source: id }),
};
},
};
}
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: [
'// Generated by wxt',
await unimport.generateTypeDeclarations(),
'',
].join('\n'),
tsReference: true,
};
}
async function getEslintConfigEntry(
unimport: Unimport,
version: 8 | 9,
options: WxtResolvedUnimportOptions,
): Promise<WxtDirFileEntry> {
const globals = (await unimport.getImports())
.map((i) => i.as ?? i.name)
.filter(Boolean)
.sort()
.reduce<Record<string, EslintGlobalsPropValue>>((globals, name) => {
globals[name] = options.eslintrc.globalsPropValue;
return globals;
}, {});
if (version <= 8) return getEslint8ConfigEntry(options, globals);
else return getEslint9ConfigEntry(options, globals);
}
export function getEslint8ConfigEntry(
options: WxtResolvedUnimportOptions,
globals: Record<string, EslintGlobalsPropValue>,
): WxtDirFileEntry {
return {
path: options.eslintrc.filePath,
text: JSON.stringify({ globals }, null, 2) + '\n',
};
}
export function getEslint9ConfigEntry(
options: WxtResolvedUnimportOptions,
globals: Record<string, EslintGlobalsPropValue>,
): WxtDirFileEntry {
return {
path: options.eslintrc.filePath,
text: `const globals = ${JSON.stringify(globals, null, 2)}
export default {
languageOptions: {
globals,
sourceType: "module",
},
};
`,
};
}
+20 -2
View File
@@ -345,14 +345,32 @@ describe('CLI', () => {
mockArgv('clean'); mockArgv('clean');
await importCli(); await importCli();
expect(cleanMock).toBeCalledWith(undefined); expect(cleanMock).toBeCalledWith({});
}); });
it('should respect passing a custom root', async () => { it('should respect passing a custom root', async () => {
mockArgv('clean', 'path/to/root'); mockArgv('clean', 'path/to/root');
await importCli(); await importCli();
expect(cleanMock).toBeCalledWith('path/to/root'); expect(cleanMock).toBeCalledWith({ root: 'path/to/root' });
});
it('should respect a custom config file', async () => {
mockArgv('clean', '-c', './path/to/config.ts');
await importCli();
expect(cleanMock).toBeCalledWith({
configFile: './path/to/config.ts',
});
});
it('should respect passing --debug', async () => {
mockArgv('clean', '--debug');
await importCli();
expect(cleanMock).toBeCalledWith({
debug: true,
});
}); });
}); });
+2 -1
View File
@@ -126,9 +126,10 @@ cli
cli cli
.command('clean [root]', 'clean generated files and caches') .command('clean [root]', 'clean generated files and caches')
.alias('cleanup') .alias('cleanup')
.option('-c, --config <file>', 'use specified config file')
.action( .action(
wrapAction(async (root, flags) => { wrapAction(async (root, flags) => {
await clean(root); await clean({ root, configFile: flags.config, debug: flags.debug });
}), }),
); );
@@ -1,22 +0,0 @@
import { describe, it, expect } from 'vitest';
import { createViteBuilder } from '../index';
import { fakeResolvedConfig } from '~/core/utils/testing/fake-objects';
import { createHooks } from 'hookable';
describe('Vite Builder', () => {
describe('importEntrypoint', () => {
it('should import entrypoints, removing runtime values (like the main function)', async () => {
const {
default: { main: _, ...expected },
} = await import('./fixtures/module');
const builder = await createViteBuilder(
fakeResolvedConfig({ root: __dirname }),
createHooks(),
);
const actual = await builder.importEntrypoint<{ default: any }>(
'./fixtures/module.ts',
);
expect(actual).toEqual(expected);
});
});
});
+54 -4
View File
@@ -20,6 +20,10 @@ import {
import { Hookable } from 'hookable'; import { Hookable } from 'hookable';
import { toArray } from '~/core/utils/arrays'; import { toArray } from '~/core/utils/arrays';
import { safeVarName } from '~/core/utils/strings'; import { safeVarName } from '~/core/utils/strings';
import { importEntrypointFile } from '~/core/utils/building';
import { ViteNodeServer } from 'vite-node/server';
import { ViteNodeRunner } from 'vite-node/client';
import { installSourcemapsSupport } from 'vite-node/source-map';
export async function createViteBuilder( export async function createViteBuilder(
wxtConfig: ResolvedConfig, wxtConfig: ResolvedConfig,
@@ -57,7 +61,6 @@ export async function createViteBuilder(
config.plugins.push( config.plugins.push(
wxtPlugins.download(wxtConfig), wxtPlugins.download(wxtConfig),
wxtPlugins.devHtmlPrerender(wxtConfig, server), wxtPlugins.devHtmlPrerender(wxtConfig, server),
wxtPlugins.unimport(wxtConfig),
wxtPlugins.resolveVirtualModules(wxtConfig), wxtPlugins.resolveVirtualModules(wxtConfig),
wxtPlugins.devServerGlobals(wxtConfig, server), wxtPlugins.devServerGlobals(wxtConfig, server),
wxtPlugins.tsconfigPaths(wxtConfig), wxtPlugins.tsconfigPaths(wxtConfig),
@@ -204,21 +207,68 @@ export async function createViteBuilder(
return { return {
name: 'Vite', name: 'Vite',
version: vite.version, version: vite.version,
async importEntrypoint(url) { async importEntrypoint(path) {
switch (wxtConfig.experimental.entrypointImporter) {
default:
case 'jiti': {
return await importEntrypointFile(path);
}
case 'vite-runtime': {
const baseConfig = await getBaseConfig(); const baseConfig = await getBaseConfig();
const envConfig: vite.InlineConfig = { const envConfig: vite.InlineConfig = {
plugins: [ plugins: [
wxtPlugins.webextensionPolyfillMock(wxtConfig), wxtPlugins.webextensionPolyfillMock(wxtConfig),
wxtPlugins.removeEntrypointMainFunction(wxtConfig, url), wxtPlugins.removeEntrypointMainFunction(wxtConfig, path),
], ],
}; };
const config = vite.mergeConfig(baseConfig, envConfig); const config = vite.mergeConfig(baseConfig, envConfig);
const server = await vite.createServer(config); const server = await vite.createServer(config);
await server.listen(); await server.listen();
const runtime = await vite.createViteRuntime(server, { hmr: false }); const runtime = await vite.createViteRuntime(server, { hmr: false });
const module = await runtime.executeUrl(url); const module = await runtime.executeUrl(path);
await server.close(); await server.close();
return module.default; return module.default;
}
case 'vite-node': {
const baseConfig = await getBaseConfig();
// Disable dep optimization, as recommended by vite-node's README
baseConfig.optimizeDeps ??= {};
baseConfig.optimizeDeps.noDiscovery = true;
baseConfig.optimizeDeps.include = [];
const envConfig: vite.InlineConfig = {
plugins: [
wxtPlugins.webextensionPolyfillMock(wxtConfig),
wxtPlugins.removeEntrypointMainFunction(wxtConfig, path),
],
};
const config = vite.mergeConfig(baseConfig, envConfig);
const server = await vite.createServer(config);
await server.pluginContainer.buildStart({});
const node = new ViteNodeServer(
// @ts-ignore: Some weird type error...
server,
);
installSourcemapsSupport({
getSourceMap: (source) => node.getSourceMap(source),
});
const runner = new ViteNodeRunner({
root: server.config.root,
base: server.config.base,
// when having the server and runner in a different context,
// you will need to handle the communication between them
// and pass to this function
fetchModule(id) {
return node.fetchModule(id);
},
resolveId(id, importer) {
return node.resolveId(id, importer);
},
});
const res = await runner.executeFile(path);
await server.close();
return res.default;
}
}
}, },
async build(group) { async build(group) {
let entryConfig; let entryConfig;
@@ -2,7 +2,6 @@ export * from './devHtmlPrerender';
export * from './devServerGlobals'; export * from './devServerGlobals';
export * from './download'; export * from './download';
export * from './multipageMove'; export * from './multipageMove';
export * from './unimport';
export * from './resolveVirtualModules'; export * from './resolveVirtualModules';
export * from './tsconfigPaths'; export * from './tsconfigPaths';
export * from './noopBackground'; export * from './noopBackground';
@@ -1,43 +0,0 @@
import { createUnimport } from 'unimport';
import { ResolvedConfig } from '~/types';
import type * as vite from 'vite';
import { extname } from 'path';
const ENABLED_EXTENSIONS = new Set([
'.js',
'.jsx',
'.ts',
'.tsx',
'.vue',
'.svelte',
]);
/**
* Inject any global imports defined by unimport
*/
export function unimport(config: ResolvedConfig): vite.PluginOption {
const options = config.imports;
if (options === false) return [];
const unimport = createUnimport(options);
return {
name: 'wxt:unimport',
async config() {
await unimport.scanImportsFromDir(undefined, { cwd: config.srcDir });
},
async transform(code, id) {
// Don't transform dependencies
if (id.includes('node_modules')) return;
// Don't transform non-js files
if (!ENABLED_EXTENSIONS.has(extname(id))) return;
const injected = await unimport.injectImports(code, id);
return {
code: injected.code,
map: injected.s.generateMap({ hires: 'boundary', source: id }),
};
},
};
}
@@ -46,13 +46,19 @@ try {
// Use "pre" so the new script is added before vite bundles all the scripts // Use "pre" so the new script is added before vite bundles all the scripts
order: 'pre', order: 'pre',
handler(html, _ctx) { handler(html, _ctx) {
const { document } = parseHTML(html); const src =
const script = document.createElement('script');
script.type = 'module';
script.src =
config.command === 'serve' config.command === 'serve'
? `http://${config.dev.server?.hostname}:${config.dev.server?.port}/@id/${virtualHtmlModuleId}` ? `http://${config.dev.server?.hostname}:${config.dev.server?.port}/@id/${virtualHtmlModuleId}`
: virtualHtmlModuleId; : virtualHtmlModuleId;
const { document } = parseHTML(html);
const existing = document.querySelector(`script[src='${src}']`);
if (existing) return;
const script = document.createElement('script');
script.type = 'module';
script.src = src;
if (document.head == null) { if (document.head == null) {
const newHead = document.createElement('head'); const newHead = document.createElement('head');
document.documentElement.prepend(newHead); document.documentElement.prepend(newHead);
+30 -9
View File
@@ -1,44 +1,65 @@
import path from 'node:path'; import path from 'node:path';
import glob from 'fast-glob'; import glob from 'fast-glob';
import fs from 'fs-extra'; import fs from 'fs-extra';
import { consola } from 'consola';
import pc from 'picocolors'; import pc from 'picocolors';
import { InlineConfig } from '~/types';
import { registerWxt, wxt } from './wxt';
/** /**
* Remove generated/temp files from the directory. * Remove generated/temp files from the directory.
* *
* @param config Optional config that will override your `<root>/wxt.config.ts`.
*
* @example
* await clean();
*/
export async function clean(config?: InlineConfig): Promise<void>;
/**
* Remove generated/temp files from the directory.
*
* @deprecated
*
* @param root The directory to look for generated/temp files in. Defaults to `process.cwd()`. Can be relative to `process.cwd()` or absolute. * @param root The directory to look for generated/temp files in. Defaults to `process.cwd()`. Can be relative to `process.cwd()` or absolute.
* *
* @example * @example
* await clean(); * await clean();
*/ */
export async function clean(root = process.cwd()) { export async function clean(root?: string): Promise<void>;
consola.info('Cleaning Project');
export async function clean(config?: string | InlineConfig) {
if (typeof config === 'string') {
config = { root: config };
}
await registerWxt('build', config);
wxt.logger.info('Cleaning Project');
const root = wxt.config.root;
const tempDirs = [ const tempDirs = [
'node_modules/.vite', 'node_modules/.vite',
'node_modules/.cache', 'node_modules/.cache',
'**/.wxt', '**/.wxt',
'.output/*', `${path.relative(root, wxt.config.outBaseDir)}/*`,
]; ];
consola.debug('Looking for:', tempDirs.map(pc.cyan).join(', ')); wxt.logger.debug('Looking for:', tempDirs.map(pc.cyan).join(', '));
const directories = await glob(tempDirs, { const directories = await glob(tempDirs, {
cwd: path.resolve(root), cwd: root,
absolute: true, absolute: true,
onlyDirectories: true, onlyDirectories: true,
deep: 2, deep: 2,
}); });
if (directories.length === 0) { if (directories.length === 0) {
consola.debug('No generated files found.'); wxt.logger.debug('No generated files found.');
return; return;
} }
consola.debug( wxt.logger.debug(
'Found:', 'Found:',
directories.map((dir) => pc.cyan(path.relative(root, dir))).join(', '), directories.map((dir) => pc.cyan(path.relative(root, dir))).join(', '),
); );
for (const directory of directories) { for (const directory of directories) {
await fs.rm(directory, { force: true, recursive: true }); await fs.rm(directory, { force: true, recursive: true });
consola.debug('Deleted ' + pc.cyan(path.relative(root, directory))); wxt.logger.debug('Deleted ' + pc.cyan(path.relative(root, directory)));
} }
} }
+4 -4
View File
@@ -20,7 +20,6 @@ import {
findEntrypoints, findEntrypoints,
} from '~/core/utils/building'; } from '~/core/utils/building';
import { createExtensionRunner } from '~/core/runners'; import { createExtensionRunner } from '~/core/runners';
import { consola } from 'consola';
import { Mutex } from 'async-mutex'; import { Mutex } from 'async-mutex';
import pc from 'picocolors'; import pc from 'picocolors';
import { relative } from 'node:path'; import { relative } from 'node:path';
@@ -153,6 +152,7 @@ function createFileReloader(server: WxtDevServer) {
// Here, "path" is a non-normalized path (ie: C:\\users\\... instead of C:/users/...) // Here, "path" is a non-normalized path (ie: C:\\users\\... instead of C:/users/...)
if (path.startsWith(wxt.config.outBaseDir)) return; if (path.startsWith(wxt.config.outBaseDir)) return;
if (path.startsWith(wxt.config.wxtDir)) return;
changeQueue.push([event, path]); changeQueue.push([event, path]);
await fileChangedMutex.runExclusive(async () => { await fileChangedMutex.runExclusive(async () => {
@@ -200,21 +200,21 @@ function createFileReloader(server: WxtDevServer) {
switch (changes.type) { switch (changes.type) {
case 'extension-reload': case 'extension-reload':
server.reloadExtension(); server.reloadExtension();
consola.success(`Reloaded extension`); wxt.logger.success(`Reloaded extension`);
break; break;
case 'html-reload': case 'html-reload':
const { reloadedNames } = reloadHtmlPages( const { reloadedNames } = reloadHtmlPages(
changes.rebuildGroups, changes.rebuildGroups,
server, server,
); );
consola.success(`Reloaded: ${getFilenameList(reloadedNames)}`); wxt.logger.success(`Reloaded: ${getFilenameList(reloadedNames)}`);
break; break;
case 'content-script-reload': case 'content-script-reload':
reloadContentScripts(changes.changedSteps, server); reloadContentScripts(changes.changedSteps, server);
const rebuiltNames = changes.rebuildGroups const rebuiltNames = changes.rebuildGroups
.flat() .flat()
.map((entry) => entry.name); .map((entry) => entry.name);
consola.success(`Reloaded: ${getFilenameList(rebuiltNames)}`); wxt.logger.success(`Reloaded: ${getFilenameList(rebuiltNames)}`);
break; break;
} }
} catch (err) { } catch (err) {
@@ -2,12 +2,12 @@ import { describe, expect, it } from 'vitest';
import path from 'node:path'; import path from 'node:path';
import { bun } from '../bun'; import { bun } from '../bun';
const cwd = path.resolve(__dirname, 'fixtures/bun-project');
describe.skipIf(() => process.platform === 'win32')( describe.skipIf(() => process.platform === 'win32')(
'Bun Package Management Utils', 'Bun Package Management Utils',
() => { () => {
describe('listDependencies', () => { describe('listDependencies', () => {
const cwd = path.resolve(__dirname, 'fixtures/simple-bun-project');
it('should list direct dependencies', async () => { it('should list direct dependencies', async () => {
const actual = await bun.listDependencies({ cwd }); const actual = await bun.listDependencies({ cwd });
expect(actual).toEqual([ expect(actual).toEqual([
@@ -4,15 +4,14 @@ import { npm } from '../npm';
import { execaCommand } from 'execa'; import { execaCommand } from 'execa';
import { exists } from 'fs-extra'; import { exists } from 'fs-extra';
const cwd = path.resolve(__dirname, 'fixtures/npm-project');
describe('NPM Package Management Utils', () => { describe('NPM Package Management Utils', () => {
describe('listDependencies', () => {
const cwd = path.resolve(__dirname, 'fixtures/simple-npm-project');
beforeAll(async () => { beforeAll(async () => {
// NPM needs the modules installed for 'npm ls' to work // NPM needs the modules installed for 'npm ls' to work
await execaCommand('npm i', { cwd }); await execaCommand('npm i', { cwd });
}, 60e3); }, 60e3);
describe('listDependencies', () => {
it('should list direct dependencies', async () => { it('should list direct dependencies', async () => {
const actual = await npm.listDependencies({ cwd }); const actual = await npm.listDependencies({ cwd });
expect(actual).toEqual([ expect(actual).toEqual([
@@ -32,6 +31,8 @@ describe('NPM Package Management Utils', () => {
}); });
describe('downloadDependency', () => { describe('downloadDependency', () => {
const cwd = path.resolve(__dirname, 'fixtures/simple-npm-project');
it('should download the dependency as a tarball', async () => { it('should download the dependency as a tarball', async () => {
const downloadDir = path.resolve(cwd, 'dist'); const downloadDir = path.resolve(cwd, 'dist');
const id = 'mime-db@1.52.0'; const id = 'mime-db@1.52.0';
@@ -3,17 +3,16 @@ import path from 'node:path';
import { pnpm } from '../pnpm'; import { pnpm } from '../pnpm';
import { execaCommand } from 'execa'; import { execaCommand } from 'execa';
const cwd = path.resolve(__dirname, 'fixtures/pnpm-project');
process.env.WXT_PNPM_IGNORE_WORKSPACE = 'true'; process.env.WXT_PNPM_IGNORE_WORKSPACE = 'true';
describe('PNPM Package Management Utils', () => { describe('PNPM Package Management Utils', () => {
describe('listDependencies', () => {
const cwd = path.resolve(__dirname, 'fixtures/simple-pnpm-project');
beforeAll(async () => { beforeAll(async () => {
// PNPM needs the modules installed, or 'pnpm ls' will return a blank list. // PNPM needs the modules installed, or 'pnpm ls' will return a blank list.
await execaCommand('pnpm i --ignore-workspace', { cwd }); await execaCommand('pnpm i --ignore-workspace', { cwd });
}); });
describe('listDependencies', () => {
it('should list direct dependencies', async () => { it('should list direct dependencies', async () => {
const actual = await pnpm.listDependencies({ cwd }); const actual = await pnpm.listDependencies({ cwd });
expect(actual).toEqual([ expect(actual).toEqual([
@@ -2,10 +2,10 @@ import { describe, expect, it } from 'vitest';
import path from 'node:path'; import path from 'node:path';
import { yarn } from '../yarn'; import { yarn } from '../yarn';
const cwd = path.resolve(__dirname, 'fixtures/yarn-project');
describe('Yarn Package Management Utils', () => { describe('Yarn Package Management Utils', () => {
describe('listDependencies', () => { describe('listDependencies', () => {
const cwd = path.resolve(__dirname, 'fixtures/simple-yarn-project');
it('should list direct dependencies', async () => { it('should list direct dependencies', async () => {
const actual = await yarn.listDependencies({ cwd }); const actual = await yarn.listDependencies({ cwd });
expect(actual).toEqual([ expect(actual).toEqual([
@@ -19,8 +19,7 @@ export const bun: WxtPackageManagerImpl = {
.slice(1) // Skip the first line, is not a dependency .slice(1) // Skip the first line, is not a dependency
.map((line) => line.trim()) .map((line) => line.trim())
.map((line) => /.* (@?\S+)@(\S+)$/.exec(line)) .map((line) => /.* (@?\S+)@(\S+)$/.exec(line))
// @ts-expect-error: Filtering to known non-null matches .filter((match) => !!match)
.filter<RegExpExecArray>((match) => !!match)
.map(([_, name, version]) => ({ name, version })), .map(([_, name, version]) => ({ name, version })),
); );
}, },
@@ -100,6 +100,11 @@ describe('Detect Dev Changes', () => {
describe('Public Assets', () => { describe('Public Assets', () => {
it("should return 'extension-reload' without any groups to rebuild when the changed file is a public asset", () => { it("should return 'extension-reload' without any groups to rebuild when the changed file is a public asset", () => {
const changes = ['/root/src/public/image.svg']; const changes = ['/root/src/public/image.svg'];
setFakeWxt({
config: {
publicDir: '/root/src/public',
},
});
const asset1 = fakeOutputAsset({ const asset1 = fakeOutputAsset({
fileName: 'image.svg', fileName: 'image.svg',
}); });
@@ -114,10 +119,7 @@ describe('Detect Dev Changes', () => {
const expected: DevModeChange = { const expected: DevModeChange = {
type: 'extension-reload', type: 'extension-reload',
rebuildGroups: [], rebuildGroups: [],
cachedOutput: { cachedOutput: currentOutput,
...currentOutput,
publicAssets: [asset2],
},
}; };
const actual = detectDevChanges(changes, currentOutput); const actual = detectDevChanges(changes, currentOutput);
@@ -1,4 +1,4 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'; import { describe, it, expect, vi, beforeEach, Mock } from 'vitest';
import { import {
BackgroundEntrypoint, BackgroundEntrypoint,
BackgroundEntrypointOptions, BackgroundEntrypointOptions,
@@ -12,16 +12,13 @@ import {
import { resolve } from 'path'; import { resolve } from 'path';
import { findEntrypoints } from '../find-entrypoints'; import { findEntrypoints } from '../find-entrypoints';
import fs from 'fs-extra'; import fs from 'fs-extra';
import { importEntrypointFile } from '../import-entrypoint';
import glob from 'fast-glob'; import glob from 'fast-glob';
import { import {
fakeResolvedConfig, fakeResolvedConfig,
setFakeWxt, setFakeWxt,
} from '~/core/utils/testing/fake-objects'; } from '~/core/utils/testing/fake-objects';
import { unnormalizePath } from '~/core/utils/paths'; import { unnormalizePath } from '~/core/utils/paths';
import { wxt } from '~/core/wxt';
vi.mock('../import-entrypoint');
const importEntrypointFileMock = vi.mocked(importEntrypointFile);
vi.mock('fast-glob'); vi.mock('fast-glob');
const globMock = vi.mocked(glob); const globMock = vi.mocked(glob);
@@ -39,9 +36,11 @@ describe('findEntrypoints', () => {
outDir: resolve('.output'), outDir: resolve('.output'),
command: 'build', command: 'build',
}); });
let importEntrypointMock: Mock;
beforeEach(() => { beforeEach(() => {
setFakeWxt({ config }); setFakeWxt({ config });
importEntrypointMock = vi.mocked(wxt.builder.importEntrypoint);
}); });
it.each<[string, string, PopupEntrypoint]>([ it.each<[string, string, PopupEntrypoint]>([
@@ -211,13 +210,13 @@ describe('findEntrypoints', () => {
matches: ['<all_urls>'], matches: ['<all_urls>'],
}; };
globMock.mockResolvedValueOnce([path]); globMock.mockResolvedValueOnce([path]);
importEntrypointFileMock.mockResolvedValue(options); importEntrypointMock.mockResolvedValue(options);
const entrypoints = await findEntrypoints(); const entrypoints = await findEntrypoints();
expect(entrypoints).toHaveLength(1); expect(entrypoints).toHaveLength(1);
expect(entrypoints[0]).toEqual({ ...expected, options }); expect(entrypoints[0]).toEqual({ ...expected, options });
expect(importEntrypointFileMock).toBeCalledWith(expected.inputPath); expect(importEntrypointMock).toBeCalledWith(expected.inputPath);
}, },
); );
@@ -249,13 +248,13 @@ describe('findEntrypoints', () => {
type: 'module', type: 'module',
}; };
globMock.mockResolvedValueOnce([path]); globMock.mockResolvedValueOnce([path]);
importEntrypointFileMock.mockResolvedValue(options); importEntrypointMock.mockResolvedValue(options);
const entrypoints = await findEntrypoints(); const entrypoints = await findEntrypoints();
expect(entrypoints).toHaveLength(1); expect(entrypoints).toHaveLength(1);
expect(entrypoints[0]).toEqual({ ...expected, options }); expect(entrypoints[0]).toEqual({ ...expected, options });
expect(importEntrypointFileMock).toBeCalledWith(expected.inputPath); expect(importEntrypointMock).toBeCalledWith(expected.inputPath);
}, },
); );
@@ -338,12 +337,13 @@ describe('findEntrypoints', () => {
config: { config: {
manifestVersion: 2, manifestVersion: 2,
}, },
builder: wxt.builder,
}); });
const options: BackgroundEntrypointOptions = { const options: BackgroundEntrypointOptions = {
type: 'module', type: 'module',
}; };
globMock.mockResolvedValueOnce(['background.ts']); globMock.mockResolvedValueOnce(['background.ts']);
importEntrypointFileMock.mockResolvedValue(options); importEntrypointMock.mockResolvedValue(options);
const entrypoints = await findEntrypoints(); const entrypoints = await findEntrypoints();
@@ -355,12 +355,13 @@ describe('findEntrypoints', () => {
config: { config: {
manifestVersion: 3, manifestVersion: 3,
}, },
builder: wxt.builder,
}); });
const options: BackgroundEntrypointOptions = { const options: BackgroundEntrypointOptions = {
type: 'module', type: 'module',
}; };
globMock.mockResolvedValueOnce(['background.ts']); globMock.mockResolvedValueOnce(['background.ts']);
importEntrypointFileMock.mockResolvedValue(options); importEntrypointMock.mockResolvedValue(options);
const entrypoints = await findEntrypoints(); const entrypoints = await findEntrypoints();
@@ -373,6 +374,7 @@ describe('findEntrypoints', () => {
...config, ...config,
command: 'serve', command: 'serve',
}, },
builder: wxt.builder,
}); });
globMock.mockResolvedValueOnce(['popup.html']); globMock.mockResolvedValueOnce(['popup.html']);
@@ -410,13 +412,13 @@ describe('findEntrypoints', () => {
}; };
const options: BaseEntrypointOptions = {}; const options: BaseEntrypointOptions = {};
globMock.mockResolvedValueOnce([path]); globMock.mockResolvedValueOnce([path]);
importEntrypointFileMock.mockResolvedValue(options); importEntrypointMock.mockResolvedValue(options);
const entrypoints = await findEntrypoints(); const entrypoints = await findEntrypoints();
expect(entrypoints).toHaveLength(1); expect(entrypoints).toHaveLength(1);
expect(entrypoints[0]).toEqual({ ...expected, options }); expect(entrypoints[0]).toEqual({ ...expected, options });
expect(importEntrypointFileMock).toBeCalledWith(expected.inputPath); expect(importEntrypointMock).toBeCalledWith(expected.inputPath);
}, },
); );
@@ -701,7 +703,7 @@ describe('findEntrypoints', () => {
describe('include option', () => { describe('include option', () => {
it("should filter out the background when include doesn't contain the target browser", async () => { it("should filter out the background when include doesn't contain the target browser", async () => {
globMock.mockResolvedValueOnce(['background.ts']); globMock.mockResolvedValueOnce(['background.ts']);
importEntrypointFileMock.mockResolvedValue({ importEntrypointMock.mockResolvedValue({
include: ['not' + config.browser], include: ['not' + config.browser],
}); });
@@ -712,7 +714,7 @@ describe('findEntrypoints', () => {
it("should filter out content scripts when include doesn't contain the target browser", async () => { it("should filter out content scripts when include doesn't contain the target browser", async () => {
globMock.mockResolvedValueOnce(['example.content.ts']); globMock.mockResolvedValueOnce(['example.content.ts']);
importEntrypointFileMock.mockResolvedValue({ importEntrypointMock.mockResolvedValue({
include: ['not' + config.browser], include: ['not' + config.browser],
}); });
@@ -776,7 +778,7 @@ describe('findEntrypoints', () => {
describe('exclude option', () => { describe('exclude option', () => {
it('should filter out the background when exclude contains the target browser', async () => { it('should filter out the background when exclude contains the target browser', async () => {
globMock.mockResolvedValueOnce(['background.ts']); globMock.mockResolvedValueOnce(['background.ts']);
importEntrypointFileMock.mockResolvedValue({ importEntrypointMock.mockResolvedValue({
exclude: [config.browser], exclude: [config.browser],
}); });
@@ -787,7 +789,7 @@ describe('findEntrypoints', () => {
it('should filter out content scripts when exclude contains the target browser', async () => { it('should filter out content scripts when exclude contains the target browser', async () => {
globMock.mockResolvedValueOnce(['example.content.ts']); globMock.mockResolvedValueOnce(['example.content.ts']);
importEntrypointFileMock.mockResolvedValue({ importEntrypointMock.mockResolvedValue({
exclude: [config.browser], exclude: [config.browser],
}); });
@@ -850,7 +852,6 @@ describe('findEntrypoints', () => {
'ui.content/index.ts', 'ui.content/index.ts',
'injected.content/index.ts', 'injected.content/index.ts',
]); ]);
importEntrypointFileMock.mockResolvedValue({});
const filterEntrypoints = ['popup', 'ui']; const filterEntrypoints = ['popup', 'ui'];
setFakeWxt({ setFakeWxt({
config: { config: {
@@ -860,8 +861,11 @@ describe('findEntrypoints', () => {
command: 'build', command: 'build',
filterEntrypoints: new Set(filterEntrypoints), filterEntrypoints: new Set(filterEntrypoints),
}, },
builder: wxt.builder,
}); });
importEntrypointMock.mockResolvedValue({});
const entrypoints = await findEntrypoints(); const entrypoints = await findEntrypoints();
const names = entrypoints.map((item) => item.name); const names = entrypoints.map((item) => item.name);
expect(names).toHaveLength(2); expect(names).toHaveLength(2);
@@ -45,14 +45,18 @@ async function copyPublicDirectory(): Promise<BuildOutput['publicAssets']> {
if (files.length === 0) return []; if (files.length === 0) return [];
const publicAssets: BuildOutput['publicAssets'] = []; const publicAssets: BuildOutput['publicAssets'] = [];
for (const { absoluteSrc, relativeDest } of files) { for (const file of files) {
const absoluteDest = resolve(wxt.config.outDir, relativeDest); const absoluteDest = resolve(wxt.config.outDir, file.relativeDest);
await fs.ensureDir(dirname(absoluteDest)); await fs.ensureDir(dirname(absoluteDest));
await fs.copyFile(absoluteSrc, absoluteDest); if ('absoluteSrc' in file) {
await fs.copyFile(file.absoluteSrc, absoluteDest);
} else {
await fs.writeFile(absoluteDest, file.contents, 'utf8');
}
publicAssets.push({ publicAssets.push({
type: 'asset', type: 'asset',
fileName: relativeDest, fileName: file.relativeDest,
}); });
} }
@@ -2,7 +2,6 @@ import {
BuildOutput, BuildOutput,
BuildStepOutput, BuildStepOutput,
EntrypointGroup, EntrypointGroup,
OutputAsset,
OutputFile, OutputFile,
} from '~/types'; } from '~/types';
import { every, some } from '~/core/utils/arrays'; import { every, some } from '~/core/utils/arrays';
@@ -50,12 +49,25 @@ export function detectDevChanges(
findEffectedSteps(changedFile, currentOutput), findEffectedSteps(changedFile, currentOutput),
), ),
); );
if (changedSteps.size === 0) return { type: 'no-change' }; if (changedSteps.size === 0) {
const hasPublicChange = some(changedFiles, (file) =>
file.startsWith(wxt.config.publicDir),
);
if (hasPublicChange) {
return {
type: 'extension-reload',
rebuildGroups: [],
cachedOutput: currentOutput,
};
} else {
return { type: 'no-change' };
}
}
const unchangedOutput: BuildOutput = { const unchangedOutput: BuildOutput = {
manifest: currentOutput.manifest, manifest: currentOutput.manifest,
steps: [], steps: [],
publicAssets: [], publicAssets: [...currentOutput.publicAssets],
}; };
const changedOutput: BuildOutput = { const changedOutput: BuildOutput = {
manifest: currentOutput.manifest, manifest: currentOutput.manifest,
@@ -70,13 +82,6 @@ export function detectDevChanges(
unchangedOutput.steps.push(step); unchangedOutput.steps.push(step);
} }
} }
for (const asset of currentOutput.publicAssets) {
if (changedSteps.has(asset)) {
changedOutput.publicAssets.push(asset);
} else {
unchangedOutput.publicAssets.push(asset);
}
}
const isOnlyHtmlChanges = const isOnlyHtmlChanges =
changedFiles.length > 0 && changedFiles.length > 0 &&
@@ -117,8 +122,8 @@ export function detectDevChanges(
function findEffectedSteps( function findEffectedSteps(
changedFile: string, changedFile: string,
currentOutput: BuildOutput, currentOutput: BuildOutput,
): DetectedChange[] { ): BuildStepOutput[] {
const changes: DetectedChange[] = []; const changes: BuildStepOutput[] = [];
const changedPath = normalizePath(changedFile); const changedPath = normalizePath(changedFile);
const isChunkEffected = (chunk: OutputFile): boolean => const isChunkEffected = (chunk: OutputFile): boolean =>
@@ -135,11 +140,6 @@ function findEffectedSteps(
if (effectedChunk) changes.push(step); if (effectedChunk) changes.push(step);
} }
const effectedAsset = currentOutput.publicAssets.find((chunk) =>
isChunkEffected(chunk),
);
if (effectedAsset) changes.push(effectedAsset);
return changes; return changes;
} }
@@ -194,10 +194,3 @@ interface ContentScriptReload extends RebuildChange {
type: 'content-script-reload'; type: 'content-script-reload';
changedSteps: BuildStepOutput[]; changedSteps: BuildStepOutput[];
} }
/**
* When figuring out what needs reloaded, this stores the step that was changed, or the public
* directory asset that was changed. It doesn't know what type of change is required yet. Just an
* intermediate type.
*/
type DetectedChange = BuildStepOutput | OutputAsset;
@@ -28,7 +28,6 @@ import { VIRTUAL_NOOP_BACKGROUND_MODULE_ID } from '~/core/utils/constants';
import { CSS_EXTENSIONS_PATTERN } from '~/core/utils/paths'; import { CSS_EXTENSIONS_PATTERN } from '~/core/utils/paths';
import pc from 'picocolors'; import pc from 'picocolors';
import { wxt } from '../../wxt'; import { wxt } from '../../wxt';
import { importEntrypointFile } from './import-entrypoint';
/** /**
* Return entrypoints and their configuration by looking through the project's files. * Return entrypoints and their configuration by looking through the project's files.
@@ -290,7 +289,7 @@ async function getUnlistedScriptEntrypoint({
skipped, skipped,
}: EntrypointInfo): Promise<GenericEntrypoint> { }: EntrypointInfo): Promise<GenericEntrypoint> {
const defaultExport = const defaultExport =
await importEntrypoint<UnlistedScriptDefinition>(inputPath); await wxt.builder.importEntrypoint<UnlistedScriptDefinition>(inputPath);
if (defaultExport == null) { if (defaultExport == null) {
throw Error( throw Error(
`${name}: Default export not found, did you forget to call "export default defineUnlistedScript(...)"?`, `${name}: Default export not found, did you forget to call "export default defineUnlistedScript(...)"?`,
@@ -315,7 +314,7 @@ async function getBackgroundEntrypoint({
let options: Omit<BackgroundDefinition, 'main'> = {}; let options: Omit<BackgroundDefinition, 'main'> = {};
if (inputPath !== VIRTUAL_NOOP_BACKGROUND_MODULE_ID) { if (inputPath !== VIRTUAL_NOOP_BACKGROUND_MODULE_ID) {
const defaultExport = const defaultExport =
await importEntrypoint<BackgroundDefinition>(inputPath); await wxt.builder.importEntrypoint<BackgroundDefinition>(inputPath);
if (defaultExport == null) { if (defaultExport == null) {
throw Error( throw Error(
`${name}: Default export not found, did you forget to call "export default defineBackground(...)"?`, `${name}: Default export not found, did you forget to call "export default defineBackground(...)"?`,
@@ -344,8 +343,15 @@ async function getContentScriptEntrypoint({
name, name,
skipped, skipped,
}: EntrypointInfo): Promise<ContentScriptEntrypoint> { }: EntrypointInfo): Promise<ContentScriptEntrypoint> {
const { main: _, ...options } = const defaultExport =
await importEntrypoint<ContentScriptDefinition>(inputPath); await wxt.builder.importEntrypoint<ContentScriptDefinition>(inputPath);
if (defaultExport == null) {
throw Error(
`${name}: Default export not found, did you forget to call "export default defineContentScript(...)"?`,
);
}
const { main: _, ...options } = defaultExport;
if (options == null) { if (options == null) {
throw Error( throw Error(
`${name}: Default export not found, did you forget to call "export default defineContentScript(...)"?`, `${name}: Default export not found, did you forget to call "export default defineContentScript(...)"?`,
@@ -488,9 +494,3 @@ const PATH_GLOB_TO_TYPE_MAP: Record<string, Entrypoint['type']> = {
}; };
const CONTENT_SCRIPT_OUT_DIR = 'content-scripts'; const CONTENT_SCRIPT_OUT_DIR = 'content-scripts';
function importEntrypoint<T>(path: string) {
return wxt.config.experimental.viteRuntime
? wxt.builder.importEntrypoint<T>(path)
: importEntrypointFile<T>(path);
}
@@ -1,11 +1,6 @@
import { Unimport, createUnimport } from 'unimport'; import { Entrypoint, WxtDirEntry, WxtDirFileEntry } from '~/types';
import {
EslintGlobalsPropValue,
Entrypoint,
WxtResolvedUnimportOptions,
} from '~/types';
import fs from 'fs-extra'; import fs from 'fs-extra';
import { relative, resolve } from 'path'; import { dirname, relative, resolve } from 'node:path';
import { import {
getEntrypointBundlePath, getEntrypointBundlePath,
isHtmlEntrypoint, isHtmlEntrypoint,
@@ -25,61 +20,57 @@ export async function generateTypesDir(
): Promise<void> { ): Promise<void> {
await fs.ensureDir(wxt.config.typesDir); await fs.ensureDir(wxt.config.typesDir);
const references: string[] = []; const entries: WxtDirEntry[] = [
// Hard-coded entries
{ module: 'wxt/vite-builder-env' },
];
if (wxt.config.imports !== false) { // Add references to modules installed from NPM to the TS project so their
const unimport = createUnimport(wxt.config.imports); // type augmentation can update InlineConfig correctly. Local modules defined
references.push(await writeImportsDeclarationFile(unimport)); // in <root>/modules are already apart of the project, so we don't need to
if (wxt.config.imports.eslintrc.enabled) { // add them.
await writeImportsEslintFile(unimport, wxt.config.imports); wxt.config.userModules.forEach((module) => {
} if (module.type === 'node_module' && module.configKey != null)
} entries.push({ module: module.id });
references.push(await writePathsDeclarationFile(entrypoints));
references.push(await writeI18nDeclarationFile());
references.push(await writeGlobalsDeclarationFile());
const mainReference = await writeMainDeclarationFile(references);
await writeTsConfigFile(mainReference);
}
async function writeImportsDeclarationFile(unimport: Unimport) {
const filePath = resolve(wxt.config.typesDir, 'imports.d.ts');
// Load project imports into unimport memory so they are output via generateTypeDeclarations
await unimport.scanImportsFromDir(undefined, { cwd: wxt.config.srcDir });
await writeFileIfDifferent(
filePath,
['// Generated by wxt', await unimport.generateTypeDeclarations()].join(
'\n',
) + '\n',
);
return filePath;
}
async function writeImportsEslintFile(
unimport: Unimport,
options: WxtResolvedUnimportOptions,
) {
const globals: Record<string, EslintGlobalsPropValue> = {};
const eslintrc = { globals };
(await unimport.getImports())
.map((i) => i.as ?? i.name)
.filter(Boolean)
.sort()
.forEach((name) => {
eslintrc.globals[name] = options.eslintrc.globalsPropValue;
}); });
await fs.writeJson(options.eslintrc.filePath, eslintrc, { spaces: 2 });
// browser.runtime.getURL
entries.push(await getPathsDeclarationEntry(entrypoints));
// browser.i18n.getMessage
entries.push(await getI18nDeclarationEntry());
// import.meta.env.*
entries.push(await getGlobalsDeclarationEntry());
// tsconfig.json
entries.push(await getTsConfigEntry());
// Let modules add more entries
await wxt.hooks.callHook('prepare:types', wxt, entries);
// Add main declaration file, not editable
entries.push(getMainDeclarationEntry(entries));
// Write all the files
const absoluteFileEntries = (
entries.filter((entry) => 'path' in entry) as WxtDirFileEntry[]
).map<WxtDirFileEntry>((entry) => ({
...entry,
path: resolve(wxt.config.wxtDir, entry.path),
}));
await Promise.all(
absoluteFileEntries.map(async (file) => {
await fs.ensureDir(dirname(file.path));
await writeFileIfDifferent(file.path, file.text);
}),
);
} }
async function writePathsDeclarationFile( async function getPathsDeclarationEntry(
entrypoints: Entrypoint[], entrypoints: Entrypoint[],
): Promise<string> { ): Promise<WxtDirFileEntry> {
const filePath = resolve(wxt.config.typesDir, 'paths.d.ts');
const unions = entrypoints const unions = entrypoints
.map((entry) => .map((entry) =>
getEntrypointBundlePath( getEntrypointBundlePath(
@@ -108,16 +99,14 @@ declare module "wxt/browser" {
} }
`; `;
await writeFileIfDifferent( return {
filePath, path: 'types/paths.d.ts',
template.replace('{{ union }}', unions || ' | never'), text: template.replace('{{ union }}', unions || ' | never'),
); tsReference: true,
};
return filePath;
} }
async function writeI18nDeclarationFile(): Promise<string> { async function getI18nDeclarationEntry(): Promise<WxtDirFileEntry> {
const filePath = resolve(wxt.config.typesDir, 'i18n.d.ts');
const defaultLocale = wxt.config.manifest.default_locale; const defaultLocale = wxt.config.manifest.default_locale;
const template = `// Generated by wxt const template = `// Generated by wxt
import "wxt/browser"; import "wxt/browser";
@@ -165,20 +154,19 @@ declare module "wxt/browser" {
options?: GetMessageOptions, options?: GetMessageOptions,
): string;`; ): string;`;
}); });
await writeFileIfDifferent(
filePath,
template.replace('{{ overrides }}', overrides.join('\n')),
);
return filePath; return {
path: 'types/i18n.d.ts',
text: template.replace('{{ overrides }}', overrides.join('\n')),
tsReference: true,
};
} }
async function writeGlobalsDeclarationFile(): Promise<string> { async function getGlobalsDeclarationEntry(): Promise<WxtDirFileEntry> {
const filePath = resolve(wxt.config.typesDir, 'globals.d.ts');
const globals = [...getGlobals(wxt.config), ...getEntrypointGlobals('')]; const globals = [...getGlobals(wxt.config), ...getEntrypointGlobals('')];
await writeFileIfDifferent( return {
filePath, path: 'types/globals.d.ts',
[ text: [
'// Generated by wxt', '// Generated by wxt',
'export {}', 'export {}',
'interface ImportMetaEnv {', 'interface ImportMetaEnv {',
@@ -187,39 +175,30 @@ async function writeGlobalsDeclarationFile(): Promise<string> {
'interface ImportMeta {', 'interface ImportMeta {',
' readonly env: ImportMetaEnv', ' readonly env: ImportMetaEnv',
'}', '}',
].join('\n') + '\n', '',
); ].join('\n'),
return filePath; tsReference: true,
};
} }
async function writeMainDeclarationFile(references: string[]): Promise<string> { function getMainDeclarationEntry(references: WxtDirEntry[]): WxtDirFileEntry {
const dir = wxt.config.wxtDir; const lines = ['// Generated by wxt'];
const filePath = resolve(dir, 'wxt.d.ts'); references.forEach((ref) => {
await writeFileIfDifferent( if ('module' in ref) {
filePath, return lines.push(`/// <reference types="${ref.module}" />`);
[ } else if (ref.tsReference) {
'// Generated by wxt', const absolutePath = resolve(wxt.config.wxtDir, ref.path);
`/// <reference types="wxt/vite-builder-env" />`, const relativePath = relative(wxt.config.wxtDir, absolutePath);
...references.map( lines.push(`/// <reference types="./${normalizePath(relativePath)}" />`);
(ref) => }
`/// <reference types="./${normalizePath(relative(dir, ref))}" />`, });
), return {
path: 'wxt.d.ts',
// Add references to modules installed from NPM to the TS project so text: lines.join('\n') + '\n',
// their type augmentation can update InlineConfig correctly. Local };
// modules defined in <root>/modules are already apart of the project, so
// we don't need to add them.
...wxt.config.modules
.filter(
(module) => module.type === 'node_module' && module.configKey != null,
)
.map((module) => `/// <reference types="${module.id}" />`),
].join('\n') + '\n',
);
return filePath;
} }
async function writeTsConfigFile(mainReference: string) { async function getTsConfigEntry(): Promise<WxtDirFileEntry> {
const dir = wxt.config.wxtDir; const dir = wxt.config.wxtDir;
const getTsconfigPath = (path: string) => normalizePath(relative(dir, path)); const getTsconfigPath = (path: string) => normalizePath(relative(dir, path));
const paths = Object.entries(wxt.config.alias) const paths = Object.entries(wxt.config.alias)
@@ -232,9 +211,7 @@ async function writeTsConfigFile(mainReference: string) {
}) })
.join(',\n'); .join(',\n');
await writeFileIfDifferent( const text = `{
resolve(dir, 'tsconfig.json'),
`{
"compilerOptions": { "compilerOptions": {
"target": "ESNext", "target": "ESNext",
"module": "ESNext", "module": "ESNext",
@@ -251,9 +228,13 @@ ${paths}
}, },
"include": [ "include": [
"${getTsconfigPath(wxt.config.root)}/**/*", "${getTsconfigPath(wxt.config.root)}/**/*",
"./${getTsconfigPath(mainReference)}" "./wxt.d.ts"
], ],
"exclude": ["${getTsconfigPath(wxt.config.outBaseDir)}"] "exclude": ["${getTsconfigPath(wxt.config.outBaseDir)}"]
}`, }`;
);
return {
path: 'tsconfig.json',
text,
};
} }
@@ -15,7 +15,6 @@ import {
ValidationResults, ValidationResults,
validateEntrypoints, validateEntrypoints,
} from '../validation'; } from '../validation';
import consola from 'consola';
import { wxt } from '../../wxt'; import { wxt } from '../../wxt';
import { mergeJsonOutputs } from '@aklinker1/rollup-plugin-visualizer'; import { mergeJsonOutputs } from '@aklinker1/rollup-plugin-visualizer';
import { isCI } from 'ci-info'; import { isCI } from 'ci-info';
@@ -134,12 +133,12 @@ function printValidationResults({
}, new Map<Entrypoint, ValidationResult[]>()); }, new Map<Entrypoint, ValidationResult[]>());
Array.from(entrypointErrors.entries()).forEach(([entrypoint, errors]) => { Array.from(entrypointErrors.entries()).forEach(([entrypoint, errors]) => {
consola.log(relative(cwd, entrypoint.inputPath)); wxt.logger.log(relative(cwd, entrypoint.inputPath));
console.log(); console.log();
errors.forEach((err) => { errors.forEach((err) => {
const type = err.type === 'error' ? pc.red('ERROR') : pc.yellow('WARN'); const type = err.type === 'error' ? pc.red('ERROR') : pc.yellow('WARN');
const recieved = pc.dim(`(recieved: ${JSON.stringify(err.value)})`); const recieved = pc.dim(`(recieved: ${JSON.stringify(err.value)})`);
consola.log(` - ${type} ${err.message} ${recieved}`); wxt.logger.log(` - ${type} ${err.message} ${recieved}`);
}); });
console.log(); console.log();
}); });
@@ -12,16 +12,19 @@ import {
WxtCommand, WxtCommand,
WxtModule, WxtModule,
WxtModuleWithMetadata, WxtModuleWithMetadata,
ResolvedEslintrc,
Eslintrc,
} from '~/types'; } from '~/types';
import path from 'node:path'; import path from 'node:path';
import { createFsCache } from '~/core/utils/cache'; import { createFsCache } from '~/core/utils/cache';
import consola, { LogLevels } from 'consola'; import consola, { LogLevels } from 'consola';
import defu from 'defu'; import defu from 'defu';
import { NullablyRequired } from '../types'; import { NullablyRequired } from '../types';
import { isModuleInstalled } from '../package';
import fs from 'fs-extra'; import fs from 'fs-extra';
import { normalizePath } from '../paths'; import { normalizePath } from '../paths';
import glob from 'fast-glob'; import glob from 'fast-glob';
import { builtinModules } from '~/builtin-modules';
import { getEslintVersion } from '../eslint';
/** /**
* Given an inline config, discover the config file if necessary, merge the results, resolve any * Given an inline config, discover the config file if necessary, merge the results, resolve any
@@ -127,15 +130,21 @@ export async function resolveConfig(
}; };
} }
const modules = await resolveWxtModules(modulesDir, mergedConfig.modules); const userModules = await resolveWxtUserModules(
const moduleOptions = modules.reduce<Record<string, any>>((map, module) => { modulesDir,
mergedConfig.modules,
);
const moduleOptions = userModules.reduce<Record<string, any>>(
(map, module) => {
if (module.configKey) { if (module.configKey) {
map[module.configKey] = map[module.configKey] =
// @ts-expect-error // @ts-expect-error
mergedConfig[module.configKey]; mergedConfig[module.configKey];
} }
return map; return map;
}, {}); },
{},
);
return { return {
browser, browser,
@@ -146,7 +155,7 @@ export async function resolveConfig(
filterEntrypoints, filterEntrypoints,
env, env,
fsCache: createFsCache(wxtDir), fsCache: createFsCache(wxtDir),
imports: await getUnimportOptions(wxtDir, logger, mergedConfig), imports: await getUnimportOptions(wxtDir, srcDir, logger, mergedConfig),
logger, logger,
manifest: await resolveManifestConfig(env, mergedConfig.manifest), manifest: await resolveManifestConfig(env, mergedConfig.manifest),
manifestVersion, manifestVersion,
@@ -160,14 +169,14 @@ export async function resolveConfig(
srcDir, srcDir,
typesDir, typesDir,
wxtDir, wxtDir,
zip: resolveZipConfig(root, mergedConfig), zip: resolveZipConfig(root, outBaseDir, mergedConfig),
transformManifest: mergedConfig.transformManifest, transformManifest: mergedConfig.transformManifest,
analysis: resolveAnalysisConfig(root, mergedConfig), analysis: resolveAnalysisConfig(root, mergedConfig),
userConfigMetadata: userConfigMetadata ?? {}, userConfigMetadata: userConfigMetadata ?? {},
alias, alias,
experimental: defu(mergedConfig.experimental, { experimental: defu(mergedConfig.experimental, {
includeBrowserPolyfill: true, includeBrowserPolyfill: true,
viteRuntime: false, entrypointImporter: 'jiti' as const,
}), }),
dev: { dev: {
server: devServerConfig, server: devServerConfig,
@@ -175,7 +184,8 @@ export async function resolveConfig(
}, },
hooks: mergedConfig.hooks ?? {}, hooks: mergedConfig.hooks ?? {},
vite: mergedConfig.vite ?? (() => ({})), vite: mergedConfig.vite ?? (() => ({})),
modules, builtinModules,
userModules,
plugins: [], plugins: [],
...moduleOptions, ...moduleOptions,
}; };
@@ -233,6 +243,7 @@ async function mergeInlineConfig(
function resolveZipConfig( function resolveZipConfig(
root: string, root: string,
outBaseDir: string,
mergedConfig: InlineConfig, mergedConfig: InlineConfig,
): NullablyRequired<ResolvedConfig['zip']> { ): NullablyRequired<ResolvedConfig['zip']> {
const downloadedPackagesDir = path.resolve(root, '.wxt/local_modules'); const downloadedPackagesDir = path.resolve(root, '.wxt/local_modules');
@@ -253,6 +264,8 @@ function resolveZipConfig(
// Tests // Tests
'**/__tests__/**', '**/__tests__/**',
'**/*.+(test|spec).?(c|m)+(j|t)s?(x)', '**/*.+(test|spec).?(c|m)+(j|t)s?(x)',
// Output directory
`${path.relative(root, outBaseDir)}/**`,
// From user // From user
...(mergedConfig.zip?.excludeSources ?? []), ...(mergedConfig.zip?.excludeSources ?? []),
], ],
@@ -285,22 +298,12 @@ function resolveAnalysisConfig(
async function getUnimportOptions( async function getUnimportOptions(
wxtDir: string, wxtDir: string,
srcDir: string,
logger: Logger, logger: Logger,
config: InlineConfig, config: InlineConfig,
): Promise<WxtResolvedUnimportOptions | false> { ): Promise<WxtResolvedUnimportOptions | false> {
if (config.imports === false) return false; if (config.imports === false) return false;
const enabledConfig = config.imports?.eslintrc?.enabled;
let enabled: boolean;
switch (enabledConfig) {
case undefined:
case 'auto':
enabled = await isModuleInstalled('eslint');
break;
default:
enabled = enabledConfig;
}
const defaultOptions: WxtResolvedUnimportOptions = { const defaultOptions: WxtResolvedUnimportOptions = {
debugLog: logger.debug, debugLog: logger.debug,
imports: [ imports: [
@@ -315,11 +318,10 @@ async function getUnimportOptions(
], ],
warn: logger.warn, warn: logger.warn,
dirs: ['components', 'composables', 'hooks', 'utils'], dirs: ['components', 'composables', 'hooks', 'utils'],
eslintrc: { dirsScanOptions: {
enabled, cwd: srcDir,
filePath: path.resolve(wxtDir, 'eslintrc-auto-import.json'),
globalsPropValue: true,
}, },
eslintrc: await getUnimportEslintOptions(wxtDir, config.imports?.eslintrc),
}; };
return defu<WxtResolvedUnimportOptions, [WxtResolvedUnimportOptions]>( return defu<WxtResolvedUnimportOptions, [WxtResolvedUnimportOptions]>(
@@ -328,6 +330,40 @@ async function getUnimportOptions(
); );
} }
async function getUnimportEslintOptions(
wxtDir: string,
options: Eslintrc | undefined,
): Promise<ResolvedEslintrc> {
const rawEslintEnabled = options?.enabled ?? 'auto';
let eslintEnabled: ResolvedEslintrc['enabled'];
switch (rawEslintEnabled) {
case 'auto':
const version = await getEslintVersion();
let major = parseInt(version[0]);
if (major <= 8) eslintEnabled = 8;
else if (major >= 9) eslintEnabled = 9;
// NaN
else eslintEnabled = 8;
break;
case true:
eslintEnabled = 8;
break;
default:
eslintEnabled = rawEslintEnabled;
}
return {
enabled: eslintEnabled,
filePath: path.resolve(
wxtDir,
eslintEnabled === 9
? 'eslint-auto-imports.mjs'
: 'eslintrc-auto-import.json',
),
globalsPropValue: true,
};
}
/** /**
* Returns the path to `node_modules/wxt`. * Returns the path to `node_modules/wxt`.
*/ */
@@ -378,7 +414,7 @@ export async function mergeBuilderConfig(
throw Error('Builder not found. Make sure vite is installed.'); throw Error('Builder not found. Make sure vite is installed.');
} }
export async function resolveWxtModules( export async function resolveWxtUserModules(
modulesDir: string, modulesDir: string,
modules: string[] = [], modules: string[] = [],
): Promise<WxtModuleWithMetadata<any>[]> { ): Promise<WxtModuleWithMetadata<any>[]> {
+11
View File
@@ -0,0 +1,11 @@
export async function getEslintVersion(): Promise<string[]> {
try {
const require = (await import('node:module')).default.createRequire(
import.meta.url,
);
const { ESLint } = require('eslint');
return ESLint.version?.split('.') ?? [];
} catch (error) {
return [];
}
}
-4
View File
@@ -20,7 +20,3 @@ export async function getPackageJson(): Promise<
return {}; return {};
} }
} }
export function isModuleInstalled(name: string) {
return import(/* @vite-ignore */ name).then(() => true).catch(() => false);
}
@@ -239,12 +239,13 @@ export const fakeResolvedConfig = fakeObjectCreator<ResolvedConfig>(() => {
command, command,
entrypointsDir: fakeDir(), entrypointsDir: fakeDir(),
modulesDir: fakeDir(), modulesDir: fakeDir(),
modules: [], builtinModules: [],
userModules: [],
env: { browser, command, manifestVersion, mode }, env: { browser, command, manifestVersion, mode },
fsCache: mock<FsCache>(), fsCache: mock<FsCache>(),
imports: { imports: {
eslintrc: { eslintrc: {
enabled: faker.datatype.boolean(), enabled: faker.helpers.arrayElement([false, 8, 9]),
filePath: fakeFile(), filePath: fakeFile(),
globalsPropValue: faker.helpers.arrayElement([ globalsPropValue: faker.helpers.arrayElement([
true, true,
@@ -297,7 +298,7 @@ export const fakeResolvedConfig = fakeObjectCreator<ResolvedConfig>(() => {
alias: {}, alias: {},
experimental: { experimental: {
includeBrowserPolyfill: true, includeBrowserPolyfill: true,
viteRuntime: false, entrypointImporter: 'jiti',
}, },
dev: { dev: {
reloadCommand: 'Alt+R', reloadCommand: 'Alt+R',
+6 -6
View File
@@ -5,11 +5,13 @@ import {
WxtCommand, WxtCommand,
WxtDevServer, WxtDevServer,
WxtHooks, WxtHooks,
WxtModule,
} from '~/types'; } from '~/types';
import { resolveConfig } from './utils/building'; import { resolveConfig } from './utils/building';
import { createHooks } from 'hookable'; import { createHooks } from 'hookable';
import { createWxtPackageManager } from './package-managers'; import { createWxtPackageManager } from './package-managers';
import { createViteBuilder } from './builders/vite'; import { createViteBuilder } from './builders/vite';
import { builtinModules } from '~/builtin-modules';
/** /**
* Global variable set once `createWxt` is called once. Since this variable is used everywhere, this * Global variable set once `createWxt` is called once. Since this variable is used everywhere, this
@@ -46,18 +48,16 @@ export async function registerWxt(
}; };
// Initialize modules // Initialize modules
for (const module of config.modules) { const initModule = async (module: WxtModule<any>) => {
if (module.hooks) wxt.hooks.addHooks(module.hooks); if (module.hooks) wxt.hooks.addHooks(module.hooks);
if (wxt.config.imports !== false && module.imports) {
wxt.config.imports.imports ??= [];
wxt.config.imports.imports.push(...module.imports);
}
await module.setup?.( await module.setup?.(
wxt, wxt,
// @ts-expect-error: Untyped configKey field // @ts-expect-error: Untyped configKey field
module.configKey ? config[module.configKey] : undefined, module.configKey ? config[module.configKey] : undefined,
); );
} };
for (const builtinModule of builtinModules) await initModule(builtinModule);
for (const userModule of config.userModules) await initModule(userModule);
// Initialize hooks // Initialize hooks
wxt.hooks.addHooks(config.hooks); wxt.hooks.addHooks(config.hooks);
+2 -1
View File
@@ -104,7 +104,8 @@ async function zipDir(
// Ignore node_modules, otherwise this glob step takes forever // Ignore node_modules, otherwise this glob step takes forever
ignore: ['**/node_modules'], ignore: ['**/node_modules'],
onlyFiles: true, onlyFiles: true,
dot: true, // TODO: Fix #738
// dot: true,
}) })
).filter((relativePath) => { ).filter((relativePath) => {
return ( return (
+5 -7
View File
@@ -104,13 +104,11 @@ export function addViteConfig(
): void { ): void {
wxt.hooks.hook('ready', (wxt) => { wxt.hooks.hook('ready', (wxt) => {
const userVite = wxt.config.vite; const userVite = wxt.config.vite;
wxt.config.vite = (env) => wxt.config.vite = async (env) => {
vite.mergeConfig( const fromUser = await userVite(env);
// Use config added by module as base const fromModule = viteConfig(env) ?? {};
viteConfig(env) ?? {}, return vite.mergeConfig(fromModule, fromUser);
// Overwrite module config with user config };
userVite(env),
);
}); });
} }
@@ -0,0 +1,85 @@
import { logger } from './utils/logger';
interface WebSocketMessage {
type: string;
event: string;
data?: any;
}
export interface WxtWebSocket extends WebSocket {
addWxtEventListener(
type: 'wxt:reload-extension',
callback: (event: CustomEvent<undefined>) => void,
options?: AddEventListenerOptions | boolean,
): void;
addWxtEventListener(
type: 'wxt:reload-content-script',
callback?: (event: CustomEvent<ReloadContentScriptPayload>) => void,
options?: AddEventListenerOptions | boolean,
): void;
addWxtEventListener(
type: 'wxt:reload-page',
callback?: (event: CustomEvent<string>) => void,
options?: AddEventListenerOptions | boolean,
): void;
sendCustom(event: string, payload?: any): void;
}
let ws: WxtWebSocket | undefined;
/**
* Connect to the websocket and listen for messages.
*
* @param onMessage Optional callback that is called when a message is recieved and we've verified
* it's structure is what we expect.
*/
export function getDevServerWebSocket(): WxtWebSocket {
if (import.meta.env.COMMAND !== 'serve')
throw Error(
'Must be running WXT dev command to connect to call getDevServerWebSocket()',
);
if (ws == null) {
const serverUrl = `${__DEV_SERVER_PROTOCOL__}//${__DEV_SERVER_HOSTNAME__}:${__DEV_SERVER_PORT__}`;
logger.debug('Connecting to dev server @', serverUrl);
ws = new WebSocket(serverUrl, 'vite-hmr') as WxtWebSocket;
ws.addWxtEventListener = ws.addEventListener.bind(ws);
ws.sendCustom = (event, payload) =>
ws?.send(JSON.stringify({ type: 'custom', event, payload }));
ws.addEventListener('open', () => {
logger.debug('Connected to dev server');
});
ws.addEventListener('close', () => {
logger.debug('Disconnected from dev server');
});
ws.addEventListener('error', (event) => {
logger.error('Failed to connect to dev server', event);
});
ws.addEventListener('message', (e) => {
try {
const message = JSON.parse(e.data) as WebSocketMessage;
if (message.type === 'custom') {
ws?.dispatchEvent(
new CustomEvent(message.event, { detail: message.data }),
);
}
} catch (err) {
logger.error('Failed to handle message', err);
}
});
}
return ws;
}
export interface ReloadContentScriptPayload {
registration?: 'manifest' | 'runtime';
contentScript: {
matches: string[];
js?: string[];
css?: string[];
};
}
+12 -4
View File
@@ -1,6 +1,5 @@
import type * as vite from 'vite'; import type * as vite from 'vite';
import { import {
unimport,
download, download,
tsconfigPaths, tsconfigPaths,
globals, globals,
@@ -8,6 +7,8 @@ import {
} from '../core/builders/vite/plugins'; } from '../core/builders/vite/plugins';
import { resolveConfig } from '~/core/utils/building'; import { resolveConfig } from '~/core/utils/building';
import { InlineConfig } from '../types'; import { InlineConfig } from '../types';
import { vitePlugin as unimportPlugin } from '~/builtin-modules/unimport';
import { createUnimport } from 'unimport';
/** /**
* Vite plugin that configures Vitest with everything required to test a WXT extension, based on the `<root>/wxt.config.ts` * Vite plugin that configures Vitest with everything required to test a WXT extension, based on the `<root>/wxt.config.ts`
@@ -25,11 +26,18 @@ import { InlineConfig } from '../types';
* @param inlineConfig Customize WXT's config for testing. Any config specified here overrides the config from your `wxt.config.ts` file. * @param inlineConfig Customize WXT's config for testing. Any config specified here overrides the config from your `wxt.config.ts` file.
*/ */
export function WxtVitest(inlineConfig?: InlineConfig): vite.PluginOption { export function WxtVitest(inlineConfig?: InlineConfig): vite.PluginOption {
return resolveConfig(inlineConfig ?? {}, 'serve').then((config) => [ return resolveConfig(inlineConfig ?? {}, 'serve').then(async (config) => {
const plugins = [
webextensionPolyfillMock(config), webextensionPolyfillMock(config),
unimport(config),
globals(config), globals(config),
download(config), download(config),
tsconfigPaths(config), tsconfigPaths(config),
]); ];
if (config.imports !== false) {
const unimport = createUnimport(config.imports);
await unimport.init();
plugins.push(unimportPlugin(unimport));
}
return plugins;
});
} }
+85 -20
View File
@@ -317,14 +317,17 @@ export interface InlineConfig {
*/ */
includeBrowserPolyfill?: boolean; includeBrowserPolyfill?: boolean;
/** /**
* When set to `true`, use the Vite Runtime API to load entrypoint options instead of the default, `jiti`. * Method used to import entrypoint files during the build process to extract their options.
* *
* Lets you use imported variables and leverage your Vite config to add support for non-standard APIs/syntax. * - "jiti": Simplest and fastest, but doesn't allow using any imported variables outside the entrypoint's main function
* - "vite-runtime" (unstable): Uses Vite 5.3's new runtime API to import the entrypoints. Automatically includes vite config based on your wxt.config.ts file
* - "vite-node" (unstable): Uses `vite-node` to import the entrypoints. Automatically includes vite config based on your wxt.config.ts file
* *
* @experimental Early access to try out the feature before it becomes the default. * @see {@link https://wxt.dev/guide/go-further/entrypoint-side-effects.html|Entrypoint Side-effect Docs}
* @default false *
* @default "jiti"
*/ */
viteRuntime?: boolean; entrypointImporter?: 'jiti' | 'vite-runtime' | 'vite-node';
}; };
/** /**
* Config effecting dev mode only. * Config effecting dev mode only.
@@ -1031,6 +1034,23 @@ export interface WxtHooks {
* @returns Promise * @returns Promise
*/ */
ready: (wxt: Wxt) => HookResult; ready: (wxt: Wxt) => HookResult;
/**
* Called before WXT writes .wxt/tsconfig.json and .wxt/wxt.d.ts, allowing
* addition of custom references and declarations in wxt.d.ts, or directly
* modifying the options in `tsconfig.json`.
*
* @example
* wxt.hooks.hook("prepare:types", (wxt, entries) => {
* // Add a file, ".wxt/types/example.d.ts", that defines a global
* // variable called "example" in the TS project.
* entries.push({
* path: "types/example.d.ts",
* textContent: "declare const a: string;",
* tsReference: true,
* });
* })
*/
'prepare:types': (wxt: Wxt, entries: WxtDirEntry[]) => HookResult;
/** /**
* Called before the build is started in both dev mode and build mode. * Called before the build is started in both dev mode and build mode.
* *
@@ -1174,7 +1194,7 @@ export interface ResolvedConfig {
alias: Record<string, string>; alias: Record<string, string>;
experimental: { experimental: {
includeBrowserPolyfill: boolean; includeBrowserPolyfill: boolean;
viteRuntime: boolean; entrypointImporter: 'jiti' | 'vite-runtime' | 'vite-node';
}; };
dev: { dev: {
/** Only defined during dev command */ /** Only defined during dev command */
@@ -1185,7 +1205,8 @@ export interface ResolvedConfig {
reloadCommand: string | false; reloadCommand: string | false;
}; };
hooks: NestedHooks<WxtHooks>; hooks: NestedHooks<WxtHooks>;
modules: WxtModuleWithMetadata<any>[]; builtinModules: WxtModule<any>[];
userModules: WxtModuleWithMetadata<any>[];
/** /**
* An array of string to import plugins from. These paths should be * An array of string to import plugins from. These paths should be
* resolvable by vite, and they should `export default defineWxtPlugin(...)`. * resolvable by vite, and they should `export default defineWxtPlugin(...)`.
@@ -1217,17 +1238,21 @@ export interface Eslintrc {
/** /**
* When true, generates a file that can be used by ESLint to know which variables are valid globals. * When true, generates a file that can be used by ESLint to know which variables are valid globals.
* *
* - `'auto'`: Check if eslint is installed, and if it is, generate the helper file * - `false`: Don't generate the file.
* - `true`: Generate the helper file * - `'auto'`: Check if eslint is installed, and if it is, generate a compatible config file.
* - `false`: Don't generate the file * - `true`: Same as `8`.
* - `8`: Generate a config file compatible with ESLint 8.
* - `9`: Generate a config file compatible with ESLint 9.
* *
* @default 'auto' * @default 'auto'
*/ */
enabled?: boolean | 'auto'; enabled?: false | true | 'auto' | 8 | 9;
/** /**
* File path to save the generated eslint config. * File path to save the generated eslint config.
* *
* @default './.wxt/eslintrc-auto-import.json' * Default depends on version of ESLint used:
* - 9 and above: './.wxt/eslint-auto-imports.mjs'
* - 8 and below: './.wxt/eslintrc-auto-import.json'
*/ */
filePath?: string; filePath?: string;
/** /**
@@ -1237,7 +1262,8 @@ export interface Eslintrc {
} }
export interface ResolvedEslintrc { export interface ResolvedEslintrc {
enabled: boolean; /** False if disabled, otherwise the major version of ESLint installed */
enabled: false | 8 | 9;
/** Absolute path */ /** Absolute path */
filePath: string; filePath: string;
globalsPropValue: EslintGlobalsPropValue; globalsPropValue: EslintGlobalsPropValue;
@@ -1331,13 +1357,9 @@ export interface WxtModuleWithMetadata<TOptions extends WxtModuleOptions>
id: string; id: string;
} }
export interface ResolvedPublicFile { export type ResolvedPublicFile = CopiedPublicFile | GeneratedPublicFile;
/**
* The absolute path to the file that will be copied to the output directory. export interface ResolvedBasePublicFile {
* @example
* "/path/to/any/file.css"
*/
absoluteSrc: string;
/** /**
* The relative path in the output directory to copy the file to. * The relative path in the output directory to copy the file to.
* @example * @example
@@ -1346,4 +1368,47 @@ export interface ResolvedPublicFile {
relativeDest: string; relativeDest: string;
} }
export interface CopiedPublicFile extends ResolvedBasePublicFile {
/**
* The absolute path to the file that will be copied to the output directory.
* @example
* "/path/to/any/file.css"
*/
absoluteSrc: string;
}
export interface GeneratedPublicFile extends ResolvedBasePublicFile {
/**
* Text to write to the file.
*/
contents: string;
}
export type WxtPlugin = () => void; export type WxtPlugin = () => void;
export type WxtDirEntry = WxtDirTypeReferenceEntry | WxtDirFileEntry;
/**
* Represents type reference to a node module to be added to `.wxt/wxt.d.ts` file
*/
export interface WxtDirTypeReferenceEntry {
module: string;
}
/**
* Represents a file to be written to the project's `.wxt/` directory.
*/
export interface WxtDirFileEntry {
/**
* Path relative to the `.wxt/` directory. So "tsconfig.json" would resolve to ".wxt/tsconfig.json".
*/
path: string;
/**
* The text that will be written to the file.
*/
text: string;
/**
* Set to `true` to add a reference to this file in `.wxt/wxt.d.ts`.
*/
tsReference?: boolean;
}
@@ -1,6 +1,6 @@
import definition from 'virtual:user-background-entrypoint'; import definition from 'virtual:user-background-entrypoint';
import { initPlugins } from 'virtual:wxt-plugins'; import { initPlugins } from 'virtual:wxt-plugins';
import { setupWebSocket } from './utils/setup-web-socket'; import { getDevServerWebSocket } from '../sandbox/dev-server-websocket';
import { logger } from '../sandbox/utils/logger'; import { logger } from '../sandbox/utils/logger';
import { browser } from 'wxt/browser'; import { browser } from 'wxt/browser';
import { keepServiceWorkerAlive } from './utils/keep-service-worker-alive'; import { keepServiceWorkerAlive } from './utils/keep-service-worker-alive';
@@ -8,18 +8,19 @@ import { reloadContentScript } from './utils/reload-content-scripts';
if (import.meta.env.COMMAND === 'serve') { if (import.meta.env.COMMAND === 'serve') {
try { try {
const ws = setupWebSocket((message) => { const ws = getDevServerWebSocket();
if (message.event === 'wxt:reload-extension') browser.runtime.reload(); ws.addWxtEventListener('wxt:reload-extension', () => {
if (message.event === 'wxt:reload-content-script' && message.data != null) browser.runtime.reload();
reloadContentScript(message.data); });
ws.addWxtEventListener('wxt:reload-content-script', (event) => {
reloadContentScript(event.detail);
}); });
if (import.meta.env.MANIFEST_VERSION === 3) { if (import.meta.env.MANIFEST_VERSION === 3) {
// Tell the server the background script is loaded and ready to go // Tell the server the background script is loaded and ready to go
ws.addEventListener('open', () => { ws.addEventListener('open', () =>
const msg = { type: 'custom', event: 'wxt:background-initialized' }; ws.sendCustom('wxt:background-initialized'),
ws.send(JSON.stringify(msg)); );
});
// Web Socket will disconnect if the service worker is killed // Web Socket will disconnect if the service worker is killed
keepServiceWorkerAlive(); keepServiceWorkerAlive();
+4 -8
View File
@@ -1,16 +1,12 @@
import { logger } from '../sandbox/utils/logger'; import { logger } from '../sandbox/utils/logger';
import { setupWebSocket } from './utils/setup-web-socket'; import { getDevServerWebSocket } from '../sandbox/dev-server-websocket';
if (import.meta.env.COMMAND === 'serve') { if (import.meta.env.COMMAND === 'serve') {
try { try {
setupWebSocket((message) => { const ws = getDevServerWebSocket();
if (message.event === 'wxt:reload-page') { ws.addWxtEventListener('wxt:reload-page', (event) => {
// We need to remove the initial slash from the path to compare correctly
// "popup.html" === "/popup.html".substring(1) // "popup.html" === "/popup.html".substring(1)
if (message.data === location.pathname.substring(1)) { if (event.detail === location.pathname.substring(1)) location.reload();
location.reload();
}
}
}); });
} catch (err) { } catch (err) {
logger.error('Failed to setup web socket connection with dev server', err); logger.error('Failed to setup web socket connection with dev server', err);
@@ -1,16 +1,7 @@
import { browser } from 'wxt/browser'; import { browser } from 'wxt/browser';
import { logger } from '../../sandbox/utils/logger'; import { logger } from '../../sandbox/utils/logger';
import { MatchPattern } from 'wxt/sandbox'; import { MatchPattern } from 'wxt/sandbox';
import type { ReloadContentScriptPayload } from '../../sandbox/dev-server-websocket';
interface ContentScript {
matches: string[];
js?: string[];
css?: string[];
}
interface ReloadContentScriptPayload {
registration?: 'manifest' | 'runtime';
contentScript: ContentScript;
}
export function reloadContentScript(payload: ReloadContentScriptPayload) { export function reloadContentScript(payload: ReloadContentScriptPayload) {
const manifest = browser.runtime.getManifest(); const manifest = browser.runtime.getManifest();
@@ -32,6 +23,8 @@ export async function reloadContentScriptMv3({
} }
} }
type ContentScript = ReloadContentScriptPayload['contentScript'];
export async function reloadManifestContentScriptMv3( export async function reloadManifestContentScriptMv3(
contentScript: ContentScript, contentScript: ContentScript,
) { ) {
@@ -88,7 +81,15 @@ async function reloadTabsForContentScript(contentScript: ContentScript) {
if (!url) return false; if (!url) return false;
return !!matchPatterns.find((pattern) => pattern.includes(url)); return !!matchPatterns.find((pattern) => pattern.includes(url));
}); });
await Promise.all(matchingTabs.map((tab) => browser.tabs.reload(tab.id))); await Promise.all(
matchingTabs.map(async (tab) => {
try {
await browser.tabs.reload(tab.id);
} catch (err) {
logger.warn('Failed to reload tab:', err);
}
}),
);
} }
export async function reloadContentScriptMv2( export async function reloadContentScriptMv2(
@@ -1,44 +0,0 @@
import { logger } from '../../sandbox/utils/logger';
interface WebSocketMessage {
type: string;
event: string;
data?: any;
}
/**
* Connect to the websocket and listen for messages.
*
* @param onMessage Optional callback that is called when a message is recieved and we've verified
* it's structure is what we expect.
*/
export function setupWebSocket(
onMessage?: (message: WebSocketMessage) => void,
) {
const serverUrl = `${__DEV_SERVER_PROTOCOL__}//${__DEV_SERVER_HOSTNAME__}:${__DEV_SERVER_PORT__}`;
logger.debug('Connecting to dev server @', serverUrl);
const ws = new WebSocket(serverUrl, 'vite-hmr');
ws.addEventListener('open', () => {
logger.debug('Connected to dev server');
});
ws.addEventListener('close', () => {
logger.debug('Disconnected from dev server');
});
ws.addEventListener('error', (event) => {
logger.error('Failed to connect to dev server', event);
});
ws.addEventListener('message', (e) => {
try {
const message = JSON.parse(e.data) as WebSocketMessage;
if (message.type === 'custom' && message.event?.startsWith?.('wxt:')) {
onMessage?.(message);
}
} catch (err) {
logger.error('Failed to handle message', err);
}
});
return ws;
}
+248 -197
View File
File diff suppressed because it is too large Load Diff

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