Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e50f2b047a | |||
| 1b41cd26ee | |||
| e32fbed1b7 | |||
| f8acdd6352 | |||
| 9ea89c6340 | |||
| a521a80d43 | |||
| 4c6a57dd33 | |||
| 3ce8c8b1f4 | |||
| 4ee8f7837e | |||
| 1e8e5488af | |||
| 5704765cc4 | |||
| 8c7adbf463 | |||
| 8c4436efe8 | |||
| 5e361c0085 | |||
| ccfc207e92 | |||
| 36be4ce771 | |||
| 18f8a387b0 | |||
| 48c2c07557 | |||
| 61548a9478 | |||
| fd678e9572 | |||
| 63fb913f76 | |||
| faffe20a62 | |||
| 3ed8bd10fd | |||
| 6f3f270573 | |||
| 5347cb4626 | |||
| d6b4d2d1f6 | |||
| 0d7ae287da | |||
| 90cb3bd3bf | |||
| 60eb97c4b2 | |||
| 0ffc6eae3e | |||
| 06a8c5fb0e | |||
| eb86413f35 | |||
| b29d49cbf1 | |||
| c0a0edb30c | |||
| b15a933c75 | |||
| 1851be22a7 | |||
| 39f6c29e4f |
@@ -33,7 +33,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup
|
||||
- uses: oven-sh/setup-bun@v1
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
- name: pnpm test:coverage
|
||||
run: pnpm test:coverage --reporter=default --reporter=hanging-process
|
||||
- uses: codecov/codecov-action@v4
|
||||
@@ -48,6 +48,7 @@ jobs:
|
||||
template:
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
template:
|
||||
- react
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<h1 align="center">
|
||||
<img style="vertical-align:middle" width="44" src="./docs/public/hero-logo.svg" alt="WXT Logo">
|
||||
<img style="vertical-align:middle" width="44" src="https://raw.githubusercontent.com/wxt-dev/wxt/HEAD/docs/public/hero-logo.svg" alt="WXT Logo">
|
||||
<span>WXT</span>
|
||||
</h1>
|
||||
|
||||
@@ -22,22 +22,22 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://wxt.dev" target="_blank">Get Started</a>
|
||||
•
|
||||
<a href="https://wxt.dev/guide/installation.html" target="_blank">Installation</a>
|
||||
<a href="https://wxt.dev/guide/installation.html" target="_blank">Get Started</a>
|
||||
•
|
||||
<a href="https://wxt.dev/api/config.html" target="_blank">Configuration</a>
|
||||
•
|
||||
<a href="https://wxt.dev/examples.html" target="_blank">Examples</a>
|
||||
•
|
||||
<a href="https://github.com/wxt-dev/wxt/blob/main/packages/wxt/CHANGELOG.md" target="_blank">Changelog</a>
|
||||
•
|
||||
<a href="https://discord.gg/ZFsZqGery9" target="_blank">Discord</a>
|
||||
</p>
|
||||
|
||||

|
||||

|
||||
|
||||
## Demo
|
||||
|
||||
https://github.com/wxt-dev/wxt/assets/10101283/4d678939-1bdb-495c-9c36-3aa281d84c94
|
||||
<video src="https://github.com/wxt-dev/wxt/assets/10101283/4d678939-1bdb-495c-9c36-3aa281d84c94"></video>
|
||||
|
||||
## Quick Start
|
||||
|
||||
|
||||
@@ -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 →</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>
|
||||
@@ -30,6 +30,8 @@ const chromeExtensionIds = [
|
||||
'oadbjpccljkplmhnjekgjamejnbadlne', // demo.fun - Interactive product demos that convert
|
||||
'iopdafdcollfgaoffingmahpffckmjni', // SmartEReply: Elevate Your LinkedIn™ Engagement with AI 🚀📈
|
||||
'khjdmjcmpolknpccmaaipmidphjokhdf', // WorkFlowy MultiFlow
|
||||
'fencadnndhdeggodopebjgdfdlhcimfk', // 香草布丁🌿🍮- https://github.com/Xdy1579883916/vanilla-pudding
|
||||
'bnacincmbaknlbegecpioobkfgejlojp', // MaxFocus: Link Preview
|
||||
];
|
||||
|
||||
const { data, err, isLoading } = useListExtensionDetails(chromeExtensionIds);
|
||||
|
||||
@@ -78,7 +78,6 @@ export default defineConfig({
|
||||
menuItem('Configuration', 'configuration'),
|
||||
menuItem('Entrypoints', 'entrypoints'),
|
||||
menuItem('Assets', 'assets'),
|
||||
menuItem('Testing', 'testing'),
|
||||
menuItem('Publishing', 'publishing'),
|
||||
menuItem('Migrate to WXT', 'migrate-to-wxt'),
|
||||
menuItem('Compare', 'compare'),
|
||||
@@ -122,6 +121,7 @@ export default defineConfig({
|
||||
|
||||
// Files
|
||||
menuItem('.env', 'env'),
|
||||
menuItem('app.config.ts', 'app-config'),
|
||||
menuItem('package.json', 'package'),
|
||||
menuItem('tsconfig.json', 'tsconfig'),
|
||||
menuItem('web-ext.config.ts', 'web-ext-config'),
|
||||
|
||||
@@ -2,7 +2,7 @@ import DefaultTheme from 'vitepress/theme';
|
||||
import Icon from '../components/Icon.vue';
|
||||
import EntrypointPatterns from '../components/EntrypointPatterns.vue';
|
||||
import UsingWxtSection from '../components/UsingWxtSection.vue';
|
||||
import ExampleList from '../components/ExampleList.vue';
|
||||
import ExampleSearch from '../components/ExampleSearch.vue';
|
||||
import './custom.css';
|
||||
|
||||
export default {
|
||||
@@ -11,6 +11,6 @@ export default {
|
||||
ctx.app.component('Icon', Icon);
|
||||
ctx.app.component('EntrypointPatterns', EntrypointPatterns);
|
||||
ctx.app.component('UsingWxtSection', UsingWxtSection);
|
||||
ctx.app.component('ExampleList', ExampleList);
|
||||
ctx.app.component('ExampleSearch', ExampleSearch);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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
@@ -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
@@ -4,30 +4,30 @@ Lets compare the features of WXT vs [Plasmo](https://docs.plasmo.com/framework)
|
||||
|
||||
## Overview
|
||||
|
||||
| Features | WXT | Plasmo | CRXJS |
|
||||
| ---------------------------------------------------- | :-------------------------: | :--------------------------------------: | :---------------------------------------------------------------------: |
|
||||
| Supports all browsers | ✅ | ✅ | ❌ See [#56](https://github.com/crxjs/chrome-extension-tools/issues/56) |
|
||||
| MV2 Support | ✅ | ✅ | 🟡 Either MV2 or MV3 |
|
||||
| MV3 Support | ✅ | ✅ | 🟡 Either MV2 or MV3 |
|
||||
| Create Extension ZIPs | ✅ | ✅ | ❌ |
|
||||
| Create Firefox Sources ZIP | ✅ | ❌ | ❌ |
|
||||
| First-class TypeScript support | ✅ | ✅ | ✅ |
|
||||
| Entrypoint discovery | File based | File based | ❌ |
|
||||
| Inline entrypoint config | ✅ | ✅ | Manifest based |
|
||||
| Auto-imports | ✅ | ❌ | ❌ |
|
||||
| Supports all frontend frameworks | ✅ | 🟡 Only React, Vue, and Svelte | ✅ |
|
||||
| Framework specific entrypoints (like `Popup.tsx`) | 🟡 `.html` `.ts` `.tsx` | ✅ `.html` `.ts` `.tsx` `.vue` `.svelte` | ❌ |
|
||||
| Automated publishing | ✅ | ✅ | ❌ |
|
||||
| Remote Code Bundling (Google Analytics) | ✅ | ✅ | ❌ |
|
||||
| Features | WXT | Plasmo | CRXJS |
|
||||
| ---------------------------------------------------- | :-------------------------: | :--------------------------------------: | :-------------------------------------: |
|
||||
| Supports all browsers | ✅ | ✅ | ✅ As of `v2.0.0-beta.23` |
|
||||
| MV2 Support | ✅ | ✅ | 🟡 Either MV2 or MV3 |
|
||||
| MV3 Support | ✅ | ✅ | 🟡 Either MV2 or MV3 |
|
||||
| Create Extension ZIPs | ✅ | ✅ | ❌ |
|
||||
| Create Firefox Sources ZIP | ✅ | ❌ | ❌ |
|
||||
| First-class TypeScript support | ✅ | ✅ | ✅ |
|
||||
| Entrypoint discovery | File based | File based | ❌ |
|
||||
| Inline entrypoint config | ✅ | ✅ | Manifest based |
|
||||
| Auto-imports | ✅ | ❌ | ❌ |
|
||||
| Supports all frontend frameworks | ✅ | 🟡 Only React, Vue, and Svelte | ✅ |
|
||||
| Framework specific entrypoints (like `Popup.tsx`) | 🟡 `.html` `.ts` `.tsx` | ✅ `.html` `.ts` `.tsx` `.vue` `.svelte` | ❌ |
|
||||
| Automated publishing | ✅ | ✅ | ❌ |
|
||||
| Remote Code Bundling (Google Analytics) | ✅ | ✅ | ❌ |
|
||||
| <strong style="opacity: 50%">Dev Mode</strong> | | |
|
||||
| `.env` Files | ✅ | ✅ | ✅ |
|
||||
| Opens browser and install extension | ✅ | ❌ | ❌ |
|
||||
| HMR for UIs | ✅ | 🟡 React only | ✅ |
|
||||
| Reload HTML Files 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 |
|
||||
| Respects Content Script `run_at` | ✅ | ✅ | ❌ ESM-style loaders run asynchronously |
|
||||
| <strong style="opacity: 50%">Built-in Utils</strong> | | | |
|
||||
| Storage | ✅ | ✅ | ❌ |
|
||||
| Messaging | ❌ | ✅ | ❌ |
|
||||
| Content Script UI | ✅ | ✅ | ❌ |
|
||||
| `.env` Files | ✅ | ✅ | ✅ |
|
||||
| Opens browser and install extension | ✅ | ❌ | ❌ |
|
||||
| HMR for UIs | ✅ | 🟡 React only | ✅ |
|
||||
| Reload HTML Files 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 |
|
||||
| Respects Content Script `run_at` | ✅ | ✅ | ❌ ESM-style loaders run asynchronously |
|
||||
| <strong style="opacity: 50%">Built-in Utils</strong> | | | |
|
||||
| Storage | ✅ | ✅ | ❌ |
|
||||
| Messaging | ❌ | ✅ | ❌ |
|
||||
| Content Script UI | ✅ | ✅ | ❌ |
|
||||
|
||||
@@ -113,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 [Guide](/guide/key-concepts/manifest) to learn in-depth about each feature WXT supports
|
||||
- [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
|
||||
|
||||
@@ -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.
|
||||
@@ -0,0 +1,61 @@
|
||||
# `<srcDir>/app.config.ts`
|
||||
|
||||
:::warning Nuxt Users
|
||||
If you're familiar with Nuxt, this file is meant to be a direct equivalent to Nuxt's `app.config.ts` file.
|
||||
|
||||
However, some of Nuxt's features, like overriding the app config based on a `.env` file or automatically generating the config's types, are not implemented. They are planned, just not implemented yet. Feel free to open a PR!
|
||||
:::
|
||||
|
||||
## Overview
|
||||
|
||||
Define runtime configuration in a single place.
|
||||
|
||||
```ts
|
||||
// <srcDir>/app.config.ts
|
||||
import { defineAppConfig } from 'wxt/sandbox';
|
||||
|
||||
// Define types for your config
|
||||
declare module 'wxt/sandbox' {
|
||||
export interface WxtAppConfig {
|
||||
theme?: 'light' | 'dark';
|
||||
}
|
||||
}
|
||||
|
||||
export default defineAppConfig({
|
||||
theme: 'dark',
|
||||
});
|
||||
```
|
||||
|
||||
Then access the config in your extension by calling `useAppConfig`:
|
||||
|
||||
```ts
|
||||
console.log(useAppConfig()); // { theme: "dark" }
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
If you have a `.env` file, you can access any variables defined in it here. You can convert them to better types (like booleans), add types for them, or leave them as is.
|
||||
|
||||
```txt
|
||||
# .env
|
||||
VITE_BUG_REPORTING_DISABLED=true
|
||||
VITE_API_KEY=...
|
||||
```
|
||||
|
||||
```ts
|
||||
// <srcDir>/app.config.ts
|
||||
|
||||
declare module 'wxt/sandbox' {
|
||||
export interface WxtAppConfig {
|
||||
bugReportingDisabled: boolean;
|
||||
apiKey?: string;
|
||||
}
|
||||
}
|
||||
|
||||
export default defineAppConfig({
|
||||
bugReportingDisabled: process.env.VITE_BUG_REPORTING_DISABLED === 'true',
|
||||
apiKey: process.env.VITE_API_KEY,
|
||||
});
|
||||
```
|
||||
|
||||
> You don't have to do this, you can use `process.env.VITE_*` anywhere in your runtime code, but putting them here consolidates them to one place and defines what variables are expected.
|
||||
@@ -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:
|
||||
|
||||
<ExampleList tag="devtools" />
|
||||
- [Devtools Setup](https://github.com/wxt-dev/wxt-examples/tree/main/examples/vanilla-devtools#readme)
|
||||
|
||||
@@ -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:
|
||||
|
||||
<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
|
||||
|
||||
|
||||
@@ -18,9 +18,7 @@ export default defineConfig({
|
||||
|
||||
## Using Plugins
|
||||
|
||||
Plugins can be passed into the `vite` configuration in you `wxt.config.ts` file, just like any other option.
|
||||
|
||||
All plugins should work in WXT, but it is worth pointing out that since WXT orchestrates multiple vite builds to bundle an extension, plugins will be executed multiple times if necessary.
|
||||
Plugins can be passed into the `vite` configuration in your `wxt.config.ts` file, just like any other option.
|
||||
|
||||
```ts
|
||||
import { defineConfig } from 'wxt';
|
||||
@@ -33,3 +31,9 @@ export default defineConfig({
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
:::warning UNEXPECTED BEHAVIOR
|
||||
Due to the way WXT orchestrates Vite builds, some plugins may not work as expected. Search [GitHub issues](https://github.com/wxt-dev/wxt/issues?q=is%3Aissue+label%3A%22vite+plugin%22) if you run into issues with a specific plugin.
|
||||
|
||||
If one doesn't exist, please open a [new issue](https://github.com/wxt-dev/wxt/issues/new/choose)!
|
||||
:::
|
||||
|
||||
@@ -79,24 +79,39 @@ export default defineConfig({
|
||||
|
||||
## 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:
|
||||
|
||||
```js
|
||||
// .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:
|
||||
ESLint doesn't know about the auto-imported variables unless they are explicitly defined in the `globals` config. By default, WXT will generate the config if it detects ESLint is installed in your project. If the config isn't generated automatically, you can manually tell WXT to generate it.
|
||||
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
export default defineConfig({
|
||||
imports: {
|
||||
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'],
|
||||
};
|
||||
```
|
||||
|
||||
@@ -25,7 +25,7 @@ Here's an example `wxt.config.ts` file:
|
||||
import { defineConfig } from 'wxt';
|
||||
|
||||
export default defineConfig({
|
||||
mainfest: {
|
||||
manifest: {
|
||||
action: {
|
||||
default_title: 'Some Title',
|
||||
},
|
||||
@@ -176,7 +176,8 @@ export default defineConfig({
|
||||
|
||||
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
|
||||
|
||||
|
||||
+5
-5
@@ -18,14 +18,14 @@
|
||||
"docs:preview": "pnpm -s docs:gen && vitepress preview docs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/buildc": "^1.0.7",
|
||||
"@aklinker1/buildc": "^1.0.10",
|
||||
"@aklinker1/check": "^1.3.1",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@vitest/coverage-v8": "^1.6.0",
|
||||
"changelogen": "^0.5.5",
|
||||
"consola": "^3.2.3",
|
||||
"dependency-graph": "^1.0.0",
|
||||
"execa": "^9.2.0",
|
||||
"execa": "^9.3.0",
|
||||
"fast-glob": "^3.3.2",
|
||||
"fs-extra": "^11.2.0",
|
||||
"hasha": "^6.0.0",
|
||||
@@ -33,16 +33,16 @@
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^3.3.2",
|
||||
"simple-git-hooks": "^2.11.1",
|
||||
"tsx": "^4.15.4",
|
||||
"tsx": "4.15.7",
|
||||
"typedoc": "^0.25.4",
|
||||
"typedoc-plugin-markdown": "4.0.0-next.23",
|
||||
"typedoc-vitepress-theme": "1.0.0-next.3",
|
||||
"typescript": "^5.4.5",
|
||||
"typescript": "^5.5.2",
|
||||
"vitepress": "^1.2.3",
|
||||
"vitest": "^1.6.0",
|
||||
"vitest-mock-extended": "^1.3.1",
|
||||
"vitest-plugin-random-seed": "^1.1.0",
|
||||
"vue": "^3.4.27",
|
||||
"vue": "^3.4.31",
|
||||
"wxt": "workspace:*",
|
||||
"yaml": "^2.4.5"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,24 @@
|
||||
# Changelog
|
||||
|
||||
## v1.1.0
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/module-react-v1.0.0...module-react-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))
|
||||
- Use `prepare` instead of `postinstall` for local dev setup ([#788](https://github.com/wxt-dev/wxt/pull/788))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Add changelog ([21e8ca0](https://github.com/wxt-dev/wxt/commit/21e8ca0))
|
||||
- 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.0
|
||||
|
||||
Initial release 🎉
|
||||
Initial release 🎉
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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');
|
||||
|
||||
// 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}',
|
||||
];
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wxt-dev/module-react",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.0",
|
||||
"type": "module",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.mjs",
|
||||
@@ -21,8 +21,10 @@
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "wxt",
|
||||
"check": "buildc --deps-only -- check",
|
||||
"build": "buildc -- unbuild",
|
||||
"check": "buildc --deps-only -- check"
|
||||
"prepare": "buildc --deps-only -- wxt prepare"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"wxt": ">=0.18.6"
|
||||
@@ -32,8 +34,12 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "^1.3.1",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"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",
|
||||
"wxt": "workspace:*"
|
||||
}
|
||||
|
||||
@@ -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/**"]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,30 @@
|
||||
# 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)
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Component } from 'solid-js';
|
||||
|
||||
export const App: Component = () => {
|
||||
const [count, setCount] = createSignal(0);
|
||||
const increment = () => setCount((count) => count + 1);
|
||||
return <button onClick={increment}>Count: {count()}</button>;
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import { defineContentScript } from 'wxt/sandbox';
|
||||
import { ContentScriptContext, createShadowRootUi } from 'wxt/client';
|
||||
import { render } from 'solid-js/web';
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ['*://*/*'],
|
||||
|
||||
async main(ctx) {
|
||||
const ui = await createUi(ctx);
|
||||
ui.mount();
|
||||
},
|
||||
});
|
||||
|
||||
function createUi(ctx: ContentScriptContext) {
|
||||
return createShadowRootUi(ctx, {
|
||||
name: 'solid-ui',
|
||||
position: 'inline',
|
||||
append: 'first',
|
||||
onMount(container) {
|
||||
return render(() => <App />, container);
|
||||
},
|
||||
onRemove(unmount) {
|
||||
unmount?.();
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Document</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="./main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,5 @@
|
||||
import { render } from 'solid-js/web';
|
||||
|
||||
const root = document.getElementById('app')!;
|
||||
|
||||
render(() => <App />, root);
|
||||
@@ -16,6 +16,17 @@ export default defineWxtModule<SolidModuleOptions>({
|
||||
}));
|
||||
|
||||
addImportPreset(wxt, 'solid-js');
|
||||
|
||||
// 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}',
|
||||
];
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wxt-dev/module-solid",
|
||||
"version": "1.0.1",
|
||||
"version": "1.1.1",
|
||||
"type": "module",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.mjs",
|
||||
@@ -21,8 +21,10 @@
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "wxt",
|
||||
"check": "buildc --deps-only -- check",
|
||||
"build": "buildc -- unbuild",
|
||||
"check": "buildc --deps-only -- check"
|
||||
"prepare": "buildc --deps-only -- wxt prepare"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"wxt": ">=0.18.6"
|
||||
@@ -33,7 +35,8 @@
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "^1.3.1",
|
||||
"publint": "^0.2.8",
|
||||
"typescript": "^5.4.5",
|
||||
"solid-js": "^1.8.17",
|
||||
"typescript": "^5.5.2",
|
||||
"unbuild": "^2.0.0",
|
||||
"wxt": "workspace:*"
|
||||
}
|
||||
|
||||
@@ -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/**"]
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "^1.3.1",
|
||||
"publint": "^0.2.8",
|
||||
"typescript": "^5.4.5",
|
||||
"typescript": "^5.5.2",
|
||||
"unbuild": "^2.0.0",
|
||||
"wxt": "workspace:*"
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "^1.3.1",
|
||||
"publint": "^0.2.8",
|
||||
"typescript": "^5.4.5",
|
||||
"typescript": "^5.5.2",
|
||||
"unbuild": "^2.0.0",
|
||||
"wxt": "workspace:*"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import autoImports from './.wxt/eslintrc-auto-import.js';
|
||||
|
||||
export default [
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...autoImports.globals,
|
||||
},
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -23,8 +23,8 @@
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"sass": "^1.77.5",
|
||||
"typescript": "^5.4.5",
|
||||
"sass": "^1.77.6",
|
||||
"typescript": "^5.5.2",
|
||||
"wxt": "workspace:*"
|
||||
},
|
||||
"buildc": {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { defineAppConfig } from 'wxt/sandbox';
|
||||
|
||||
declare module 'wxt/sandbox' {
|
||||
export interface WxtAppConfig {
|
||||
example: string;
|
||||
}
|
||||
}
|
||||
|
||||
export default defineAppConfig({
|
||||
example: 'value',
|
||||
});
|
||||
@@ -15,6 +15,8 @@ export default defineBackground({
|
||||
messages,
|
||||
});
|
||||
|
||||
console.log(useAppConfig());
|
||||
|
||||
// @ts-expect-error: should only accept entrypoints or public assets
|
||||
browser.runtime.getURL('/');
|
||||
browser.runtime.getURL('/background.js');
|
||||
|
||||
@@ -1,5 +1,75 @@
|
||||
# Changelog
|
||||
|
||||
## v0.18.12
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.18.11...wxt-v0.18.12)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Support runtime config in `app.config.ts` ([#792](https://github.com/wxt-dev/wxt/pull/792))
|
||||
|
||||
### 🔥 Performance
|
||||
|
||||
- Create zip using streams ([#793](https://github.com/wxt-dev/wxt/pull/793))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Add missing name to ESLint v9 autoImports config ([#801](https://github.com/wxt-dev/wxt/pull/801))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Update README ([#802](https://github.com/wxt-dev/wxt/pull/802))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- **deps:** Upgrade `web-ext-run` (0.2.0 to 0.2.1) ([#804](https://github.com/wxt-dev/wxt/pull/804))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Ntnyq ([@ntnyq](http://github.com/ntnyq))
|
||||
- Florian Metz ([@Timeraa](http://github.com/Timeraa))
|
||||
|
||||
## v0.18.11
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.18.10...wxt-v0.18.11)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Add eslint 9 config support ([#762](https://github.com/wxt-dev/wxt/pull/762))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Respect custom `outDir` when cleaning and zipping ([#774](https://github.com/wxt-dev/wxt/pull/774))
|
||||
- **dev:** Catch error when attempting to reload a tab in a saved tab group ([#786](https://github.com/wxt-dev/wxt/pull/786))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Replace consola with wxt.logger ([#776](https://github.com/wxt-dev/wxt/pull/776))
|
||||
- **deps:** Upgrade non-major deps ([#778](https://github.com/wxt-dev/wxt/pull/778))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- KnightYoshi ([@KnightYoshi](http://github.com/KnightYoshi))
|
||||
- Asakura Mizu ([@AsakuraMizu](http://github.com/AsakuraMizu))
|
||||
|
||||
## 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)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<h1 align="center">
|
||||
<img style="vertical-align:middle" width="44" src="./docs/public/hero-logo.svg" alt="WXT Logo">
|
||||
<img style="vertical-align:middle" width="44" src="https://raw.githubusercontent.com/wxt-dev/wxt/HEAD/docs/public/hero-logo.svg" alt="WXT Logo">
|
||||
<span>WXT</span>
|
||||
</h1>
|
||||
|
||||
@@ -22,22 +22,22 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://wxt.dev" target="_blank">Get Started</a>
|
||||
•
|
||||
<a href="https://wxt.dev/guide/installation.html" target="_blank">Installation</a>
|
||||
<a href="https://wxt.dev/guide/installation.html" target="_blank">Get Started</a>
|
||||
•
|
||||
<a href="https://wxt.dev/api/config.html" target="_blank">Configuration</a>
|
||||
•
|
||||
<a href="https://wxt.dev/examples.html" target="_blank">Examples</a>
|
||||
•
|
||||
<a href="https://github.com/wxt-dev/wxt/blob/main/packages/wxt/CHANGELOG.md" target="_blank">Changelog</a>
|
||||
•
|
||||
<a href="https://discord.gg/ZFsZqGery9" target="_blank">Discord</a>
|
||||
</p>
|
||||
|
||||

|
||||

|
||||
|
||||
## Demo
|
||||
|
||||
https://github.com/wxt-dev/wxt/assets/10101283/4d678939-1bdb-495c-9c36-3aa281d84c94
|
||||
<video src="https://github.com/wxt-dev/wxt/assets/10101283/4d678939-1bdb-495c-9c36-3aa281d84c94"></video>
|
||||
|
||||
## Quick Start
|
||||
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
// 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,
|
||||
"defineAppConfig": true,
|
||||
"defineBackground": true,
|
||||
"defineConfig": true,
|
||||
"defineContentScript": true,
|
||||
"defineUnlistedScript": true,
|
||||
"defineWxtPlugin": true,
|
||||
"fakeBrowser": true,
|
||||
"storage": true,
|
||||
"useAppConfig": 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,
|
||||
"defineAppConfig": true,
|
||||
"defineBackground": true,
|
||||
"defineConfig": true,
|
||||
"defineContentScript": true,
|
||||
"defineUnlistedScript": true,
|
||||
"defineWxtPlugin": true,
|
||||
"fakeBrowser": true,
|
||||
"storage": true,
|
||||
"useAppConfig": true
|
||||
}
|
||||
|
||||
export default {
|
||||
name: "wxt/auto-imports",
|
||||
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,
|
||||
"defineAppConfig": true,
|
||||
"defineBackground": true,
|
||||
"defineConfig": true,
|
||||
"defineContentScript": true,
|
||||
"defineUnlistedScript": true,
|
||||
"defineWxtPlugin": true,
|
||||
"fakeBrowser": true,
|
||||
"storage": true,
|
||||
"useAppConfig": 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",
|
||||
"defineAppConfig": "readonly",
|
||||
"defineBackground": "readonly",
|
||||
"defineConfig": "readonly",
|
||||
"defineContentScript": "readonly",
|
||||
"defineUnlistedScript": "readonly",
|
||||
"defineWxtPlugin": "readonly",
|
||||
"fakeBrowser": "readonly",
|
||||
"storage": "readonly",
|
||||
"useAppConfig": "readonly"
|
||||
}
|
||||
}
|
||||
"
|
||||
`;
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { TestProject } from '../utils';
|
||||
import { execaCommand } from 'execa';
|
||||
|
||||
describe('Auto Imports', () => {
|
||||
describe('imports: { ... }', () => {
|
||||
@@ -23,6 +24,7 @@ describe('Auto Imports', () => {
|
||||
const createIframeUi: typeof import('wxt/client')['createIframeUi']
|
||||
const createIntegratedUi: typeof import('wxt/client')['createIntegratedUi']
|
||||
const createShadowRootUi: typeof import('wxt/client')['createShadowRootUi']
|
||||
const defineAppConfig: typeof import('wxt/sandbox')['defineAppConfig']
|
||||
const defineBackground: typeof import('wxt/sandbox')['defineBackground']
|
||||
const defineConfig: typeof import('wxt')['defineConfig']
|
||||
const defineContentScript: typeof import('wxt/sandbox')['defineContentScript']
|
||||
@@ -30,6 +32,7 @@ describe('Auto Imports', () => {
|
||||
const defineWxtPlugin: typeof import('wxt/sandbox')['defineWxtPlugin']
|
||||
const fakeBrowser: typeof import('wxt/testing')['fakeBrowser']
|
||||
const storage: typeof import('wxt/storage')['storage']
|
||||
const useAppConfig: typeof import('wxt/client')['useAppConfig']
|
||||
}
|
||||
"
|
||||
`);
|
||||
@@ -47,10 +50,10 @@ describe('Auto Imports', () => {
|
||||
----------------------------------------
|
||||
// Generated by wxt
|
||||
/// <reference types="wxt/vite-builder-env" />
|
||||
/// <reference types="./types/imports.d.ts" />
|
||||
/// <reference types="./types/paths.d.ts" />
|
||||
/// <reference types="./types/i18n.d.ts" />
|
||||
/// <reference types="./types/globals.d.ts" />
|
||||
/// <reference types="./types/imports.d.ts" />
|
||||
"
|
||||
`);
|
||||
});
|
||||
@@ -96,7 +99,7 @@ describe('Auto Imports', () => {
|
||||
});
|
||||
|
||||
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();
|
||||
project.addFile('entrypoints/popup.html', `<html></html>`);
|
||||
|
||||
@@ -108,30 +111,43 @@ describe('Auto Imports', () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(await project.serializeFile('.wxt/eslintrc-auto-import.json'))
|
||||
.toMatchInlineSnapshot(`
|
||||
".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
|
||||
}
|
||||
}
|
||||
"
|
||||
`);
|
||||
expect(
|
||||
await project.serializeFile('.wxt/eslintrc-auto-import.json'),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('"enabled: 8" should output a JSON config file compatible with ESlint 8', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/popup.html', `<html></html>`);
|
||||
|
||||
await project.prepare({
|
||||
imports: {
|
||||
eslintrc: {
|
||||
enabled: 8,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
await project.serializeFile('.wxt/eslintrc-auto-import.json'),
|
||||
).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 () => {
|
||||
@@ -148,30 +164,122 @@ describe('Auto Imports', () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(await project.serializeFile('example.json'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"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"
|
||||
}
|
||||
}
|
||||
"
|
||||
`);
|
||||
expect(await project.serializeFile('example.json')).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('Actual linting results', () => {
|
||||
async function runEslint(
|
||||
project: TestProject,
|
||||
version: boolean | 'auto' | 8 | 9,
|
||||
) {
|
||||
project.addFile(
|
||||
'entrypoints/background.js',
|
||||
`export default defineBackground(() => {})`,
|
||||
);
|
||||
await project.prepare({
|
||||
imports: { eslintrc: { enabled: version } },
|
||||
});
|
||||
return await execaCommand('pnpm eslint entrypoints/background.js', {
|
||||
cwd: project.root,
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import { WxtHooks } from '~/types';
|
||||
|
||||
const hooks: WxtHooks = {
|
||||
ready: vi.fn(),
|
||||
'prepare:types': vi.fn(),
|
||||
'build:before': vi.fn(),
|
||||
'build:done': vi.fn(),
|
||||
'build:manifestGenerated': vi.fn(),
|
||||
@@ -41,6 +42,7 @@ describe('Hooks', () => {
|
||||
|
||||
expectHooksToBeCalled({
|
||||
ready: true,
|
||||
'prepare:types': true,
|
||||
'build:before': false,
|
||||
'build:done': false,
|
||||
'build:publicAssets': false,
|
||||
@@ -60,6 +62,7 @@ describe('Hooks', () => {
|
||||
|
||||
expectHooksToBeCalled({
|
||||
ready: true,
|
||||
'prepare:types': true,
|
||||
'build:before': true,
|
||||
'build:done': true,
|
||||
'build:publicAssets': true,
|
||||
@@ -79,6 +82,7 @@ describe('Hooks', () => {
|
||||
|
||||
expectHooksToBeCalled({
|
||||
ready: true,
|
||||
'prepare:types': true,
|
||||
'build:before': true,
|
||||
'build:done': true,
|
||||
'build:publicAssets': true,
|
||||
@@ -104,6 +108,7 @@ describe('Hooks', () => {
|
||||
|
||||
expectHooksToBeCalled({
|
||||
ready: true,
|
||||
'prepare:types': true,
|
||||
'build:before': true,
|
||||
'build:done': true,
|
||||
'build:publicAssets': true,
|
||||
|
||||
@@ -101,7 +101,13 @@ describe('Module Helpers', () => {
|
||||
|
||||
export default defineWxtModule((wxt) => {
|
||||
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(
|
||||
project.fileExists('.output/chrome-mv3/module.txt'),
|
||||
).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 () => {
|
||||
|
||||
@@ -233,10 +233,10 @@ describe('TypeScript Project', () => {
|
||||
----------------------------------------
|
||||
// Generated by wxt
|
||||
/// <reference types="wxt/vite-builder-env" />
|
||||
/// <reference types="./types/imports.d.ts" />
|
||||
/// <reference types="./types/paths.d.ts" />
|
||||
/// <reference types="./types/i18n.d.ts" />
|
||||
/// <reference types="./types/globals.d.ts" />
|
||||
/// <reference types="./types/imports.d.ts" />
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
+10
-10
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "wxt",
|
||||
"type": "module",
|
||||
"version": "0.18.9",
|
||||
"version": "0.18.12",
|
||||
"description": "Next gen framework for developing web extensions",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -124,12 +124,12 @@
|
||||
"jiti": "^1.21.6",
|
||||
"json5": "^2.2.3",
|
||||
"jszip": "^3.10.1",
|
||||
"linkedom": "^0.18.3",
|
||||
"linkedom": "^0.18.4",
|
||||
"magicast": "^0.3.4",
|
||||
"minimatch": "^9.0.4",
|
||||
"minimatch": "^9.0.5",
|
||||
"natural-compare": "^1.4.0",
|
||||
"normalize-path": "^3.0.0",
|
||||
"nypm": "^0.3.8",
|
||||
"nypm": "^0.3.9",
|
||||
"ohash": "^1.1.3",
|
||||
"open": "^10.1.0",
|
||||
"ora": "^7.0.1",
|
||||
@@ -137,9 +137,9 @@
|
||||
"prompts": "^2.4.2",
|
||||
"publish-browser-extension": "^2.1.3",
|
||||
"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.1",
|
||||
"webextension-polyfill": "^0.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -148,17 +148,17 @@
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/lodash.merge": "^4.6.9",
|
||||
"@types/natural-compare": "^1.4.3",
|
||||
"@types/node": "^20.14.2",
|
||||
"@types/node": "^20.14.9",
|
||||
"@types/normalize-path": "^3.0.2",
|
||||
"@types/prompts": "^2.4.9",
|
||||
"execa": "^9.2.0",
|
||||
"execa": "^9.3.0",
|
||||
"extract-zip": "^2.0.1",
|
||||
"happy-dom": "^13.10.1",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"p-map": "^7.0.2",
|
||||
"publint": "^0.2.8",
|
||||
"tsup": "^8.1.0",
|
||||
"tsx": "^4.15.4",
|
||||
"typescript": "^5.4.5"
|
||||
"tsx": "4.15.7",
|
||||
"typescript": "^5.5.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ const preset = {
|
||||
external: [
|
||||
...virtualEntrypointModuleNames.map((name) => `virtual:user-${name}`),
|
||||
'virtual:wxt-plugins',
|
||||
'virtual:app-config',
|
||||
],
|
||||
} satisfies tsup.Options;
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import { WxtModule } from '~/types';
|
||||
import unimport from './unimport';
|
||||
|
||||
export const builtinModules: WxtModule<any>[] = [unimport];
|
||||
@@ -0,0 +1,150 @@
|
||||
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 {
|
||||
name: "wxt/auto-imports",
|
||||
languageOptions: {
|
||||
globals,
|
||||
sourceType: "module",
|
||||
},
|
||||
};
|
||||
`,
|
||||
};
|
||||
}
|
||||
@@ -345,14 +345,32 @@ describe('CLI', () => {
|
||||
mockArgv('clean');
|
||||
await importCli();
|
||||
|
||||
expect(cleanMock).toBeCalledWith(undefined);
|
||||
expect(cleanMock).toBeCalledWith({});
|
||||
});
|
||||
|
||||
it('should respect passing a custom root', async () => {
|
||||
mockArgv('clean', 'path/to/root');
|
||||
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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -126,9 +126,10 @@ cli
|
||||
cli
|
||||
.command('clean [root]', 'clean generated files and caches')
|
||||
.alias('cleanup')
|
||||
.option('-c, --config <file>', 'use specified config file')
|
||||
.action(
|
||||
wrapAction(async (root, flags) => {
|
||||
await clean(root);
|
||||
await clean({ root, configFile: flags.config, debug: flags.debug });
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
// @ts-expect-error: Untyped virtual module
|
||||
import appConfig from 'virtual:app-config';
|
||||
import type { WxtAppConfig } from '~/sandbox/define-app-config';
|
||||
|
||||
export function useAppConfig(): WxtAppConfig {
|
||||
return appConfig;
|
||||
}
|
||||
@@ -4,3 +4,4 @@
|
||||
* @module wxt/client
|
||||
*/
|
||||
export * from './content-scripts';
|
||||
export * from './app-config';
|
||||
|
||||
@@ -61,7 +61,6 @@ export async function createViteBuilder(
|
||||
config.plugins.push(
|
||||
wxtPlugins.download(wxtConfig),
|
||||
wxtPlugins.devHtmlPrerender(wxtConfig, server),
|
||||
wxtPlugins.unimport(wxtConfig),
|
||||
wxtPlugins.resolveVirtualModules(wxtConfig),
|
||||
wxtPlugins.devServerGlobals(wxtConfig, server),
|
||||
wxtPlugins.tsconfigPaths(wxtConfig),
|
||||
@@ -70,6 +69,7 @@ export async function createViteBuilder(
|
||||
wxtPlugins.excludeBrowserPolyfill(wxtConfig),
|
||||
wxtPlugins.defineImportMeta(),
|
||||
wxtPlugins.wxtPluginLoader(wxtConfig),
|
||||
wxtPlugins.resolveAppConfig(wxtConfig),
|
||||
);
|
||||
if (wxtConfig.analysis.enabled) {
|
||||
config.plugins.push(wxtPlugins.bundleAnalysis(wxtConfig));
|
||||
@@ -245,7 +245,10 @@ export async function createViteBuilder(
|
||||
const config = vite.mergeConfig(baseConfig, envConfig);
|
||||
const server = await vite.createServer(config);
|
||||
await server.pluginContainer.buildStart({});
|
||||
const node = new ViteNodeServer(server);
|
||||
const node = new ViteNodeServer(
|
||||
// @ts-ignore: Some weird type error...
|
||||
server,
|
||||
);
|
||||
installSourcemapsSupport({
|
||||
getSourceMap: (source) => node.getSourceMap(source),
|
||||
});
|
||||
|
||||
@@ -2,7 +2,6 @@ export * from './devHtmlPrerender';
|
||||
export * from './devServerGlobals';
|
||||
export * from './download';
|
||||
export * from './multipageMove';
|
||||
export * from './unimport';
|
||||
export * from './resolveVirtualModules';
|
||||
export * from './tsconfigPaths';
|
||||
export * from './noopBackground';
|
||||
@@ -15,3 +14,4 @@ export * from './entrypointGroupGlobals';
|
||||
export * from './defineImportMeta';
|
||||
export * from './removeEntrypointMainFunction';
|
||||
export * from './wxtPluginLoader';
|
||||
export * from './resolveAppConfig';
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { exists } from 'fs-extra';
|
||||
import { resolve } from 'node:path';
|
||||
import type * as vite from 'vite';
|
||||
import { ResolvedConfig } from '~/types';
|
||||
|
||||
/**
|
||||
* When importing `virtual:app-config`, resolve it to the `app.config.ts` file in the project.
|
||||
*/
|
||||
export function resolveAppConfig(config: ResolvedConfig): vite.Plugin {
|
||||
const virtualModuleId = 'virtual:app-config';
|
||||
const resolvedVirtualModuleId = '\0' + virtualModuleId;
|
||||
const appConfigFile = resolve(config.srcDir, 'app.config.ts');
|
||||
|
||||
return {
|
||||
name: 'wxt:resolve-app-config',
|
||||
async resolveId(id) {
|
||||
if (id !== virtualModuleId) return;
|
||||
|
||||
return (await exists(appConfigFile))
|
||||
? appConfigFile
|
||||
: resolvedVirtualModuleId;
|
||||
},
|
||||
load(id) {
|
||||
if (id === resolvedVirtualModuleId) return `export default {}`;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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 }),
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,44 +1,65 @@
|
||||
import path from 'node:path';
|
||||
import glob from 'fast-glob';
|
||||
import fs from 'fs-extra';
|
||||
import { consola } from 'consola';
|
||||
import pc from 'picocolors';
|
||||
import { InlineConfig } from '~/types';
|
||||
import { registerWxt, wxt } from './wxt';
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @example
|
||||
* await clean();
|
||||
*/
|
||||
export async function clean(root = process.cwd()) {
|
||||
consola.info('Cleaning Project');
|
||||
export async function clean(root?: string): Promise<void>;
|
||||
|
||||
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 = [
|
||||
'node_modules/.vite',
|
||||
'node_modules/.cache',
|
||||
'**/.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, {
|
||||
cwd: path.resolve(root),
|
||||
cwd: root,
|
||||
absolute: true,
|
||||
onlyDirectories: true,
|
||||
deep: 2,
|
||||
});
|
||||
if (directories.length === 0) {
|
||||
consola.debug('No generated files found.');
|
||||
wxt.logger.debug('No generated files found.');
|
||||
return;
|
||||
}
|
||||
|
||||
consola.debug(
|
||||
wxt.logger.debug(
|
||||
'Found:',
|
||||
directories.map((dir) => pc.cyan(path.relative(root, dir))).join(', '),
|
||||
);
|
||||
for (const directory of directories) {
|
||||
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)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
findEntrypoints,
|
||||
} from '~/core/utils/building';
|
||||
import { createExtensionRunner } from '~/core/runners';
|
||||
import { consola } from 'consola';
|
||||
import { Mutex } from 'async-mutex';
|
||||
import pc from 'picocolors';
|
||||
import { relative } from 'node:path';
|
||||
@@ -201,21 +200,21 @@ function createFileReloader(server: WxtDevServer) {
|
||||
switch (changes.type) {
|
||||
case 'extension-reload':
|
||||
server.reloadExtension();
|
||||
consola.success(`Reloaded extension`);
|
||||
wxt.logger.success(`Reloaded extension`);
|
||||
break;
|
||||
case 'html-reload':
|
||||
const { reloadedNames } = reloadHtmlPages(
|
||||
changes.rebuildGroups,
|
||||
server,
|
||||
);
|
||||
consola.success(`Reloaded: ${getFilenameList(reloadedNames)}`);
|
||||
wxt.logger.success(`Reloaded: ${getFilenameList(reloadedNames)}`);
|
||||
break;
|
||||
case 'content-script-reload':
|
||||
reloadContentScripts(changes.changedSteps, server);
|
||||
const rebuiltNames = changes.rebuildGroups
|
||||
.flat()
|
||||
.map((entry) => entry.name);
|
||||
consola.success(`Reloaded: ${getFilenameList(rebuiltNames)}`);
|
||||
wxt.logger.success(`Reloaded: ${getFilenameList(rebuiltNames)}`);
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
@@ -2,12 +2,12 @@ import { describe, expect, it } from 'vitest';
|
||||
import path from 'node:path';
|
||||
import { bun } from '../bun';
|
||||
|
||||
const cwd = path.resolve(__dirname, 'fixtures/bun-project');
|
||||
|
||||
describe.skipIf(() => process.platform === 'win32')(
|
||||
'Bun Package Management Utils',
|
||||
() => {
|
||||
describe('listDependencies', () => {
|
||||
const cwd = path.resolve(__dirname, 'fixtures/simple-bun-project');
|
||||
|
||||
it('should list direct dependencies', async () => {
|
||||
const actual = await bun.listDependencies({ cwd });
|
||||
expect(actual).toEqual([
|
||||
|
||||
@@ -4,15 +4,14 @@ import { npm } from '../npm';
|
||||
import { execaCommand } from 'execa';
|
||||
import { exists } from 'fs-extra';
|
||||
|
||||
const cwd = path.resolve(__dirname, 'fixtures/npm-project');
|
||||
|
||||
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', () => {
|
||||
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 () => {
|
||||
const actual = await npm.listDependencies({ cwd });
|
||||
expect(actual).toEqual([
|
||||
@@ -32,6 +31,8 @@ describe('NPM Package Management Utils', () => {
|
||||
});
|
||||
|
||||
describe('downloadDependency', () => {
|
||||
const cwd = path.resolve(__dirname, 'fixtures/simple-npm-project');
|
||||
|
||||
it('should download the dependency as a tarball', async () => {
|
||||
const downloadDir = path.resolve(cwd, 'dist');
|
||||
const id = 'mime-db@1.52.0';
|
||||
|
||||
@@ -3,17 +3,16 @@ import path from 'node:path';
|
||||
import { pnpm } from '../pnpm';
|
||||
import { execaCommand } from 'execa';
|
||||
|
||||
const cwd = path.resolve(__dirname, 'fixtures/pnpm-project');
|
||||
|
||||
process.env.WXT_PNPM_IGNORE_WORKSPACE = 'true';
|
||||
|
||||
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', () => {
|
||||
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 () => {
|
||||
const actual = await pnpm.listDependencies({ cwd });
|
||||
expect(actual).toEqual([
|
||||
|
||||
@@ -2,10 +2,10 @@ import { describe, expect, it } from 'vitest';
|
||||
import path from 'node:path';
|
||||
import { yarn } from '../yarn';
|
||||
|
||||
const cwd = path.resolve(__dirname, 'fixtures/yarn-project');
|
||||
|
||||
describe('Yarn Package Management Utils', () => {
|
||||
describe('listDependencies', () => {
|
||||
const cwd = path.resolve(__dirname, 'fixtures/simple-yarn-project');
|
||||
|
||||
it('should list direct dependencies', async () => {
|
||||
const actual = await yarn.listDependencies({ cwd });
|
||||
expect(actual).toEqual([
|
||||
|
||||
@@ -19,8 +19,7 @@ export const bun: WxtPackageManagerImpl = {
|
||||
.slice(1) // Skip the first line, is not a dependency
|
||||
.map((line) => line.trim())
|
||||
.map((line) => /.* (@?\S+)@(\S+)$/.exec(line))
|
||||
// @ts-expect-error: Filtering to known non-null matches
|
||||
.filter<RegExpExecArray>((match) => !!match)
|
||||
.filter((match) => !!match)
|
||||
.map(([_, name, version]) => ({ name, version })),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -59,6 +59,9 @@ export function createWebExtRunner(): ExtensionRunner {
|
||||
target:
|
||||
wxt.config.browser === 'firefox' ? 'firefox-desktop' : 'chromium',
|
||||
sourceDir: wxt.config.outDir,
|
||||
// Don't add a "Reload Manager" extension alongside dev extension, WXT
|
||||
// already handles reloads intenrally.
|
||||
noReloadManagerExtension: true,
|
||||
// WXT handles reloads, so disable auto-reload behaviors in web-ext
|
||||
noReload: true,
|
||||
noInput: true,
|
||||
|
||||
@@ -45,14 +45,18 @@ async function copyPublicDirectory(): Promise<BuildOutput['publicAssets']> {
|
||||
if (files.length === 0) return [];
|
||||
|
||||
const publicAssets: BuildOutput['publicAssets'] = [];
|
||||
for (const { absoluteSrc, relativeDest } of files) {
|
||||
const absoluteDest = resolve(wxt.config.outDir, relativeDest);
|
||||
for (const file of files) {
|
||||
const absoluteDest = resolve(wxt.config.outDir, file.relativeDest);
|
||||
|
||||
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({
|
||||
type: 'asset',
|
||||
fileName: relativeDest,
|
||||
fileName: file.relativeDest,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import { Unimport, createUnimport } from 'unimport';
|
||||
import {
|
||||
EslintGlobalsPropValue,
|
||||
Entrypoint,
|
||||
WxtResolvedUnimportOptions,
|
||||
} from '~/types';
|
||||
import { Entrypoint, WxtDirEntry, WxtDirFileEntry } from '~/types';
|
||||
import fs from 'fs-extra';
|
||||
import { relative, resolve } from 'path';
|
||||
import { dirname, relative, resolve } from 'node:path';
|
||||
import {
|
||||
getEntrypointBundlePath,
|
||||
isHtmlEntrypoint,
|
||||
@@ -25,61 +20,57 @@ export async function generateTypesDir(
|
||||
): Promise<void> {
|
||||
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) {
|
||||
const unimport = createUnimport(wxt.config.imports);
|
||||
references.push(await writeImportsDeclarationFile(unimport));
|
||||
if (wxt.config.imports.eslintrc.enabled) {
|
||||
await writeImportsEslintFile(unimport, wxt.config.imports);
|
||||
}
|
||||
}
|
||||
// Add references to modules installed from NPM to the TS project so 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.userModules.forEach((module) => {
|
||||
if (module.type === 'node_module' && module.configKey != null)
|
||||
entries.push({ module: module.id });
|
||||
});
|
||||
|
||||
references.push(await writePathsDeclarationFile(entrypoints));
|
||||
references.push(await writeI18nDeclarationFile());
|
||||
references.push(await writeGlobalsDeclarationFile());
|
||||
// browser.runtime.getURL
|
||||
entries.push(await getPathsDeclarationEntry(entrypoints));
|
||||
|
||||
const mainReference = await writeMainDeclarationFile(references);
|
||||
await writeTsConfigFile(mainReference);
|
||||
}
|
||||
// browser.i18n.getMessage
|
||||
entries.push(await getI18nDeclarationEntry());
|
||||
|
||||
async function writeImportsDeclarationFile(unimport: Unimport) {
|
||||
const filePath = resolve(wxt.config.typesDir, 'imports.d.ts');
|
||||
// import.meta.env.*
|
||||
entries.push(await getGlobalsDeclarationEntry());
|
||||
|
||||
// Load project imports into unimport memory so they are output via generateTypeDeclarations
|
||||
await unimport.scanImportsFromDir(undefined, { cwd: wxt.config.srcDir });
|
||||
// tsconfig.json
|
||||
entries.push(await getTsConfigEntry());
|
||||
|
||||
await writeFileIfDifferent(
|
||||
filePath,
|
||||
['// Generated by wxt', await unimport.generateTypeDeclarations()].join(
|
||||
'\n',
|
||||
) + '\n',
|
||||
// Let modules add more entries
|
||||
await wxt.hooks.callHook('prepare:types', wxt, entries);
|
||||
|
||||
// Add main declaration file, not editable
|
||||
entries.push(getMainDeclarationEntry(entries));
|
||||
|
||||
// Write all the files
|
||||
const absoluteFileEntries = (
|
||||
entries.filter((entry) => 'path' in entry) as WxtDirFileEntry[]
|
||||
).map<WxtDirFileEntry>((entry) => ({
|
||||
...entry,
|
||||
path: resolve(wxt.config.wxtDir, entry.path),
|
||||
}));
|
||||
|
||||
await Promise.all(
|
||||
absoluteFileEntries.map(async (file) => {
|
||||
await fs.ensureDir(dirname(file.path));
|
||||
await writeFileIfDifferent(file.path, file.text);
|
||||
}),
|
||||
);
|
||||
|
||||
return filePath;
|
||||
}
|
||||
|
||||
async function writeImportsEslintFile(
|
||||
unimport: Unimport,
|
||||
options: WxtResolvedUnimportOptions,
|
||||
) {
|
||||
const globals: Record<string, EslintGlobalsPropValue> = {};
|
||||
const eslintrc = { globals };
|
||||
|
||||
(await unimport.getImports())
|
||||
.map((i) => i.as ?? i.name)
|
||||
.filter(Boolean)
|
||||
.sort()
|
||||
.forEach((name) => {
|
||||
eslintrc.globals[name] = options.eslintrc.globalsPropValue;
|
||||
});
|
||||
await fs.writeJson(options.eslintrc.filePath, eslintrc, { spaces: 2 });
|
||||
}
|
||||
|
||||
async function writePathsDeclarationFile(
|
||||
async function getPathsDeclarationEntry(
|
||||
entrypoints: Entrypoint[],
|
||||
): Promise<string> {
|
||||
const filePath = resolve(wxt.config.typesDir, 'paths.d.ts');
|
||||
): Promise<WxtDirFileEntry> {
|
||||
const unions = entrypoints
|
||||
.map((entry) =>
|
||||
getEntrypointBundlePath(
|
||||
@@ -108,16 +99,14 @@ declare module "wxt/browser" {
|
||||
}
|
||||
`;
|
||||
|
||||
await writeFileIfDifferent(
|
||||
filePath,
|
||||
template.replace('{{ union }}', unions || ' | never'),
|
||||
);
|
||||
|
||||
return filePath;
|
||||
return {
|
||||
path: 'types/paths.d.ts',
|
||||
text: template.replace('{{ union }}', unions || ' | never'),
|
||||
tsReference: true,
|
||||
};
|
||||
}
|
||||
|
||||
async function writeI18nDeclarationFile(): Promise<string> {
|
||||
const filePath = resolve(wxt.config.typesDir, 'i18n.d.ts');
|
||||
async function getI18nDeclarationEntry(): Promise<WxtDirFileEntry> {
|
||||
const defaultLocale = wxt.config.manifest.default_locale;
|
||||
const template = `// Generated by wxt
|
||||
import "wxt/browser";
|
||||
@@ -165,20 +154,19 @@ declare module "wxt/browser" {
|
||||
options?: GetMessageOptions,
|
||||
): 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> {
|
||||
const filePath = resolve(wxt.config.typesDir, 'globals.d.ts');
|
||||
async function getGlobalsDeclarationEntry(): Promise<WxtDirFileEntry> {
|
||||
const globals = [...getGlobals(wxt.config), ...getEntrypointGlobals('')];
|
||||
await writeFileIfDifferent(
|
||||
filePath,
|
||||
[
|
||||
return {
|
||||
path: 'types/globals.d.ts',
|
||||
text: [
|
||||
'// Generated by wxt',
|
||||
'export {}',
|
||||
'interface ImportMetaEnv {',
|
||||
@@ -187,39 +175,30 @@ async function writeGlobalsDeclarationFile(): Promise<string> {
|
||||
'interface ImportMeta {',
|
||||
' readonly env: ImportMetaEnv',
|
||||
'}',
|
||||
].join('\n') + '\n',
|
||||
);
|
||||
return filePath;
|
||||
'',
|
||||
].join('\n'),
|
||||
tsReference: true,
|
||||
};
|
||||
}
|
||||
|
||||
async function writeMainDeclarationFile(references: string[]): Promise<string> {
|
||||
const dir = wxt.config.wxtDir;
|
||||
const filePath = resolve(dir, 'wxt.d.ts');
|
||||
await writeFileIfDifferent(
|
||||
filePath,
|
||||
[
|
||||
'// Generated by wxt',
|
||||
`/// <reference types="wxt/vite-builder-env" />`,
|
||||
...references.map(
|
||||
(ref) =>
|
||||
`/// <reference types="./${normalizePath(relative(dir, ref))}" />`,
|
||||
),
|
||||
|
||||
// Add references to modules installed from NPM to the TS project so
|
||||
// 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;
|
||||
function getMainDeclarationEntry(references: WxtDirEntry[]): WxtDirFileEntry {
|
||||
const lines = ['// Generated by wxt'];
|
||||
references.forEach((ref) => {
|
||||
if ('module' in ref) {
|
||||
return lines.push(`/// <reference types="${ref.module}" />`);
|
||||
} else if (ref.tsReference) {
|
||||
const absolutePath = resolve(wxt.config.wxtDir, ref.path);
|
||||
const relativePath = relative(wxt.config.wxtDir, absolutePath);
|
||||
lines.push(`/// <reference types="./${normalizePath(relativePath)}" />`);
|
||||
}
|
||||
});
|
||||
return {
|
||||
path: 'wxt.d.ts',
|
||||
text: lines.join('\n') + '\n',
|
||||
};
|
||||
}
|
||||
|
||||
async function writeTsConfigFile(mainReference: string) {
|
||||
async function getTsConfigEntry(): Promise<WxtDirFileEntry> {
|
||||
const dir = wxt.config.wxtDir;
|
||||
const getTsconfigPath = (path: string) => normalizePath(relative(dir, path));
|
||||
const paths = Object.entries(wxt.config.alias)
|
||||
@@ -232,9 +211,7 @@ async function writeTsConfigFile(mainReference: string) {
|
||||
})
|
||||
.join(',\n');
|
||||
|
||||
await writeFileIfDifferent(
|
||||
resolve(dir, 'tsconfig.json'),
|
||||
`{
|
||||
const text = `{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
@@ -251,9 +228,13 @@ ${paths}
|
||||
},
|
||||
"include": [
|
||||
"${getTsconfigPath(wxt.config.root)}/**/*",
|
||||
"./${getTsconfigPath(mainReference)}"
|
||||
"./wxt.d.ts"
|
||||
],
|
||||
"exclude": ["${getTsconfigPath(wxt.config.outBaseDir)}"]
|
||||
}`,
|
||||
);
|
||||
}`;
|
||||
|
||||
return {
|
||||
path: 'tsconfig.json',
|
||||
text,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -55,6 +55,13 @@ export async function importEntrypointFile<T>(path: string): Promise<T> {
|
||||
wxt.config.wxtModuleDir,
|
||||
'dist/virtual/mock-browser.js',
|
||||
),
|
||||
// TODO: Resolve this virtual module to some file with
|
||||
// `export default {}` instead of this hack of using another file with
|
||||
// a default export.
|
||||
'virtual:app-config': resolve(
|
||||
wxt.config.wxtModuleDir,
|
||||
'dist/virtual/mock-browser.js',
|
||||
),
|
||||
},
|
||||
// Continue using node to load TS files even if `bun run --bun` is detected. Jiti does not
|
||||
// respect the custom transform function when using it's native bun option.
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
ValidationResults,
|
||||
validateEntrypoints,
|
||||
} from '../validation';
|
||||
import consola from 'consola';
|
||||
import { wxt } from '../../wxt';
|
||||
import { mergeJsonOutputs } from '@aklinker1/rollup-plugin-visualizer';
|
||||
import { isCI } from 'ci-info';
|
||||
@@ -134,12 +133,12 @@ function printValidationResults({
|
||||
}, new Map<Entrypoint, ValidationResult[]>());
|
||||
|
||||
Array.from(entrypointErrors.entries()).forEach(([entrypoint, errors]) => {
|
||||
consola.log(relative(cwd, entrypoint.inputPath));
|
||||
wxt.logger.log(relative(cwd, entrypoint.inputPath));
|
||||
console.log();
|
||||
errors.forEach((err) => {
|
||||
const type = err.type === 'error' ? pc.red('ERROR') : pc.yellow('WARN');
|
||||
const recieved = pc.dim(`(recieved: ${JSON.stringify(err.value)})`);
|
||||
consola.log(` - ${type} ${err.message} ${recieved}`);
|
||||
wxt.logger.log(` - ${type} ${err.message} ${recieved}`);
|
||||
});
|
||||
console.log();
|
||||
});
|
||||
|
||||
@@ -12,16 +12,19 @@ import {
|
||||
WxtCommand,
|
||||
WxtModule,
|
||||
WxtModuleWithMetadata,
|
||||
ResolvedEslintrc,
|
||||
Eslintrc,
|
||||
} from '~/types';
|
||||
import path from 'node:path';
|
||||
import { createFsCache } from '~/core/utils/cache';
|
||||
import consola, { LogLevels } from 'consola';
|
||||
import defu from 'defu';
|
||||
import { NullablyRequired } from '../types';
|
||||
import { isModuleInstalled } from '../package';
|
||||
import fs from 'fs-extra';
|
||||
import { normalizePath } from '../paths';
|
||||
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
|
||||
@@ -127,15 +130,21 @@ export async function resolveConfig(
|
||||
};
|
||||
}
|
||||
|
||||
const modules = await resolveWxtModules(modulesDir, mergedConfig.modules);
|
||||
const moduleOptions = modules.reduce<Record<string, any>>((map, module) => {
|
||||
if (module.configKey) {
|
||||
map[module.configKey] =
|
||||
// @ts-expect-error
|
||||
mergedConfig[module.configKey];
|
||||
}
|
||||
return map;
|
||||
}, {});
|
||||
const userModules = await resolveWxtUserModules(
|
||||
modulesDir,
|
||||
mergedConfig.modules,
|
||||
);
|
||||
const moduleOptions = userModules.reduce<Record<string, any>>(
|
||||
(map, module) => {
|
||||
if (module.configKey) {
|
||||
map[module.configKey] =
|
||||
// @ts-expect-error
|
||||
mergedConfig[module.configKey];
|
||||
}
|
||||
return map;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
return {
|
||||
browser,
|
||||
@@ -146,7 +155,7 @@ export async function resolveConfig(
|
||||
filterEntrypoints,
|
||||
env,
|
||||
fsCache: createFsCache(wxtDir),
|
||||
imports: await getUnimportOptions(wxtDir, logger, mergedConfig),
|
||||
imports: await getUnimportOptions(wxtDir, srcDir, logger, mergedConfig),
|
||||
logger,
|
||||
manifest: await resolveManifestConfig(env, mergedConfig.manifest),
|
||||
manifestVersion,
|
||||
@@ -160,7 +169,7 @@ export async function resolveConfig(
|
||||
srcDir,
|
||||
typesDir,
|
||||
wxtDir,
|
||||
zip: resolveZipConfig(root, mergedConfig),
|
||||
zip: resolveZipConfig(root, outBaseDir, mergedConfig),
|
||||
transformManifest: mergedConfig.transformManifest,
|
||||
analysis: resolveAnalysisConfig(root, mergedConfig),
|
||||
userConfigMetadata: userConfigMetadata ?? {},
|
||||
@@ -175,7 +184,8 @@ export async function resolveConfig(
|
||||
},
|
||||
hooks: mergedConfig.hooks ?? {},
|
||||
vite: mergedConfig.vite ?? (() => ({})),
|
||||
modules,
|
||||
builtinModules,
|
||||
userModules,
|
||||
plugins: [],
|
||||
...moduleOptions,
|
||||
};
|
||||
@@ -233,6 +243,7 @@ async function mergeInlineConfig(
|
||||
|
||||
function resolveZipConfig(
|
||||
root: string,
|
||||
outBaseDir: string,
|
||||
mergedConfig: InlineConfig,
|
||||
): NullablyRequired<ResolvedConfig['zip']> {
|
||||
const downloadedPackagesDir = path.resolve(root, '.wxt/local_modules');
|
||||
@@ -253,6 +264,8 @@ function resolveZipConfig(
|
||||
// Tests
|
||||
'**/__tests__/**',
|
||||
'**/*.+(test|spec).?(c|m)+(j|t)s?(x)',
|
||||
// Output directory
|
||||
`${path.relative(root, outBaseDir)}/**`,
|
||||
// From user
|
||||
...(mergedConfig.zip?.excludeSources ?? []),
|
||||
],
|
||||
@@ -285,22 +298,12 @@ function resolveAnalysisConfig(
|
||||
|
||||
async function getUnimportOptions(
|
||||
wxtDir: string,
|
||||
srcDir: string,
|
||||
logger: Logger,
|
||||
config: InlineConfig,
|
||||
): Promise<WxtResolvedUnimportOptions | 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 = {
|
||||
debugLog: logger.debug,
|
||||
imports: [
|
||||
@@ -315,11 +318,10 @@ async function getUnimportOptions(
|
||||
],
|
||||
warn: logger.warn,
|
||||
dirs: ['components', 'composables', 'hooks', 'utils'],
|
||||
eslintrc: {
|
||||
enabled,
|
||||
filePath: path.resolve(wxtDir, 'eslintrc-auto-import.json'),
|
||||
globalsPropValue: true,
|
||||
dirsScanOptions: {
|
||||
cwd: srcDir,
|
||||
},
|
||||
eslintrc: await getUnimportEslintOptions(wxtDir, config.imports?.eslintrc),
|
||||
};
|
||||
|
||||
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`.
|
||||
*/
|
||||
@@ -378,7 +414,7 @@ export async function mergeBuilderConfig(
|
||||
throw Error('Builder not found. Make sure vite is installed.');
|
||||
}
|
||||
|
||||
export async function resolveWxtModules(
|
||||
export async function resolveWxtUserModules(
|
||||
modulesDir: string,
|
||||
modules: string[] = [],
|
||||
): Promise<WxtModuleWithMetadata<any>[]> {
|
||||
|
||||
@@ -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 [];
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,3 @@ export async function getPackageJson(): Promise<
|
||||
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,
|
||||
entrypointsDir: fakeDir(),
|
||||
modulesDir: fakeDir(),
|
||||
modules: [],
|
||||
builtinModules: [],
|
||||
userModules: [],
|
||||
env: { browser, command, manifestVersion, mode },
|
||||
fsCache: mock<FsCache>(),
|
||||
imports: {
|
||||
eslintrc: {
|
||||
enabled: faker.datatype.boolean(),
|
||||
enabled: faker.helpers.arrayElement([false, 8, 9]),
|
||||
filePath: fakeFile(),
|
||||
globalsPropValue: faker.helpers.arrayElement([
|
||||
true,
|
||||
|
||||
@@ -5,11 +5,13 @@ import {
|
||||
WxtCommand,
|
||||
WxtDevServer,
|
||||
WxtHooks,
|
||||
WxtModule,
|
||||
} from '~/types';
|
||||
import { resolveConfig } from './utils/building';
|
||||
import { createHooks } from 'hookable';
|
||||
import { createWxtPackageManager } from './package-managers';
|
||||
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
|
||||
@@ -46,18 +48,16 @@ export async function registerWxt(
|
||||
};
|
||||
|
||||
// Initialize modules
|
||||
for (const module of config.modules) {
|
||||
const initModule = async (module: WxtModule<any>) => {
|
||||
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?.(
|
||||
wxt,
|
||||
// @ts-expect-error: Untyped configKey field
|
||||
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
|
||||
wxt.hooks.addHooks(config.hooks);
|
||||
|
||||
@@ -133,16 +133,22 @@ async function zipDir(
|
||||
}
|
||||
}
|
||||
await options?.additionalWork?.(archive);
|
||||
const buffer = await archive.generateAsync({
|
||||
type: 'base64',
|
||||
...(wxt.config.zip.compressionLevel === 0
|
||||
? { compression: 'STORE' }
|
||||
: {
|
||||
compression: 'DEFLATE',
|
||||
compressionOptions: { level: wxt.config.zip.compressionLevel },
|
||||
}),
|
||||
});
|
||||
await fs.writeFile(outputPath, buffer, 'base64');
|
||||
|
||||
await new Promise<void>((resolve, reject) =>
|
||||
archive
|
||||
.generateNodeStream({
|
||||
type: 'nodebuffer',
|
||||
...(wxt.config.zip.compressionLevel === 0
|
||||
? { compression: 'STORE' }
|
||||
: {
|
||||
compression: 'DEFLATE',
|
||||
compressionOptions: { level: wxt.config.zip.compressionLevel },
|
||||
}),
|
||||
})
|
||||
.pipe(fs.createWriteStream(outputPath))
|
||||
.on('error', reject)
|
||||
.on('close', resolve),
|
||||
);
|
||||
}
|
||||
|
||||
async function downloadPrivatePackages() {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
export interface WxtAppConfig {}
|
||||
|
||||
/**
|
||||
* Runtime app config defined in `<srcDir>/app.config.ts`.
|
||||
*
|
||||
* You can add fields to this interface via ["Module Augmentation"](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation):
|
||||
*
|
||||
* ```ts
|
||||
* // app.config.ts
|
||||
* import 'wxt/sandbox';
|
||||
*
|
||||
* declare module "wxt/sandbox" {
|
||||
* export interface WxtAppConfig {
|
||||
* analytics: AnalyticsConfig
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function defineAppConfig(config: WxtAppConfig): WxtAppConfig {
|
||||
return config;
|
||||
}
|
||||
@@ -7,4 +7,5 @@ export * from './define-unlisted-script';
|
||||
export * from './define-background';
|
||||
export * from './define-content-script';
|
||||
export * from './define-wxt-plugin';
|
||||
export * from './define-app-config';
|
||||
export * from '@webext-core/match-patterns';
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import type * as vite from 'vite';
|
||||
import {
|
||||
unimport,
|
||||
download,
|
||||
tsconfigPaths,
|
||||
globals,
|
||||
webextensionPolyfillMock,
|
||||
resolveAppConfig,
|
||||
} from '../core/builders/vite/plugins';
|
||||
import { resolveConfig } from '~/core/utils/building';
|
||||
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`
|
||||
@@ -25,11 +27,19 @@ 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.
|
||||
*/
|
||||
export function WxtVitest(inlineConfig?: InlineConfig): vite.PluginOption {
|
||||
return resolveConfig(inlineConfig ?? {}, 'serve').then((config) => [
|
||||
webextensionPolyfillMock(config),
|
||||
unimport(config),
|
||||
globals(config),
|
||||
download(config),
|
||||
tsconfigPaths(config),
|
||||
]);
|
||||
return resolveConfig(inlineConfig ?? {}, 'serve').then(async (config) => {
|
||||
const plugins = [
|
||||
webextensionPolyfillMock(config),
|
||||
globals(config),
|
||||
download(config),
|
||||
tsconfigPaths(config),
|
||||
resolveAppConfig(config),
|
||||
];
|
||||
if (config.imports !== false) {
|
||||
const unimport = createUnimport(config.imports);
|
||||
await unimport.init();
|
||||
plugins.push(unimportPlugin(unimport));
|
||||
}
|
||||
return plugins;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -924,6 +924,10 @@ export interface ExtensionRunnerConfig {
|
||||
* }
|
||||
*/
|
||||
chromiumPref?: string;
|
||||
/**
|
||||
* By default, chrome opens a random port for debugging. Set this value to use a specific port.
|
||||
*/
|
||||
chromiumPort?: number;
|
||||
/**
|
||||
* @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#pref
|
||||
*/
|
||||
@@ -1034,6 +1038,23 @@ export interface WxtHooks {
|
||||
* @returns Promise
|
||||
*/
|
||||
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.
|
||||
*
|
||||
@@ -1188,7 +1209,8 @@ export interface ResolvedConfig {
|
||||
reloadCommand: string | false;
|
||||
};
|
||||
hooks: NestedHooks<WxtHooks>;
|
||||
modules: WxtModuleWithMetadata<any>[];
|
||||
builtinModules: WxtModule<any>[];
|
||||
userModules: WxtModuleWithMetadata<any>[];
|
||||
/**
|
||||
* An array of string to import plugins from. These paths should be
|
||||
* resolvable by vite, and they should `export default defineWxtPlugin(...)`.
|
||||
@@ -1220,17 +1242,21 @@ export interface Eslintrc {
|
||||
/**
|
||||
* 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
|
||||
* - `true`: Generate the helper file
|
||||
* - `false`: Don't generate the file
|
||||
* - `false`: Don't generate the file.
|
||||
* - `'auto'`: Check if eslint is installed, and if it is, generate a compatible config 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'
|
||||
*/
|
||||
enabled?: boolean | 'auto';
|
||||
enabled?: false | true | 'auto' | 8 | 9;
|
||||
/**
|
||||
* 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;
|
||||
/**
|
||||
@@ -1240,7 +1266,8 @@ export interface Eslintrc {
|
||||
}
|
||||
|
||||
export interface ResolvedEslintrc {
|
||||
enabled: boolean;
|
||||
/** False if disabled, otherwise the major version of ESLint installed */
|
||||
enabled: false | 8 | 9;
|
||||
/** Absolute path */
|
||||
filePath: string;
|
||||
globalsPropValue: EslintGlobalsPropValue;
|
||||
@@ -1334,13 +1361,9 @@ export interface WxtModuleWithMetadata<TOptions extends WxtModuleOptions>
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface ResolvedPublicFile {
|
||||
/**
|
||||
* The absolute path to the file that will be copied to the output directory.
|
||||
* @example
|
||||
* "/path/to/any/file.css"
|
||||
*/
|
||||
absoluteSrc: string;
|
||||
export type ResolvedPublicFile = CopiedPublicFile | GeneratedPublicFile;
|
||||
|
||||
export interface ResolvedBasePublicFile {
|
||||
/**
|
||||
* The relative path in the output directory to copy the file to.
|
||||
* @example
|
||||
@@ -1349,4 +1372,47 @@ export interface ResolvedPublicFile {
|
||||
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 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;
|
||||
}
|
||||
|
||||
@@ -81,7 +81,15 @@ async function reloadTabsForContentScript(contentScript: ContentScript) {
|
||||
if (!url) return false;
|
||||
return !!matchPatterns.find((pattern) => pattern.includes(url));
|
||||
});
|
||||
await Promise.all(matchingTabs.map((tab) => browser.tabs.reload(tab.id)));
|
||||
await Promise.all(
|
||||
matchingTabs.map(async (tab) => {
|
||||
try {
|
||||
await browser.tabs.reload(tab.id);
|
||||
} catch (err) {
|
||||
logger.warn('Failed to reload tab:', err);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export async function reloadContentScriptMv2(
|
||||
|
||||
Generated
+266
-252
File diff suppressed because it is too large
Load Diff
@@ -22,7 +22,7 @@
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@wxt-dev/module-react": "^1.0.0",
|
||||
"typescript": "^5.4.5",
|
||||
"wxt": "^0.18.6"
|
||||
"typescript": "^5.5.2",
|
||||
"wxt": "^0.18.10"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
"solid-js": "^1.8.17"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.4.5",
|
||||
"@wxt-dev/module-solid": "^1.0.0",
|
||||
"wxt": "^0.18.6"
|
||||
"@wxt-dev/module-solid": "^1.1.1",
|
||||
"typescript": "^5.5.2",
|
||||
"wxt": "^0.18.10"
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user