Compare commits

..

49 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
GitHub Actions 35f6d28977 chore(release): @wxt-dev/module-solid v1.0.1 2024-06-14 05:29:40 +00:00
Aaron 43fcbec0eb fix: Add target: esnext by default (#733) 2024-06-14 00:25:11 -05:00
Aaron a0d56b4d27 chore: Update templates to use new modules (#732) 2024-06-14 00:19:13 -05:00
Aaron 38ccb6ace6 docs: Update frontend framework docs with new modules (#731) 2024-06-14 00:06:23 -05:00
Aaron aaf75f1de1 ci: Only mark wxt releases as latest 2024-06-14 00:02:41 -05:00
Aaron 21e8ca0a2c chore: Add changelog 2024-06-13 23:52:58 -05:00
116 changed files with 2262 additions and 1081 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>
+26 -26
View File
@@ -4,30 +4,30 @@ 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 | ✅ | ✅ | |
| Create Firefox Sources ZIP | ✅ | ❌ | | | Create Firefox Sources ZIP | ✅ | ❌ | |
| First-class TypeScript support | ✅ | ✅ | | | First-class TypeScript support | ✅ | ✅ | |
| Entrypoint discovery | File based | File based | | | Entrypoint discovery | File based | File based | |
| Inline entrypoint config | ✅ | ✅ | Manifest based | | Inline entrypoint config | ✅ | ✅ | Manifest based |
| Auto-imports | ✅ | ❌ | | | Auto-imports | ✅ | ❌ | |
| Supports all frontend frameworks | ✅ | 🟡 Only React, Vue, and Svelte | | | Supports all frontend frameworks | ✅ | 🟡 Only React, Vue, and Svelte | |
| Framework specific entrypoints (like `Popup.tsx`) | 🟡 `.html` `.ts` `.tsx` | ✅ `.html` `.ts` `.tsx` `.vue` `.svelte` | | | Framework specific entrypoints (like `Popup.tsx`) | 🟡 `.html` `.ts` `.tsx` | ✅ `.html` `.ts` `.tsx` `.vue` `.svelte` | |
| Automated publishing | ✅ | ✅ | | | Automated publishing | ✅ | ✅ | |
| Remote Code Bundling (Google Analytics) | ✅ | ✅ | | | Remote Code Bundling (Google Analytics) | ✅ | ✅ | |
| <strong style="opacity: 50%">Dev Mode</strong> | | | | <strong style="opacity: 50%">Dev Mode</strong> | | |
| `.env` Files | ✅ | ✅ | | | `.env` Files | ✅ | ✅ | |
| Opens browser and install extension | ✅ | ❌ | | | Opens browser and install extension | ✅ | ❌ | |
| HMR for UIs | ✅ | 🟡 React only | | | HMR for UIs | ✅ | 🟡 React only | |
| Reload HTML Files on Change | ✅ | 🟡 Reloads entire extension | | | Reload HTML Files on Change | ✅ | 🟡 Reloads entire extension | |
| Reload Content Scripts on Change | ✅ | 🟡 Reloads entire extension | | | Reload Content Scripts on Change | ✅ | 🟡 Reloads entire extension | |
| Reload Background on Change | 🟡 Reloads entire extension | 🟡 Reloads entire extension | 🟡 Reloads entire extension | | Reload Background on Change | 🟡 Reloads entire extension | 🟡 Reloads entire extension | 🟡 Reloads entire extension |
| Respects Content Script `run_at` | ✅ | ✅ | ❌ ESM-style loaders run asynchronously | | Respects Content Script `run_at` | ✅ | ✅ | ❌ ESM-style loaders run asynchronously |
| <strong style="opacity: 50%">Built-in Utils</strong> | | | | | <strong style="opacity: 50%">Built-in Utils</strong> | | | |
| Storage | ✅ | ✅ | | | Storage | ✅ | ✅ | |
| Messaging | ❌ | ✅ | | | Messaging | ❌ | ✅ | |
| Content Script UI | ✅ | ✅ | | | Content Script UI | ✅ | ✅ | |
+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'],
};
```
+49 -29
View File
@@ -1,28 +1,59 @@
# Frontend Frameworks # Frontend Frameworks
WXT supports all frontend frameworks with a Vite plugin: ## Built-in Modules
- `@vitejs/plugin-vue` WXT has preconfigured modules for 4 frameworks:
- `@vitejs/plugin-react`
- `@vitejs/plugin-react-swc`
- And more!
Just add the vite plugin to your config and you're good to go! Use the framework in HTML pages or content scripts, it will just work 👍 - [`@wxt-dev/module-react`](https://github.com/wxt-dev/wxt/tree/main/packages/module-react)
- [`@wxt-dev/module-vue`](https://github.com/wxt-dev/wxt/tree/main/packages/module-vue)
- [`@wxt-dev/module-svelte`](https://github.com/wxt-dev/wxt/tree/main/packages/module-svelte)
- [`@wxt-dev/module-solid`](https://github.com/wxt-dev/wxt/tree/main/packages/module-solid)
Install the module for your framework, then add it to your config:
:::code-group :::code-group
```ts [Vue] ```ts [React]
import { defineConfig } from 'wxt'; import { defineConfig } from 'wxt';
import vue from '@vitejs/plugin-vue';
export default defineConfig({ export default defineConfig({
vite: () => ({ modules: ['@wxt-dev/module-react'],
plugins: [vue()],
}),
}); });
``` ```
```ts [React] ```ts [Vue]
import { defineConfig } from 'wxt';
export default defineConfig({
modules: ['@wxt-dev/module-vue'],
});
```
```ts [Svelte]
import { defineConfig } from 'wxt';
export default defineConfig({
modules: ['@wxt-dev/module-svelte'],
});
```
```ts [Solid]
import { defineConfig } from 'wxt';
export default defineConfig({
modules: ['@wxt-dev/module-solid'],
});
```
:::
## Adding Vite Plugins
If your framework doesn't have an official WXT module, no worries! WXT supports any framework with a Vite plugin.
Just add the Vite plugin to your config and you're good to go! Use the framework in HTML pages or content scripts, it will just work 👍
```ts
import { defineConfig } from 'wxt'; import { defineConfig } from 'wxt';
import react from '@vitejs/plugin-react'; import react from '@vitejs/plugin-react';
@@ -33,18 +64,7 @@ export default defineConfig({
}); });
``` ```
```ts [Svelte] The WXT modules just simplify the configuration and add auto-imports. They're not much different than the above.
import { defineConfig } from 'wxt';
import { svelte } from '@sveltejs/vite-plugin-svelte';
export default defineConfig({
vite: () => ({
plugins: [svelte()],
}),
});
```
:::
## Multiple Apps ## Multiple Apps
@@ -84,9 +104,9 @@ Lots of frameworks come with routers for building a multi-page app using the URL
Instead, you need to configure the router to run in "hash" mode, where the routing information is apart of the URL's hash, not the path (ie: `popup.html#/` and `popup.html#/account/settings`) Instead, you need to configure the router to run in "hash" mode, where the routing information is apart of the URL's hash, not the path (ie: `popup.html#/` and `popup.html#/account/settings`)
Refer to your router's docs for information about "hash" mode and how to enable it. Here's a non-extensive list of a few popular routers: Refer to your router's docs for information about hash mode and how to enable it. Here's a non-extensive list of a few popular routers:
- [React](https://reactrouter.com/en/main/routers/create-hash-router) - [`react-router`](https://reactrouter.com/en/main/routers/create-hash-router)
- [Vue](https://router.vuejs.org/guide/essentials/history-mode.html#Hash-Mode) - [`vue-router`](https://router.vuejs.org/guide/essentials/history-mode.html#Hash-Mode)
- [Svelte](https://www.npmjs.com/package/svelte-spa-router#hash-based-routing) - [`svelte-spa-router`](https://www.npmjs.com/package/svelte-spa-router#hash-based-routing)
- [Solid](https://github.com/solidjs/solid-router?tab=readme-ov-file#hash-mode-router) - [`solid-router`](https://github.com/solidjs/solid-router?tab=readme-ov-file#hash-mode-router)
+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"
}, },
+5
View File
@@ -0,0 +1,5 @@
# Changelog
## v1.0.0
Initial release 🎉
+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/**"]
} }
+42
View File
@@ -0,0 +1,42 @@
# 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
[compare changes](https://github.com/wxt-dev/wxt/compare/module-solid-v1.0.0...module-solid-v1.0.1)
### 🩹 Fixes
- Add `target: esnext` by default ([#733](https://github.com/wxt-dev/wxt/pull/733))
### 🏡 Chore
- Add changelog ([21e8ca0](https://github.com/wxt-dev/wxt/commit/21e8ca0))
## v1.0.0
Initial release 🎉
+1 -1
View File
@@ -4,7 +4,7 @@ Enables the use of [SolidJS](https://www.solidjs.com/) in your web extension, in
This plugin makes a few changes: This plugin makes a few changes:
1. Adds `vite-plugin-solid` to vite 1. Adds `vite-plugin-solid` to and sets `build.target: esnext` in the vite config
2. Adds the [`solid-js` preset](https://github.com/unjs/unimport/blob/main/src/presets/solid.ts) to auto-imports 2. Adds the [`solid-js` preset](https://github.com/unjs/unimport/blob/main/src/presets/solid.ts) to auto-imports
## Usage ## Usage
+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);
@@ -10,9 +10,23 @@ export default defineWxtModule<SolidModuleOptions>({
addViteConfig(wxt, () => ({ addViteConfig(wxt, () => ({
plugins: [solid(vite)], plugins: [solid(vite)],
build: {
target: 'esnext',
},
})); }));
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.0", "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/**"]
} }
+5
View File
@@ -0,0 +1,5 @@
# Changelog
## v1.0.0
Initial release 🎉
+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:*"
} }
+5
View File
@@ -0,0 +1,5 @@
# Changelog
## v1.0.0
Initial release 🎉
+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"
}
}
"
`;
+156 -50
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);
});
});
});
+66 -16
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) {
const baseConfig = await getBaseConfig(); switch (wxtConfig.experimental.entrypointImporter) {
const envConfig: vite.InlineConfig = { default:
plugins: [ case 'jiti': {
wxtPlugins.webextensionPolyfillMock(wxtConfig), return await importEntrypointFile(path);
wxtPlugins.removeEntrypointMainFunction(wxtConfig, url), }
], case 'vite-runtime': {
}; const baseConfig = await getBaseConfig();
const config = vite.mergeConfig(baseConfig, envConfig); const envConfig: vite.InlineConfig = {
const server = await vite.createServer(config); plugins: [
await server.listen(); wxtPlugins.webextensionPolyfillMock(wxtConfig),
const runtime = await vite.createViteRuntime(server, { hmr: false }); wxtPlugins.removeEntrypointMainFunction(wxtConfig, path),
const module = await runtime.executeUrl(url); ],
await server.close(); };
return module.default; const config = vite.mergeConfig(baseConfig, envConfig);
const server = await vite.createServer(config);
await server.listen();
const runtime = await vite.createViteRuntime(server, { hmr: false });
const module = await runtime.executeUrl(path);
await server.close();
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', () => {
beforeAll(async () => {
// NPM needs the modules installed for 'npm ls' to work
await execaCommand('npm i', { cwd });
}, 60e3);
describe('listDependencies', () => { describe('listDependencies', () => {
const cwd = path.resolve(__dirname, 'fixtures/simple-npm-project');
beforeAll(async () => {
// NPM needs the modules installed for 'npm ls' to work
await execaCommand('npm i', { cwd });
}, 60e3);
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', () => {
beforeAll(async () => {
// PNPM needs the modules installed, or 'pnpm ls' will return a blank list.
await execaCommand('pnpm i --ignore-workspace', { cwd });
});
describe('listDependencies', () => { describe('listDependencies', () => {
const cwd = path.resolve(__dirname, 'fixtures/simple-pnpm-project');
beforeAll(async () => {
// PNPM needs the modules installed, or 'pnpm ls' will return a blank list.
await execaCommand('pnpm i --ignore-workspace', { cwd });
});
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)); // browser.runtime.getURL
references.push(await writeI18nDeclarationFile()); entries.push(await getPathsDeclarationEntry(entrypoints));
references.push(await writeGlobalsDeclarationFile());
const mainReference = await writeMainDeclarationFile(references); // browser.i18n.getMessage
await writeTsConfigFile(mainReference); entries.push(await getI18nDeclarationEntry());
}
async function writeImportsDeclarationFile(unimport: Unimport) { // import.meta.env.*
const filePath = resolve(wxt.config.typesDir, 'imports.d.ts'); entries.push(await getGlobalsDeclarationEntry());
// Load project imports into unimport memory so they are output via generateTypeDeclarations // tsconfig.json
await unimport.scanImportsFromDir(undefined, { cwd: wxt.config.srcDir }); entries.push(await getTsConfigEntry());
await writeFileIfDifferent( // Let modules add more entries
filePath, await wxt.hooks.callHook('prepare:types', wxt, entries);
['// Generated by wxt', await unimport.generateTypeDeclarations()].join(
'\n', // Add main declaration file, not editable
) + '\n', 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);
}),
); );
return filePath;
} }
async function writeImportsEslintFile( async function getPathsDeclarationEntry(
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 });
}
async function writePathsDeclarationFile(
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,
if (module.configKey) { mergedConfig.modules,
map[module.configKey] = );
// @ts-expect-error const moduleOptions = userModules.reduce<Record<string, any>>(
mergedConfig[module.configKey]; (map, module) => {
} if (module.configKey) {
return map; map[module.configKey] =
}, {}); // @ts-expect-error
mergedConfig[module.configKey];
}
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[];
};
}
+16 -8
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) => {
webextensionPolyfillMock(config), const plugins = [
unimport(config), webextensionPolyfillMock(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;
}

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