Compare commits
61 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 36be4ce771 | |||
| 18f8a387b0 | |||
| 48c2c07557 | |||
| 61548a9478 | |||
| fd678e9572 | |||
| 63fb913f76 | |||
| faffe20a62 | |||
| 3ed8bd10fd | |||
| 6f3f270573 | |||
| 5347cb4626 | |||
| d6b4d2d1f6 | |||
| 0d7ae287da | |||
| 90cb3bd3bf | |||
| 60eb97c4b2 | |||
| 0ffc6eae3e | |||
| 06a8c5fb0e | |||
| eb86413f35 | |||
| b29d49cbf1 | |||
| c0a0edb30c | |||
| b15a933c75 | |||
| 1851be22a7 | |||
| 39f6c29e4f | |||
| 936e89fbfb | |||
| ad365ecde4 | |||
| b4283e37bd | |||
| b467ad760a | |||
| 9fc6408ef9 | |||
| 394cb1a520 | |||
| ffec88549b | |||
| 4758bea187 | |||
| 6671471f75 | |||
| ae78dc7fb3 | |||
| b62af3d007 | |||
| 43ae44ecd5 | |||
| 5bab9d2f59 | |||
| 7edf1c8103 | |||
| 53b4eac955 | |||
| f9a10c2c52 | |||
| 8206ca9164 | |||
| 1a823cc008 | |||
| efc011eb81 | |||
| 35f6d28977 | |||
| 43fcbec0eb | |||
| a0d56b4d27 | |||
| 38ccb6ace6 | |||
| aaf75f1de1 | |||
| 21e8ca0a2c | |||
| 55444858bc | |||
| 771ea03069 | |||
| 38b71d1350 | |||
| cb781aceb5 | |||
| d53c792218 | |||
| 907f4d3672 | |||
| 8432d73a6b | |||
| 94a5435089 | |||
| edf3cf3f17 | |||
| 48783baec3 | |||
| c70f047696 | |||
| e6e0c713bc | |||
| 3691554a73 | |||
| 1a3436606b |
@@ -8,6 +8,10 @@ on:
|
||||
type: choice
|
||||
options:
|
||||
- wxt
|
||||
- module-react
|
||||
- module-vue
|
||||
- module-svelte
|
||||
- module-solid
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
|
||||
@@ -8,6 +8,10 @@ on:
|
||||
type: choice
|
||||
options:
|
||||
- wxt
|
||||
- module-react
|
||||
- module-vue
|
||||
- module-svelte
|
||||
- module-solid
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
|
||||
@@ -13,6 +13,12 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup
|
||||
- run: pnpm check
|
||||
builds:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup
|
||||
- run: pnpm buildc all
|
||||
build-demo:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
@@ -27,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
|
||||
|
||||
@@ -112,3 +112,20 @@ cp -r templates/vanilla templates/<new-template-name>
|
||||
```
|
||||
|
||||
That's it. Once your template is merged, it will be available inside `wxt init` immediately. You don't need to release a new version of WXT to release a new template.
|
||||
|
||||
## Releasing Updates
|
||||
|
||||
Releases are done with GitHub actions:
|
||||
|
||||
- Use the [Release workflow](https://github.com/wxt-dev/wxt/actions/workflows/release.yml) to release a single package in the monorepo. This automatically detects the version change with conventional commits, builds and uploads the package to NPM, and creates a GitHub release.
|
||||
- Use the [Sync Releases workflow](https://github.com/wxt-dev/wxt/actions/workflows/sync-releases.yml) to sync the GitHub releases with changes to the changelog. To change a release, update the `CHANGELOG.md` file and run the workflow. It will sync the releases of a single package in the monorepo.
|
||||
|
||||
## Upgrading Dependencies
|
||||
|
||||
Use [`taze`](https://www.npmjs.com/package/taze) to upgrade dependencies throughout the entire monorepo.
|
||||
|
||||
```ts
|
||||
pnpm dlx taze -r
|
||||
```
|
||||
|
||||
Configuration is in [`taze.config.ts`](./taze.config.ts).
|
||||
|
||||
@@ -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>
|
||||
@@ -29,6 +29,7 @@ const chromeExtensionIds = [
|
||||
'nedcanggplmbbgmlpcjiafgjcpdimpea', // YTBlock - Block any content from YouTube™
|
||||
'oadbjpccljkplmhnjekgjamejnbadlne', // demo.fun - Interactive product demos that convert
|
||||
'iopdafdcollfgaoffingmahpffckmjni', // SmartEReply: Elevate Your LinkedIn™ Engagement with AI 🚀📈
|
||||
'khjdmjcmpolknpccmaaipmidphjokhdf', // WorkFlowy MultiFlow
|
||||
];
|
||||
|
||||
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'),
|
||||
|
||||
@@ -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 | ✅ | ✅ | ❌ |
|
||||
|
||||
@@ -15,7 +15,14 @@ npx wxt@latest init <project-name>
|
||||
```
|
||||
|
||||
```sh [bun]
|
||||
bunx wxt@latest init <project-name>
|
||||
# The "wxt init" command currently fails when ran with bunx.
|
||||
# Use NPX as a workaround, and select "bun" as your package
|
||||
# manager. To stay up to date with this issue, follow
|
||||
# https://github.com/wxt-dev/wxt/issues/707
|
||||
#
|
||||
# bunx wxt@latest init <project-name>
|
||||
|
||||
npx wxt@latest init <project-name>
|
||||
```
|
||||
|
||||
:::
|
||||
@@ -96,7 +103,6 @@ pnpm dev
|
||||
```
|
||||
|
||||
:::tip 🎉 Well done!
|
||||
|
||||
The dev command will build the extension for development, open the browser, and reload the different parts of the extension when you save changes.
|
||||
:::
|
||||
|
||||
@@ -107,4 +113,4 @@ You're ready to build your web extension!
|
||||
- Read the rest of the "Get Started" pages for a high-overview of what WXT can do
|
||||
- Read the [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.
|
||||
@@ -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)
|
||||
|
||||
@@ -12,8 +12,8 @@ Firefox does not support sandboxed pages.
|
||||
:patterns="[
|
||||
['sandbox.html', 'sandbox.html'],
|
||||
['sandbox/index.html', 'sandbox.html'],
|
||||
['<name>.sandbox.html', '<name>.html` '],
|
||||
['<name>.sandbox/index.html', '<name>.html` '],
|
||||
['<name>.sandbox.html', '<name>.html'],
|
||||
['<name>.sandbox/index.html', '<name>.html'],
|
||||
]"
|
||||
/>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -79,24 +79,41 @@ 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:
|
||||
By default, ESLint thinks auto-imported variables are not defined. You have to add them to the `globals` config so it knows these variables exist at runtime.
|
||||
|
||||
```js
|
||||
// .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:
|
||||
WXT generates a config file containing the globals you can extend! By default, WXT will attempt to detect if ESLint is installed in your project and generate the config file if so. If the config file isn't being generated automatically, you can manually tell WXT to generate it:
|
||||
|
||||
```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'],
|
||||
};
|
||||
```
|
||||
|
||||
@@ -1,28 +1,59 @@
|
||||
# Frontend Frameworks
|
||||
|
||||
WXT supports all frontend frameworks with a Vite plugin:
|
||||
## Built-in Modules
|
||||
|
||||
- `@vitejs/plugin-vue`
|
||||
- `@vitejs/plugin-react`
|
||||
- `@vitejs/plugin-react-swc`
|
||||
- And more!
|
||||
WXT has preconfigured modules for 4 frameworks:
|
||||
|
||||
Just add the vite plugin to your config and you're good to go! Use the framework in HTML pages or content scripts, it will just work 👍
|
||||
- [`@wxt-dev/module-react`](https://github.com/wxt-dev/wxt/tree/main/packages/module-react)
|
||||
- [`@wxt-dev/module-vue`](https://github.com/wxt-dev/wxt/tree/main/packages/module-vue)
|
||||
- [`@wxt-dev/module-svelte`](https://github.com/wxt-dev/wxt/tree/main/packages/module-svelte)
|
||||
- [`@wxt-dev/module-solid`](https://github.com/wxt-dev/wxt/tree/main/packages/module-solid)
|
||||
|
||||
Install the module for your framework, then add it to your config:
|
||||
|
||||
:::code-group
|
||||
|
||||
```ts [Vue]
|
||||
```ts [React]
|
||||
import { defineConfig } from 'wxt';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
|
||||
export default defineConfig({
|
||||
vite: () => ({
|
||||
plugins: [vue()],
|
||||
}),
|
||||
modules: ['@wxt-dev/module-react'],
|
||||
});
|
||||
```
|
||||
|
||||
```ts [React]
|
||||
```ts [Vue]
|
||||
import { defineConfig } from 'wxt';
|
||||
|
||||
export default defineConfig({
|
||||
modules: ['@wxt-dev/module-vue'],
|
||||
});
|
||||
```
|
||||
|
||||
```ts [Svelte]
|
||||
import { defineConfig } from 'wxt';
|
||||
|
||||
export default defineConfig({
|
||||
modules: ['@wxt-dev/module-svelte'],
|
||||
});
|
||||
```
|
||||
|
||||
```ts [Solid]
|
||||
import { defineConfig } from 'wxt';
|
||||
|
||||
export default defineConfig({
|
||||
modules: ['@wxt-dev/module-solid'],
|
||||
});
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
## Adding Vite Plugins
|
||||
|
||||
If your framework doesn't have an official WXT module, no worries! WXT supports any framework with a Vite plugin.
|
||||
|
||||
Just add the Vite plugin to your config and you're good to go! Use the framework in HTML pages or content scripts, it will just work 👍
|
||||
|
||||
```ts
|
||||
import { defineConfig } from 'wxt';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
@@ -33,18 +64,7 @@ export default defineConfig({
|
||||
});
|
||||
```
|
||||
|
||||
```ts [Svelte]
|
||||
import { defineConfig } from 'wxt';
|
||||
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
export default defineConfig({
|
||||
vite: () => ({
|
||||
plugins: [svelte()],
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
:::
|
||||
The WXT modules just simplify the configuration and add auto-imports. They're not much different than the above.
|
||||
|
||||
## Multiple Apps
|
||||
|
||||
@@ -84,9 +104,9 @@ Lots of frameworks come with routers for building a multi-page app using the URL
|
||||
|
||||
Instead, you need to configure the router to run in "hash" mode, where the routing information is apart of the URL's hash, not the path (ie: `popup.html#/` and `popup.html#/account/settings`)
|
||||
|
||||
Refer to your router's docs for information about "hash" mode and how to enable it. Here's a non-extensive list of a few popular routers:
|
||||
Refer to your router's docs for information about hash mode and how to enable it. Here's a non-extensive list of a few popular routers:
|
||||
|
||||
- [React](https://reactrouter.com/en/main/routers/create-hash-router)
|
||||
- [Vue](https://router.vuejs.org/guide/essentials/history-mode.html#Hash-Mode)
|
||||
- [Svelte](https://www.npmjs.com/package/svelte-spa-router#hash-based-routing)
|
||||
- [Solid](https://github.com/solidjs/solid-router?tab=readme-ov-file#hash-mode-router)
|
||||
- [`react-router`](https://reactrouter.com/en/main/routers/create-hash-router)
|
||||
- [`vue-router`](https://router.vuejs.org/guide/essentials/history-mode.html#Hash-Mode)
|
||||
- [`svelte-spa-router`](https://www.npmjs.com/package/svelte-spa-router#hash-based-routing)
|
||||
- [`solid-router`](https://github.com/solidjs/solid-router?tab=readme-ov-file#hash-mode-router)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+18
-18
@@ -5,9 +5,9 @@
|
||||
"node": ">=18.20.3",
|
||||
"pnpm": "^8.6.3"
|
||||
},
|
||||
"packageManager": "pnpm@8.6.3",
|
||||
"packageManager": "pnpm@8.15.8",
|
||||
"scripts": {
|
||||
"check": "check && pnpm -r run check",
|
||||
"check": "check && pnpm -r --sequential run check",
|
||||
"test": "vitest",
|
||||
"test:coverage": "vitest run --coverage.enabled \"--coverage.include=packages/wxt/src/**\" \"--coverage.exclude=packages/wxt/src/core/utils/testing/**\" \"--coverage.exclude=**/*.d.ts\" \"--coverage.exclude=**/fixtures/**\"",
|
||||
"prepare": "simple-git-hooks",
|
||||
@@ -15,34 +15,34 @@
|
||||
"docs:gen": "typedoc --options docs/typedoc.json",
|
||||
"docs:dev": "pnpm -s docs:gen && vitepress dev docs",
|
||||
"docs:build": "pnpm -s docs:gen && vitepress build docs",
|
||||
"docs:preview": "pnpm -s docs:gen && vitepress preview docs",
|
||||
"build-deps": "pnpm tsx scripts/build-deps.ts"
|
||||
"docs:preview": "pnpm -s docs:gen && vitepress preview docs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "^1.1.1",
|
||||
"@aklinker1/buildc": "^1.0.10",
|
||||
"@aklinker1/check": "^1.3.1",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@vitest/coverage-v8": "^1.0.1",
|
||||
"@vitest/coverage-v8": "^1.6.0",
|
||||
"changelogen": "^0.5.5",
|
||||
"consola": "^3.2.3",
|
||||
"dependency-graph": "^1.0.0",
|
||||
"execa": "^9.1.0",
|
||||
"fast-glob": "^3.3.1",
|
||||
"fs-extra": "^11.1.1",
|
||||
"execa": "^9.3.0",
|
||||
"fast-glob": "^3.3.2",
|
||||
"fs-extra": "^11.2.0",
|
||||
"hasha": "^6.0.0",
|
||||
"lint-staged": "^15.2.0",
|
||||
"lint-staged": "^15.2.7",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^3.3.0",
|
||||
"simple-git-hooks": "^2.9.0",
|
||||
"tsx": "^4.11.2",
|
||||
"prettier": "^3.3.2",
|
||||
"simple-git-hooks": "^2.11.1",
|
||||
"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",
|
||||
"vitepress": "^1.2.2",
|
||||
"vitest": "^1.5.3",
|
||||
"typescript": "^5.5.2",
|
||||
"vitepress": "^1.2.3",
|
||||
"vitest": "^1.6.0",
|
||||
"vitest-mock-extended": "^1.3.1",
|
||||
"vitest-plugin-random-seed": "^1.0.2",
|
||||
"vue": "^3.3.10",
|
||||
"vitest-plugin-random-seed": "^1.1.0",
|
||||
"vue": "^3.4.31",
|
||||
"wxt": "workspace:*",
|
||||
"yaml": "^2.4.5"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## v1.0.0
|
||||
|
||||
Initial release 🎉
|
||||
@@ -0,0 +1,32 @@
|
||||
# `@wxt-dev/module-react`
|
||||
|
||||
Enables the use of [React](https://react.dev/) in your web extension, in HTML pages and content scripts.
|
||||
|
||||
This plugin makes a few changes:
|
||||
|
||||
1. Adds `@vitejs/plugin-react` to vite
|
||||
2. Adds the [`react` preset](https://github.com/unjs/unimport/blob/main/src/presets/react.ts) to auto-imports
|
||||
|
||||
## Usage
|
||||
|
||||
```sh
|
||||
pnpm i react react-dom
|
||||
pnpm i -D @wxt-dev/module-react
|
||||
```
|
||||
|
||||
Then add the module to your config:
|
||||
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
export default defineConfig({
|
||||
// Required
|
||||
modules: ['@wxt-dev/module-react'],
|
||||
|
||||
// Optional: Pass options to the module:
|
||||
react: {
|
||||
vite: {
|
||||
// ...
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
@@ -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>,
|
||||
);
|
||||
@@ -0,0 +1,38 @@
|
||||
import 'wxt';
|
||||
import { addImportPreset, addViteConfig, defineWxtModule } from 'wxt/modules';
|
||||
import react, { Options as PluginOptions } from '@vitejs/plugin-react';
|
||||
|
||||
export default defineWxtModule<ReactModuleOptions>({
|
||||
name: '@wxt-dev/module-react',
|
||||
configKey: 'react',
|
||||
setup(wxt, options) {
|
||||
const { vite } = options ?? {};
|
||||
|
||||
addViteConfig(wxt, () => ({
|
||||
plugins: [react(vite)],
|
||||
}));
|
||||
|
||||
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}',
|
||||
];
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export interface ReactModuleOptions {
|
||||
vite?: PluginOptions;
|
||||
}
|
||||
|
||||
declare module 'wxt' {
|
||||
export interface InlineConfig {
|
||||
react?: ReactModuleOptions;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "@wxt-dev/module-react",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": {
|
||||
"types": "./dist/index.d.mts",
|
||||
"default": "./dist/index.mjs"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/index.d.cts",
|
||||
"default": "./dist/index.cjs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "wxt",
|
||||
"check": "buildc --deps-only -- check",
|
||||
"build": "buildc -- unbuild",
|
||||
"postinstall": "buildc --deps-only -- wxt prepare"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"wxt": ">=0.18.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vitejs/plugin-react": "^4.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "^1.3.1",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"publint": "^0.2.8",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"typescript": "^5.5.2",
|
||||
"unbuild": "^2.0.0",
|
||||
"wxt": "workspace:*"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": ["../../tsconfig.base.json", "./.wxt/tsconfig.json"],
|
||||
"compilerOptions": {
|
||||
"allowImportingTsExtensions": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"exclude": ["node_modules/**", "dist/**"]
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
# Changelog
|
||||
|
||||
## v1.1.0
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/module-solid-v1.0.1...module-solid-v1.1.0)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Enable auto-imports for JSX/TSX files ([#773](https://github.com/wxt-dev/wxt/pull/773))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Upgrade wxt peer to >= 0.18.6 ([7edf1c8](https://github.com/wxt-dev/wxt/commit/7edf1c8))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Extract build cache script to NPM package ([#737](https://github.com/wxt-dev/wxt/pull/737))
|
||||
- **deps:** Upgrade non-major deps ([#778](https://github.com/wxt-dev/wxt/pull/778))
|
||||
|
||||
## v1.0.1
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/module-solid-v1.0.0...module-solid-v1.0.1)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Add `target: esnext` by default ([#733](https://github.com/wxt-dev/wxt/pull/733))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Add changelog ([21e8ca0](https://github.com/wxt-dev/wxt/commit/21e8ca0))
|
||||
|
||||
## v1.0.0
|
||||
|
||||
Initial release 🎉
|
||||
@@ -0,0 +1,32 @@
|
||||
# `@wxt-dev/module-solid`
|
||||
|
||||
Enables the use of [SolidJS](https://www.solidjs.com/) in your web extension, in HTML pages and content scripts.
|
||||
|
||||
This plugin makes a few changes:
|
||||
|
||||
1. Adds `vite-plugin-solid` to and sets `build.target: esnext` in the vite config
|
||||
2. Adds the [`solid-js` preset](https://github.com/unjs/unimport/blob/main/src/presets/solid.ts) to auto-imports
|
||||
|
||||
## Usage
|
||||
|
||||
```sh
|
||||
pnpm i solid-js
|
||||
pnpm i -D @wxt-dev/module-solid
|
||||
```
|
||||
|
||||
Then add the module to your config:
|
||||
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
export default defineConfig({
|
||||
// Required
|
||||
modules: ['@wxt-dev/module-solid'],
|
||||
|
||||
// Optional: Pass options to the module:
|
||||
solid: {
|
||||
vite: {
|
||||
// ...
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
@@ -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);
|
||||
@@ -0,0 +1,41 @@
|
||||
import 'wxt';
|
||||
import { addImportPreset, addViteConfig, defineWxtModule } from 'wxt/modules';
|
||||
import solid, { Options as PluginOptions } from 'vite-plugin-solid';
|
||||
|
||||
export default defineWxtModule<SolidModuleOptions>({
|
||||
name: '@wxt-dev/module-solid',
|
||||
configKey: 'solid',
|
||||
setup(wxt, options) {
|
||||
const { vite } = options ?? {};
|
||||
|
||||
addViteConfig(wxt, () => ({
|
||||
plugins: [solid(vite)],
|
||||
build: {
|
||||
target: 'esnext',
|
||||
},
|
||||
}));
|
||||
|
||||
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}',
|
||||
];
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export interface SolidModuleOptions {
|
||||
vite?: PluginOptions;
|
||||
}
|
||||
|
||||
declare module 'wxt' {
|
||||
export interface InlineConfig {
|
||||
solid?: SolidModuleOptions;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "@wxt-dev/module-solid",
|
||||
"version": "1.1.0",
|
||||
"type": "module",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": {
|
||||
"types": "./dist/index.d.mts",
|
||||
"default": "./dist/index.mjs"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/index.d.cts",
|
||||
"default": "./dist/index.cjs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "wxt",
|
||||
"check": "buildc --deps-only -- check",
|
||||
"build": "buildc -- unbuild",
|
||||
"postinstall": "buildc --deps-only -- wxt prepare"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"wxt": ">=0.18.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"vite-plugin-solid": "^2.10.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "^1.3.1",
|
||||
"publint": "^0.2.8",
|
||||
"solid-js": "^1.8.17",
|
||||
"typescript": "^5.5.2",
|
||||
"unbuild": "^2.0.0",
|
||||
"wxt": "workspace:*"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": ["../../tsconfig.base.json", "./.wxt/tsconfig.json"],
|
||||
"compilerOptions": {
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "solid-js"
|
||||
},
|
||||
"exclude": ["node_modules/**", "dist/**"]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## v1.0.0
|
||||
|
||||
Initial release 🎉
|
||||
@@ -0,0 +1,32 @@
|
||||
# `@wxt-dev/module-svelte`
|
||||
|
||||
Enables the use of [Svelte](https://svelte.dev/) in your web extension, in HTML pages and content scripts.
|
||||
|
||||
This plugin makes a few changes:
|
||||
|
||||
1. Adds `@sveltejs/vite-plugin-svelte` to vite
|
||||
2. Adds the [`svelte` preset](https://github.com/unjs/unimport/blob/main/src/presets/vue.ts) to auto-imports
|
||||
|
||||
## Usage
|
||||
|
||||
```sh
|
||||
pnpm i svelte
|
||||
pnpm i -D @wxt-dev/module-svelte
|
||||
```
|
||||
|
||||
Then add the module to your config:
|
||||
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
export default defineConfig({
|
||||
// Required
|
||||
modules: ['@wxt-dev/module-svelte'],
|
||||
|
||||
// Optional: Pass options to the module:
|
||||
svelte: {
|
||||
vite: {
|
||||
// ...
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "@wxt-dev/module-svelte",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": {
|
||||
"types": "./dist/index.d.mts",
|
||||
"default": "./dist/index.mjs"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/index.d.cts",
|
||||
"default": "./dist/index.cjs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "buildc -- unbuild",
|
||||
"check": "buildc --deps-only -- check"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"wxt": ">=0.18.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^3.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "^1.3.1",
|
||||
"publint": "^0.2.8",
|
||||
"typescript": "^5.5.2",
|
||||
"unbuild": "^2.0.0",
|
||||
"wxt": "workspace:*"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import 'wxt';
|
||||
import { addImportPreset, addViteConfig, defineWxtModule } from 'wxt/modules';
|
||||
import {
|
||||
svelte,
|
||||
vitePreprocess,
|
||||
Options as PluginOptions,
|
||||
} from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
export default defineWxtModule<SvelteModuleOptions>({
|
||||
name: '@wxt-dev/module-svelte',
|
||||
configKey: 'svelte',
|
||||
setup(wxt, options) {
|
||||
const { vite } = options ?? {};
|
||||
|
||||
addViteConfig(wxt, () => ({
|
||||
plugins: [
|
||||
svelte({
|
||||
// Using a svelte.config.js file causes a segmentation fault when importing the file
|
||||
configFile: false,
|
||||
preprocess: [vitePreprocess()],
|
||||
...vite,
|
||||
}),
|
||||
],
|
||||
}));
|
||||
|
||||
addImportPreset(wxt, 'svelte');
|
||||
},
|
||||
});
|
||||
|
||||
export interface SvelteModuleOptions {
|
||||
vite?: Partial<PluginOptions>;
|
||||
}
|
||||
|
||||
declare module 'wxt' {
|
||||
export interface InlineConfig {
|
||||
svelte?: SvelteModuleOptions;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"exclude": ["node_modules/**", "dist/**"]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## v1.0.0
|
||||
|
||||
Initial release 🎉
|
||||
@@ -0,0 +1,36 @@
|
||||
# `@wxt-dev/module-vue`
|
||||
|
||||
Enables the use of [Vue](https://vuejs.org/) in your web extension, in HTML pages and content scripts.
|
||||
|
||||
This plugin makes a few changes:
|
||||
|
||||
1. Adds `@vitejs/plugin-vue` to vite config
|
||||
2. Adds the [`vue` preset](https://github.com/unjs/unimport/blob/main/src/presets/vue.ts) to auto-imports
|
||||
3. Applies sourcemap fix to prevent HMR errors during development
|
||||
4. Enable auto-imports in `.vue` files
|
||||
|
||||
## Usage
|
||||
|
||||
```sh
|
||||
pnpm i vue
|
||||
pnpm i -D @wxt-dev/module-vue
|
||||
```
|
||||
|
||||
Then add the module to your config:
|
||||
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
export default defineConfig({
|
||||
// Required
|
||||
modules: ['@wxt-dev/module-vue'],
|
||||
|
||||
// Optional: Pass options to the module:
|
||||
vue: {
|
||||
vite: {
|
||||
script: {
|
||||
propsDestructure: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "@wxt-dev/module-vue",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": {
|
||||
"types": "./dist/index.d.mts",
|
||||
"default": "./dist/index.mjs"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/index.d.cts",
|
||||
"default": "./dist/index.cjs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "buildc -- unbuild",
|
||||
"check": "buildc --deps-only -- check"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"wxt": ">=0.18.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vitejs/plugin-vue": "^5.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "^1.3.1",
|
||||
"publint": "^0.2.8",
|
||||
"typescript": "^5.5.2",
|
||||
"unbuild": "^2.0.0",
|
||||
"wxt": "workspace:*"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import 'wxt';
|
||||
import { addImportPreset, addViteConfig, defineWxtModule } from 'wxt/modules';
|
||||
import vue, { Options as ViteOptions } from '@vitejs/plugin-vue';
|
||||
|
||||
export default defineWxtModule<VueModuleOptions>({
|
||||
name: '@wxt-dev/module-vue',
|
||||
configKey: 'vue',
|
||||
setup(wxt, options) {
|
||||
const { vite } = options ?? {};
|
||||
|
||||
// Add plugin & set sourcemap option
|
||||
addViteConfig(wxt, ({ command }) => ({
|
||||
// @ts-ignore: Ignore vite version issues
|
||||
plugins: [vue(vite)],
|
||||
build: {
|
||||
// Fixes known issue: https://github.com/wxt-dev/wxt/pull/607
|
||||
sourcemap: false,
|
||||
},
|
||||
}));
|
||||
|
||||
// Enable auto-imports in template files
|
||||
wxt.hooks.hook('ready', (wxt) => {
|
||||
if (!wxt.config.imports) return;
|
||||
|
||||
wxt.config.imports.addons ??= {};
|
||||
if (!Array.isArray(wxt.config.imports.addons)) {
|
||||
wxt.config.imports.addons.vueTemplate = true;
|
||||
} else {
|
||||
wxt.logger.warn(
|
||||
'Could not enable auto-imports in vue templates when using and array for imports.addons',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
addImportPreset(wxt, 'vue');
|
||||
},
|
||||
});
|
||||
|
||||
export interface VueModuleOptions {
|
||||
vite?: ViteOptions;
|
||||
}
|
||||
|
||||
declare module 'wxt' {
|
||||
export interface InlineConfig {
|
||||
vue?: VueModuleOptions;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"exclude": ["node_modules/**", "dist/**"]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import autoImports from './.wxt/eslintrc-auto-import.js';
|
||||
|
||||
export default [
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...autoImports.globals,
|
||||
},
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -4,28 +4,30 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "pnpm -s build-deps && wxt",
|
||||
"build": "pnpm -s build-deps && wxt build",
|
||||
"build:all": "pnpm -s build-deps && run-s -s 'build:all:*'",
|
||||
"dev": "buildc --deps-only -- wxt",
|
||||
"build": "buildc -- wxt build",
|
||||
"build:all": "buildc --deps-only -- run-s -s 'build:all:*'",
|
||||
"build:all:chrome-mv3": "wxt build",
|
||||
"build:all:chrome-mv2": "wxt build --mv2",
|
||||
"build:all:firefox-mv3": "wxt build -b firefox --mv3",
|
||||
"build:all:firefox-mv2": "wxt build -b firefox",
|
||||
"test": "pnpm -s build-deps && vitest",
|
||||
"zip": "pnpm -s build-deps && wxt zip",
|
||||
"check": "pnpm -s build-deps && check",
|
||||
"postinstall": "pnpm -s build-deps && wxt prepare",
|
||||
"build-deps": "pnpm -sw build-deps wxt-demo"
|
||||
"test": "buildc --deps-only -- vitest",
|
||||
"zip": "buildc --deps-only -- wxt zip",
|
||||
"check": "buildc --deps-only -- check",
|
||||
"postinstall": "buildc --deps-only -- wxt prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.34",
|
||||
"@types/react-dom": "^18.2.14",
|
||||
"sass": "^1.69.5",
|
||||
"typescript": "^5.4.5",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"sass": "^1.77.6",
|
||||
"typescript": "^5.5.2",
|
||||
"wxt": "workspace:*"
|
||||
},
|
||||
"buildc": {
|
||||
"outDir": ".output/chrome-mv3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ export default defineContentScript({
|
||||
anchor: 'form[role=search]',
|
||||
onMount: (container) => {
|
||||
const app = document.createElement('div');
|
||||
app.textContent = 'Custom content script UI';
|
||||
app.textContent = browser.i18n.getMessage('prompt_for_name');
|
||||
container.append(app);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -6,6 +6,5 @@ export default defineProject({
|
||||
mockReset: true,
|
||||
restoreMocks: true,
|
||||
},
|
||||
// @ts-expect-error
|
||||
plugins: [WxtVitest()],
|
||||
});
|
||||
|
||||
@@ -19,7 +19,10 @@ export default defineConfig({
|
||||
open: true,
|
||||
},
|
||||
experimental: {
|
||||
viteRuntime: true,
|
||||
entrypointImporter: 'vite-node',
|
||||
},
|
||||
runner: {
|
||||
startUrls: ['https://duckduckgo.com'],
|
||||
},
|
||||
example: {
|
||||
a: 'a',
|
||||
|
||||
@@ -1,5 +1,65 @@
|
||||
# Changelog
|
||||
|
||||
## v0.18.10
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.18.9...wxt-v0.18.10)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Add `prepare:types` hook to extend `.wxt/` directory generation ([#767](https://github.com/wxt-dev/wxt/pull/767))
|
||||
- **modules:** Allow adding generated public files ([#769](https://github.com/wxt-dev/wxt/pull/769))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Await `prepare:types` hook ([b29d49c](https://github.com/wxt-dev/wxt/commit/b29d49c))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Refactor package manager test fixtures ([39f6c29](https://github.com/wxt-dev/wxt/commit/39f6c29))
|
||||
- Consolidate `unimport` code into a built-in module ([#771](https://github.com/wxt-dev/wxt/pull/771))
|
||||
|
||||
## v0.18.9
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.18.8...wxt-v0.18.9)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- **experimental:** Replace `viteRuntime` option with `entrypointImporter` option, and implement `vite-node` importer ([#761](https://github.com/wxt-dev/wxt/pull/761))
|
||||
|
||||
## v0.18.8
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.18.7...wxt-v0.18.8)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- **dev:** Reload extension when public files change ([#752](https://github.com/wxt-dev/wxt/pull/752))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Don't load plugins twice in HTML pages ([#746](https://github.com/wxt-dev/wxt/pull/746))
|
||||
- Ignore .wxt file changes in dev ([#755](https://github.com/wxt-dev/wxt/pull/755))
|
||||
- **modules:** `addViteConfig` ignored user vite config ([#760](https://github.com/wxt-dev/wxt/pull/760))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Refactor client web socket util ([#753](https://github.com/wxt-dev/wxt/pull/753))
|
||||
- Add E2E test for `addImportPreset` ([9fc6408](https://github.com/wxt-dev/wxt/commit/9fc6408))
|
||||
|
||||
## v0.18.7
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.18.6...wxt-v0.18.7)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- **dev:** Fix CSP error loading HTML plugins ([#723](https://github.com/wxt-dev/wxt/pull/723))
|
||||
- Generalize react-refresh preamble logic to virtualize all inline scripts ([#728](https://github.com/wxt-dev/wxt/pull/728))
|
||||
- **zip:** Revert dotfile changes from #674 ([#742](https://github.com/wxt-dev/wxt/pull/742), [#674](https://github.com/wxt-dev/wxt/issues/674))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- **deps:** Upgrade and sync all dependencies ([#725](https://github.com/wxt-dev/wxt/pull/725))
|
||||
- Extract build cache script to NPM package ([#737](https://github.com/wxt-dev/wxt/pull/737))
|
||||
|
||||
## v0.18.6
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.18.5...wxt-v0.18.6)
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`Auto Imports > eslintrc > "enabled: 8" should output a JSON config file compatible with ESlint 8 1`] = `
|
||||
".wxt/eslintrc-auto-import.json
|
||||
----------------------------------------
|
||||
{
|
||||
"globals": {
|
||||
"ContentScriptContext": true,
|
||||
"InvalidMatchPattern": true,
|
||||
"MatchPattern": true,
|
||||
"browser": true,
|
||||
"createIframeUi": true,
|
||||
"createIntegratedUi": true,
|
||||
"createShadowRootUi": true,
|
||||
"defineBackground": true,
|
||||
"defineConfig": true,
|
||||
"defineContentScript": true,
|
||||
"defineUnlistedScript": true,
|
||||
"defineWxtPlugin": true,
|
||||
"fakeBrowser": true,
|
||||
"storage": true
|
||||
}
|
||||
}
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`Auto Imports > eslintrc > "enabled: 9" should output a flat config file compatible with ESlint 9 1`] = `
|
||||
".wxt/eslint-auto-imports.mjs
|
||||
----------------------------------------
|
||||
const globals = {
|
||||
"ContentScriptContext": true,
|
||||
"InvalidMatchPattern": true,
|
||||
"MatchPattern": true,
|
||||
"browser": true,
|
||||
"createIframeUi": true,
|
||||
"createIntegratedUi": true,
|
||||
"createShadowRootUi": true,
|
||||
"defineBackground": true,
|
||||
"defineConfig": true,
|
||||
"defineContentScript": true,
|
||||
"defineUnlistedScript": true,
|
||||
"defineWxtPlugin": true,
|
||||
"fakeBrowser": true,
|
||||
"storage": true
|
||||
}
|
||||
|
||||
export default {
|
||||
languageOptions: {
|
||||
globals,
|
||||
sourceType: "module",
|
||||
},
|
||||
};
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`Auto Imports > eslintrc > "enabled: true" should output a JSON config file compatible with ESlint 8 1`] = `
|
||||
".wxt/eslintrc-auto-import.json
|
||||
----------------------------------------
|
||||
{
|
||||
"globals": {
|
||||
"ContentScriptContext": true,
|
||||
"InvalidMatchPattern": true,
|
||||
"MatchPattern": true,
|
||||
"browser": true,
|
||||
"createIframeUi": true,
|
||||
"createIntegratedUi": true,
|
||||
"createShadowRootUi": true,
|
||||
"defineBackground": true,
|
||||
"defineConfig": true,
|
||||
"defineContentScript": true,
|
||||
"defineUnlistedScript": true,
|
||||
"defineWxtPlugin": true,
|
||||
"fakeBrowser": true,
|
||||
"storage": true
|
||||
}
|
||||
}
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`Auto Imports > eslintrc > should allow customizing the output 1`] = `
|
||||
"example.json
|
||||
----------------------------------------
|
||||
{
|
||||
"globals": {
|
||||
"ContentScriptContext": "readonly",
|
||||
"InvalidMatchPattern": "readonly",
|
||||
"MatchPattern": "readonly",
|
||||
"browser": "readonly",
|
||||
"createIframeUi": "readonly",
|
||||
"createIntegratedUi": "readonly",
|
||||
"createShadowRootUi": "readonly",
|
||||
"defineBackground": "readonly",
|
||||
"defineConfig": "readonly",
|
||||
"defineContentScript": "readonly",
|
||||
"defineUnlistedScript": "readonly",
|
||||
"defineWxtPlugin": "readonly",
|
||||
"fakeBrowser": "readonly",
|
||||
"storage": "readonly"
|
||||
}
|
||||
}
|
||||
"
|
||||
`;
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { TestProject } from '../utils';
|
||||
import { execaCommand } from 'execa';
|
||||
|
||||
describe('Auto Imports', () => {
|
||||
describe('imports: { ... }', () => {
|
||||
@@ -47,10 +48,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 +97,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 +109,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 +162,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,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { TestProject } from '../utils';
|
||||
|
||||
describe.each([true, false])(
|
||||
describe.each(['jiti', 'vite-runtime', 'vite-node'] as const)(
|
||||
'Manifest Content (Vite runtime? %s)',
|
||||
(viteRuntime) => {
|
||||
(entrypointImporter) => {
|
||||
it.each([
|
||||
{ browser: undefined, outDir: 'chrome-mv3', expected: undefined },
|
||||
{ browser: 'chrome', outDir: 'chrome-mv3', expected: undefined },
|
||||
@@ -24,7 +24,7 @@ describe.each([true, false])(
|
||||
main: () => {},
|
||||
})`,
|
||||
);
|
||||
await project.build({ browser, experimental: { viteRuntime } });
|
||||
await project.build({ browser, experimental: { entrypointImporter } });
|
||||
|
||||
const safariManifest = await project.getOutputManifest(
|
||||
`.output/${outDir}/manifest.json`,
|
||||
|
||||
@@ -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 () => {
|
||||
@@ -287,5 +296,29 @@ describe('Module Helpers', () => {
|
||||
|
||||
await expect(project.serializeOutput()).resolves.toContain(expectedText);
|
||||
});
|
||||
|
||||
it('should add preset', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile(
|
||||
'entrypoints/background.ts',
|
||||
`export default defineBackground(() => {
|
||||
customImport();
|
||||
});`,
|
||||
);
|
||||
project.addFile(
|
||||
'modules/test.ts',
|
||||
`import { defineWxtModule, addImportPreset } from 'wxt/modules';
|
||||
|
||||
export default defineWxtModule((wxt) => {
|
||||
addImportPreset(wxt, "vue");
|
||||
})`,
|
||||
);
|
||||
|
||||
await project.build();
|
||||
|
||||
await expect(
|
||||
project.serializeFile('.wxt/types/imports.d.ts'),
|
||||
).resolves.toContain("const ref: typeof import('vue')['ref']");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -280,8 +280,7 @@ describe('Output Directory Structure', () => {
|
||||
----------------------------------------
|
||||
import { l as logHello, i as initPlugins } from "./chunks/_virtual_wxt-plugins-OjKtWpmY.js";
|
||||
function defineBackground(arg) {
|
||||
if (typeof arg === "function")
|
||||
return { main: arg };
|
||||
if (typeof arg === "function") return { main: arg };
|
||||
return arg;
|
||||
}
|
||||
const definition = defineBackground({
|
||||
@@ -363,8 +362,7 @@ describe('Output Directory Structure', () => {
|
||||
var _background = function() {
|
||||
"use strict";
|
||||
function defineBackground(arg) {
|
||||
if (typeof arg === "function")
|
||||
return { main: arg };
|
||||
if (typeof arg === "function") return { main: arg };
|
||||
return arg;
|
||||
}
|
||||
function logHello(name) {
|
||||
|
||||
@@ -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" />
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -97,7 +97,8 @@ describe('Zipping', () => {
|
||||
expect(await project.fileExists(sourcesZip)).toBe(true);
|
||||
});
|
||||
|
||||
it('should not zip hidden files into sources by default', async () => {
|
||||
// TODO: Fix #738 and re-enable
|
||||
it.skip('should not zip hidden files into sources by default', async () => {
|
||||
const project = new TestProject({
|
||||
name: 'test',
|
||||
version: '1.0.0',
|
||||
@@ -117,7 +118,8 @@ describe('Zipping', () => {
|
||||
expect(await project.fileExists(unzipDir, '.env')).toBe(false);
|
||||
});
|
||||
|
||||
it('should allow zipping hidden files into sources when explicitly listed', async () => {
|
||||
// TODO: Fix #738 and re-enable
|
||||
it.skip('should allow zipping hidden files into sources when explicitly listed', async () => {
|
||||
const project = new TestProject({
|
||||
name: 'test',
|
||||
version: '1.0.0',
|
||||
|
||||
+36
-34
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "wxt",
|
||||
"type": "module",
|
||||
"version": "0.18.6",
|
||||
"version": "0.18.10",
|
||||
"description": "Next gen framework for developing web extensions",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -92,71 +92,73 @@
|
||||
},
|
||||
"scripts": {
|
||||
"wxt": "tsx src/cli/index.ts",
|
||||
"build": "tsx scripts/build.ts",
|
||||
"check": "run-s -c check:*",
|
||||
"build": "buildc -- tsx scripts/build.ts",
|
||||
"check": "buildc --deps-only -- run-s -c check:*",
|
||||
"check:default": "check",
|
||||
"check:tsc-virtual": "tsc --noEmit -p src/virtual",
|
||||
"test": "vitest",
|
||||
"test:e2e": "vitest -r e2e",
|
||||
"test": "buildc --deps-only -- vitest",
|
||||
"test:e2e": "buildc --deps-only -- vitest -r e2e",
|
||||
"sync-releases": "pnpx changelogen@latest gh release"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aklinker1/rollup-plugin-visualizer": "5.12.0",
|
||||
"@types/webextension-polyfill": "^0.10.5",
|
||||
"@types/webextension-polyfill": "^0.10.7",
|
||||
"@webext-core/fake-browser": "^1.3.1",
|
||||
"@webext-core/isolated-element": "^1.1.2",
|
||||
"@webext-core/match-patterns": "^1.0.3",
|
||||
"async-mutex": "^0.4.0",
|
||||
"c12": "^1.5.1",
|
||||
"async-mutex": "^0.4.1",
|
||||
"c12": "^1.11.1",
|
||||
"cac": "^6.7.14",
|
||||
"chokidar": "^3.5.3",
|
||||
"chokidar": "^3.6.0",
|
||||
"ci-info": "^4.0.0",
|
||||
"defu": "^6.1.3",
|
||||
"defu": "^6.1.4",
|
||||
"dequal": "^2.0.3",
|
||||
"esbuild": "^0.19.5",
|
||||
"fast-glob": "^3.3.1",
|
||||
"filesize": "^10.0.8",
|
||||
"fs-extra": "^11.1.1",
|
||||
"get-port": "^7.0.0",
|
||||
"giget": "^1.1.3",
|
||||
"esbuild": "^0.19.12",
|
||||
"fast-glob": "^3.3.2",
|
||||
"filesize": "^10.1.2",
|
||||
"fs-extra": "^11.2.0",
|
||||
"get-port": "^7.1.0",
|
||||
"giget": "^1.2.3",
|
||||
"hookable": "^5.5.3",
|
||||
"is-wsl": "^3.0.0",
|
||||
"jiti": "^1.21.0",
|
||||
"is-wsl": "^3.1.0",
|
||||
"jiti": "^1.21.6",
|
||||
"json5": "^2.2.3",
|
||||
"jszip": "^3.10.1",
|
||||
"linkedom": "^0.18.2",
|
||||
"linkedom": "^0.18.4",
|
||||
"magicast": "^0.3.4",
|
||||
"minimatch": "^9.0.3",
|
||||
"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",
|
||||
"picocolors": "^1.0.0",
|
||||
"picocolors": "^1.0.1",
|
||||
"prompts": "^2.4.2",
|
||||
"publish-browser-extension": "^2.1.3",
|
||||
"unimport": "^3.4.0",
|
||||
"vite": "^5.2.8",
|
||||
"unimport": "^3.7.2",
|
||||
"vite": "^5.3.2",
|
||||
"vite-node": "^1.6.0",
|
||||
"web-ext-run": "^0.2.0",
|
||||
"webextension-polyfill": "^0.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/check": "^1.1.1",
|
||||
"@faker-js/faker": "^8.3.1",
|
||||
"@aklinker1/check": "^1.3.1",
|
||||
"@faker-js/faker": "^8.4.1",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/lodash.merge": "^4.6.9",
|
||||
"@types/natural-compare": "^1.4.3",
|
||||
"@types/node": "^20.10.3",
|
||||
"@types/node": "^20.14.9",
|
||||
"@types/normalize-path": "^3.0.2",
|
||||
"@types/prompts": "^2.4.9",
|
||||
"execa": "^9.1.0",
|
||||
"execa": "^9.3.0",
|
||||
"extract-zip": "^2.0.1",
|
||||
"happy-dom": "^13.3.8",
|
||||
"happy-dom": "^13.10.1",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"p-map": "^7.0.0",
|
||||
"publint": "^0.2.6",
|
||||
"tsup": "^8.0.1",
|
||||
"tsx": "^4.11.2",
|
||||
"typescript": "^5.4.5"
|
||||
"p-map": "^7.0.2",
|
||||
"publint": "^0.2.8",
|
||||
"tsup": "^8.1.0",
|
||||
"tsx": "4.15.7",
|
||||
"typescript": "^5.5.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,10 +13,10 @@ describe('Module Utilities', () => {
|
||||
const userConfig = {};
|
||||
const moduleConfig = { build: { sourcemap: true } };
|
||||
|
||||
wxt.config.vite = () => userConfig;
|
||||
wxt.config.vite = () => Promise.resolve(userConfig);
|
||||
addViteConfig(wxt, () => moduleConfig);
|
||||
await wxt.hooks.callHook('ready', wxt);
|
||||
const actual: any = wxt.config.vite(wxt.config.env);
|
||||
const actual = await wxt.config.vite(wxt.config.env);
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
@@ -32,7 +32,7 @@ describe('Module Utilities', () => {
|
||||
wxt.config.vite = () => userConfig;
|
||||
addViteConfig(wxt, () => moduleConfig);
|
||||
await wxt.hooks.callHook('ready', wxt);
|
||||
const actual: any = wxt.config.vite(wxt.config.env);
|
||||
const actual = await wxt.config.vite(wxt.config.env);
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import { WxtModule } from '~/types';
|
||||
import unimport from './unimport';
|
||||
|
||||
export const builtinModules: WxtModule<any>[] = [unimport];
|
||||
@@ -0,0 +1,149 @@
|
||||
import { addViteConfig, defineWxtModule } from '~/modules';
|
||||
import type {
|
||||
EslintGlobalsPropValue,
|
||||
WxtDirFileEntry,
|
||||
WxtModule,
|
||||
WxtResolvedUnimportOptions,
|
||||
} from '~/types';
|
||||
import { type Unimport, createUnimport } from 'unimport';
|
||||
import { Plugin } from 'vite';
|
||||
import { extname } from 'node:path';
|
||||
|
||||
export default defineWxtModule({
|
||||
name: 'wxt:built-in:unimport',
|
||||
setup(wxt) {
|
||||
const options = wxt.config.imports;
|
||||
if (options === false) return;
|
||||
|
||||
let unimport: Unimport;
|
||||
|
||||
// Add user module imports to config
|
||||
wxt.hooks.hook('ready', () => {
|
||||
const addModuleImports = (module: WxtModule<any>) => {
|
||||
if (!module.imports) return;
|
||||
|
||||
options.imports ??= [];
|
||||
options.imports.push(...module.imports);
|
||||
};
|
||||
|
||||
wxt.config.builtinModules.forEach(addModuleImports);
|
||||
wxt.config.userModules.forEach(addModuleImports);
|
||||
});
|
||||
|
||||
// Create unimport instance AFTER "ready" so any modifications to the
|
||||
// config inside "ready" are applied.
|
||||
wxt.hooks.afterEach((event) => {
|
||||
if (event.name === 'ready') {
|
||||
unimport = createUnimport(options);
|
||||
}
|
||||
});
|
||||
|
||||
// Generate types
|
||||
wxt.hooks.hook('prepare:types', async (_, entries) => {
|
||||
// Update cache before each rebuild
|
||||
await unimport.init();
|
||||
|
||||
entries.push(await getImportsDeclarationEntry(unimport));
|
||||
|
||||
if (options.eslintrc.enabled === false) return;
|
||||
entries.push(
|
||||
await getEslintConfigEntry(unimport, options.eslintrc.enabled, options),
|
||||
);
|
||||
});
|
||||
|
||||
// Add vite plugin
|
||||
addViteConfig(wxt, () => ({
|
||||
plugins: [vitePlugin(unimport)],
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
export function vitePlugin(unimport: Unimport): Plugin {
|
||||
const ENABLED_EXTENSIONS = new Set([
|
||||
'.js',
|
||||
'.jsx',
|
||||
'.ts',
|
||||
'.tsx',
|
||||
'.vue',
|
||||
'.svelte',
|
||||
]);
|
||||
return {
|
||||
name: 'wxt:unimport',
|
||||
async transform(code, id) {
|
||||
// Don't transform dependencies
|
||||
if (id.includes('node_modules')) return;
|
||||
|
||||
// Don't transform non-js files
|
||||
if (!ENABLED_EXTENSIONS.has(extname(id))) return;
|
||||
|
||||
const injected = await unimport.injectImports(code, id);
|
||||
return {
|
||||
code: injected.code,
|
||||
map: injected.s.generateMap({ hires: 'boundary', source: id }),
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function getImportsDeclarationEntry(
|
||||
unimport: Unimport,
|
||||
): Promise<WxtDirFileEntry> {
|
||||
// Load project imports into unimport memory so they are output via generateTypeDeclarations
|
||||
await unimport.init();
|
||||
|
||||
return {
|
||||
path: 'types/imports.d.ts',
|
||||
text: [
|
||||
'// Generated by wxt',
|
||||
await unimport.generateTypeDeclarations(),
|
||||
'',
|
||||
].join('\n'),
|
||||
tsReference: true,
|
||||
};
|
||||
}
|
||||
|
||||
async function getEslintConfigEntry(
|
||||
unimport: Unimport,
|
||||
version: 8 | 9,
|
||||
options: WxtResolvedUnimportOptions,
|
||||
): Promise<WxtDirFileEntry> {
|
||||
const globals = (await unimport.getImports())
|
||||
.map((i) => i.as ?? i.name)
|
||||
.filter(Boolean)
|
||||
.sort()
|
||||
.reduce<Record<string, EslintGlobalsPropValue>>((globals, name) => {
|
||||
globals[name] = options.eslintrc.globalsPropValue;
|
||||
return globals;
|
||||
}, {});
|
||||
|
||||
if (version <= 8) return getEslint8ConfigEntry(options, globals);
|
||||
else return getEslint9ConfigEntry(options, globals);
|
||||
}
|
||||
|
||||
export function getEslint8ConfigEntry(
|
||||
options: WxtResolvedUnimportOptions,
|
||||
globals: Record<string, EslintGlobalsPropValue>,
|
||||
): WxtDirFileEntry {
|
||||
return {
|
||||
path: options.eslintrc.filePath,
|
||||
text: JSON.stringify({ globals }, null, 2) + '\n',
|
||||
};
|
||||
}
|
||||
|
||||
export function getEslint9ConfigEntry(
|
||||
options: WxtResolvedUnimportOptions,
|
||||
globals: Record<string, EslintGlobalsPropValue>,
|
||||
): WxtDirFileEntry {
|
||||
return {
|
||||
path: options.eslintrc.filePath,
|
||||
text: `const globals = ${JSON.stringify(globals, null, 2)}
|
||||
|
||||
export default {
|
||||
languageOptions: {
|
||||
globals,
|
||||
sourceType: "module",
|
||||
},
|
||||
};
|
||||
`,
|
||||
};
|
||||
}
|
||||
@@ -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 });
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { createViteBuilder } from '../index';
|
||||
import { fakeResolvedConfig } from '~/core/utils/testing/fake-objects';
|
||||
import { createHooks } from 'hookable';
|
||||
|
||||
describe('Vite Builder', () => {
|
||||
describe('importEntrypoint', () => {
|
||||
it('should import entrypoints, removing runtime values (like the main function)', async () => {
|
||||
const {
|
||||
default: { main: _, ...expected },
|
||||
} = await import('./fixtures/module');
|
||||
const builder = await createViteBuilder(
|
||||
fakeResolvedConfig({ root: __dirname }),
|
||||
createHooks(),
|
||||
);
|
||||
const actual = await builder.importEntrypoint<{ default: any }>(
|
||||
'./fixtures/module.ts',
|
||||
);
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -20,6 +20,10 @@ import {
|
||||
import { Hookable } from 'hookable';
|
||||
import { toArray } from '~/core/utils/arrays';
|
||||
import { safeVarName } from '~/core/utils/strings';
|
||||
import { importEntrypointFile } from '~/core/utils/building';
|
||||
import { ViteNodeServer } from 'vite-node/server';
|
||||
import { ViteNodeRunner } from 'vite-node/client';
|
||||
import { installSourcemapsSupport } from 'vite-node/source-map';
|
||||
|
||||
export async function createViteBuilder(
|
||||
wxtConfig: ResolvedConfig,
|
||||
@@ -57,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),
|
||||
@@ -204,21 +207,68 @@ export async function createViteBuilder(
|
||||
return {
|
||||
name: 'Vite',
|
||||
version: vite.version,
|
||||
async importEntrypoint(url) {
|
||||
const baseConfig = await getBaseConfig();
|
||||
const envConfig: vite.InlineConfig = {
|
||||
plugins: [
|
||||
wxtPlugins.webextensionPolyfillMock(wxtConfig),
|
||||
wxtPlugins.removeEntrypointMainFunction(wxtConfig, url),
|
||||
],
|
||||
};
|
||||
const config = vite.mergeConfig(baseConfig, envConfig);
|
||||
const server = await vite.createServer(config);
|
||||
await server.listen();
|
||||
const runtime = await vite.createViteRuntime(server, { hmr: false });
|
||||
const module = await runtime.executeUrl(url);
|
||||
await server.close();
|
||||
return module.default;
|
||||
async importEntrypoint(path) {
|
||||
switch (wxtConfig.experimental.entrypointImporter) {
|
||||
default:
|
||||
case 'jiti': {
|
||||
return await importEntrypointFile(path);
|
||||
}
|
||||
case 'vite-runtime': {
|
||||
const baseConfig = await getBaseConfig();
|
||||
const envConfig: vite.InlineConfig = {
|
||||
plugins: [
|
||||
wxtPlugins.webextensionPolyfillMock(wxtConfig),
|
||||
wxtPlugins.removeEntrypointMainFunction(wxtConfig, path),
|
||||
],
|
||||
};
|
||||
const config = vite.mergeConfig(baseConfig, envConfig);
|
||||
const server = await vite.createServer(config);
|
||||
await server.listen();
|
||||
const runtime = await vite.createViteRuntime(server, { hmr: false });
|
||||
const module = await runtime.executeUrl(path);
|
||||
await server.close();
|
||||
return module.default;
|
||||
}
|
||||
case 'vite-node': {
|
||||
const baseConfig = await getBaseConfig();
|
||||
// Disable dep optimization, as recommended by vite-node's README
|
||||
baseConfig.optimizeDeps ??= {};
|
||||
baseConfig.optimizeDeps.noDiscovery = true;
|
||||
baseConfig.optimizeDeps.include = [];
|
||||
const envConfig: vite.InlineConfig = {
|
||||
plugins: [
|
||||
wxtPlugins.webextensionPolyfillMock(wxtConfig),
|
||||
wxtPlugins.removeEntrypointMainFunction(wxtConfig, path),
|
||||
],
|
||||
};
|
||||
const config = vite.mergeConfig(baseConfig, envConfig);
|
||||
const server = await vite.createServer(config);
|
||||
await server.pluginContainer.buildStart({});
|
||||
const node = new ViteNodeServer(
|
||||
// @ts-ignore: Some weird type error...
|
||||
server,
|
||||
);
|
||||
installSourcemapsSupport({
|
||||
getSourceMap: (source) => node.getSourceMap(source),
|
||||
});
|
||||
const runner = new ViteNodeRunner({
|
||||
root: server.config.root,
|
||||
base: server.config.base,
|
||||
// when having the server and runner in a different context,
|
||||
// you will need to handle the communication between them
|
||||
// and pass to this function
|
||||
fetchModule(id) {
|
||||
return node.fetchModule(id);
|
||||
},
|
||||
resolveId(id, importer) {
|
||||
return node.resolveId(id, importer);
|
||||
},
|
||||
});
|
||||
const res = await runner.executeFile(path);
|
||||
await server.close();
|
||||
return res.default;
|
||||
}
|
||||
}
|
||||
},
|
||||
async build(group) {
|
||||
let entryConfig;
|
||||
|
||||
@@ -4,9 +4,10 @@ import { getEntrypointName } from '~/core/utils/entrypoints';
|
||||
import { parseHTML } from 'linkedom';
|
||||
import { dirname, relative, resolve } from 'node:path';
|
||||
import { normalizePath } from '~/core/utils/paths';
|
||||
import { murmurHash } from 'ohash';
|
||||
|
||||
// Cache the preamble script for all devHtmlPrerender plugins, not just one
|
||||
let reactRefreshPreamble = '';
|
||||
// Stored outside the plugin to effect all instances of the devHtmlPrerender plugin.
|
||||
const inlineScriptContents: Record<number, string> = {};
|
||||
|
||||
/**
|
||||
* Pre-renders the HTML entrypoints when building the extension to connect to the dev server.
|
||||
@@ -20,8 +21,9 @@ export function devHtmlPrerender(
|
||||
config.wxtModuleDir,
|
||||
'dist/virtual/reload-html.js',
|
||||
);
|
||||
const virtualReactRefreshId = '@wxt/virtual-react-refresh';
|
||||
const resolvedVirtualReactRefreshId = '\0' + virtualReactRefreshId;
|
||||
|
||||
const virtualInlineScript = 'virtual:wxt-inline-script';
|
||||
const resolvedVirtualInlineScript = '\0' + virtualInlineScript;
|
||||
|
||||
return [
|
||||
{
|
||||
@@ -76,22 +78,22 @@ export function devHtmlPrerender(
|
||||
const serverHtml = await server.transformHtml(url, html, originalUrl);
|
||||
const { document } = parseHTML(serverHtml);
|
||||
|
||||
// React pages include a preamble as an unsafe-inline type="module" script to enable fast refresh, as shown here:
|
||||
// https://github.com/wxt-dev/wxt/issues/157#issuecomment-1756497616
|
||||
// Since unsafe-inline scripts are blocked by MV3 CSPs, we need to virtualize it.
|
||||
const reactRefreshScript = Array.from(
|
||||
document.querySelectorAll('script[type=module]'),
|
||||
).find((script) => script.innerHTML.includes('@react-refresh'));
|
||||
if (reactRefreshScript) {
|
||||
// Save preamble to serve from server
|
||||
reactRefreshPreamble = reactRefreshScript.innerHTML;
|
||||
// Replace inline script with virtual module served via dev server.
|
||||
// Extension CSP blocks inline scripts, so that's why we're pulling them
|
||||
// out.
|
||||
const inlineScripts = document.querySelectorAll('script:not([src])');
|
||||
inlineScripts.forEach((script) => {
|
||||
// Save the text content for later
|
||||
const textContent = script.textContent ?? '';
|
||||
const hash = murmurHash(textContent);
|
||||
inlineScriptContents[hash] = textContent;
|
||||
|
||||
// Replace unsafe inline script
|
||||
const virtualScript = document.createElement('script');
|
||||
virtualScript.type = 'module';
|
||||
virtualScript.src = `${server.origin}/${virtualReactRefreshId}`;
|
||||
reactRefreshScript.replaceWith(virtualScript);
|
||||
}
|
||||
virtualScript.src = `${server.origin}/@id/${virtualInlineScript}?${hash}`;
|
||||
script.replaceWith(virtualScript);
|
||||
});
|
||||
|
||||
// Change /@vite/client -> http://localhost:3000/@vite/client
|
||||
const viteClientScript = document.querySelector<HTMLScriptElement>(
|
||||
@@ -112,18 +114,25 @@ export function devHtmlPrerender(
|
||||
name: 'wxt:virtualize-react-refresh',
|
||||
apply: 'serve',
|
||||
resolveId(id) {
|
||||
if (id === `/${virtualReactRefreshId}`) {
|
||||
return resolvedVirtualReactRefreshId;
|
||||
// Resolve inline scripts
|
||||
if (id.startsWith(virtualInlineScript)) {
|
||||
return '\0' + id;
|
||||
}
|
||||
// Ignore chunk contents when pre-rendering
|
||||
|
||||
// Ignore chunks during HTML file pre-rendering
|
||||
if (id.startsWith('/chunks/')) {
|
||||
return '\0noop';
|
||||
}
|
||||
},
|
||||
load(id) {
|
||||
if (id === resolvedVirtualReactRefreshId) {
|
||||
return reactRefreshPreamble;
|
||||
// Resolve virtualized inline scripts
|
||||
if (id.startsWith(resolvedVirtualInlineScript)) {
|
||||
// id="virtual:wxt-inline-script?<hash>"
|
||||
const hash = Number(id.substring(id.indexOf('?') + 1));
|
||||
return inlineScriptContents[hash];
|
||||
}
|
||||
|
||||
// Ignore chunks during HTML file pre-rendering
|
||||
if (id === '\0noop') {
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import { createUnimport } from 'unimport';
|
||||
import { ResolvedConfig } from '~/types';
|
||||
import type * as vite from 'vite';
|
||||
import { extname } from 'path';
|
||||
|
||||
const ENABLED_EXTENSIONS = new Set([
|
||||
'.js',
|
||||
'.jsx',
|
||||
'.ts',
|
||||
'.tsx',
|
||||
'.vue',
|
||||
'.svelte',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Inject any global imports defined by unimport
|
||||
*/
|
||||
export function unimport(config: ResolvedConfig): vite.PluginOption {
|
||||
const options = config.imports;
|
||||
if (options === false) return [];
|
||||
|
||||
const unimport = createUnimport(options);
|
||||
|
||||
return {
|
||||
name: 'wxt:unimport',
|
||||
async config() {
|
||||
await unimport.scanImportsFromDir(undefined, { cwd: config.srcDir });
|
||||
},
|
||||
async transform(code, id) {
|
||||
// Don't transform dependencies
|
||||
if (id.includes('node_modules')) return;
|
||||
|
||||
// Don't transform non-js files
|
||||
if (!ENABLED_EXTENSIONS.has(extname(id))) return;
|
||||
|
||||
const injected = await unimport.injectImports(code, id);
|
||||
return {
|
||||
code: injected.code,
|
||||
map: injected.s.generateMap({ hires: 'boundary', source: id }),
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -46,10 +46,19 @@ try {
|
||||
// Use "pre" so the new script is added before vite bundles all the scripts
|
||||
order: 'pre',
|
||||
handler(html, _ctx) {
|
||||
const src =
|
||||
config.command === 'serve'
|
||||
? `http://${config.dev.server?.hostname}:${config.dev.server?.port}/@id/${virtualHtmlModuleId}`
|
||||
: virtualHtmlModuleId;
|
||||
|
||||
const { document } = parseHTML(html);
|
||||
const existing = document.querySelector(`script[src='${src}']`);
|
||||
if (existing) return;
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.type = 'module';
|
||||
script.src = 'virtual:wxt-html-plugins';
|
||||
script.src = src;
|
||||
|
||||
if (document.head == null) {
|
||||
const newHead = document.createElement('head');
|
||||
document.documentElement.prepend(newHead);
|
||||
|
||||
@@ -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';
|
||||
@@ -153,6 +152,7 @@ function createFileReloader(server: WxtDevServer) {
|
||||
|
||||
// Here, "path" is a non-normalized path (ie: C:\\users\\... instead of C:/users/...)
|
||||
if (path.startsWith(wxt.config.outBaseDir)) return;
|
||||
if (path.startsWith(wxt.config.wxtDir)) return;
|
||||
changeQueue.push([event, path]);
|
||||
|
||||
await fileChangedMutex.runExclusive(async () => {
|
||||
@@ -200,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([
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "yarn-ls",
|
||||
"packageManager": "yarn@1.22.19",
|
||||
"packageManager": "yarn@1.22.22",
|
||||
"dependencies": {
|
||||
"mime-types": "2.1.35"
|
||||
},
|
||||
@@ -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 })),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -100,6 +100,11 @@ describe('Detect Dev Changes', () => {
|
||||
describe('Public Assets', () => {
|
||||
it("should return 'extension-reload' without any groups to rebuild when the changed file is a public asset", () => {
|
||||
const changes = ['/root/src/public/image.svg'];
|
||||
setFakeWxt({
|
||||
config: {
|
||||
publicDir: '/root/src/public',
|
||||
},
|
||||
});
|
||||
const asset1 = fakeOutputAsset({
|
||||
fileName: 'image.svg',
|
||||
});
|
||||
@@ -114,10 +119,7 @@ describe('Detect Dev Changes', () => {
|
||||
const expected: DevModeChange = {
|
||||
type: 'extension-reload',
|
||||
rebuildGroups: [],
|
||||
cachedOutput: {
|
||||
...currentOutput,
|
||||
publicAssets: [asset2],
|
||||
},
|
||||
cachedOutput: currentOutput,
|
||||
};
|
||||
|
||||
const actual = detectDevChanges(changes, currentOutput);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { describe, it, expect, vi, beforeEach, Mock } from 'vitest';
|
||||
import {
|
||||
BackgroundEntrypoint,
|
||||
BackgroundEntrypointOptions,
|
||||
@@ -12,16 +12,13 @@ import {
|
||||
import { resolve } from 'path';
|
||||
import { findEntrypoints } from '../find-entrypoints';
|
||||
import fs from 'fs-extra';
|
||||
import { importEntrypointFile } from '../import-entrypoint';
|
||||
import glob from 'fast-glob';
|
||||
import {
|
||||
fakeResolvedConfig,
|
||||
setFakeWxt,
|
||||
} from '~/core/utils/testing/fake-objects';
|
||||
import { unnormalizePath } from '~/core/utils/paths';
|
||||
|
||||
vi.mock('../import-entrypoint');
|
||||
const importEntrypointFileMock = vi.mocked(importEntrypointFile);
|
||||
import { wxt } from '~/core/wxt';
|
||||
|
||||
vi.mock('fast-glob');
|
||||
const globMock = vi.mocked(glob);
|
||||
@@ -39,9 +36,11 @@ describe('findEntrypoints', () => {
|
||||
outDir: resolve('.output'),
|
||||
command: 'build',
|
||||
});
|
||||
let importEntrypointMock: Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
setFakeWxt({ config });
|
||||
importEntrypointMock = vi.mocked(wxt.builder.importEntrypoint);
|
||||
});
|
||||
|
||||
it.each<[string, string, PopupEntrypoint]>([
|
||||
@@ -211,13 +210,13 @@ describe('findEntrypoints', () => {
|
||||
matches: ['<all_urls>'],
|
||||
};
|
||||
globMock.mockResolvedValueOnce([path]);
|
||||
importEntrypointFileMock.mockResolvedValue(options);
|
||||
importEntrypointMock.mockResolvedValue(options);
|
||||
|
||||
const entrypoints = await findEntrypoints();
|
||||
|
||||
expect(entrypoints).toHaveLength(1);
|
||||
expect(entrypoints[0]).toEqual({ ...expected, options });
|
||||
expect(importEntrypointFileMock).toBeCalledWith(expected.inputPath);
|
||||
expect(importEntrypointMock).toBeCalledWith(expected.inputPath);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -249,13 +248,13 @@ describe('findEntrypoints', () => {
|
||||
type: 'module',
|
||||
};
|
||||
globMock.mockResolvedValueOnce([path]);
|
||||
importEntrypointFileMock.mockResolvedValue(options);
|
||||
importEntrypointMock.mockResolvedValue(options);
|
||||
|
||||
const entrypoints = await findEntrypoints();
|
||||
|
||||
expect(entrypoints).toHaveLength(1);
|
||||
expect(entrypoints[0]).toEqual({ ...expected, options });
|
||||
expect(importEntrypointFileMock).toBeCalledWith(expected.inputPath);
|
||||
expect(importEntrypointMock).toBeCalledWith(expected.inputPath);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -338,12 +337,13 @@ describe('findEntrypoints', () => {
|
||||
config: {
|
||||
manifestVersion: 2,
|
||||
},
|
||||
builder: wxt.builder,
|
||||
});
|
||||
const options: BackgroundEntrypointOptions = {
|
||||
type: 'module',
|
||||
};
|
||||
globMock.mockResolvedValueOnce(['background.ts']);
|
||||
importEntrypointFileMock.mockResolvedValue(options);
|
||||
importEntrypointMock.mockResolvedValue(options);
|
||||
|
||||
const entrypoints = await findEntrypoints();
|
||||
|
||||
@@ -355,12 +355,13 @@ describe('findEntrypoints', () => {
|
||||
config: {
|
||||
manifestVersion: 3,
|
||||
},
|
||||
builder: wxt.builder,
|
||||
});
|
||||
const options: BackgroundEntrypointOptions = {
|
||||
type: 'module',
|
||||
};
|
||||
globMock.mockResolvedValueOnce(['background.ts']);
|
||||
importEntrypointFileMock.mockResolvedValue(options);
|
||||
importEntrypointMock.mockResolvedValue(options);
|
||||
|
||||
const entrypoints = await findEntrypoints();
|
||||
|
||||
@@ -373,6 +374,7 @@ describe('findEntrypoints', () => {
|
||||
...config,
|
||||
command: 'serve',
|
||||
},
|
||||
builder: wxt.builder,
|
||||
});
|
||||
globMock.mockResolvedValueOnce(['popup.html']);
|
||||
|
||||
@@ -410,13 +412,13 @@ describe('findEntrypoints', () => {
|
||||
};
|
||||
const options: BaseEntrypointOptions = {};
|
||||
globMock.mockResolvedValueOnce([path]);
|
||||
importEntrypointFileMock.mockResolvedValue(options);
|
||||
importEntrypointMock.mockResolvedValue(options);
|
||||
|
||||
const entrypoints = await findEntrypoints();
|
||||
|
||||
expect(entrypoints).toHaveLength(1);
|
||||
expect(entrypoints[0]).toEqual({ ...expected, options });
|
||||
expect(importEntrypointFileMock).toBeCalledWith(expected.inputPath);
|
||||
expect(importEntrypointMock).toBeCalledWith(expected.inputPath);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -701,7 +703,7 @@ describe('findEntrypoints', () => {
|
||||
describe('include option', () => {
|
||||
it("should filter out the background when include doesn't contain the target browser", async () => {
|
||||
globMock.mockResolvedValueOnce(['background.ts']);
|
||||
importEntrypointFileMock.mockResolvedValue({
|
||||
importEntrypointMock.mockResolvedValue({
|
||||
include: ['not' + config.browser],
|
||||
});
|
||||
|
||||
@@ -712,7 +714,7 @@ describe('findEntrypoints', () => {
|
||||
|
||||
it("should filter out content scripts when include doesn't contain the target browser", async () => {
|
||||
globMock.mockResolvedValueOnce(['example.content.ts']);
|
||||
importEntrypointFileMock.mockResolvedValue({
|
||||
importEntrypointMock.mockResolvedValue({
|
||||
include: ['not' + config.browser],
|
||||
});
|
||||
|
||||
@@ -776,7 +778,7 @@ describe('findEntrypoints', () => {
|
||||
describe('exclude option', () => {
|
||||
it('should filter out the background when exclude contains the target browser', async () => {
|
||||
globMock.mockResolvedValueOnce(['background.ts']);
|
||||
importEntrypointFileMock.mockResolvedValue({
|
||||
importEntrypointMock.mockResolvedValue({
|
||||
exclude: [config.browser],
|
||||
});
|
||||
|
||||
@@ -787,7 +789,7 @@ describe('findEntrypoints', () => {
|
||||
|
||||
it('should filter out content scripts when exclude contains the target browser', async () => {
|
||||
globMock.mockResolvedValueOnce(['example.content.ts']);
|
||||
importEntrypointFileMock.mockResolvedValue({
|
||||
importEntrypointMock.mockResolvedValue({
|
||||
exclude: [config.browser],
|
||||
});
|
||||
|
||||
@@ -850,7 +852,6 @@ describe('findEntrypoints', () => {
|
||||
'ui.content/index.ts',
|
||||
'injected.content/index.ts',
|
||||
]);
|
||||
importEntrypointFileMock.mockResolvedValue({});
|
||||
const filterEntrypoints = ['popup', 'ui'];
|
||||
setFakeWxt({
|
||||
config: {
|
||||
@@ -860,8 +861,11 @@ describe('findEntrypoints', () => {
|
||||
command: 'build',
|
||||
filterEntrypoints: new Set(filterEntrypoints),
|
||||
},
|
||||
builder: wxt.builder,
|
||||
});
|
||||
|
||||
importEntrypointMock.mockResolvedValue({});
|
||||
|
||||
const entrypoints = await findEntrypoints();
|
||||
const names = entrypoints.map((item) => item.name);
|
||||
expect(names).toHaveLength(2);
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import {
|
||||
BuildOutput,
|
||||
BuildStepOutput,
|
||||
EntrypointGroup,
|
||||
OutputAsset,
|
||||
OutputFile,
|
||||
} from '~/types';
|
||||
import { every, some } from '~/core/utils/arrays';
|
||||
@@ -50,12 +49,25 @@ export function detectDevChanges(
|
||||
findEffectedSteps(changedFile, currentOutput),
|
||||
),
|
||||
);
|
||||
if (changedSteps.size === 0) return { type: 'no-change' };
|
||||
if (changedSteps.size === 0) {
|
||||
const hasPublicChange = some(changedFiles, (file) =>
|
||||
file.startsWith(wxt.config.publicDir),
|
||||
);
|
||||
if (hasPublicChange) {
|
||||
return {
|
||||
type: 'extension-reload',
|
||||
rebuildGroups: [],
|
||||
cachedOutput: currentOutput,
|
||||
};
|
||||
} else {
|
||||
return { type: 'no-change' };
|
||||
}
|
||||
}
|
||||
|
||||
const unchangedOutput: BuildOutput = {
|
||||
manifest: currentOutput.manifest,
|
||||
steps: [],
|
||||
publicAssets: [],
|
||||
publicAssets: [...currentOutput.publicAssets],
|
||||
};
|
||||
const changedOutput: BuildOutput = {
|
||||
manifest: currentOutput.manifest,
|
||||
@@ -70,13 +82,6 @@ export function detectDevChanges(
|
||||
unchangedOutput.steps.push(step);
|
||||
}
|
||||
}
|
||||
for (const asset of currentOutput.publicAssets) {
|
||||
if (changedSteps.has(asset)) {
|
||||
changedOutput.publicAssets.push(asset);
|
||||
} else {
|
||||
unchangedOutput.publicAssets.push(asset);
|
||||
}
|
||||
}
|
||||
|
||||
const isOnlyHtmlChanges =
|
||||
changedFiles.length > 0 &&
|
||||
@@ -117,8 +122,8 @@ export function detectDevChanges(
|
||||
function findEffectedSteps(
|
||||
changedFile: string,
|
||||
currentOutput: BuildOutput,
|
||||
): DetectedChange[] {
|
||||
const changes: DetectedChange[] = [];
|
||||
): BuildStepOutput[] {
|
||||
const changes: BuildStepOutput[] = [];
|
||||
const changedPath = normalizePath(changedFile);
|
||||
|
||||
const isChunkEffected = (chunk: OutputFile): boolean =>
|
||||
@@ -135,11 +140,6 @@ function findEffectedSteps(
|
||||
if (effectedChunk) changes.push(step);
|
||||
}
|
||||
|
||||
const effectedAsset = currentOutput.publicAssets.find((chunk) =>
|
||||
isChunkEffected(chunk),
|
||||
);
|
||||
if (effectedAsset) changes.push(effectedAsset);
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
@@ -194,10 +194,3 @@ interface ContentScriptReload extends RebuildChange {
|
||||
type: 'content-script-reload';
|
||||
changedSteps: BuildStepOutput[];
|
||||
}
|
||||
|
||||
/**
|
||||
* When figuring out what needs reloaded, this stores the step that was changed, or the public
|
||||
* directory asset that was changed. It doesn't know what type of change is required yet. Just an
|
||||
* intermediate type.
|
||||
*/
|
||||
type DetectedChange = BuildStepOutput | OutputAsset;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user