Compare commits

..

55 Commits

Author SHA1 Message Date
GitHub Actions ec01f048ed chore(release): v0.3.1 2023-07-30 03:35:56 +00:00
Aaron Klinker 8751c55063 Switch back to default reporter for CI 2023-07-29 22:27:17 -05:00
Aaron Klinker dfe424f35d chore: Increate E2E test timeout to fix flakey test 2023-07-29 22:26:05 -05:00
Aaron Klinker 25677ba445 ci: Fix flakey failure when validating templates 2023-07-29 22:24:38 -05:00
Aaron Klinker 390f65cf39 Add bootstrap command to docs 2023-07-29 22:22:35 -05:00
Aaron 310f994ccb feat: init command for bootstrapping new projects (#65) 2023-07-29 22:20:31 -05:00
Aaron Klinker 23e4295e08 Fix typo 2023-07-29 15:59:03 -05:00
Aaron Klinker abdef08aeb Fix typos in publishing docs 2023-07-29 15:21:07 -05:00
Aaron Klinker 38d4f9c879 docs: Add a comparison page to compare and contrast against Plasmo 2023-07-29 15:20:55 -05:00
Aaron Klinker 709b61a174 docs: Add a section for extensions using WXT 2023-07-29 15:00:25 -05:00
Aaron Klinker 4184b0529d docs: Add publishing docs 2023-07-22 10:46:45 -05:00
Aaron d3ff4c6afe Cleanup templates (#64) 2023-07-22 09:10:59 -05:00
BeanWei 16de4da27f chore: Add Solid template (#63)
Co-authored-by: Aaron Klinker <aklinker1@users.noreply.github.com>
2023-07-22 08:43:10 -05:00
Aaron 936d83bc08 chore: Simplify binary setup (#62) 2023-07-21 19:58:57 -05:00
Aaron Klinker aea866c9cd docs: Update installation docs 2023-07-21 14:10:34 -05:00
Aaron Klinker 3107d27184 Fix copy-paste error for sidepanel docs 2023-07-21 13:50:23 -05:00
Aaron Klinker 3a336eba89 docs: Add output paths to entrypoint docs 2023-07-21 13:47:29 -05:00
Aaron 044a24fd6a feat: CSS entrypoints (#61) 2023-07-21 12:57:09 -05:00
Aaron Klinker 5b269f4369 Remove gradient from hero logo 2023-07-21 09:19:41 -05:00
Aaron 7d55faff20 chore: Branding and logo (#60) 2023-07-21 01:51:04 -05:00
Aaron Klinker 386f8db5db Update readme features 2023-07-20 23:30:41 -05:00
Aaron Klinker 94a1097df5 docs: Add zip command to installation scripts 2023-07-20 23:26:20 -05:00
Aaron be95a778ab chore: Update template projects to v0.3 (#56) 2023-07-20 23:13:20 -05:00
Aaron Klinker d3b1536f39 Update changelog 2023-07-20 23:01:46 -05:00
Aaron Klinker 9f2b989a2f Merge branch 'main' of github.com:aklinker1/wxt 2023-07-20 23:00:31 -05:00
GitHub Actions e621aa8f8c chore(release): v0.3.0 2023-07-21 03:59:08 +00:00
Aaron 488d7885ca feat: Windows support (#50) 2023-07-20 22:54:30 -05:00
Aaron Klinker 3a9fd3909f ci: Speed up demo validation 2023-07-20 22:53:51 -05:00
Aaron Klinker 2c70246af5 fix: Add WebWorker lib to generated tsconfig 2023-07-20 22:34:16 -05:00
Aaron Klinker a82a66ec37 Fix installation docs syntax colors 2023-07-20 22:31:39 -05:00
Aaron Klinker 19c0948d95 feat!: Change default publicDir to <rootDir>/public
BREAKING CHANGE: `config.publicDir`'s default changed from `<srcDir>/public` to `rootDir/<public>` to align with other frameworks and standards
2023-07-20 15:47:38 -05:00
Aaron Klinker 04e5400a46 Fix awkward sentences in the docs 2023-07-20 14:13:52 -05:00
Aaron Klinker 33c1c171db Consistent capitalization on docs homepage 2023-07-20 14:02:35 -05:00
Aaron Klinker 9cc464f48b ci: Improve checks against demo/ extension 2023-07-20 13:56:22 -05:00
Aaron Klinker 58a84ec253 feat!: Add type safety to browser.runtime.getURL
Entrypoint outputs and files in the public directory are typed properly.
Chunks and other generated files are not typed since they aren't often accessed at runtime.

BREAKING CHANGE: `browser` is now imported from `wxt/browser` instead of `webextension-polyfill`
2023-07-20 13:55:35 -05:00
Aaron Klinker 609223566c types: Allow any string for the __BROWSER__ global 2023-07-20 13:14:16 -05:00
Aaron Klinker 0aebb67b73 docs: Update entrypoint directory links 2023-07-20 12:14:45 -05:00
GitHub Actions 5fa5fd01cb chore(release): v0.2.5 2023-07-20 16:19:32 +00:00
Aaron Klinker de16423e02 Add hanging-process reporter to CI to diagnose hung processes 2023-07-20 11:13:55 -05:00
Aaron Klinker aa4c0449e9 Fix failing tests 2023-07-20 11:13:55 -05:00
Aaron Klinker 9d00eb2466 chore: Update templates to wxt@0.2 2023-07-20 11:13:55 -05:00
Aaron Klinker 9ac756fb43 ci: Validate project templates 2023-07-20 11:13:55 -05:00
Aaron Klinker f195aa429c Fix type error 2023-07-20 10:40:03 -05:00
Aaron Klinker ca20a210ea feat: Include background script in dev mode if user doesn't define one 2023-07-20 10:38:53 -05:00
Aaron Klinker 547fee0e0e feat: Auto-import from subdirectories 2023-07-20 09:54:44 -05:00
Aaron Klinker 54b18cc66e fix: Properly load entrypoints that reference import.meta 2023-07-20 09:43:16 -05:00
Aaron Klinker d8c190365a fix: Don't crash when generating types in dev mode 2023-07-20 09:06:32 -05:00
Aaron Klinker b625f41919 Update changelog 2023-07-20 09:06:32 -05:00
GitHub Actions 236a135ac2 chore(release): v0.2.4 2023-07-16 19:41:24 +00:00
Aaron dca43efe0d feat: Add wxt zip command (#47) 2023-07-16 14:38:56 -05:00
GitHub Actions eedb136e13 chore(release): v0.2.3 2023-07-16 17:05:39 +00:00
Aaron c71b5e12f2 fix: Some fields cannot be overridden from config.manifest (#46) 2023-07-16 12:03:14 -05:00
Aaron Klinker 495c5c8e4c fix: Read boolean maniest options from meta tags correctly 2023-07-16 11:29:34 -05:00
Aaron Klinker 63c4359d83 Fix changelog 2023-07-16 11:17:33 -05:00
Aaron efc3230674 fix: Correctly lookup open port (#45) 2023-07-16 11:14:50 -05:00
163 changed files with 3770 additions and 4203 deletions
+2 -5
View File
@@ -7,7 +7,7 @@ jobs:
uses: './.github/workflows/validate.yml'
publish:
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
needs:
- validate
steps:
@@ -28,10 +28,7 @@ jobs:
cache: 'pnpm'
- name: Install dependencies
run: |
pnpm install --ignore-scripts
pnpm build
pnpm install
run: pnpm install
- name: Bump and Tag
run: |
+67 -7
View File
@@ -7,8 +7,13 @@ on:
- main
jobs:
validate:
runs-on: ubuntu-20.04
wxt:
name: WXT
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v3
@@ -25,10 +30,7 @@ jobs:
cache: 'pnpm'
- name: Install dependencies
run: |
pnpm install --ignore-scripts
pnpm build
pnpm install
run: pnpm install
- name: Formatting
run: pnpm format:check
@@ -42,7 +44,65 @@ jobs:
pnpm build:all:chrome-mv3
pnpm build:all:firefox-mv2
pnpm build:all:firefox-mv3
pnpm tsc --noEmit
pnpm wxt zip
working-directory: demo
- name: Tests
run: pnpm test:coverage
run: pnpm test:coverage --reporter=default --reporter=hanging-process
project-templates:
name: Project Templates
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup PNPM
uses: pnpm/action-setup@v2
with:
version: 8
- name: Setup NodeJS
uses: actions/setup-node@v3
with:
node-version: 18
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Validate Vanilla
working-directory: templates/vanilla
run: |
npm i
npm run build
npm run compile
- name: Validate Vue
working-directory: templates/vue
run: |
npm i
npm run build
npm run compile
- name: Validate React
working-directory: templates/react
run: |
npm i
npm run build
npm run compile
- name: Validate Svelte
working-directory: templates/svelte
run: |
npm i
npm run build
npm run check
- name: Validate Solid
working-directory: templates/solid
run: |
npm i
npm run build
npm run compile
+1
View File
@@ -5,3 +5,4 @@ e2e/project
.wxt
docs/.vitepress/cache
pnpm-lock.yaml
CHANGELOG.md
+125 -2
View File
@@ -1,12 +1,136 @@
# Changelog
## v0.3.1
[compare changes](https://github.com/aklinker1/wxt/compare/v0.3.0...v0.3.1)
### 🚀 Enhancements
- CSS entrypoints ([#61](https://github.com/aklinker1/wxt/pull/61))
- `init` command for bootstrapping new projects ([#65](https://github.com/aklinker1/wxt/pull/65))
### 📖 Documentation
- Add zip command to installation scripts ([94a1097](https://github.com/aklinker1/wxt/commit/94a1097))
- Add output paths to entrypoint docs ([3a336eb](https://github.com/aklinker1/wxt/commit/3a336eb))
- Update installation docs ([aea866c](https://github.com/aklinker1/wxt/commit/aea866c))
- Add publishing docs ([4184b05](https://github.com/aklinker1/wxt/commit/4184b05))
- Add a section for extensions using WXT ([709b61a](https://github.com/aklinker1/wxt/commit/709b61a))
- Add a comparison page to compare and contrast against Plasmo ([38d4f9c](https://github.com/aklinker1/wxt/commit/38d4f9c))
### 🏡 Chore
- Update template projects to v0.3 ([#56](https://github.com/aklinker1/wxt/pull/56))
- Branding and logo ([#60](https://github.com/aklinker1/wxt/pull/60))
- Simplify binary setup ([#62](https://github.com/aklinker1/wxt/pull/62))
- Add Solid template ([#63](https://github.com/aklinker1/wxt/pull/63))
- Increate E2E test timeout to fix flakey test ([dfe424f](https://github.com/aklinker1/wxt/commit/dfe424f))
### 🤖 CI
- Speed up demo validation ([3a9fd39](https://github.com/aklinker1/wxt/commit/3a9fd39))
- Fix flakey failure when validating templates ([25677ba](https://github.com/aklinker1/wxt/commit/25677ba))
### ❤️ Contributors
- Aaron Klinker <aaronklinker1@gmail.com>
- Aaron
- BeanWei <bean0wei@dingtalk.com>
## v0.3.0
[compare changes](https://github.com/aklinker1/wxt/compare/v0.2.5...v0.3.0)
### 🚀 Enhancements
- ⚠️ Add type safety to `browser.runtime.getURL` ([58a84ec](https://github.com/aklinker1/wxt/commit/58a84ec))
- ⚠️ Change default `publicDir` to `<rootDir>/public` ([19c0948](https://github.com/aklinker1/wxt/commit/19c0948))
- Windows support ([#50](https://github.com/aklinker1/wxt/pull/50))
### 🩹 Fixes
- Add `WebWorker` lib to generated tsconfig ([2c70246](https://github.com/aklinker1/wxt/commit/2c70246))
### 📖 Documentation
- Update entrypoint directory links ([0aebb67](https://github.com/aklinker1/wxt/commit/0aebb67))
### 🌊 Types
- Allow any string for the `__BROWSER__` global ([6092235](https://github.com/aklinker1/wxt/commit/6092235))
### 🤖 CI
- Improve checks against `demo/` extension ([9cc464f](https://github.com/aklinker1/wxt/commit/9cc464f))
#### ⚠️ Breaking Changes
- ⚠️ Add type safety to `browser.runtime.getURL` ([58a84ec](https://github.com/aklinker1/wxt/commit/58a84ec))
- ⚠️ Change default `publicDir` to `<rootDir>/public` ([19c0948](https://github.com/aklinker1/wxt/commit/19c0948))
### ❤️ Contributors
- Aaron Klinker
## v0.2.5
[compare changes](https://github.com/aklinker1/wxt/compare/v0.2.4...v0.2.5)
### 🚀 Enhancements
- Auto-import from subdirectories ([547fee0](https://github.com/aklinker1/wxt/commit/547fee0))
- Include background script in dev mode if user doesn't define one ([ca20a21](https://github.com/aklinker1/wxt/commit/ca20a21))
### 🩹 Fixes
- Don't crash when generating types in dev mode ([d8c1903](https://github.com/aklinker1/wxt/commit/d8c1903))
- Properly load entrypoints that reference `import.meta` ([54b18cc](https://github.com/aklinker1/wxt/commit/54b18cc))
### 🏡 Chore
- Update templates to wxt@0.2 ([9d00eb2](https://github.com/aklinker1/wxt/commit/9d00eb2))
### 🤖 CI
- Validate project templates ([9ac756f](https://github.com/aklinker1/wxt/commit/9ac756f))
### ❤️ Contributors
- Aaron Klinker
## v0.2.4
[compare changes](https://github.com/aklinker1/wxt/compare/v0.2.3...v0.2.4)
### 🚀 Enhancements
- Add `wxt zip` command ([#47](https://github.com/aklinker1/wxt/pull/47))
### ❤️ Contributors
- Aaron
## v0.2.3
[compare changes](https://github.com/aklinker1/wxt/compare/v0.2.2...v0.2.3)
### 🩹 Fixes
- Correctly lookup open port ([#45](https://github.com/aklinker1/wxt/pull/45))
- Read boolean maniest options from meta tags correctly ([495c5c8](https://github.com/aklinker1/wxt/commit/495c5c8))
- Some fields cannot be overridden from `config.manifest` ([#46](https://github.com/aklinker1/wxt/pull/46))
### ❤️ Contributors
- Aaron Klinker
## v0.2.2
[compare changes](https://github.com/aklinker1/wxt/compare/v0.2.1...v0.2.2)
### 🩹 Fixes
- Register content scripts correctly ([2fb5a54](https://github.com/aklinker1/wxt/commit/2fb5a54))
- Register content scripts correctly in dev mode ([2fb5a54](https://github.com/aklinker1/wxt/commit/2fb5a54))
### ❤️ Contributors
@@ -131,7 +255,6 @@
### ❤️ Contributors
- Aaron
- Aaron Klinker
## v0.1.2
+4 -2
View File
@@ -1,5 +1,7 @@
<h1 align="center">WXT</h1>
<p align="center"><img align="center" width="44" src="./docs/public/hero-logo.svg" alt="WXT Logo"></p>
<p align="center"><i>Next gen framework for developing web extensions.<br/>Powered by <a href="https://vitejs.dev/" target="_blank">Vite</a>. Inspired by <a href="https://nuxt.com/" target="_blank">Nuxt</a>.</i></p>
![Example CLI Output](./docs/assets/cli-output.png)
@@ -8,16 +10,16 @@
- 🌐 Supports all browsers
- ✅ Supports both MV2 and MV3
- ⚡ Dev mode with HMR & auto-reload
- ⚡ Dev mode with HMR & fast reload
- 📂 File based entrypoints
- 🚔 TypeScript
- 🦾 Auto-imports
- ⬇️ Download and bundle remote URL imports
- 🎨 Frontend framework agnostic: works with Vue, React, Svelte, etc
- 🖍️ Quickly bootstrap a new project
### Todo
- 🖍️ Quickly bootstrap a new project
- 📏 Bundle analysis
- 🤖 Automated publishing
Executable
+2
View File
@@ -0,0 +1,2 @@
#!/usr/bin/env node
require('../dist/cli.cjs');
+4 -1
View File
@@ -1,5 +1,5 @@
{
"name": "Demo",
"name": "WXT Demo",
"version": "1.0.0",
"description": "Demo extension for WXT",
"type": "module",
@@ -11,6 +11,8 @@
"build:all:chrome-mv2": "wxt build --mv2",
"build:all:firefox-mv3": "wxt build -b firefox --mv3",
"build:all:firefox-mv2": "wxt build -b firefox",
"zip": "pnpm -w build && wxt zip",
"compile": "pnpm -w build && tsc --noEmit",
"postinstall": "pnpm -w build && wxt prepare"
},
"dependencies": {
@@ -18,6 +20,7 @@
},
"devDependencies": {
"@types/webextension-polyfill": "^0.10.0",
"sass": "^1.64.0",
"wxt": "workspace:*"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 504 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 936 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

+5
View File
@@ -7,4 +7,9 @@ export default defineBackground(() => {
firefox: __IS_FIREFOX__,
manifestVersion: __MANIFEST_VERSION__,
});
// @ts-expect-error: should only accept entrypoints or public assets
browser.runtime.getURL('/');
browser.runtime.getURL('/background.js');
browser.runtime.getURL('/icon/128.png');
});
+3
View File
@@ -0,0 +1,3 @@
body {
background-color: red;
}
@@ -0,0 +1,3 @@
body {
color: blue;
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 698 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

@@ -0,0 +1,40 @@
<script lang="ts" setup>
const props = defineProps<{
patterns: Array<[intput: string, output: string]>;
}>();
</script>
<template>
<table class="no-vertical-dividers">
<thead>
<tr>
<th>Input Pattern</th>
<th></th>
<th>Output Path</th>
</tr>
</thead>
<tbody>
<tr v-for="pattern of patterns">
<td style="white-space: nowrap">
<code>entrypoints/{{ pattern[0] }}</code>
</td>
<td style="padding: 6px; opacity: 50%">
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M4 11v2h12l-5.5 5.5l1.42 1.42L19.84 12l-7.92-7.92L10.5 5.5L16 11H4Z"
/>
</svg>
</td>
<td style="white-space: nowrap">
<code>/{{ pattern[1] }}</code>
</td>
</tr>
</tbody>
</table>
</template>
+9 -7
View File
@@ -1,17 +1,19 @@
<script lang="ts" setup>
defineProps<{
import { computed } from 'vue';
const props = defineProps<{
name: string;
icon?: string;
}>();
const src = computed(() => {
if (props.icon) return props.icon;
return `https://raw.githubusercontent.com/PKief/vscode-material-icon-theme/main/icons/${props.name.toLowerCase()}.svg`;
});
</script>
<template>
<img
:src="`https://raw.githubusercontent.com/PKief/vscode-material-icon-theme/main/icons/${
name?.toLowerCase() ?? icon
}.svg`"
:alt="`${name} Logo`"
/>
<img :src="src" :alt="`${name} Logo`" />
</template>
<style scoped>
@@ -0,0 +1,109 @@
<script lang="ts" setup>
const extensions = [
{
name: 'GitHub: Better Line Counts',
description: 'Remove generated files from GitHub line counts.',
icon: 'https://lh3.googleusercontent.com/GcffNyCJaxT2G9dsQCJHhUEMlu_E0vEzph5cLPrQj7UHKat7QyCzGu69Dmp_DDUL8rY-bPMFJceQarS1wcqdwTalTg=s256',
link: 'https://chrome.google.com/webstore/detail/github-better-line-counts/ocfdgncpifmegplaglcnglhioflaimkd',
},
];
</script>
<template>
<section class="vp-doc">
<div class="container">
<h2>Who's Using WXT?</h2>
<ul>
<li v-for="extension of extensions">
<img :src="extension.icon" :alt="`${extension.name} icon`" />
<a :href="extension.link" target="_blank">{{ extension.name }}</a>
<small>{{ extension.description }}</small>
</li>
</ul>
<p>Open a PR to add your extension to the list!</p>
</div>
</section>
</template>
<style scoped>
.vp-doc {
padding: 0 24px;
}
h2 {
margin-bottom: 32px;
}
@media (min-width: 640px) {
.vp-doc {
padding: 0 48px;
}
}
@media (min-width: 960px) {
.vp-doc {
padding: 0 64px;
}
}
.container {
max-width: 1152px;
margin: 0 auto;
display: flex;
flex-direction: column;
}
img {
width: 96px;
height: 96px;
margin-bottom: 16px;
}
ul {
display: grid;
grid-template-columns: repeat(2, 1fr);
align-items: stretch;
gap: 16px;
list-style: none;
margin: 0;
padding: 0;
}
@media (min-width: 640px) {
ul {
grid-template-columns: repeat(3, 1fr);
}
}
@media (min-width: 960px) {
ul {
grid-template-columns: repeat(4, 1fr);
}
}
li {
margin: 0 !important;
padding: 12px;
display: flex;
flex-direction: column;
align-items: center;
background-color: var(--vp-c-bg-soft);
border-radius: 12px;
flex: 1;
}
a,
small {
text-align: center;
}
small {
opacity: 50%;
}
p {
text-align: center;
opacity: 50%;
}
a {
color: var(--vp-c-text-1);
cursor: pointer;
}
</style>
+4 -1
View File
@@ -8,7 +8,7 @@ export default defineConfig({
themeConfig: {
// https://vitepress.dev/reference/default-theme-config
// logo: '/logo.svg',
logo: '/logo.svg',
editLink: {
pattern: 'https://github.com/aklinker1/wxt/edit/main/docs/:path',
},
@@ -36,6 +36,7 @@ export default defineConfig({
{ text: 'Build Targets', link: '/get-started/build-targets.md' },
{ text: 'Publishing', link: '/get-started/publishing.md' },
{ text: 'Testing', link: '/get-started/testing.md' },
{ text: 'Compare', link: '/get-started/compare.md' },
],
},
],
@@ -54,12 +55,14 @@ export default defineConfig({
{ text: 'Background', link: '/guide/background.md' },
{ text: 'Bookmarks', link: '/guide/bookmarks.md' },
{ text: 'Content Scripts', link: '/guide/content-scripts.md' },
{ text: 'CSS', link: '/guide/css.md' },
{ text: 'Devtools', link: '/guide/devtools.md' },
{ text: 'History', link: '/guide/history.md' },
{ text: 'Newtab', link: '/guide/newtab.md' },
{ text: 'Options', link: '/guide/options.md' },
{ text: 'Popup', link: '/guide/popup.md' },
{ text: 'Sandbox', link: '/guide/sandbox.md' },
{ text: 'Sidepanel', link: '/guide/sidepanel.md' },
{ text: 'Unlisted Pages', link: '/guide/unlisted-pages.md' },
{ text: 'Unlisted Scripts', link: '/guide/unlisted-scripts.md' },
],
+58
View File
@@ -0,0 +1,58 @@
/* Colors */
:root {
--wxt-c-green: #53bc4a;
--wxt-c-green-light: #67d45e;
--wxt-c-green-lighter: #67d45e;
--wxt-c-green-dark: #4fa048;
--wxt-c-green-darker: #447e3f;
}
/* https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css */
:root {
--vp-c-brand: var(--wxt-c-green);
--vp-c-brand-light: var(--wxt-c-green-light);
--vp-c-brand-lighter: var(--wxt-c-green-lighter);
--vp-c-brand-dark: var(--wxt-c-green-dark);
--vp-c-brand-darker: var(--wxt-c-green-darker);
--vp-button-brand-text: var(--vp-c-black);
--vp-button-brand-hover-text: var(--vp-c-black);
--vp-button-brand-active-text: var(--vp-c-black);
--vp-custom-block-tip-border: var(--wxt-c-green-dark);
--vp-custom-block-tip-text: var(--wxt-c-green-dark);
--vp-code-block-bg: #222422;
--vp-code-copy-code-bg: #313431;
--vp-code-copy-code-hover-bg: #3c403c;
}
.dark {
--vp-c-bg: #131413;
--vp-c-bg-soft: #1a1b1a;
--vp-c-bg-soft-up: #1f201f;
--vp-c-bg-soft-down: #262926;
--vp-c-bg-soft-mute: #242424;
--vp-c-bg-alt: #171817;
--vp-c-mute: #313136;
--vp-c-mute-light: #3a3a3c;
--vp-c-mute-lighter: #505053;
--vp-c-mute-dark: #2c2c30;
--vp-c-mute-darker: #252529;
--vp-code-block-bg: #191a19;
--vp-code-copy-code-bg: #212321;
--vp-code-copy-code-hover-bg: #292d29;
}
.vp-doc .no-vertical-dividers th,
.vp-doc .no-vertical-dividers td {
border: none;
}
.vp-doc .no-vertical-dividers tr {
border: 1px solid var(--vp-c-divider);
}
+5
View File
@@ -1,9 +1,14 @@
import DefaultTheme from 'vitepress/theme';
import Icon from '../components/Icon.vue';
import EntrypointPatterns from '../components/EntrypointPatterns.vue';
import UsingWxtSection from '../components/UsingWxtSection.vue';
import './custom.css';
export default {
extends: DefaultTheme,
enhanceApp(ctx) {
ctx.app.component('Icon', Icon);
ctx.app.component('EntrypointPatterns', EntrypointPatterns);
ctx.app.component('UsingWxtSection', UsingWxtSection);
},
};
+1 -1
View File
@@ -5,7 +5,7 @@ You can build an extension for any combination of browser and manifest verison.
Separate build targets are written to their own output directories:
```
<root>
<rootDir>
└─ .output
├─ chrome-mv3
├─ firefox-mv2
+49
View File
@@ -0,0 +1,49 @@
# Compare
Lets compare the features of WXT vs [Plasmo](https://docs.plasmo.com/framework), another web extension framework.
## Overview
| Features | WXT | Plasmo |
| ---------------------------------------------------- | :-------------------------: | :--------------------------------------: |
| Supports all browsers | ✅ | ✅ |
| MV2 Support | ✅ | ✅ |
| MV3 Support | ✅ | ✅ |
| Create Extension ZIPs | ✅ | ✅ |
| Create Firefox Sources ZIP | ✅ | ❌ |
| First-class TypeScript support | ✅ | ✅ |
| File based entrypoint discovery | ✅ | ✅ |
| Inline entrypoint config | ✅ | ✅ |
| 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 | 🟡 Coming soon | ✅ |
| 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 Scripta on Change | ✅ | 🟡 Reloads entire extension |
| Reload Background on Change | 🟡 Reloads entire extension | 🟡 Reloads entire extension |
| <strong style="opacity: 50%">Built-in Utils</strong> | | |
| Storage | ❌ | ✅ |
| Messaging | ❌ | ✅ |
| Content Script UI | ❌ | ✅ |
## Dev Mode
WXT's main goal is improving the development experience (DX) of creating web extensions. There are two things WXT does differently:
1. Automatically opens a browser with the extension installed when starting development
2. Reload each part of the extension individually rather than reloading the entire extension
Opening a browser automatically makes it super easy to start and stop development without having to manually load the extension in your browser.
Reloading each part of the extension individually improves your iteration speed while developing UIs. This is because reloading the entire extension on every change will close the popup and any tabs open to an extension page, like options. If you save a file associated with a UI and a content script while working on the UI, it will randomly close because it needed to reload the extension when the content script changed. This interupts your development flow and is really annoying.
WXT solves this problem by reloading HTML pages and content scripts individually (when possible) to keep your UIs open while you develop them. This is a MV3 feature, so if you're developing a MV2 extension, you'll get the same dev experience as Plasmo.
:::info
Unfortunately, there isn't an API for reloading the background page/service worker individually, so if you change a file used by the background, the entire extension will reload. See [Issue #53](https://github.com/aklinker1/wxt/issues/53) for more details.
:::
+15 -6
View File
@@ -1,8 +1,6 @@
# Configuration
WXT's behavior can be configured via the `wxt.config.ts` file. In this file, you can add Vite plugins, change the directory strucutre of your project, and provide permissions or other fields to the `<outdir>/manifest.json`.
However, since WXT is an opinionated framework, some things cannot be configured.
WXT's behavior can be configured via the `wxt.config.ts` file. In this file, you can add Vite plugins, change the directory strucutre of your project, and set fields on your `manifest.json`.
## Config File
@@ -17,7 +15,7 @@ export default defineConfig({
```
:::info
See the [API reference](/api.md) for a full list of options.
See the [Config reference](/config.md) for a full list of options.
:::
## Directory Config
@@ -25,13 +23,24 @@ See the [API reference](/api.md) for a full list of options.
WXT allows you to edit several directories to your liking:
- `root` (default: `process.cwd()`) - Root of the WXT project
- `srcDir` (default: `<root>`) - Location of all your source code
- `srcDir` (default: `<rootDir>`) - Location of all your source code
- `entrypointsDir` (default: `<srcDir>/entrypoints`) - Folder containing all the entrypoints.
- `publicDir` (default: `<srcDir>/public`) - Folder containing [public assets](/get-started/assets.md)
### Example
If you want a `src/` directory to contain all your source code, and you want to rename `entrypoints/` to `entries/`, your config would look like this:
You want a `src/` directory to contain all your source code, and you want to rename `entrypoints/` &rarr; `entries/`:
```
<rootDir>
├─ src/
│ └─ entries/
│ ├─ background.ts
│ └─ ...
└─ wxt.config.ts
```
Your config would look like this:
```ts
import { defineConfig } from 'wxt';
+6 -6
View File
@@ -1,6 +1,6 @@
# Defining Entrypoints
Entrypoints are any HTML, JS, or CSS file that needs to be bundled and included with the extension.
An entrypoints is any HTML, JS, or CSS file that needs to be bundled and included with the extension.
They may or may not be listed in the extension's `manifest.json`.
@@ -11,7 +11,7 @@ In WXT, entrypoints are defined by adding a file to the `entrypoints/` directory
For example, a project that looks like this:
```
<root>
<rootDir>
├─ entrypoints/
│ ├─ background.ts
│ ├─ content.ts
@@ -46,15 +46,15 @@ would result in the following `manifest.json`:
If a file uses a [special name recognized by WXT](/get-started/entrypoints.md), it will be added to the manifest. In this case:
- `popup.html` &rarr; `action.default_popup`
- `content.ts` &rarr; `content_scripts.*.js`
- `content.ts` &rarr; `content_scripts.0.js.0`
- `background.ts` &rarr; `background.service_worker`
But not all entrypoints are added to the `manifest.json`. If they have a name that is not recognized by WXT, they are still built and included in the extension, but they are unlisted and do not show up in the manifest.
But not all entrypoints are added to the `manifest.json`. If they have a name that is not recognized by WXT, they are still built and included in the extension, but they are considered "unlisted" and are not apart of the manifest.
In this case, `injected.ts` gets bundled to `<outdir>/injected.js` and is accessible via `browser.runtime.getURL("/injected.js")`.
:::info
See [`/entrypoints` folder](/get-started/entrypoints.md) documentation for a full list of recognized entrypoint filenames.
See [`/entrypoints` folder](/guide/background.md) documentation for a full list of recognized entrypoint filenames.
:::
## Entrypoint Options
@@ -85,5 +85,5 @@ export default defineContentScript({
```
:::info
For a full list of entrypoints and each of their options, see the [`/entrypoints` folder](/get-started/entrypoints.md) documentation.
For a full list of entrypoints and each of their options, see the [`/entrypoints` folder](/guide/background.md) documentation.
:::
+39 -24
View File
@@ -2,14 +2,14 @@
Bootstrap a new project or start from scratch.
## Bootstrap Project
:::warning 🚧&ensp;The `wxt init` command is not implemented yet.
See [From Scratch](#from-scratch) or reference one of the templates below.
:::warning 🚧&ensp;WSL Support
**_WXT does not support [Windows Subsystem for Linux](https://learn.microsoft.com/en-us/windows/wsl/) yet_**. See [Issue #55](https://github.com/aklinker1/wxt/issues/55) to track progress.
In the meantime, you can use `cmd` instead.
:::
## Bootstrap Project
:::code-group
```sh [pnpm]
@@ -24,12 +24,13 @@ npx wxt@latest init <project-name>
There are several starting templates available.
| TypeScript |
| ---------------------------------------------------------------------------------------------------- |
| <Icon name="TypeScript" /> [`vanilla`](https://github.com/aklinker1/wxt/tree/main/templates/vanilla) |
| <Icon name="Vue" /> [`vue`](https://github.com/aklinker1/wxt/tree/main/templates/vue) |
| <Icon name="React" /> [`react`](https://github.com/aklinker1/wxt/tree/main/templates/react) |
| <Icon name="Svelte" /> [`svelte`](https://github.com/aklinker1/wxt/tree/main/templates/svelte) |
| TypeScript |
| --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| <Icon name="TypeScript" /> [`vanilla`](https://github.com/aklinker1/wxt/tree/main/templates/vanilla) |
| <Icon name="Vue" /> [`vue`](https://github.com/aklinker1/wxt/tree/main/templates/vue) |
| <Icon name="React" /> [`react`](https://github.com/aklinker1/wxt/tree/main/templates/react) |
| <Icon name="Svelte" /> [`svelte`](https://github.com/aklinker1/wxt/tree/main/templates/svelte) |
| <Icon name="Solid" icon="https://www.solidjs.com/img/favicons/favicon-32x32.png" /> [`solid`](https://github.com/aklinker1/wxt/tree/main/templates/solid) |
> All templates are in TypeScript. WXT does not support JS at this time.
@@ -40,19 +41,22 @@ Create a new NPM project:
:::code-group
```sh [pnpm]
pnpm init <project-name>
cd <project-name>
mkdir project-name
cd project-name
pnpm init
echo 'shamefully-hoist=true' >> .npmrc
```
```sh [npm]
npm init <project-name>
cd <project-name>
mkdir project-name
cd project-name
npm init
```
```sh [yarn]
yarn init <project-name>
cd <project-name>
mkdir project-name
cd project-name
yarn init
```
:::
@@ -62,15 +66,15 @@ Then install `wxt`:
:::code-group
```sh [pnpm]
pnpm add wxt
pnpm add -D wxt
```
```sh [npm]
npm i --save wxt
npm i --save-dev wxt
```
```sh [yarn]
yarn add wxt
yarn add --dev wxt
```
:::
@@ -93,13 +97,13 @@ Finally, add scripts to your `package.json`:
"dev:firefox": "wxt --browser firefox", // [!code ++]
"build": "wxt build", // [!code ++]
"build:firefox": "wxt build --browser firefox", // [!code ++]
"zip": "wxt zip", // [!code ++]
"zip:firefox": "wxt zip --browser firefox", // [!code ++]
"postinstall": "wxt prepare" // [!code ++]
}
}
```
> You can skip `*:firefox` scripts if you don't want to support Firefox
## Development
Once you've installed WXT, you can start the development server using the `dev` script.
@@ -113,9 +117,20 @@ pnpm dev
The dev command will build the extension for development, open the browser, and reload the different parts of the extension when you save changes.
:::
:::details Development Manifest
When running the dev command, WXT will make several changes to your `manifest.json` to improve your development experience:
- If missing, add a background script/service worker to enable fast reloads
- Add serveral `permissions` and `host_permissions` to enable HMR and fast reloads
- Modify the CSP to allow connections with the dev server
- Remove `content_scripts` and register them at runtime so they can be easily reloaded when you save a file
If you're an experienced web extension developer and think the dev manifest looks wrong, this is why. Run a production build with `wxt build` to see the unmodified `manifest.json`.
:::
## Next Steps
You're ready to build a out your web extension!
You're ready to build your web extension!
- Learn how to [add entrypoints](./entrypoints.md) like the popup, background, or content scripts
- Learn how to [add entrypoints](./entrypoints.md) like the popup, options page, or content scripts
- [Configure WXT](./configuration.md) by creating a `wxt.config.ts` file
+175 -2
View File
@@ -1,5 +1,178 @@
# Publishing
:::warning 🚧&ensp;Not implemented yet!
For now, manually zip the output directory and upload to stores by hand.
WXT offers several utilities that simplify the publishing process.
## First Time Publishing
If you're publishing an extension to a store for the first time, it's recommended that you manually navigate the process. Each store has unique steps and requirements that you need to familiarize yourself with.
Each store requires that a ZIP file be uploaded. You can generate these using the `wxt zip` command:
```sh
wxt zip
wxt zip -b firefox
# etc
```
Generated ZIP files are stored in the `.output` directory.
## Setup Automated Submissions
To automate submissions, use the [`publish-browser-extension`](https://www.npmjs.com/package/publish-browser-extension) package.
:::info
🚧 WXT plans to eventually incorporate the `publish-browser-extension` package into its own `wxt submit` command.
:::
1. Install the necessary dependencies:
```sh
pnpm add -D publish-browser-extension env-cmd
```
2. Add scripts to your `package.json` file:
```json
{
"scripts": {
"submit": "env-cmd -f .env.submit -- publish-extension",
"submit:dry": "env-cmd -f .env.submit -- publish-extension --dry-run"
}
}
```
3. Create a `.env.submit` file and include the code below. If you're not publishing to certain stores, simply ignore their respective variables.
```txt
CHROME_EXTENSION_ID=""
CHROME_CLIENT_ID=""
CHROME_CLIENT_SECRET=""
CHROME_REFRESH_TOKEN=""
FIREFOX_EXTENSION_ID=""
FIREFOX_JWT_ISSUER=""
FIREFOX_JWT_SECRET=""
EDGE_PRODUCT_ID=""
EDGE_CLIENT_ID=""
EDGE_CLIENT_SECRET=""
EDGE_ACCESS_TOKEN_URL=""
```
> Each value will be filled in during the next step.
4. Run `npx publish-extension --help` for assistance with filling out all the values. Insert the obtained values within the double quotes.
5. ZIP all the targets you plan to publish, in this case Chrome and Firefox.
```sh
wxt zip
wxt zip -b firefox
```
6. Test your credentials by running the `submit:dry` command:
```sh
pnpm submit:dry \
--chrome-zip .output/your-extension-X.Y.Z-chrome.zip \
--firefox-zip .output/your-extension-X.Y.Z-firefox.zip \
--firefox-sources-zip .output/your-extension-X.Y.Z-sources.zip \
--edge-zip .output/your-extension-X.Y.Z-chrome.zip
```
7. Upload and submit your extension for review:
```sh
pnpm submit \
--chrome-zip .output/your-extension-X.Y.Z-chrome.zip \
--firefox-zip .output/your-extension-X.Y.Z-firefox.zip \
--firefox-sources-zip .output/your-extension-X.Y.Z-sources.zip \
--edge-zip .output/your-extension-X.Y.Z-chrome.zip
```
## GitHub Action
Here's an example of a GitHub Action to automate submiting new versions of your extension for review. Ensure that you've added all required secrets used in the workflow to the repo's settings.
```yml
# TODO
```
## Chrome Web Store
✅ Automated &bull; [Developer Dashboard](https://chrome.google.com/webstore/developer/dashboard) &bull; [Publishing Docs](https://developer.chrome.com/docs/webstore/publish/)
To create a ZIP for Chrome:
```sh
wxt zip
```
## Firefox Addon Store
✅ Automated &bull; [Developer Dashboard](https://addons.mozilla.org/developers/) &bull; [Publishing Docs](https://extensionworkshop.com/documentation/publish/submitting-an-add-on/)
Firefox requires you to upload a ZIP of your source code. This allows them to rebuild your extension and review the code in a readable way. More details can be found in [Firefox's docs](https://extensionworkshop.com/documentation/publish/source-code-submission/).
WXT and `publish-browser-extension` both fully support generating and automatically submitting a source code ZIP.
When you run `wxt zip -b firefox`, your sources are zipped into the `.output` directory along with your built extension. WXT is configured to exclude certain files such as config files, hidden files, and tests. However, it's important to manually check the ZIP to ensure it only contains the files necessary to rebuild your extension.
To customize which files are zipped, add the `zip` option to your config file.
```ts
// wxt.config.ts
import { defineConfig } from 'wxt';
export default defineConfig({
zip: {
// ...
},
});
```
If it's your first time submitting to the Firefox Addon Store, or if you've updated your project layout, always test your sources ZIP! The commands below should allow you to rebuild your extension from inside the extracted ZIP.
:::code-group
```sh [pnpm]
pnpm i
pnpm zip:firefox
```
```sh [npm]
npm i
npm run zip:firefox
```
```sh [yarn]
yarn
yarn zip:firefox
```
:::
Ensure that you have a `README.md` or `SOURCE_CODE_REVIEW.md` file with the above commands so that the Firefox team knows how to build your extension.
## Safari
🚧 Not automated at this time
:::warning
🚧 WXT does not currently support automated publishing for Safari. Safari extensions require a native MacOS or iOS app wrapper, which WXT isn't configured to create. For now, if you want to publish to Safari, follow this guide:
https://developer.apple.com/documentation/safariservices/safari_web_extensions/distributing_your_safari_web_extension
:::
## Edge Addons
✅ Automated &bull; [Developer Dashboard](https://aka.ms/PartnerCenterLogin) &bull; [Publishing Docs](https://learn.microsoft.com/en-us/microsoft-edge/extensions-chromium/publish/publish-extension)
No need to create a specific ZIP for Edge. If you're already publishing to the Chrome Web Store, you can reuse your Chrome ZIP.
However, if you have features specifically for Edge, create a separate ZIP with:
```sh
wxt zip -b edge
```
+1 -1
View File
@@ -10,7 +10,7 @@ To setup your test environment for auto-imports, see [Testing](/get-started/test
Some WXT APIs can be used without importing them:
- [`browser`](/config.md#browser) from `webextension-polyfill`
- [`browser`](/config.md#browser) from `wxt/browser`, a small wrapper around `webextension-polyfill`
- [`defineContentScript`](/config.md#defiencontentscript) from `wxt/client`
- [`defineBackground`](/config.md#definebackgroundscript) from `wxt/client`
+5 -1
View File
@@ -6,7 +6,11 @@ For MV2, the background is added as a script to the background page. For MV3, th
## Filenames
`entrypoints/background.ts` is the only recoginzed filename for the background script.
<EntrypointPatterns
:patterns="[
['background.ts', 'background.js'],
]"
/>
## Definition
+6 -2
View File
@@ -4,8 +4,12 @@
## Filenames
- `entrypoints/bookmarks.html`
- `entrypoints/bookmarks/index.html`
<EntrypointPatterns
:patterns="[
['bookmarks.html', 'bookmarks.html'],
['bookmarks/index.html', 'bookmarks.html'],
]"
/>
## Definition
+28 -7
View File
@@ -2,14 +2,18 @@
[Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/content_scripts/) &bull; [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Content_scripts)
When creating content script entrypoints, they are automatically included in the `manifest.json` along with any CSS they import.
## Filenames
When a filename matches the pattern below, it is added as a content script in the `manifest.json`.
- `entrypoints/content.tsx?`
- `entrypoints/<name>.content.tsx?`
- `entrypoints/content/index.tsx?`
- `entrypoints/<name>.content/index.tsx?`
<EntrypointPatterns
:patterns="[
['content.tsx?', 'content-scripts/content.js'],
['content/index.tsx?', 'content-scripts/content.js'],
['<name>.content.tsx?', 'content-scripts/<name>.js'],
['<name>.content/index.tsx?', 'content-scripts/<name>.js'],
]"
/>
## Definition
@@ -34,9 +38,11 @@ export default defineContentScript({
> All manifest options default to `undefined`.
When defining multiple content scripts, content script entrypoints that have the same set of options will be merged into a single `content_script` item in the manifest.
## CSS
To include CSS with your content script, import the CSS file at the top of your entrypoint:
To include CSS with your content script, import the CSS file at the top of your entrypoint.
```
@@ -58,3 +64,18 @@ export default defineContentScript({
},
});
```
Any styles imported in your content script will be added to that content script's `css` array in your `manifest.json`:
```json
// .output/chrome-mv3/manifest.json
{
"content_scripts": [
{
"matches": ["*://google.com/*", "*://duckduckgo.com/*"],
"js": ["content-scripts/overlay.js"],
"css": ["content-scripts/overlay.css"]
}
]
}
```
+44
View File
@@ -0,0 +1,44 @@
# CSS
WXT can build CSS entrypoints individually. CSS entrypoints are always unlisted.
See [Content Script CSS](/guide/content-scripts.md#css) documentation for the recomended approach to include CSS with a content script.
:::info
If the recommended approach doesn't work for your use case, you can use any of the filename patterns below to build the styles separate from the JS and use the [`transformManifest` hook](/config.md#transformmanifest) to manually add your CSS file to the manifest.
:::
## Filenames
<EntrypointPatterns
:patterns="[
['<name>.(css|scss|sass|less|styl|stylus)', '<name>.css'],
['<name>/index.(css|scss|sass|less|styl|stylus)', '<name>.css'],
['content.(css|scss|sass|less|styl|stylus)', 'content-scripts/content.css'],
['content/index.(css|scss|sass|less|styl|stylus)', 'content-scripts/content.css'],
['<name>.content.(css|scss|sass|less|styl|stylus)', 'content-scripts/<name>.css'],
['<name>.content/index.(css|scss|sass|less|styl|stylus)', 'content-scripts/<name>.css'],
]"
/>
## Definition
```css
body {
/* Plain CSS file */
}
```
Follow Vite's guide to setup a preprocessor: https://vitejs.dev/guide/features.html#css-pre-processors
```sh
pnpm i sass
```
```scss
body {
h1 {
/* ...*/
}
}
```
+6 -2
View File
@@ -4,8 +4,12 @@
## Filenames
- `entrypoints/devtools.html`
- `entrypoints/devtools/index.html`
<EntrypointPatterns
:patterns="[
['devtools.html', 'devtools.html'],
['devtools/index.html', 'devtools.html'],
]"
/>
## Definition
+6 -2
View File
@@ -4,8 +4,12 @@
## Filenames
- `entrypoints/history.html`
- `entrypoints/history/index.html`
<EntrypointPatterns
:patterns="[
['history.html', 'history.html'],
['history/index.html', 'history.html'],
]"
/>
## Definition
+6 -2
View File
@@ -4,8 +4,12 @@
## Filenames
- `entrypoints/newtab.html`
- `entrypoints/newtab/index.html`
<EntrypointPatterns
:patterns="[
['newtab.html', 'newtab.html'],
['newtab/index.html', 'newtab.html'],
]"
/>
## Definition
+6 -2
View File
@@ -4,8 +4,12 @@
## Filenames
- `entrypoints/options.html`
- `entrypoints/options/index.html`
<EntrypointPatterns
:patterns="[
['options.html', 'options.html'],
['options/index.html', 'options.html'],
]"
/>
## Definition
+6 -2
View File
@@ -4,8 +4,12 @@
## Filenames
- `entrypoints/popup.html`
- `entrypoints/popup/index.html`
<EntrypointPatterns
:patterns="[
['popup.html', 'popup.html'],
['popup/index.html', 'popup.html'],
]"
/>
## Definition
+8 -4
View File
@@ -8,10 +8,14 @@ Firefox does not support sandboxed pages.
## Filenames
- `entrypoints/sandbox.html`
- `entrypoints/<name>.sandbox.html`
- `entrypoints/sandbox/index.html`
- `entrypoints/<name>.sandbox/index.html`
<EntrypointPatterns
:patterns="[
['entrypoints/sandbox.html', 'sandbox.html'],
['entrypoints/sandbox/index.html', 'sandbox.html'],
['entrypoints/<name>.sandbox.html', '<name>.html` '],
['entrypoints/<name>.sandbox/index.html', '<name>.html` '],
]"
/>
## Definition
+9 -5
View File
@@ -3,15 +3,19 @@
[Chrome Docs](https://developer.chrome.com/docs/extensions/reference/sidePanel/)
:::tip Chromium Only
Firefox does not support sandboxed pages.
Firefox does not support sidepanel pages.
:::
## Filenames
- `entrypoints/sidepanel.html`
- `entrypoints/<name>.sidepanel.html`
- `entrypoints/sidepanel/index.html`
- `entrypoints/<name>.sidepanel/index.html`
<EntrypointPatterns
:patterns="[
['entrypoints/sidepanel.html', 'sidepanel.html'],
['entrypoints/sidepanel/index.html', 'sidepanel.html'],
['entrypoints/<name>.sidepanel.html', '<name>.html` '],
['entrypoints/<name>.sidepanel/index.html', '<name>.html` '],
]"
/>
## Definition
+6 -2
View File
@@ -12,8 +12,12 @@ HTML pages that are built by Vite, but are not included in the manifest.
## Filenames
- `entrypoints/<name>.html`
- `entrypoints/<name>/index.html`
<EntrypointPatterns
:patterns="[
['<name>.html', '<name>.html'],
['<name>/index.html', '<name>.html'],
]"
/>
Pages are accessible at `'/<name>.html'`:
+7 -3
View File
@@ -4,12 +4,16 @@ TypeScript files that are built, but are not included in the manifest.
## Filenames
- `entrypoints/<name>.tsx?`
- `entrypoints/<name>/index.tsx?`
<EntrypointPatterns
:patterns="[
['<name>.tsx?', '<name>.js'],
['<name>/index.tsx?', '<name>.js'],
]"
/>
## Definition
Unlike the background and content scripts, you can define this script's logic in the main scope.
Unlike the background or content scripts, you can define this script's logic in the top level scope.
```ts
// Code goes here
+11 -3
View File
@@ -5,8 +5,11 @@ titleTemplate: 'Next Generation Web Extension Framework'
hero:
name: WXT
text: Next gen framework for web extensions
text: Next-gen Web Extension Framework
tagline: Powered by Vite, inspired by Nuxt.
image:
src: /hero-logo.svg
alt: WXT
actions:
- theme: brand
text: Get Started
@@ -38,12 +41,17 @@ features:
title: Bundle Remote Code
details: Downloads and bundles remote code imported from URLs.
- icon: 🎨
title: Frontend framework agnostic
title: Frontend Framework Agnostic
details: Works with any front-end framework with a Vite plugin.
- icon: 🖍️
title: Bootstrap a New Project
details: Comes with starter templates for all major frontend frameworks.
- icon: 🤖
title: Automated Publishing
details: 'TODO: Automatically zip, upload, and release extensions.'
- icon: 📏
title: Bundle analysis
title: Bundle Analysis
details: 'TODO: Tools for analyizing the final extension bundle.'
---
<UsingWxtSection />
Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M354.435 418.141C383.217 418.141 406.55 394.808 406.55 366.026V313.91H416.026C444.808 313.91 468.141 290.578 468.141 261.795C468.141 233.013 444.808 209.68 416.026 209.68H406.55V157.565C406.55 128.783 383.217 105.45 354.435 105.45H302.32V95.9745C302.32 67.1921 278.987 43.8594 250.205 43.8594C221.422 43.8594 198.09 67.1921 198.09 95.9745V105.45H145.974C117.192 105.45 93.8594 128.783 93.8594 157.565V209.68H103.335C132.117 209.68 155.45 233.013 155.45 261.795C155.45 290.578 132.117 313.91 103.335 313.91H93.8594V418.141H198.09V408.665C198.09 379.883 221.422 356.55 250.205 356.55C278.987 356.55 302.32 379.883 302.32 408.665V418.141H354.435Z" stroke="#67D55E" stroke-width="20"/>
</svg>

After

Width:  |  Height:  |  Size: 798 B

+10
View File
@@ -0,0 +1,10 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_305_490)">
<path d="M348.608 492C384.905 492 414.329 462.576 414.329 426.279V360.557H426.279C462.576 360.557 492 331.132 492 294.835C492 258.538 462.576 229.114 426.279 229.114H414.329V163.392C414.329 127.095 384.905 97.6709 348.608 97.6709H282.886V85.7215C282.886 49.4245 253.462 20 217.165 20C180.868 20 151.443 49.4245 151.443 85.7215V97.6709H85.7215C49.4245 97.6709 20 127.095 20 163.392V229.114H31.9494C68.2464 229.114 97.6709 258.538 97.6709 294.835C97.6709 331.132 68.2464 360.557 31.9494 360.557H20V492H151.443V480.051C151.443 443.754 180.868 414.329 217.165 414.329C253.462 414.329 282.886 443.754 282.886 480.051V492H348.608Z" stroke="#67D55E" stroke-width="40"/>
</g>
<defs>
<clipPath id="clip0_305_490">
<rect width="512" height="512" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 914 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 487 KiB

+24 -17
View File
@@ -21,10 +21,17 @@ describe('Auto Imports', () => {
".wxt/types/paths.d.ts
----------------------------------------
// Generated by wxt
type EntrypointPath =
| \\"/background.js\\"
| \\"/content-scripts/overlay.js\\"
| \\"/popup.html\\"
import \\"wxt/browser\\";
declare module \\"wxt/browser\\" {
type PublicPath =
| \\"/background.js\\"
| \\"/content-scripts/overlay.js\\"
| \\"/popup.html\\"
export interface ProjectRuntime extends Runtime.Static {
getURL(path: PublicPath): string;
}
}
"
`);
});
@@ -37,18 +44,18 @@ describe('Auto Imports', () => {
expect(await project.serializeFile('.wxt/types/imports.d.ts'))
.toMatchInlineSnapshot(`
".wxt/types/imports.d.ts
----------------------------------------
// Generated by wxt
export {}
declare global {
const browser: typeof import('webextension-polyfill')
const defineBackground: typeof import('wxt/client')['defineBackground']
const defineConfig: typeof import('wxt')['defineConfig']
const defineContentScript: typeof import('wxt/client')['defineContentScript']
const mountContentScriptUi: typeof import('wxt/client')['mountContentScriptUi']
}
"
`);
".wxt/types/imports.d.ts
----------------------------------------
// Generated by wxt
export {}
declare global {
const browser: typeof import('wxt/browser')['browser']
const defineBackground: typeof import('wxt/client')['defineBackground']
const defineConfig: typeof import('wxt')['defineConfig']
const defineContentScript: typeof import('wxt/client')['defineContentScript']
const mountContentScriptUi: typeof import('wxt/client')['mountContentScriptUi']
}
"
`);
});
});
+44
View File
@@ -0,0 +1,44 @@
import { describe, it, expect } from 'vitest';
import { TestProject } from '../utils';
import { execaCommand } from 'execa';
import glob from 'fast-glob';
describe('Init command', () => {
it('should download and create a template', async () => {
const project = new TestProject();
await execaCommand(`pnpm -s wxt init ${project.root} -t vue --pm npm`, {
env: { ...process.env, CI: 'true' },
stdio: 'ignore',
});
const files = await glob('**/*', {
cwd: project.root,
onlyFiles: true,
dot: true,
});
expect(files.sort()).toMatchInlineSnapshot(`
[
".gitignore",
".vscode/extensions.json",
"README.md",
"assets/vue.svg",
"components/HelloWorld.vue",
"entrypoints/background.ts",
"entrypoints/popup/App.vue",
"entrypoints/popup/index.html",
"entrypoints/popup/main.ts",
"entrypoints/popup/style.css",
"package.json",
"public/icon/128.png",
"public/icon/16.png",
"public/icon/32.png",
"public/icon/48.png",
"public/icon/96.png",
"public/wxt.svg",
"tsconfig.json",
"wxt.config.ts",
]
`);
}, 30e3);
});
+3 -3
View File
@@ -57,7 +57,7 @@ describe('Manifest Content', () => {
const optionsContent = `
<html>
<head>
<meta name="manifest.open_in_tab" content="true">
<meta name="manifest.open_in_tab" content="false">
<meta name="manifest.chrome_style" content="true">
<meta name="manifest.browser_style" content="true">
</head>
@@ -72,7 +72,7 @@ describe('Manifest Content', () => {
const manifest = await project.getOutputManifest();
expect(manifest.options_ui).toEqual({
open_in_tab: true,
open_in_tab: false,
chrome_style: true,
page: 'options.html',
});
@@ -88,7 +88,7 @@ describe('Manifest Content', () => {
);
expect(manifest.options_ui).toEqual({
open_in_tab: true,
open_in_tab: false,
browser_style: true,
page: 'options.html',
});
+3 -3
View File
@@ -13,7 +13,7 @@ describe('Output Directory Structure', () => {
expect(await project.serializeOutput()).toMatchInlineSnapshot(`
".output/chrome-mv3/manifest.json
----------------------------------------
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\"}"
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\"}"
`);
});
@@ -66,7 +66,7 @@ describe('Output Directory Structure', () => {
================================================================================
.output/chrome-mv3/manifest.json
----------------------------------------
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"content_scripts\\":[{\\"matches\\":[\\"*://*/*\\"],\\"css\\":[\\"assets/one.css\\",\\"assets/two.css\\"],\\"js\\":[\\"content-scripts/one.js\\",\\"content-scripts/two.js\\"]}]}"
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"content_scripts\\":[{\\"matches\\":[\\"*://*/*\\"],\\"css\\":[\\"assets/one.css\\",\\"assets/two.css\\"],\\"js\\":[\\"content-scripts/one.js\\",\\"content-scripts/two.js\\"]}]}"
`);
});
@@ -90,7 +90,7 @@ describe('Output Directory Structure', () => {
================================================================================
.output/chrome-mv3/manifest.json
----------------------------------------
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"content_scripts\\":[{\\"matches\\":[\\"*://*/*\\"],\\"js\\":[\\"content-scripts/overlay-one.js\\"]}]}"
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"content_scripts\\":[{\\"matches\\":[\\"*://*/*\\"],\\"js\\":[\\"content-scripts/overlay-one.js\\"]}]}"
`);
});
});
+3 -3
View File
@@ -27,7 +27,7 @@ describe('User Config', () => {
================================================================================
.output/chrome-mv3/manifest.json
----------------------------------------
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"background\\":{\\"service_worker\\":\\"background.js\\"}}"
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"background\\":{\\"service_worker\\":\\"background.js\\"}}"
`);
});
@@ -52,7 +52,7 @@ describe('User Config', () => {
================================================================================
.output/chrome-mv3/manifest.json
----------------------------------------
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"background\\":{\\"service_worker\\":\\"background.js\\"}}"
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"background\\":{\\"service_worker\\":\\"background.js\\"}}"
`);
});
@@ -80,7 +80,7 @@ describe('User Config', () => {
expect(output).toMatchInlineSnapshot(`
".output/chrome-mv3/manifest.json
----------------------------------------
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"example_customization\\":[\\"production\\",\\"chrome\\",\\"3\\",\\"build\\"]}"
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"example_customization\\":[\\"production\\",\\"chrome\\",\\"3\\",\\"build\\"]}"
`);
});
});
+4 -2
View File
@@ -3,11 +3,12 @@ import fs from 'fs-extra';
import glob from 'fast-glob';
import { execSync } from 'child_process';
import { InlineConfig, UserConfig, build } from '../src';
import { normalizePath } from '../src/core/utils/paths';
export class TestProject {
files: Array<[string, string]> = [];
config: UserConfig | undefined;
private readonly root: string;
readonly root: string;
constructor(root = 'e2e/project') {
// We can't put each test's project inside e2e/project directly, otherwise the wxt.config.ts
@@ -97,6 +98,7 @@ export class TestProject {
private async serializeDir(dir: string): Promise<string> {
const outputFiles = await glob('**/*', {
cwd: resolve(this.root, dir),
ignore: ['**/node_modules', '**/.output'],
});
outputFiles.sort();
const fileContents = [];
@@ -113,7 +115,7 @@ export class TestProject {
async serializeFile(path: string): Promise<string> {
const absolutePath = resolve(this.root, path);
return [
relative(this.root, absolutePath),
normalizePath(relative(this.root, absolutePath)),
await fs.readFile(absolutePath),
].join(`\n${''.padEnd(40, '-')}\n`);
}
+15 -6
View File
@@ -1,7 +1,7 @@
{
"name": "wxt",
"type": "module",
"version": "0.2.2",
"version": "0.3.1",
"description": "Next gen framework for developing web extensions",
"engines": {
"node": ">=18.16.0",
@@ -26,9 +26,10 @@
},
"license": "MIT",
"files": [
"bin",
"dist"
],
"bin": "dist/cli.cjs",
"bin": "./bin/wxt.cjs",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -39,9 +40,12 @@
"types": "./dist/index.d.ts"
},
"./client": {
"require": "./dist/client.cjs",
"import": "./dist/client.js",
"types": "./dist/client.d.ts"
},
"./browser": {
"import": "./dist/browser.js",
"types": "./dist/browser.d.ts"
}
},
"scripts": {
@@ -69,24 +73,29 @@
"fast-glob": "^3.2.12",
"filesize": "^10.0.7",
"fs-extra": "^11.1.1",
"get-port": "^7.0.0",
"giget": "^1.1.2",
"jiti": "^1.18.2",
"json5": "^2.2.3",
"linkedom": "^0.14.26",
"minimatch": "^9.0.3",
"picocolors": "^1.0.0",
"picomatch": "^2.3.1",
"prompts": "^2.4.2",
"unimport": "^3.0.8",
"vite": "^4.3.9",
"vite-tsconfig-paths": "^4.2.0",
"web-ext": "^7.6.2",
"webextension-polyfill": "^0.10.0"
"webextension-polyfill": "^0.10.0",
"zip-dir": "^2.0.0"
},
"devDependencies": {
"@faker-js/faker": "^8.0.2",
"@types/fs-extra": "^11.0.1",
"@types/lodash.merge": "^4.6.7",
"@types/node": "^20.3.1",
"@types/picomatch": "^2.3.0",
"@types/prompts": "^2.4.4",
"@vitest/coverage-v8": "^0.32.2",
"execa": "^7.2.0",
"lodash.merge": "^4.6.2",
"npm-run-all": "^4.1.5",
"ora": "^6.3.1",
+1431 -3826
View File
File diff suppressed because it is too large Load Diff
+10 -16
View File
@@ -1,7 +1,6 @@
import tsup from 'tsup';
import glob from 'fast-glob';
import pc from 'picocolors';
import path from 'path';
import { printFileList } from '../src/core/log/printFileList';
import { formatDuration } from '../src/core/utils/formatDuration';
import ora from 'ora';
import fs from 'fs-extra';
@@ -38,6 +37,13 @@ await Promise.all([
dts: true,
silent: true,
}),
tsup.build({
entry: { browser: 'src/client/browser.ts' },
format: ['esm'],
sourcemap: 'inline',
dts: true,
silent: true,
}),
...virtualEntrypoints.map((entryName) =>
tsup.build({
entry: {
@@ -73,18 +79,6 @@ await Promise.all([
spinner.succeed();
const duration = Date.now() - startTime;
const outFiles = await glob(`${outDir}/**`);
outFiles.forEach((file, i) => {
const color = file.endsWith('.map')
? pc.dim
: file.endsWith('.d.ts')
? pc.blue
: pc.cyan;
const prefix = i === outFiles.length - 1 ? ' └─' : ' ├─';
console.log(
`${pc.gray(prefix)} ${pc.dim(outDir + path.sep)}${color(
path.relative(outDir, file),
)}`,
);
});
const outFiles = await glob(`${outDir}/**`, { absolute: true });
await printFileList(consola.log, outDir, outFiles);
consola.success(`Finished in ${formatDuration(duration)}`);
+16
View File
@@ -0,0 +1,16 @@
declare module 'zip-dir' {
// Represents the options object for zipdir function
interface ZipDirOptions {
saveTo?: string;
filter?: (path: string, stat: import('fs').Stats) => boolean;
each?: (path: string) => void;
}
function zipdir(
dirPath: string,
options?: ZipDirOptions,
callback?: (error: Error | null, buffer: Buffer) => void,
): Promise<Buffer>;
export = zipdir;
}
+1
View File
@@ -3,3 +3,4 @@ export * from './dev';
export * from './init';
export * from './prepare';
export * from './publish';
export * from './zip';
+176 -4
View File
@@ -1,6 +1,178 @@
import { consola } from 'consola';
import { defineCommand } from '../utils/defineCommand';
import prompts from 'prompts';
import ora from 'ora';
import { consola } from 'consola';
import { downloadTemplate } from 'giget';
import fs from 'fs-extra';
import path from 'node:path';
import pc from 'picocolors';
import { Formatter } from 'picocolors/types';
export const init = defineCommand<[directory?: string]>(async (directory) => {
consola.warn('wxt init: Not implemented');
});
export const init = defineCommand<
[directory: string | undefined, options: { template?: string; pm?: string }]
>(
async (userDirectory, flags) => {
consola.info('Initalizing new project');
const templates = await listTemplates();
const defaultTemplate = templates.find(
(template) => template.name === flags.template?.toLowerCase().trim(),
);
const input = await prompts(
[
{
name: 'directory',
type: () => (userDirectory == null ? 'text' : undefined),
message: 'Project Directory',
initial: userDirectory,
},
{
name: 'template',
type: () => (defaultTemplate == null ? 'select' : undefined),
message: 'Choose a template',
choices: templates.map((template) => ({
title:
TEMPLATE_COLORS[template.name]?.(template.name) ?? template.name,
value: template,
})),
},
{
name: 'packageManager',
type: () => (flags.pm == null ? 'select' : undefined),
message: 'Package Manager',
choices: [
{ title: 'npm', value: 'npm' },
{ title: 'pnpm', value: 'pnpm' },
{ title: 'yarn', value: 'yarn' },
],
},
],
{
onCancel: () => process.exit(1),
},
);
input.directory ??= userDirectory;
input.template ??= defaultTemplate;
input.packageManager ??= flags.pm;
await cloneProject(input);
const cdPath = path.relative(process.cwd(), path.resolve(input.directory));
console.log();
consola.log(
`✨ WXT project created with the ${
TEMPLATE_COLORS[input.template.name]?.(input.template.name) ??
input.template.name
} template.`,
);
console.log();
consola.log('Next steps:');
let step = 0;
if (cdPath !== '.') consola.log(` ${++step}.`, pc.cyan(`cd ${cdPath}`));
consola.log(` ${++step}.`, pc.cyan(`${input.packageManager} install`));
console.log();
},
{ disableFinishedLog: true },
);
interface Template {
/**
* Template's name.
*/
name: string;
/**
* Path to template directory in github repo.
*/
path: string;
}
async function listTemplates(): Promise<Template[]> {
try {
const res = await fetch(
'https://api.github.com/repos/aklinker1/wxt/contents/templates',
{
headers: {
Accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28',
},
},
);
if (res.status >= 300)
throw Error(`Request failed with status ${res.status} ${res.statusText}`);
const data = (await res.json()) as Array<{
type: 'file' | 'dir';
name: string;
path: string;
}>;
return data
.filter((item: any) => item.type === 'dir')
.map((item) => ({ name: item.name, path: item.path }))
.sort((l, r) => {
const lWeight = TEMPLATE_SORT_WEIGHT[l.name] ?? Number.MAX_SAFE_INTEGER;
const rWeight = TEMPLATE_SORT_WEIGHT[r.name] ?? Number.MAX_SAFE_INTEGER;
const diff = lWeight - rWeight;
if (diff !== 0) return diff;
return l.name.localeCompare(r.name);
});
} catch (err) {
throw Error(`Cannot load templates: ${JSON.stringify(err, null, 2)}`);
}
}
async function cloneProject({
directory,
template,
packageManager,
}: {
directory: string;
template: Template;
packageManager: string;
}) {
const spinner = ora('Downloading template').start();
try {
// 1. Clone repo
await downloadTemplate(`gh:aklinker1/wxt/${template.path}`, {
dir: directory,
force: true,
});
// 2. Move _gitignore -> .gitignore
await fs
.move(
path.join(directory, '_gitignore'),
path.join(directory, '.gitignore'),
)
.catch((err) =>
consola.warn('Failed to move _gitignore to .gitignore:', err),
);
// 3. Add .npmrc for pnpm
if (packageManager === 'pnpm') {
await fs.writeFile(
path.join(directory, '.npmrc'),
'shamefully-hoist=true\n',
);
}
spinner.succeed();
} catch (err) {
spinner.fail();
throw Error(`Failed to setup new project: ${JSON.stringify(err, null, 2)}`);
}
}
const TEMPLATE_COLORS: Record<string, Formatter> = {
vanilla: pc.blue,
vue: pc.green,
react: pc.cyan,
svelte: pc.red,
solid: pc.blue,
};
const TEMPLATE_SORT_WEIGHT: Record<string, number> = {
vanilla: 0,
vue: 1,
react: 2,
};
+31
View File
@@ -0,0 +1,31 @@
import * as wxt from '../..';
import { buildInternal } from '../../core/build';
import { getInternalConfig } from '../../core/utils/getInternalConfig';
import { zipExtension } from '../../core/zip';
import { defineCommand } from '../utils/defineCommand';
export const zip = defineCommand<
[
root: string | undefined,
flags: {
mode?: string;
config?: string;
browser?: wxt.TargetBrowser;
mv3?: boolean;
mv2?: boolean;
},
]
>(async (root, flags) => {
const mode = flags.mode ?? 'production';
const cliConfig: wxt.InlineConfig = {
root,
mode,
browser: flags.browser,
manifestVersion: flags.mv3 ? 3 : flags.mv2 ? 2 : undefined,
configFile: flags.config,
};
const config = await getInternalConfig(cliConfig, 'build');
const output = await buildInternal(config);
await zipExtension(config, output);
});
+12 -2
View File
@@ -1,5 +1,3 @@
#!/usr/bin/env node
import cac from 'cac';
import { version } from '../../package.json';
import * as commands from './commands';
@@ -28,6 +26,16 @@ cli
.option('--mv2', 'target manifest v2')
.action(commands.build);
// ZIP
cli
.command('zip [root]', 'build for production and zip output')
.option('-c, --config <file>', 'use specified config file')
.option('-m, --mode <mode>', 'set env mode')
.option('-b, --browser <browser>', 'specify a browser')
.option('--mv3', 'target manifest v3')
.option('--mv2', 'target manifest v2')
.action(commands.zip);
// PREPARE
cli
.command('prepare [root]', 'prepare')
@@ -40,6 +48,8 @@ cli.command('publish [root]', 'publish to stores').action(commands.publish);
// INIT
cli
.command('init [directory]', 'initialize a new project')
.option('-t, --template <template>', 'template to use')
.option('--pm <packageManager>', 'which package manager to use')
.action(commands.init);
cli.parse();
+4 -1
View File
@@ -4,6 +4,9 @@ import { formatDuration } from '../../core/utils/formatDuration';
export function defineCommand<TArgs extends any[]>(
cb: (...args: TArgs) => void | boolean | Promise<void | boolean>,
options?: {
disableFinishedLog?: boolean;
},
) {
return async (...args: TArgs) => {
const startTime = Date.now();
@@ -12,7 +15,7 @@ export function defineCommand<TArgs extends any[]>(
const ongoing = await cb(...args);
if (!ongoing)
if (!ongoing && !options?.disableFinishedLog)
consola.success(
`Finished in ${formatDuration(Date.now() - startTime)}`,
);
+11
View File
@@ -0,0 +1,11 @@
import originalBrowser, { Browser, Runtime } from 'webextension-polyfill';
export interface AugmentedBrowser extends Browser {
runtime: ProjectRuntime;
}
export interface ProjectRuntime extends Runtime.Static {
// Overriden per-project
}
export const browser: AugmentedBrowser = originalBrowser;
+6 -2
View File
@@ -39,7 +39,7 @@ export async function buildInternal(
const entrypoints = await findEntrypoints(config);
const groups = groupEntrypoints(entrypoints);
const { output } = await rebuild(config, groups);
const { output } = await rebuild(config, groups, undefined);
// Post-build
config.logger.success(
@@ -70,7 +70,11 @@ export async function rebuild(
): Promise<{ output: BuildOutput; manifest: Manifest.WebExtensionManifest }> {
// Update types directory with new files and types
const allEntrypoints = await findEntrypoints(config);
await generateTypesDir(allEntrypoints, config);
await generateTypesDir(allEntrypoints, config).catch((err) => {
config.logger.warn('Failed to update .wxt directory:', err);
// Throw the error if doing a regular build, don't for dev mode.
if (config.command === 'build') throw err;
});
// Build and merge the outputs
const newOutput = await buildEntrypoints(entrypointGroups, config);
@@ -12,6 +12,7 @@ import fs from 'fs-extra';
import { importTsFile } from '../../utils/importTsFile';
import glob from 'fast-glob';
import { fakeInternalConfig } from '../../../testing/fake-objects';
import { unnormalizePath } from '../../utils/paths';
vi.mock('../../utils/importTsFile');
const importTsFileMock = vi.mocked(importTsFile);
@@ -29,6 +30,7 @@ describe('findEntrypoints', () => {
root: '/',
entrypointsDir: resolve('/src/entrypoints'),
outDir: resolve('.output'),
command: 'build',
});
it.each<[string, string, PopupEntrypoint]>([
@@ -210,10 +212,10 @@ describe('findEntrypoints', () => {
},
],
])(
'should find and load content script entrypoint config from %s',
'should find and load background entrypoint config from %s',
async (path, expected) => {
const options: ContentScriptEntrypoint['options'] = {
matches: ['<all_urls>'],
const options: BackgroundEntrypoint['options'] = {
type: 'module',
};
globMock.mockResolvedValueOnce([path]);
importTsFileMock.mockResolvedValue(options);
@@ -226,6 +228,24 @@ describe('findEntrypoints', () => {
},
);
it("should include a virtual background script so dev reloading works when there isn't a background entrypoint defined by the user", async () => {
globMock.mockResolvedValueOnce([]);
const entrypoints = await findEntrypoints({
...config,
command: 'serve',
});
expect(entrypoints).toHaveLength(1);
expect(entrypoints[0]).toEqual({
type: 'background',
inputPath: 'virtual:user-background',
name: 'background',
options: {},
outputDir: config.outDir,
});
});
it.each<[string, GenericEntrypoint]>([
// Sandbox
[
@@ -413,6 +433,73 @@ describe('findEntrypoints', () => {
outputDir: config.outDir,
},
],
[
'injected/index.ts',
{
type: 'unlisted-script',
name: 'injected',
inputPath: resolve(config.entrypointsDir, 'injected/index.ts'),
outputDir: config.outDir,
},
],
// unlisted-style
[
'iframe.scss',
{
type: 'unlisted-style',
name: 'iframe',
inputPath: resolve(config.entrypointsDir, 'iframe.scss'),
outputDir: config.outDir,
},
],
[
'iframe.css',
{
type: 'unlisted-style',
name: 'iframe',
inputPath: resolve(config.entrypointsDir, 'iframe.css'),
outputDir: config.outDir,
},
],
// content-script-style
[
'content.css',
{
type: 'content-script-style',
name: 'content',
inputPath: resolve(config.entrypointsDir, 'content.css'),
outputDir: resolve(config.outDir, 'content-scripts'),
},
],
[
'overlay.content.css',
{
type: 'content-script-style',
name: 'overlay',
inputPath: resolve(config.entrypointsDir, 'overlay.content.css'),
outputDir: resolve(config.outDir, 'content-scripts'),
},
],
[
'content/index.css',
{
type: 'content-script-style',
name: 'content',
inputPath: resolve(config.entrypointsDir, 'content/index.css'),
outputDir: resolve(config.outDir, 'content-scripts'),
},
],
[
'overlay.content/index.css',
{
type: 'content-script-style',
name: 'overlay',
inputPath: resolve(config.entrypointsDir, 'overlay.content/index.css'),
outputDir: resolve(config.outDir, 'content-scripts'),
},
],
])('should find entrypoint for %s', async (path, expected) => {
globMock.mockResolvedValueOnce([path]);
@@ -424,9 +511,14 @@ describe('findEntrypoints', () => {
it('should not allow multiple entrypoints with the same name', async () => {
globMock.mockResolvedValueOnce(['popup.html', 'popup/index.html']);
const expectedPaths = [
'src/entrypoints/popup.html',
'src/entrypoints/popup/index.html',
].map(unnormalizePath);
await expect(() => findEntrypoints(config)).rejects.toThrowError(
'Multiple entrypoints with the name "popup" detected, but only one is allowed: src/entrypoints/popup.html, src/entrypoints/popup/index.html',
'Multiple entrypoints with the name "popup" detected, but only one is allowed: ' +
expectedPaths.join(', '),
);
});
});
+15 -7
View File
@@ -6,12 +6,12 @@ import {
EntrypointGroup,
InternalConfig,
} from '../types';
import * as plugins from '../vite-plugins';
import * as wxtPlugins from '../vite-plugins';
import { removeEmptyDirs } from '../utils/removeEmptyDirs';
import { getEntrypointBundlePath } from '../utils/entrypoints';
import glob from 'fast-glob';
import fs from 'fs-extra';
import { dirname, resolve } from 'path';
import { getPublicFiles } from '../utils/public';
export async function buildEntrypoints(
groups: EntrypointGroup[],
@@ -45,7 +45,16 @@ async function buildSingleEntrypoint(
? `virtual:wxt-${entrypoint.type}?${entrypoint.inputPath}`
: entrypoint.inputPath;
const plugins: NonNullable<vite.UserConfig['plugins']> = [];
if (
entrypoint.type === 'content-script-style' ||
entrypoint.type === 'unlisted-style'
) {
plugins.push(wxtPlugins.cssEntrypoints(entrypoint, config));
}
const libMode: vite.UserConfig = {
plugins,
build: {
lib: {
entry,
@@ -90,7 +99,7 @@ async function buildMultipleEntrypoints(
config: InternalConfig,
): Promise<BuildStepOutput> {
const multiPage: vite.UserConfig = {
plugins: [plugins.multipageMove(entrypoints, config)],
plugins: [wxtPlugins.multipageMove(entrypoints, config)],
build: {
rollupOptions: {
input: entrypoints.reduce<Record<string, string>>((input, entry) => {
@@ -132,11 +141,10 @@ function getBuildOutputChunks(
async function copyPublicDirectory(
config: InternalConfig,
): Promise<BuildOutput['publicAssets']> {
const files = await getPublicFiles(config);
if (files.length === 0) return [];
const publicAssets: BuildOutput['publicAssets'] = [];
if (!(await fs.exists(config.publicDir))) return publicAssets;
const files = await glob('**/*', { cwd: config.publicDir });
for (const file of files) {
const srcPath = resolve(config.publicDir, file);
const outPath = resolve(config.outDir, file);
+45 -11
View File
@@ -10,15 +10,17 @@ import {
PopupEntrypoint,
} from '../types';
import fs from 'fs-extra';
import picomatch from 'picomatch';
import { minimatch } from 'minimatch';
import { parseHTML } from 'linkedom';
import JSON5 from 'json5';
import { importTsFile } from '../utils/importTsFile';
import glob from 'fast-glob';
import { getEntrypointName } from '../utils/entrypoints';
import { VIRTUAL_NOOP_BACKGROUND_MODULE_ID } from '../vite-plugins/noopBackground';
import { CSS_EXTENSIONS_PATTERN } from '../utils/paths';
/**
* Return entrypoints and their configuration by looking through the
* Return entrypoints and their configuration by looking through the project's files.
*/
export async function findEntrypoints(
config: InternalConfig,
@@ -33,11 +35,12 @@ export async function findEntrypoints(
const existingNames: Record<string, Entrypoint | undefined> = {};
const entrypoints: Entrypoint[] = [];
let hasBackground = false;
await Promise.all(
relativePaths.map(async (relativePath) => {
const path = resolve(config.entrypointsDir, relativePath);
const matchingGlob = pathGlobs.find((glob) =>
picomatch.isMatch(relativePath, glob),
minimatch(relativePath, glob),
);
if (matchingGlob == null) {
@@ -63,6 +66,7 @@ export async function findEntrypoints(
break;
case 'background':
entrypoint = await getBackgroundEntrypoint(config, path);
hasBackground = true;
break;
case 'content-script':
entrypoint = await getContentScriptEntrypoint(
@@ -71,6 +75,14 @@ export async function findEntrypoints(
path,
);
break;
case 'content-script-style':
entrypoint = {
type,
name: getEntrypointName(config.entrypointsDir, path),
inputPath: path,
outputDir: resolve(config.outDir, CONTENT_SCRIPT_OUT_DIR),
};
break;
default:
entrypoint = {
type,
@@ -95,6 +107,11 @@ export async function findEntrypoints(
existingNames[entrypoint.name] = entrypoint;
}),
);
if (config.command === 'serve' && !hasBackground) {
entrypoints.push(
await getBackgroundEntrypoint(config, VIRTUAL_NOOP_BACKGROUND_MODULE_ID),
);
}
return entrypoints;
}
@@ -162,21 +179,21 @@ async function getOptionsEntrypoint(
.querySelector("meta[name='manifest.open_in_tab']")
?.getAttribute('content');
if (openInTabContent) {
options.openInTab = Boolean(openInTabContent);
options.openInTab = openInTabContent === 'true';
}
const chromeStyleContent = document
.querySelector("meta[name='manifest.chrome_style']")
?.getAttribute('content');
if (chromeStyleContent) {
options.chromeStyle = Boolean(chromeStyleContent);
options.chromeStyle = chromeStyleContent === 'true';
}
const browserStyleContent = document
.querySelector("meta[name='manifest.browser_style']")
?.getAttribute('content');
if (browserStyleContent) {
options.browserStyle = Boolean(browserStyleContent);
options.browserStyle = browserStyleContent === 'true';
}
return {
@@ -195,10 +212,17 @@ async function getBackgroundEntrypoint(
config: InternalConfig,
path: string,
): Promise<BackgroundEntrypoint> {
const { main: _, ...options } =
await importTsFile<BackgroundScriptDefintition>(path, config);
if (options == null) {
throw Error('Background script does not have a default export');
let options: Omit<BackgroundScriptDefintition, 'main'> = {};
if (path !== VIRTUAL_NOOP_BACKGROUND_MODULE_ID) {
const defaultExport = await importTsFile<BackgroundScriptDefintition>(
path,
config,
);
if (defaultExport == null) {
throw Error('Background script does not have a default export');
}
const { main: _, ...moduleOptions } = defaultExport;
options = moduleOptions;
}
return {
type: 'background',
@@ -228,7 +252,7 @@ async function getContentScriptEntrypoint(
type: 'content-script',
name: getEntrypointName(config.entrypointsDir, path),
inputPath: path,
outputDir: resolve(config.outDir, 'content-scripts'),
outputDir: resolve(config.outDir, CONTENT_SCRIPT_OUT_DIR),
options,
};
}
@@ -257,11 +281,16 @@ const PATH_GLOB_TO_TYPE_MAP: Record<string, Entrypoint['type'] | 'ignored'> = {
'devtools/index.html': 'devtools',
'background.ts': 'background',
[VIRTUAL_NOOP_BACKGROUND_MODULE_ID]: 'background',
'content.ts?(x)': 'content-script',
'content/index.ts?(x)': 'content-script',
'*.content.ts?(x)': 'content-script',
'*.content/index.ts?(x)': 'content-script',
[`content.${CSS_EXTENSIONS_PATTERN}`]: 'content-script-style',
[`*.content.${CSS_EXTENSIONS_PATTERN}`]: 'content-script-style',
[`content/index.${CSS_EXTENSIONS_PATTERN}`]: 'content-script-style',
[`*.content/index.${CSS_EXTENSIONS_PATTERN}`]: 'content-script-style',
'popup.html': 'popup',
'popup/index.html': 'popup',
@@ -272,7 +301,12 @@ const PATH_GLOB_TO_TYPE_MAP: Record<string, Entrypoint['type'] | 'ignored'> = {
'*.html': 'unlisted-page',
'*/index.html': 'unlisted-page',
'*.ts': 'unlisted-script',
'*/index.ts': 'unlisted-script',
[`*.${CSS_EXTENSIONS_PATTERN}`]: 'unlisted-style',
[`*/index.${CSS_EXTENSIONS_PATTERN}`]: 'unlisted-style',
// Don't warn about any files in subdirectories, like CSS or JS entrypoints for HTML files
'*/*': 'ignored',
};
const CONTENT_SCRIPT_OUT_DIR = 'content-scripts';
+35 -26
View File
@@ -5,6 +5,8 @@ import { relative, resolve } from 'path';
import { getEntrypointBundlePath } from '../utils/entrypoints';
import { getUnimportOptions } from '../utils/auto-imports';
import { getGlobals } from '../utils/globals';
import { getPublicFiles } from '../utils/public';
import { normalizePath } from '../utils/paths';
/**
* Generate and write all the files inside the `InternalConfig.typesDir` directory.
@@ -49,23 +51,34 @@ async function writePathsDeclarationFile(
): Promise<string> {
const filePath = resolve(config.typesDir, 'paths.d.ts');
const unions = entrypoints
.map((entry) => {
const path = getEntrypointBundlePath(
.map((entry) =>
getEntrypointBundlePath(
entry,
config.outDir,
entry.inputPath.endsWith('.html') ? '.html' : '.js',
);
return ` | "/${path}"`;
})
.sort();
),
)
.concat(await getPublicFiles(config))
.map(normalizePath)
.map((path) => ` | "/${path}"`)
.sort()
.join('\n');
const template = `// Generated by wxt
import "wxt/browser";
declare module "wxt/browser" {
type PublicPath =
{{ union }}
export interface ProjectRuntime extends Runtime.Static {
getURL(path: PublicPath): string;
}
}
`;
await fs.writeFile(
filePath,
[
'// Generated by wxt',
'type EntrypointPath =',
...(unions.length === 0 ? [' never'] : unions),
].join('\n') + '\n',
template.replace('{{ union }}', unions || ' | never'),
);
return filePath;
@@ -102,7 +115,8 @@ async function writeMainDeclarationFile(
'// Generated by wxt',
`/// <reference types="vite/client" />`,
...references.map(
(ref) => `/// <reference types="./${relative(dir, ref)}" />`,
(ref) =>
`/// <reference types="./${normalizePath(relative(dir, ref))}" />`,
),
].join('\n') + '\n',
);
@@ -125,31 +139,26 @@ async function writeTsConfigFile(
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
/* Type Checking */
"strict": true,
/* Completeness */
"lib": ["DOM", "WebWorker"],
"skipLibCheck": true,
/* Aliases */
"baseUrl": "${relative(dir, config.root)}",
"baseUrl": "${normalizePath(relative(dir, config.root))}",
"paths": {
"@@": ["."],
"@@/*": ["./*"],
"~~": ["."],
"~~/*": ["./*"],
"@": ["${relative(config.root, config.srcDir)}"],
"@/*": ["${relative(config.root, config.srcDir)}/*"],
"~": ["${relative(config.root, config.srcDir)}"],
"~/*": ["${relative(config.root, config.srcDir)}/*"]
"@": ["${normalizePath(relative(config.root, config.srcDir))}"],
"@/*": ["${normalizePath(relative(config.root, config.srcDir))}/*"],
"~": ["${normalizePath(relative(config.root, config.srcDir))}"],
"~/*": ["${normalizePath(relative(config.root, config.srcDir))}/*"]
}
},
"include": [
"${relative(dir, config.root)}/**/*",
"./${relative(dir, mainReference)}"
"${normalizePath(relative(dir, config.root))}/**/*",
"./${normalizePath(relative(dir, mainReference))}"
],
"exclude": ["${relative(dir, config.outBaseDir)}"]
"exclude": ["${normalizePath(relative(dir, config.outBaseDir))}"]
}`,
);
}
+4 -44
View File
@@ -1,9 +1,6 @@
import path, { relative, resolve } from 'path';
import { resolve } from 'path';
import { BuildOutput, InternalConfig } from '../types';
import { printTable } from './printTable';
import pc from 'picocolors';
import fs from 'fs-extra';
import { filesize } from 'filesize';
import { printFileList } from './printFileList';
export async function printBuildSummary(
output: BuildOutput,
@@ -20,31 +17,8 @@ export async function printBuildSummary(
return l.fileName.localeCompare(r.fileName);
});
let totalSize = 0;
const chunkRows: string[][] = await Promise.all(
chunks.map(async (chunk, i) => {
const file = [
relative(process.cwd(), config.outDir) + path.sep,
chunk.fileName,
];
const prefix = i === chunks.length - 1 ? ' └─' : ' ├─';
const color = getChunkColor(chunk.fileName);
const stats = await fs.lstat(resolve(config.outDir, chunk.fileName));
totalSize += stats.size;
const size = String(filesize(stats.size));
return [
`${pc.gray(prefix)} ${pc.dim(file[0])}${color(file[1])}`,
pc.dim(size),
];
}),
);
printTable(config.logger.log, chunkRows);
config.logger.log(
`${pc.cyan('Σ Total size:')} ${String(filesize(totalSize))}`,
);
const files = chunks.map((chunk) => resolve(config.outDir, chunk.fileName));
await printFileList(config.logger.log, config.outDir, files);
}
const DEFAULT_SORT_WEIGHT = 100;
@@ -62,17 +36,3 @@ function getChunkSortWeight(filename: string) {
)?.[1] ?? DEFAULT_SORT_WEIGHT
);
}
const DEFAULT_COLOR = pc.blue;
const CHUNK_COLORS: Record<string, (text: string) => string> = {
'.js.map': pc.gray,
'.html': pc.green,
'.css': pc.magenta,
'.js': pc.cyan,
};
function getChunkColor(filename: string) {
return (
Object.entries(CHUNK_COLORS).find(([key]) => filename.endsWith(key))?.[1] ??
DEFAULT_COLOR
);
}
+50
View File
@@ -0,0 +1,50 @@
import path from 'node:path';
import pc from 'picocolors';
import fs from 'fs-extra';
import { filesize } from 'filesize';
import { printTable } from './printTable';
export async function printFileList(
log: (message: string) => void,
baseDir: string,
files: string[],
): Promise<void> {
let totalSize = 0;
const fileRows: string[][] = await Promise.all(
files.map(async (file, i) => {
const parts = [
path.relative(process.cwd(), baseDir) + path.sep,
path.relative(baseDir, file),
];
const prefix = i === files.length - 1 ? ' └─' : ' ├─';
const color = getChunkColor(file);
const stats = await fs.lstat(file);
totalSize += stats.size;
const size = String(filesize(stats.size));
return [
`${pc.gray(prefix)} ${pc.dim(parts[0])}${color(parts[1])}`,
pc.dim(size),
];
}),
);
printTable(log, fileRows);
log(`${pc.cyan('Σ Total size:')} ${String(filesize(totalSize))}`);
}
const DEFAULT_COLOR = pc.blue;
const CHUNK_COLORS: Record<string, (text: string) => string> = {
'.js.map': pc.gray,
'.html': pc.green,
'.css': pc.magenta,
'.js': pc.cyan,
'.zip': pc.yellow,
};
function getChunkColor(filename: string) {
return (
Object.entries(CHUNK_COLORS).find(([key]) => filename.endsWith(key))?.[1] ??
DEFAULT_COLOR
);
}
+2 -2
View File
@@ -5,7 +5,6 @@ import {
WxtDevServer,
} from './types';
import * as vite from 'vite';
import { findOpenPort } from './utils/findOpenPort';
import { Scripting } from 'webextension-polyfill';
import { getEntrypointBundlePath } from './utils/entrypoints';
import { getContentScriptCssFiles } from './utils/manifest';
@@ -13,7 +12,8 @@ import { createWebExtRunner } from './runners/createWebExtRunner';
import { buildInternal } from './build';
export async function getServerInfo(): Promise<ServerInfo> {
const port = await findOpenPort(3000, 3010);
const { default: getPort, portNumbers } = await import('get-port');
const port = await getPort({ port: portNumbers(3000, 3010) });
const hostname = 'localhost';
const origin = `http://${hostname}:${port}`;
const serverConfig: vite.InlineConfig = {
+76 -26
View File
@@ -16,14 +16,14 @@ export interface InlineConfig {
* directory.
*
* @default
* "<root>"
* "<rootDir>"
*/
srcDir?: string;
/**
* Directory containing files that will be copied to the output directory as-is.
*
* @default
* "<srcDir>/publicDir"
* "<rootDir>/publicDir"
*/
publicDir?: string;
/**
@@ -92,6 +92,57 @@ export interface InlineConfig {
* Custom runner options. Options set here can be overridden in a `web-ext.config.ts` file.
*/
runner?: ExtensionRunnerConfig;
zip?: {
/**
* Configure the filename output when zipping files.
*
* Available template variables:
*
* - `{{name}}` - The project's name converted to kebab-case
* - `{{version}} - The version_name or version from the manifest
* - `{{browser}} - The target browser from the `--browser` CLI flag
* - `{{manifestVersion}}` - Either "2" or "3"
*
* @default "{{name}}-{{version}}-{{browser}}.zip"
*/
artifactTemplate?: string;
/**
* Configure the filename output when zipping files.
*
* Available template variables:
*
* - `{{name}}` - The project's name converted to kebab-case
* - `{{version}} - The version_name or version from the manifest
* - `{{browser}} - The target browser from the `--browser` CLI flag
* - `{{manifestVersion}}` - Either "2" or "3"
*
* @default "{{name}}-{{version}}-sources.zip"
*/
sourcesTemplate?: string;
/**
* Override the artifactTemplate's `{{name}}` template variable. Defaults to the package.json's
* name, or if that doesn't exist, the current working directories name.
*/
name?: string;
/**
* Root directory to ZIP. The ZIP can be uploaded to the Firefox Addon Store as your source
* code. Defaults to the `config.root` directory.
*/
sourcesRoot?: string;
/**
* [Minimatch](https://www.npmjs.com/package/minimatch) patterns of files to exclude when
* creating a ZIP of all your source code for Firfox. Patterns are relative to your
* `config.zip.sourcesRoot`.
*
* Hidden files, node_modules, and tests are ignored by default.
*
* @example
* [
* "coverage", // Ignore the coverage directory in the `sourcesRoot`
* ]
*/
ignoredSources?: string[];
};
}
export interface WxtInlineViteConfig
@@ -210,7 +261,9 @@ export interface GenericEntrypoint extends BaseEntrypoint {
| 'sidepanel'
| 'devtools'
| 'unlisted-page'
| 'unlisted-script';
| 'unlisted-script'
| 'unlisted-style'
| 'content-script-style';
}
export interface BackgroundEntrypoint extends BaseEntrypoint {
@@ -310,30 +363,27 @@ export interface BackgroundScriptDefintition {
}
/**
* Manifest customization available in the `wxt.config.ts` file. Any missing fields like "name"
* and "version" are managed automatically, and don't need to be listed here.
* Manifest customization available in the `wxt.config.ts` file. You cannot configure entrypoints
* here, they are configured inline.
*/
export type UserManifest = Omit<
Manifest.WebExtensionManifest,
| 'action'
| 'background'
| 'browser_action'
| 'chrome_url_overrides'
| 'content_scripts'
| 'description'
| 'devtools_page'
| 'manifest_version'
| 'name'
| 'options_page'
| 'options_ui'
| 'sandbox'
| 'page_action'
| 'popup'
| 'short_name'
| 'sidepanel'
| 'sidebar_action'
| 'version'
| 'version_name'
export type UserManifest = Partial<
Omit<
Manifest.WebExtensionManifest,
| 'action'
| 'background'
| 'browser_action'
| 'chrome_url_overrides'
| 'content_scripts'
| 'devtools_page'
| 'manifest_version'
| 'options_page'
| 'options_ui'
| 'sandbox'
| 'page_action'
| 'popup'
| 'sidepanel'
| 'sidebar_action'
>
>;
export type UserManifestFn = (
+7
View File
@@ -36,6 +36,13 @@ export interface InternalConfig {
fsCache: FsCache;
server?: WxtDevServer;
runnerConfig: ResolvedConfig<ExtensionRunnerConfig>;
zip: {
name?: string;
artifactTemplate: string;
sourcesTemplate: string;
ignoredSources: string[];
sourcesRoot: string;
};
}
export type EntrypointGroup = Entrypoint | Entrypoint[];
@@ -56,6 +56,18 @@ const sandbox2: Entrypoint = {
inputPath: '/sandbox2.html',
outputDir: '/.output/sandbox2',
};
const unlistedStyle: Entrypoint = {
type: 'unlisted-style',
name: 'injected',
inputPath: '/injected.scss',
outputDir: '/.output',
};
const contentScriptStyle: Entrypoint = {
type: 'content-script-style',
name: 'injected',
inputPath: '/overlay.content.scss',
outputDir: '/.output/content-scripts',
};
describe('groupEntrypoints', () => {
it('should keep scripts separate', () => {
@@ -72,6 +84,19 @@ describe('groupEntrypoints', () => {
expect(actual).toEqual(expected);
});
it('should keep styles separate', () => {
const entrypoints: Entrypoint[] = [
unlistedStyle,
contentScriptStyle,
popup,
];
const expected = [unlistedStyle, contentScriptStyle, [popup]];
const actual = groupEntrypoints(entrypoints);
expect(actual).toEqual(expected);
});
it('should group extension pages together', () => {
const entrypoints: Entrypoint[] = [
popup,
+45
View File
@@ -0,0 +1,45 @@
import { describe, expect, it } from 'vitest';
import { kebabCaseAlphanumeric, removeImportStatements } from '../strings';
describe('String utils', () => {
describe('kebabCaseAlphanumeric', () => {
it.each([
['HELLO', 'hello'],
['Hello, World!', 'hello-world'],
['hello123', 'hello123'],
['Hello World This Is A Test', 'hello-world-this-is-a-test'],
['Hello World', 'hello-world'],
['hello-world', 'hello-world'], // Ensure hyphens are preserved
])('should convert "%s" to "%s"', (input, expected) => {
expect(kebabCaseAlphanumeric(input)).toBe(expected);
});
});
describe('removeImportStatements', () => {
it('should remove all import formats', () => {
const imports = `
import { registerGithubService, createGithubApi } from "@/utils/github";
import {
registerGithubService,
createGithubApi
} from "@/utils/github";
import{ registerGithubService, createGithubApi }from "@/utils/github";
import GitHub from "@/utils/github";
import "@/utils/github";
import '@/utils/github';
import"@/utils/github"
import'@/utils/github';
`;
expect(removeImportStatements(imports).trim()).toEqual('');
});
it('should not remove import.meta or inline import statements', () => {
const imports = `
import.meta.env.DEV
const a = await import("example");
import("example");
`;
expect(removeImportStatements(imports)).toEqual(imports);
});
});
});
+3 -6
View File
@@ -7,13 +7,10 @@ export function getUnimportOptions(
): Partial<UnimportOptions> {
const defaultOptions: Partial<UnimportOptions> = {
debugLog: config.logger.debug,
imports: [
{ name: '*', as: 'browser', from: 'webextension-polyfill' },
{ name: 'defineConfig', from: 'wxt' },
],
presets: [{ package: 'wxt/client' }],
imports: [{ name: 'defineConfig', from: 'wxt' }],
presets: [{ package: 'wxt/client' }, { package: 'wxt/browser' }],
warn: config.logger.warn,
dirs: ['components', 'composables', 'hooks', 'utils'],
dirs: ['./components/*', './composables/*', './hooks/*', './utils/*'],
};
return mergeConfig(
+4 -2
View File
@@ -1,6 +1,7 @@
import { BuildOutput, BuildStepOutput, EntrypointGroup } from '../types';
import * as vite from 'vite';
import { every } from './arrays';
import { normalizePath } from './paths';
/**
* Compare the changed files vs the build output and determine what kind of reload needs to happen:
@@ -102,15 +103,16 @@ function findEffectedSteps(
currentOutput: BuildOutput,
): DetectedChange[] {
const changes: DetectedChange[] = [];
const changedPath = changedFile[1];
const changedPath = normalizePath(changedFile[1]);
const isChunkEffected = (
chunk: vite.Rollup.OutputChunk | vite.Rollup.OutputAsset,
): boolean =>
// If it's an HTML file with the same path, is is effected because HTML files need to be pre-rendered
// TODO: use bundle path to support `<name>/index.html`?
// fileName is normalized, relative bundle path
(chunk.type === 'asset' && changedPath.endsWith(chunk.fileName)) ||
// If it's a chunk that depends on the changed file, it is effected
// moduleIds are absolute, normalized paths
(chunk.type === 'chunk' && chunk.moduleIds.includes(changedPath));
for (const step of currentOutput.steps) {
+6 -3
View File
@@ -1,5 +1,6 @@
import { Entrypoint } from '../types';
import path, { relative, resolve } from 'node:path';
import { normalizePath } from './paths';
export function getEntrypointName(
entrypointsDir: string,
@@ -7,8 +8,8 @@ export function getEntrypointName(
// type: Entrypoint['type'],
): string {
const relativePath = path.relative(entrypointsDir, inputPath);
// Grab the string up to the first . or /
const name = relativePath.split(/[\.\/]/, 2)[0];
// Grab the string up to the first . or / or \\
const name = relativePath.split(/[\.\/\\]/, 2)[0];
return name;
}
@@ -29,5 +30,7 @@ export function getEntrypointBundlePath(
outDir: string,
ext: string,
): string {
return relative(outDir, getEntrypointOutputFile(entrypoint, ext));
return normalizePath(
relative(outDir, getEntrypointOutputFile(entrypoint, ext)),
);
}
-33
View File
@@ -1,33 +0,0 @@
import net from 'node:net';
/**
* Finds the first open port in a range of ports.
*/
export function findOpenPort(
startPort: number,
endPort: number,
): Promise<number> {
return findOpenPortRecursive(startPort, startPort, endPort);
}
function findOpenPortRecursive(
port: number,
startPort: number,
endPort: number,
): Promise<number> {
return new Promise((resolve, reject) => {
if (port > endPort)
return reject(
Error(`Could not find open port between ${startPort}-${endPort}`),
);
const server = net.createServer();
server.listen(port, () => {
server.once('close', () => resolve(port));
server.close();
});
server.on('error', () =>
resolve(findOpenPortRecursive(port + 1, startPort, endPort)),
);
});
}
+23 -1
View File
@@ -80,7 +80,7 @@ export async function getInternalConfig(
srcDir,
userConfig.entrypointsDir ?? 'entrypoints',
);
const publicDir = resolve(srcDir, userConfig.publicDir ?? 'public');
const publicDir = resolve(root, userConfig.publicDir ?? 'public');
const wxtDir = resolve(srcDir, '.wxt');
const typesDir = resolve(wxtDir, 'types');
@@ -99,6 +99,26 @@ export async function getInternalConfig(
typesDir,
fsCache: createFsCache(wxtDir),
manifest,
zip: {
sourcesTemplate: '{{name}}-{{version}}-sources.zip',
artifactTemplate: '{{name}}-{{version}}-{{browser}}.zip',
sourcesRoot: root,
...userConfig.zip,
...config.zip,
ignoredSources: [
'**/node_modules',
// WXT files
'**/web-ext.config.ts',
// Hidden files
'**/.*',
// Tests
'**/__tests__/**',
'**/*.+(test|spec).?(c|m)+(j|t)s?(x)',
// User config
...(userConfig.zip?.ignoredSources ?? []),
...(config.zip?.ignoredSources ?? []),
],
},
};
// Customize the default vite config
@@ -122,6 +142,7 @@ export async function getInternalConfig(
);
finalConfig.vite.plugins.push(plugins.devServerGlobals(finalConfig));
finalConfig.vite.plugins.push(plugins.tsconfigPaths(finalConfig));
finalConfig.vite.plugins.push(plugins.noopBackground());
finalConfig.vite.define ??= {};
getGlobals(finalConfig).forEach((global) => {
@@ -144,6 +165,7 @@ type InternalConfigNoUserDirs = Omit<
| 'typesDir'
| 'fsCache'
| 'manifest'
| 'zip'
>;
async function resolveManifestConfig(
+1 -1
View File
@@ -12,7 +12,7 @@ export function getGlobals(
{
name: '__BROWSER__',
value: config.browser,
type: `"chromium" | "firefox"`,
type: `string`,
},
{
name: '__IS_CHROME__',
+2
View File
@@ -43,6 +43,8 @@ const ENTRY_TYPE_TO_GROUP_MAP: Record<Entrypoint['type'], Group> = {
background: 'no-group',
'content-script': 'no-group',
'unlisted-script': 'no-group',
'unlisted-style': 'no-group',
'content-script-style': 'no-group',
};
type Group = 'extension-page' | 'sandbox-page' | 'no-group';
+7 -3
View File
@@ -5,6 +5,8 @@ import fs from 'fs-extra';
import { resolve } from 'path';
import transform from 'jiti/dist/babel';
import { getUnimportOptions } from './auto-imports';
import { removeImportStatements } from './strings';
import { normalizePath } from './paths';
/**
* Get the value from the default export of a `path`.
@@ -26,17 +28,18 @@ export async function importTsFile<T>(
config: InternalConfig,
): Promise<T> {
config.logger.debug('Loading file metadata:', path);
// JITI & Babel uses normalized paths.
const normalPath = normalizePath(path);
const unimport = createUnimport({
...getUnimportOptions(config),
// Only allow specific imports, not all from the project
imports: [{ name: '*', as: 'browser', from: 'webextension-polyfill' }],
dirs: [],
});
await unimport.init();
const text = await fs.readFile(path, 'utf-8');
const textNoImports = text.replace(/import.*[\n;]/gm, '');
const textNoImports = removeImportStatements(text);
const { code } = await unimport.injectImports(textNoImports);
config.logger.debug(
['Text:', text, 'No imports:', textNoImports, 'Code:', code].join('\n'),
@@ -53,7 +56,8 @@ export async function importTsFile<T>(
),
},
transform(opts) {
if (opts.filename === path) return transform({ ...opts, source: code });
if (opts.filename === normalPath)
return transform({ ...opts, source: code });
else return transform(opts);
},
});
+28 -23
View File
@@ -16,6 +16,7 @@ import {
hashContentScriptOptions,
mapWxtOptionsToContentScript,
} from './content-scripts';
import { getPackageJson } from './package';
/**
* Writes the manifest to the output directory and the build output.
@@ -51,36 +52,41 @@ export async function generateMainfest(
config: InternalConfig,
): Promise<Manifest.WebExtensionManifest> {
const pkg = await getPackageJson(config);
if (pkg.version == null)
throw Error('package.json does not include a version');
if (pkg.name == null) throw Error('package.json does not include a name');
if (pkg.description == null)
throw Error('package.json does not include a description');
const manifest: Manifest.WebExtensionManifest = {
manifest_version: config.manifestVersion,
name: pkg.name,
short_name: pkg.shortName,
version: simplifyVersion(pkg.version),
version_name: config.browser === 'firefox' ? undefined : pkg.version,
...config.manifest,
};
const manifest: Manifest.WebExtensionManifest = Object.assign(
{
manifest_version: config.manifestVersion,
name: pkg?.name,
description: pkg?.description,
version: pkg?.version && simplifyVersion(pkg.version),
// Only add the version name to chromium and if the user hasn't specified a custom version.
version_name:
config.browser !== 'firefox' && !config.manifest.version
? pkg?.version
: undefined,
short_name: pkg?.shortName,
},
config.manifest,
);
addEntrypoints(manifest, entrypoints, buildOutput, config);
if (config.command === 'serve') addDevModeCsp(manifest, config);
if (config.command === 'serve') addDevModePermissions(manifest, config);
return manifest;
}
// TODO: transform manifest here.
/**
* Read the package.json from the current directory.
*
* TODO: look in root and up directories until it's found
*/
async function getPackageJson(config: InternalConfig): Promise<any> {
return await fs.readJson(resolve(config.root, 'package.json'));
if (manifest.name == null)
throw Error(
"Manifest 'name' is missing. Either:\n1. Set the name in your <rootDir>/package.json\n2. Set a name via the manifest option in your wxt.config.ts",
);
if (manifest.version == null) {
throw Error(
"Manifest 'version' is missing. Either:\n1. Add a version in your <rootDir>/package.json\n2. Pass the version via the manifest option in your wxt.config.ts",
);
}
return manifest;
}
/**
@@ -89,7 +95,6 @@ async function getPackageJson(config: InternalConfig): Promise<any> {
*/
function simplifyVersion(versionName: string): string {
// Regex adapted from here: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/version#version_format
const version = /^((0|[1-9][0-9]{0,8})([.](0|[1-9][0-9]{0,8})){0,3}).*$/.exec(
versionName,
)?.[1];
+22
View File
@@ -0,0 +1,22 @@
import { resolve } from 'node:path';
import { InternalConfig } from '../types';
import fs from 'fs-extra';
/**
* Read the project's package.json.
*
* TODO: look in root and up directories until it's found
*/
export async function getPackageJson(
config: InternalConfig,
): Promise<Partial<Record<string, any>> | undefined> {
const file = resolve(config.root, 'package.json');
try {
return await fs.readJson(file);
} catch (err) {
config.logger.debug(
`Failed to read package.json at: ${file}. Returning undefined.`,
);
return {};
}
}
+22
View File
@@ -0,0 +1,22 @@
import nodePath from 'node:path';
import * as vite from 'vite';
/**
* Converts system paths to normalized bundler path. On windows and unix, this returns paths with /
* instead of \.
*/
export function normalizePath(path: string): string {
return vite.normalizePath(path);
}
/**
* Given a normalized path, convert it to the system path style. On Windows, switch to \, otherwise use /.
*/
export function unnormalizePath(path: string): string {
return nodePath.normalize(path);
}
export const CSS_EXTENSIONS = ['css', 'scss', 'sass', 'less', 'styl', 'stylus'];
// .module.css files are not supported because these are global CSS files, so using CSS modules doesn't make sense.
export const CSS_EXTENSIONS_PATTERN = `+(${CSS_EXTENSIONS.join('|')})`;
+17
View File
@@ -0,0 +1,17 @@
import { InternalConfig } from '../types';
import fs from 'fs-extra';
import glob from 'fast-glob';
import { unnormalizePath } from './paths';
/**
* Get all the files in the project's public directory. Returned paths are relative to the
* `config.publicDir`.
*/
export async function getPublicFiles(
config: InternalConfig,
): Promise<string[]> {
if (!(await fs.exists(config.publicDir))) return [];
const files = await glob('**/*', { cwd: config.publicDir });
return files.map(unnormalizePath);
}
+17
View File
@@ -0,0 +1,17 @@
export function kebabCaseAlphanumeric(str: string): string {
return str
.toLowerCase()
.replace(/[^a-z0-9-\s]/g, '') // Remove all non-alphanumeric, non-hyphen characters
.replace(/\s+/g, '-'); // Replace spaces with hyphens
}
/**
* Removes import statements from the top of a file. Keeps import.meta and inline, async `import()`
* calls.
*/
export function removeImportStatements(text: string): string {
return text.replace(
/(import\s?[{\w][\s\S]*?from\s?["'][\s\S]*?["'];?|import\s?["'][\s\S]*?["'];?)/gm,
'',
);
}
+39
View File
@@ -0,0 +1,39 @@
import * as vite from 'vite';
import { Entrypoint, InternalConfig } from '../types';
import { getEntrypointBundlePath } from '../utils/entrypoints';
/**
* Rename CSS entrypoint outputs to ensure a JS file is not generated, and that the CSS file is
* placed in the correct place.
*
* It:
* 1. Renames CSS files to their final paths
* 2. Removes the JS file that get's output by lib mode
*
* THIS PLUGIN SHOULD ONLY BE APPLIED TO CSS LIB MODE BUILDS. It should not be added to every build.
*/
export function cssEntrypoints(
entrypoint: Entrypoint,
config: InternalConfig,
): vite.Plugin {
return {
name: 'wxt:css-entrypoint',
config() {
return {
build: {
rollupOptions: {
output: {
assetFileNames: () =>
getEntrypointBundlePath(entrypoint, config.outDir, '.css'),
},
},
},
};
},
generateBundle(_, bundle) {
Object.keys(bundle).forEach((file) => {
if (file.endsWith('.js')) delete bundle[file];
});
},
};
}
+2
View File
@@ -5,3 +5,5 @@ export * from './multipageMove';
export * from './unimport';
export * from './virtualEntrypoint';
export * from './tsconfigPaths';
export * from './noopBackground';
export * from './cssEntrypoints';
+5 -2
View File
@@ -3,6 +3,7 @@ import { Entrypoint, InternalConfig } from '../types';
import { dirname, extname, resolve } from 'node:path';
import { getEntrypointBundlePath } from '../utils/entrypoints';
import fs, { ensureDir } from 'fs-extra';
import { normalizePath } from '../utils/paths';
/**
* Ensures the HTML files output by a multipage build are in the correct location. This does two
@@ -14,6 +15,8 @@ import fs, { ensureDir } from 'fs-extra';
* Assets (JS and CSS) are output to the `<outDir>/assets` directory, and don't need to be modified.
* HTML files access them via absolute URLs, so we don't need to update any import paths in the HTML
* files either.
*
* THIS PLUGIN SHOULD ONLY BE APPLIED TO MULTIPAGE BUILDS. It should not be added to every build.
*/
export function multipageMove(
entrypoints: Entrypoint[],
@@ -23,11 +26,11 @@ export function multipageMove(
name: 'wxt:multipage-move',
async writeBundle(_, bundle) {
for (const oldBundlePath in bundle) {
// oldBundlePath = 'entrypoints/popup.html' or 'entrypoints/o ptions/index.html'
// oldBundlePath = 'entrypoints/popup.html' or 'entrypoints/options/index.html'
// Find a matching entrypoint - oldBundlePath is the same as end end of the input path.
const entrypoint = entrypoints.find(
(entry) => !!entry.inputPath.endsWith(oldBundlePath),
(entry) => !!normalizePath(entry.inputPath).endsWith(oldBundlePath),
);
if (entrypoint == null) {
config.logger.debug('No entrypoint found for', oldBundlePath);
+24
View File
@@ -0,0 +1,24 @@
import { Plugin } from 'vite';
/**
* In dev mode, if there's not a background script listed, we need to add one.
*
* This define's a virtual module that is basically just a noop.
*/
export function noopBackground(): Plugin {
const virtualModuleId = VIRTUAL_NOOP_BACKGROUND_MODULE_ID;
const resolvedVirtualModuleId = '\0' + virtualModuleId;
return {
name: 'wxt:noop-background',
resolveId(id) {
if (id === virtualModuleId) return resolvedVirtualModuleId;
},
load(id) {
if (id === resolvedVirtualModuleId) {
return `import { defineBackground } from 'wxt/client';\nexport default defineBackground(() => void 0)`;
}
},
};
}
export const VIRTUAL_NOOP_BACKGROUND_MODULE_ID = 'virtual:user-background';
+2 -1
View File
@@ -2,6 +2,7 @@ import { Plugin } from 'vite';
import { Entrypoint, InternalConfig } from '../types';
import fs from 'fs-extra';
import { resolve } from 'path';
import { normalizePath } from '../utils/paths';
/**
* Wraps a user's entrypoint with a vitual version with additional logic.
@@ -21,7 +22,7 @@ export function virtualEntrypoin(
const index = id.indexOf(virtualId);
if (index === -1) return;
const inputPath = id.substring(index + virtualId.length);
const inputPath = normalizePath(id.substring(index + virtualId.length));
return resolvedVirtualId + inputPath;
},
async load(id) {
+75
View File
@@ -0,0 +1,75 @@
import { BuildOutput, InternalConfig } from './types';
import zipdir from 'zip-dir';
import { dirname, relative, resolve } from 'node:path';
import fs from 'fs-extra';
import { kebabCaseAlphanumeric } from './utils/strings';
import { getPackageJson } from './utils/package';
import { minimatch } from 'minimatch';
import { formatDuration } from './utils/formatDuration';
import { printFileList } from './log/printFileList';
/**
* Zip the extension for distribution. Does not build, just zips the output that should exist for
* the given config.
*
* Returns a list of ZIP files that were created.
*/
export async function zipExtension(
config: InternalConfig,
buildOutput: BuildOutput,
): Promise<string[]> {
const start = Date.now();
config.logger.info('Zipping extension...');
const zipFiles: string[] = [];
const projectName =
config.zip.name ??
kebabCaseAlphanumeric(
(await getPackageJson(config))?.name || dirname(process.cwd()),
);
const applyTemplate = (template: string): string =>
template
.replaceAll('{{name}}', projectName)
.replaceAll('{{browser}}', config.browser)
.replaceAll(
'{{version}}',
buildOutput.manifest.version_name ?? buildOutput.manifest.version,
)
.replaceAll('{{manifestVersion}}', `mv${config.manifestVersion}`);
await fs.ensureDir(config.outBaseDir);
// ZIP output directory
const outZipFilename = applyTemplate(config.zip.artifactTemplate);
const outZipPath = resolve(config.outBaseDir, outZipFilename);
await zipdir(config.outDir, {
saveTo: outZipPath,
});
zipFiles.push(outZipPath);
// ZIP sources for Firefox
if (config.browser === 'firefox') {
const sourcesZipFilename = applyTemplate(config.zip.sourcesTemplate);
const sourcesZipPath = resolve(config.outBaseDir, sourcesZipFilename);
await zipdir(config.zip.sourcesRoot, {
saveTo: sourcesZipPath,
filter(path) {
const relativePath = relative(config.zip.sourcesRoot, path);
const matchedPattern = config.zip.ignoredSources.find((pattern) =>
minimatch(relativePath, pattern),
);
return matchedPattern == null;
},
});
zipFiles.push(sourcesZipPath);
}
config.logger.success(
`Zipped extension in ${formatDuration(Date.now() - start)}`,
);
await printFileList(config.logger.log, config.outBaseDir, zipFiles);
return zipFiles;
}
+7 -1
View File
@@ -28,6 +28,10 @@ export async function build(config: InlineConfig): Promise<BuildOutput> {
return await buildInternal(internalConfig);
}
/**
* Creates a dev server, pre-builds all the files that need to exist to load the extension, and open
* the browser with the extension installed.
*/
export async function createServer(
config?: InlineConfig,
): Promise<WxtDevServer> {
@@ -54,13 +58,15 @@ export async function createServer(
});
server.watcher.on('all', async (event, path, _stats) => {
// Here, "path" is a non-normalized path (ie: C:\\users\\... instead of C:/users/...)
if (path.startsWith(internalConfig.outBaseDir)) return;
changeQueue.push([event, path]);
await fileChangedMutex.runExclusive(async () => {
const fileChanges = changeQueue.splice(0, changeQueue.length);
const changes = detectDevChanges(fileChanges, server.currentOutput);
if (fileChanges.length === 0) return;
const changes = detectDevChanges(fileChanges, server.currentOutput);
if (changes.type === 'no-change') return;
// Log the entrypoints that were effected
+7
View File
@@ -218,4 +218,11 @@ export const fakeInternalConfig = fakeObjectCreator<InternalConfig>(() => ({
vite: {},
wxtDir: fakeDir(),
server: mock<WxtDevServer>(),
zip: {
artifactTemplate: '{{name}}-{{version}}.zip',
ignoredSources: [],
sourcesRoot: fakeDir(),
sourcesTemplate: '{{name}}-sources.zip',
name: faker.person.firstName().toLowerCase(),
},
}));

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