Compare commits
117 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ab672e9dbd | |||
| 7b3ea52e02 | |||
| e0929a68ba | |||
| 4c430725a5 | |||
| 7d745f6ec7 | |||
| 10091d7769 | |||
| 2e142b3038 | |||
| 361bb2dcc8 | |||
| 26fca5c0a2 | |||
| 30a61f4384 | |||
| 8e0a189d77 | |||
| cae44c9732 | |||
| fba8f0d017 | |||
| 3f260ee777 | |||
| 1f6a931b02 | |||
| 07891d028b | |||
| 1f448d1bf0 | |||
| 7722986537 | |||
| 4b73168618 | |||
| 16cebc538f | |||
| e4aaba98ab | |||
| c190d8c44c | |||
| 5801f96f79 | |||
| 92039b855e | |||
| f145e00cc9 | |||
| 87e8df90f5 | |||
| 419fab8193 | |||
| 71743b3bf3 | |||
| 8e3169eb1e | |||
| 4ec904ea3c | |||
| b284305605 | |||
| 234bd39a11 | |||
| b5acea106d | |||
| 33ed04ff6b | |||
| f51bbc0a68 | |||
| 6ee5b22e6c | |||
| b4de93d2e6 | |||
| 2570debc5c | |||
| e6a2c15d3c | |||
| dbd84c8815 | |||
| fe966b6ca8 | |||
| e2350fecc6 | |||
| b33b6631a5 | |||
| 5f15f9ccec | |||
| beafa6afc4 | |||
| 46a5036482 | |||
| eaa15e0d57 | |||
| 2c22948d2e | |||
| 3e701e22c9 | |||
| 6ee1fa17a1 | |||
| b76bf2831b | |||
| ac7cbfce1f | |||
| 06f027b889 | |||
| e996686082 | |||
| 1bb3474a91 | |||
| 79426671cf | |||
| 89b34403ee | |||
| 749bc38234 | |||
| e086374828 | |||
| 2c3721e5f4 | |||
| c2946aa1eb | |||
| 4dd6320eae | |||
| a8285fff78 | |||
| d89be2544d | |||
| f234c85357 | |||
| 93bfee0578 | |||
| 2c7922ca4c | |||
| 72ae812c42 | |||
| 2a0842bb62 | |||
| b301ee324e | |||
| 4ef3eeaca1 | |||
| e803bfecb5 | |||
| 0dcf540393 | |||
| 062e25d834 | |||
| b58a15e783 | |||
| 5f5f1d90f8 | |||
| 83e62a1d63 | |||
| b428a62a90 | |||
| 1fe4eedc9a | |||
| 39d3192904 | |||
| de30509d94 | |||
| d5ae2332ca | |||
| f8b367dd7b | |||
| 6bcb71baa0 | |||
| 54bdf54839 | |||
| 83f457f40f | |||
| f9df2d0809 | |||
| 2b079bb7f6 | |||
| 0194448af4 | |||
| b409def211 | |||
| 62bbef3487 | |||
| 87013bc616 | |||
| 4599bf2aa0 | |||
| d629acf518 | |||
| 2bde9175b7 | |||
| 8d5f4f5201 | |||
| d05f126eb3 | |||
| e164bd51fe | |||
| d66293c56e | |||
| 6aa827438c | |||
| 6ee8a43677 | |||
| d54d6111e6 | |||
| aefc8d3167 | |||
| 7ccff533ff | |||
| edfa075030 | |||
| ef140dc7f8 | |||
| 39467d10f3 | |||
| 97f0938c99 | |||
| 18741eae8e | |||
| 3f3ef37c96 | |||
| 323045a7c3 | |||
| 9b34180467 | |||
| 7f15305037 | |||
| 35cc1c94bf | |||
| 547c1850ac | |||
| 798f02f626 | |||
| d5948abfe5 |
@@ -0,0 +1,42 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Report an issue with WXT
|
||||
title: ''
|
||||
labels: triage
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
### Describe the bug
|
||||
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
### To Reproduce
|
||||
|
||||
Upload a ZIP or share a link to a repo representing the minimal reproduction. **_If you don't upload a minimal reproduction, you bug report will be closed._**
|
||||
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
### Expected behavior
|
||||
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
### Screenshots
|
||||
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
### Environment
|
||||
|
||||
<!--- Run `npx envinfo --system --browsers --binaries --npmPackages wxt,vite` and paste the output below -->
|
||||
|
||||
```
|
||||
Paste output here
|
||||
```
|
||||
|
||||
### Additional context
|
||||
|
||||
Add any other context about the problem here, otherwise you can delete this section.
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for WXT
|
||||
title: ''
|
||||
labels: feature
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
### Feature Request
|
||||
|
||||
Please describe your feature, be clear and concise. If you have a proposal for required type or API changes, list them here.
|
||||
|
||||
#### Is your feature request related to a bug?
|
||||
|
||||
If so, add a link here. If not, write "N/A"
|
||||
|
||||
### What are the alternatives?
|
||||
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
### Additional context
|
||||
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -0,0 +1,11 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: npm
|
||||
directory: '/' # Location of package manifests
|
||||
schedule:
|
||||
interval: 'monthly'
|
||||
@@ -72,10 +72,15 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build Local Tarball
|
||||
run: pnpm pack
|
||||
|
||||
- name: Validate Vanilla
|
||||
working-directory: templates/vanilla
|
||||
run: |
|
||||
npm i
|
||||
npm i -D ../../wxt-*.tgz
|
||||
npm ls vite
|
||||
npm run build
|
||||
npm run compile
|
||||
|
||||
@@ -83,6 +88,8 @@ jobs:
|
||||
working-directory: templates/vue
|
||||
run: |
|
||||
npm i
|
||||
npm i -D ../../wxt-*.tgz
|
||||
npm ls vite
|
||||
npm run build
|
||||
npm run compile
|
||||
|
||||
@@ -90,6 +97,8 @@ jobs:
|
||||
working-directory: templates/react
|
||||
run: |
|
||||
npm i
|
||||
npm i -D ../../wxt-*.tgz
|
||||
npm ls vite
|
||||
npm run build
|
||||
npm run compile
|
||||
|
||||
@@ -97,6 +106,8 @@ jobs:
|
||||
working-directory: templates/svelte
|
||||
run: |
|
||||
npm i
|
||||
npm i -D ../../wxt-*.tgz
|
||||
npm ls vite
|
||||
npm run build
|
||||
npm run check
|
||||
|
||||
@@ -104,5 +115,7 @@ jobs:
|
||||
working-directory: templates/solid
|
||||
run: |
|
||||
npm i
|
||||
npm i -D ../../wxt-*.tgz
|
||||
npm ls vite
|
||||
npm run build
|
||||
npm run compile
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
/docs/.vitepress/cache
|
||||
coverage
|
||||
dist
|
||||
e2e/project
|
||||
node_modules
|
||||
TODOs.md
|
||||
web-ext.config.js
|
||||
@@ -18,3 +17,5 @@ web-ext.config.ts
|
||||
templates/*/pnpm-lock.yaml
|
||||
templates/*/yarn.lock
|
||||
templates/*/package-lock.json
|
||||
docs/config.md
|
||||
stats.html
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
.output
|
||||
coverage
|
||||
dist
|
||||
e2e/project
|
||||
.wxt
|
||||
docs/.vitepress/cache
|
||||
pnpm-lock.yaml
|
||||
CHANGELOG.md
|
||||
.gitattributes
|
||||
.gitignore
|
||||
.prettierignore
|
||||
*.png
|
||||
*.svg
|
||||
_gitignore
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
singleQuote: true
|
||||
trailingComma: all
|
||||
endOfLine: lf
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
# Contributing
|
||||
|
||||
Everyone is welcome to contribute to WXT!
|
||||
|
||||
If you are changing the docs or fixing a bug, feel free to fork and open a PR.
|
||||
|
||||
If you want to add a new feature, please create an issue or discussion first so we can decide if the feature is inline with the vision for WXT.
|
||||
|
||||
## Conventional Commits
|
||||
|
||||
This project uses [Conventional Commits](https://www.conventionalcommits.org/en) to automate versioning. If you're a new contributor, don't worry about this. When you open a PR, a maintainer will change the PR's title so it's in the style of conventional commits, but that's all.
|
||||
|
||||
Maintainers, commits to the `main` branch (either directly or via PRs) must be valid conventional commits.
|
||||
|
||||
## Setup
|
||||
|
||||
WXT uses `pnpm`, so make sure you have it installed.
|
||||
|
||||
```sh
|
||||
corepack enable
|
||||
```
|
||||
|
||||
Then, simply run the install command:
|
||||
|
||||
```sh
|
||||
pnpm i
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
Here are some helpful commands:
|
||||
|
||||
```sh
|
||||
# Build WXT package
|
||||
pnpm build
|
||||
```
|
||||
|
||||
```sh
|
||||
# Build WXT package, then build demo extension
|
||||
cd demo
|
||||
pnpm build
|
||||
```
|
||||
|
||||
```sh
|
||||
# Build WXT package, then start the demo extension in dev mode
|
||||
cd demo
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
```sh
|
||||
# Run unit and E2E tests
|
||||
pnpm test
|
||||
```
|
||||
|
||||
```sh
|
||||
# Start the docs website locally
|
||||
pnpm docs:dev
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
WXT has unit and E2E tests. When making a change or adding a feature, make sure to update the tests or add new ones.
|
||||
|
||||
To run tests for a specific file, add the filename at the end of the test command:
|
||||
|
||||
```sh
|
||||
pnpm test manifest-contents
|
||||
```
|
||||
|
||||
Unit and E2E tests are ran together via [Vitest workspaces](https://vitest.dev/guide/#workspaces-support).
|
||||
|
||||
If you want to manually test a change, you can modify the demo project for your test, but please don't leave those changes committed once you open a PR.
|
||||
@@ -1,11 +1,38 @@
|
||||
<h1 align="center">WXT</h1>
|
||||
<h1 align="center">
|
||||
<img style="vertical-align:middle" width="44" src="./docs/public/hero-logo.svg" alt="WXT Logo">
|
||||
<span>WXT</span>
|
||||
</h1>
|
||||
|
||||
<p align="center"><img align="center" width="44" src="./docs/public/hero-logo.svg" alt="WXT Logo"></p>
|
||||
<p align="center">
|
||||
<span>Next-gen framework for developing web extensions.</span>
|
||||
<br/>
|
||||
<span>⚡</span>
|
||||
<br/>
|
||||
<q><i>It's like Nuxt, but for Chrome Extensions</i></q>
|
||||
</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>
|
||||
<p align="center">
|
||||
<a href="https://wxt.dev" target="_blank">Get Started</a>
|
||||
•
|
||||
<a href="https://wxt.dev/get-started/installation.html" target="_blank">Installation</a>
|
||||
•
|
||||
<a href="https://wxt.dev/guide/background.html" target="_blank">Entrypoints</a>
|
||||
•
|
||||
<a href="https://wxt.dev/config.html" target="_blank">Configuration</a>
|
||||
</p>
|
||||
|
||||

|
||||
|
||||
## Quick Start
|
||||
|
||||
Bootstrap a new project:
|
||||
|
||||
```sh
|
||||
pnpx wxt@latest init <project-name>
|
||||
```
|
||||
|
||||
Or see the [installation guide](https://wxt.dev/get-started/installation.html) to get started with WXT.
|
||||
|
||||
## Features
|
||||
|
||||
- 🌐 Supports all browsers
|
||||
@@ -17,18 +44,14 @@
|
||||
- ⬇️ Download and bundle remote URL imports
|
||||
- 🎨 Frontend framework agnostic: works with Vue, React, Svelte, etc
|
||||
- 🖍️ Quickly bootstrap a new project
|
||||
|
||||
### Todo
|
||||
|
||||
- 📏 Bundle analysis
|
||||
|
||||
### Coming Soon
|
||||
|
||||
- 🤖 Automated publishing
|
||||
|
||||
## Get Started
|
||||
|
||||
Checkout the [installation guide](https://wxt.dev/get-started/installation.html) to get started with WXT.
|
||||
|
||||
## Contributors
|
||||
|
||||
<a href="https://github.com/aklinker1/wxt/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=aklinker1/wxt" />
|
||||
<a href="https://github.com/wxt-dev/wxt/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=wxt-dev/wxt" />
|
||||
</a>
|
||||
|
||||
@@ -12,4 +12,13 @@ export default defineBackground(() => {
|
||||
browser.runtime.getURL('/');
|
||||
browser.runtime.getURL('/background.js');
|
||||
browser.runtime.getURL('/icon/128.png');
|
||||
|
||||
// @ts-expect-error: should only accept known message names
|
||||
browser.i18n.getMessage('test');
|
||||
browser.i18n.getMessage('prompt_for_name');
|
||||
browser.i18n.getMessage('hello', 'Aaron');
|
||||
browser.i18n.getMessage('bye', ['Aaron']);
|
||||
browser.i18n.getMessage('@@extension_id');
|
||||
|
||||
console.log('WXT MODE:', import.meta.env.MODE);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'url:https://stats.aklinker1.io/umami.js';
|
||||
import 'url:https://code.jquery.com/jquery-3.7.1.slim.min.js';
|
||||
|
||||
console.log(browser.runtime.id);
|
||||
logId();
|
||||
console.log(2);
|
||||
|
||||
console.log('WXT MODE:', import.meta.env.MODE);
|
||||
|
||||
@@ -2,9 +2,17 @@ import '../../common/style.css';
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ['*://*/*'],
|
||||
async main() {
|
||||
async main(ctx) {
|
||||
console.log(browser.runtime.id);
|
||||
logId();
|
||||
|
||||
console.log('WXT MODE:', import.meta.env.MODE);
|
||||
|
||||
const n = (Math.random() * 100).toFixed(1);
|
||||
ctx.setInterval(() => {
|
||||
console.log(n, browser.runtime.id);
|
||||
}, 1e3);
|
||||
|
||||
mountContentScriptUi();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"prompt_for_name": {
|
||||
"message": "What's your name?",
|
||||
"description": "Ask for the user's name"
|
||||
},
|
||||
"hello": {
|
||||
"message": "Hello, $USER$",
|
||||
"description": "Greet the user",
|
||||
"placeholders": {
|
||||
"user": {
|
||||
"content": "$1",
|
||||
"example": "Cira"
|
||||
}
|
||||
}
|
||||
},
|
||||
"bye": {
|
||||
"message": "Goodbye, $USER$. Come back to $OUR_SITE$ soon!",
|
||||
"description": "Say goodbye to the user",
|
||||
"placeholders": {
|
||||
"our_site": {
|
||||
"content": "Example.com"
|
||||
},
|
||||
"user": {
|
||||
"content": "$1",
|
||||
"example": "Cira"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 504 B After Width: | Height: | Size: 504 B |
|
Before Width: | Height: | Size: 936 B After Width: | Height: | Size: 936 B |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"extends": ["../tsconfig.base.json", "./src/.wxt/tsconfig.json"]
|
||||
"extends": ["../tsconfig.base.json", "./.wxt/tsconfig.json"]
|
||||
}
|
||||
|
||||
@@ -2,16 +2,7 @@ import { defineConfig } from 'wxt';
|
||||
|
||||
export default defineConfig({
|
||||
srcDir: 'src',
|
||||
storeIds: {
|
||||
chrome: '123',
|
||||
},
|
||||
manifest: {
|
||||
icons: {
|
||||
'16': 'icon/16.png',
|
||||
'32': 'icon/32.png',
|
||||
'48': 'icon/48.png',
|
||||
'96': 'icon/96.png',
|
||||
'128': 'icon/128.png',
|
||||
},
|
||||
default_locale: 'en',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<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',
|
||||
},
|
||||
import useListExtensionDetails from '../composables/useListExtensionDetails';
|
||||
|
||||
const chromeExtensionIds = [
|
||||
'ocfdgncpifmegplaglcnglhioflaimkd', // GitHub: Better Line Counts
|
||||
];
|
||||
|
||||
const { data } = useListExtensionDetails(chromeExtensionIds);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -14,10 +13,14 @@ const extensions = [
|
||||
<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 v-for="extension of data">
|
||||
<img
|
||||
:src="extension.iconUrl"
|
||||
:alt="`${extension.name} icon`"
|
||||
referrerpolicy="no-referrer"
|
||||
/>
|
||||
<a :href="extension.storeUrl" target="_blank">{{ extension.name }}</a>
|
||||
<small>{{ extension.shortDescription }}</small>
|
||||
</li>
|
||||
</ul>
|
||||
<p>Open a PR to add your extension to the list!</p>
|
||||
@@ -57,6 +60,7 @@ img {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
margin-bottom: 16px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
ul {
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { ref } from 'vue';
|
||||
|
||||
export interface ChromeExtension {
|
||||
id: string;
|
||||
name: string;
|
||||
iconUrl: string;
|
||||
weeklyActiveUsers: number;
|
||||
shortDescription: string;
|
||||
storeUrl: string;
|
||||
}
|
||||
|
||||
const operationName = 'WxtDocsUsedBy';
|
||||
const query = `query ${operationName}($ids:[String!]!) {
|
||||
chromeExtensions(ids: $ids) {
|
||||
id
|
||||
name
|
||||
iconUrl
|
||||
weeklyActiveUsers
|
||||
shortDescription
|
||||
storeUrl
|
||||
}
|
||||
}`;
|
||||
|
||||
export default function (ids: string[]) {
|
||||
const data = ref<ChromeExtension[]>();
|
||||
const err = ref<unknown>();
|
||||
|
||||
fetch('https://queue.wxt.dev/api', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
operationName,
|
||||
query,
|
||||
variables: { ids },
|
||||
}),
|
||||
})
|
||||
.then(async (res) => {
|
||||
const {
|
||||
data: { chromeExtensions },
|
||||
} = await res.json();
|
||||
data.value = chromeExtensions;
|
||||
err.value = undefined;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
data.value = undefined;
|
||||
err.value = error;
|
||||
});
|
||||
|
||||
return {
|
||||
data,
|
||||
err,
|
||||
};
|
||||
}
|
||||
@@ -1,16 +1,32 @@
|
||||
import { defineConfig } from 'vitepress';
|
||||
import { generateConfigDocs } from './plugins/generate-config-docs';
|
||||
|
||||
// https://vitepress.dev/reference/site-config
|
||||
export default defineConfig({
|
||||
title: 'WXT',
|
||||
vite: {
|
||||
clearScreen: false,
|
||||
plugins: [generateConfigDocs()],
|
||||
},
|
||||
description: 'Next gen framework for developing web extensions',
|
||||
lastUpdated: true,
|
||||
|
||||
head: [
|
||||
[
|
||||
'script',
|
||||
{
|
||||
async: '',
|
||||
'data-website-id': 'c1840c18-a12c-4a45-a848-55ae85ef7915',
|
||||
src: 'https://umami.aklinker1.io/script.js',
|
||||
},
|
||||
],
|
||||
],
|
||||
|
||||
themeConfig: {
|
||||
// https://vitepress.dev/reference/default-theme-config
|
||||
logo: '/logo.svg',
|
||||
editLink: {
|
||||
pattern: 'https://github.com/aklinker1/wxt/edit/main/docs/:path',
|
||||
pattern: 'https://github.com/wxt-dev/wxt/edit/main/docs/:path',
|
||||
},
|
||||
search: {
|
||||
provider: 'local',
|
||||
@@ -47,6 +63,8 @@ export default defineConfig({
|
||||
{ text: 'Auto-imports', link: '/guide/auto-imports.md' },
|
||||
{ text: 'Manifest.json', link: '/guide/manifest.md' },
|
||||
{ text: 'Extension APIs', link: '/guide/extension-apis.md' },
|
||||
{ text: 'Remote Code', link: '/guide/remote-code.md' },
|
||||
{ text: 'Vite', link: '/guide/vite.md' },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -70,6 +88,6 @@ export default defineConfig({
|
||||
],
|
||||
},
|
||||
|
||||
socialLinks: [{ icon: 'github', link: 'https://github.com/aklinker1/wxt' }],
|
||||
socialLinks: [{ icon: 'github', link: 'https://github.com/wxt-dev/wxt' }],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
import { resolve } from 'node:path';
|
||||
import { Project, ts, Type, Node, JSDocableNode } from 'ts-morph';
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import { format } from 'prettier';
|
||||
import consola from 'consola';
|
||||
|
||||
let hasGenerated = false;
|
||||
|
||||
const externalTypesPath = resolve('src/core/types/external.ts');
|
||||
const configTemplatePath = resolve('docs/config.tpl.md');
|
||||
const configPath = resolve('docs/config.md');
|
||||
|
||||
const PREFACE = `<!--
|
||||
DO NOT EDIT
|
||||
Generated by \`wxt/docs/.vitepress/plugins/generate-config-docs.ts\`
|
||||
To make changes to the config reference, update the JSDoc in \`src/core/types/external.ts\`.
|
||||
-->`;
|
||||
|
||||
/**
|
||||
* Custom property paths that should not be recursively inspected. Usually 3rd party types.
|
||||
*/
|
||||
const LEAF_PATHS = ['imports', 'vite', 'server'];
|
||||
|
||||
/**
|
||||
* Override any types that resolve to `import(...)` instead of their type names when calling
|
||||
* `type.getText()`.
|
||||
*
|
||||
* This also stops any further type inspection for objects, meaning the object will be documented,
|
||||
* not it's properties.
|
||||
*/
|
||||
const CUSTOM_TYPES = {
|
||||
manifest:
|
||||
'Manifest | Promise<Manifest> | () => Manifest | () => Promise<Manifest>',
|
||||
imports: "false | Partial<import('unimport').UnimportOptions>",
|
||||
vite: "Omit<import('vite').UserConfig, 'root' | 'configFile' | 'mode'>",
|
||||
};
|
||||
|
||||
export function generateConfigDocs() {
|
||||
writeFileSync(configPath, '');
|
||||
|
||||
const generateDocs = async () => {
|
||||
consola.info('Generating /config.md');
|
||||
try {
|
||||
const project = new Project({
|
||||
tsConfigFilePath: resolve('tsconfig.json'),
|
||||
});
|
||||
|
||||
// Load file containing "UserConfig"
|
||||
const externalTypesFile = project.addSourceFileAtPath(externalTypesPath);
|
||||
project.resolveSourceFileDependencies();
|
||||
|
||||
const inlineConfigInterface =
|
||||
externalTypesFile.getInterfaceOrThrow('InlineConfig');
|
||||
|
||||
const getDocsFor = (path: string[], node: Node<ts.Node>): string[] => {
|
||||
const pathStr = path.join('.');
|
||||
|
||||
let type: Type<ts.Type>;
|
||||
if (node.isKind(ts.SyntaxKind.InterfaceDeclaration)) {
|
||||
type = node.getType();
|
||||
} else if (node.isKind(ts.SyntaxKind.PropertySignature)) {
|
||||
type = node.getTypeNodeOrThrow()?.getType();
|
||||
} else if (node.isKind(ts.SyntaxKind.MethodSignature)) {
|
||||
type = node.getType();
|
||||
} else {
|
||||
throw Error('Unsupported type node: ' + node.getKindName());
|
||||
}
|
||||
|
||||
if (type.isObject() && !type.isArray() && !CUSTOM_TYPES[pathStr]) {
|
||||
return type.getProperties().flatMap((property) => {
|
||||
const childPath = [...path, property.getName()];
|
||||
|
||||
return getDocsFor(childPath, property.getDeclarations()[0]);
|
||||
});
|
||||
}
|
||||
|
||||
if ('getJsDocs' in node) {
|
||||
const lines: string[] = [];
|
||||
const docs = (node as unknown as JSDocableNode).getJsDocs();
|
||||
let typeText: string;
|
||||
if (CUSTOM_TYPES[pathStr]) {
|
||||
typeText = CUSTOM_TYPES[pathStr];
|
||||
} else if (type.isUnion() && !type.isBoolean()) {
|
||||
typeText = type
|
||||
.getUnionTypes()
|
||||
.map((type) => type.getText())
|
||||
.join(' | ');
|
||||
} else {
|
||||
typeText = type.getText();
|
||||
}
|
||||
const defaultValue = docs
|
||||
.flatMap((doc) => doc.getTags())
|
||||
.find((tag) => tag.getTagName() === 'default')
|
||||
?.getCommentText();
|
||||
lines.push(
|
||||
'',
|
||||
`## ${pathStr}`,
|
||||
'',
|
||||
`- **Type**: \`${typeText}\``,
|
||||
`- **Default**: \`${defaultValue}\``,
|
||||
...docs.flatMap((doc) => doc.getDescription()),
|
||||
);
|
||||
return lines;
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
const lines = getDocsFor([], inlineConfigInterface);
|
||||
const text = await format(
|
||||
PREFACE +
|
||||
'\n\n' +
|
||||
readFileSync(configTemplatePath, 'utf-8').replace(
|
||||
'{{ DOCS }}',
|
||||
lines.join('\n'),
|
||||
),
|
||||
{ parser: 'markdown' },
|
||||
);
|
||||
|
||||
writeFileSync(configPath, text);
|
||||
consola.success('Generated /config.md');
|
||||
} catch (err) {
|
||||
consola.fail('Failed to generate /config.md');
|
||||
consola.error(err.message);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
name: 'docs:generate-config-docs',
|
||||
async config() {
|
||||
if (!hasGenerated) {
|
||||
hasGenerated = true;
|
||||
await generateDocs();
|
||||
}
|
||||
},
|
||||
configureServer(server: any) {
|
||||
server.watcher.add(externalTypesPath);
|
||||
},
|
||||
async handleHotUpdate(ctx: { file: string }) {
|
||||
if ([externalTypesPath, configTemplatePath].includes(ctx.file)) {
|
||||
await generateDocs();
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,31 +1,46 @@
|
||||
/* 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;
|
||||
--wxt-c-green-1: #67d45e;
|
||||
--wxt-c-green-2: #4fa048;
|
||||
--wxt-c-green-3: #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-c-brand-1: var(--wxt-c-green-1);
|
||||
--vp-c-brand-2: var(--wxt-c-green-2);
|
||||
--vp-c-brand-3: var(--wxt-c-green-3);
|
||||
|
||||
--vp-button-brand-bg: var(--wxt-c-green);
|
||||
--vp-button-brand-hover-bg: var(--wxt-c-green-2);
|
||||
--vp-button-brand-active-bg: var(--wxt-c-green-3);
|
||||
|
||||
--vp-code-link-color: var(--wxt-c-green);
|
||||
|
||||
/* --vp-c-text-1: var(--wxt-c-green-1); */
|
||||
|
||||
--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-custom-block-tip-border: var(--wxt-c-green);
|
||||
--vp-custom-block-tip-text: var(--wxt-c-green);
|
||||
|
||||
--vp-code-block-bg: #222422;
|
||||
/* --vp-code-block-bg: #222422;
|
||||
--vp-code-copy-code-bg: #313431;
|
||||
--vp-code-copy-code-hover-bg: #3c403c;
|
||||
--vp-code-copy-code-hover-bg: #3c403c; */
|
||||
|
||||
--vp-custom-block-tip-bg: var(--vp-code-block-bg);
|
||||
--vp-custom-block-info-bg: var(--vp-code-block-bg);
|
||||
|
||||
--vp-code-color: #476582;
|
||||
}
|
||||
|
||||
.vp-doc a {
|
||||
color: var(--wxt-c-green);
|
||||
}
|
||||
|
||||
.dark {
|
||||
@@ -38,15 +53,19 @@
|
||||
|
||||
--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-c-default: #313136;
|
||||
--vp-c-default-1: #3a3a3c;
|
||||
--vp-c-default-2: #505053;
|
||||
--vp-c-default-3: #2c2c30;
|
||||
--vp-c-default-soft: #252529;
|
||||
|
||||
--vp-code-block-bg: #191a19;
|
||||
--vp-code-copy-code-bg: #212321;
|
||||
--vp-code-copy-code-hover-bg: #292d29;
|
||||
|
||||
--vp-custom-block-info-bg: #191a19;
|
||||
|
||||
--vp-code-color: #c9def1;
|
||||
}
|
||||
|
||||
.vp-doc .no-vertical-dividers th,
|
||||
|
||||
|
Before Width: | Height: | Size: 415 KiB After Width: | Height: | Size: 238 KiB |
@@ -1,5 +0,0 @@
|
||||
# Config
|
||||
|
||||
:::warning 🚧 Under construction
|
||||
This documentation does not exist yet.
|
||||
:::
|
||||
@@ -0,0 +1,5 @@
|
||||
# Config Reference
|
||||
|
||||
Discover all the options you can use in your `wxt.config.ts` file.
|
||||
|
||||
{{ DOCS }}
|
||||
@@ -24,7 +24,7 @@ Lets compare the features of WXT vs [Plasmo](https://docs.plasmo.com/framework),
|
||||
| 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 Content Scripts on Change | ✅ | 🟡 Reloads entire extension |
|
||||
| Reload Background on Change | 🟡 Reloads entire extension | 🟡 Reloads entire extension |
|
||||
| <strong style="opacity: 50%">Built-in Utils</strong> | | |
|
||||
| Storage | ❌ | ✅ |
|
||||
@@ -45,5 +45,5 @@ Reloading each part of the extension individually improves your iteration speed
|
||||
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.
|
||||
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/wxt-dev/wxt/issues/53) for more details.
|
||||
:::
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Defining Entrypoints
|
||||
|
||||
An entrypoints is any HTML, JS, or CSS file that needs to be bundled and included with the extension.
|
||||
An "entrypoint" 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`.
|
||||
Entrypoints may or may not be listed in the extension's `manifest.json`.
|
||||
|
||||
## `/entrypoints` Directory
|
||||
|
||||
@@ -12,12 +12,11 @@ For example, a project that looks like this:
|
||||
|
||||
```
|
||||
<rootDir>
|
||||
├─ entrypoints/
|
||||
│ ├─ background.ts
|
||||
│ ├─ content.ts
|
||||
│ ├─ injected.ts
|
||||
│ └─ popup.html
|
||||
└─ wxt.config.ts
|
||||
└─ entrypoints/
|
||||
├─ background.ts
|
||||
├─ content.ts
|
||||
├─ injected.ts
|
||||
└─ popup.html
|
||||
```
|
||||
|
||||
would result in the following `manifest.json`:
|
||||
@@ -49,9 +48,9 @@ If a file uses a [special name recognized by WXT](/get-started/entrypoints.md),
|
||||
- `content.ts` → `content_scripts.0.js.0`
|
||||
- `background.ts` → `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 considered "unlisted" and are not apart of the manifest.
|
||||
But not all entrypoints are added to the `manifest.json`. If the filename 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")`.
|
||||
In this case, `injected.ts` gets output to `<outdir>/injected.js` and is accessible via `browser.runtime.getURL("/injected.js")`.
|
||||
|
||||
:::info
|
||||
See [`/entrypoints` folder](/guide/background.md) documentation for a full list of recognized entrypoint filenames.
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
# Installation
|
||||
|
||||
Bootstrap a new project or start from scratch.
|
||||
|
||||
:::warning 🚧 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 a new project, start from scratch, or migrate an existing project.
|
||||
|
||||
## Bootstrap Project
|
||||
|
||||
@@ -24,13 +18,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) |
|
||||
| <Icon name="Solid" icon="https://www.solidjs.com/img/favicons/favicon-32x32.png" /> [`solid`](https://github.com/aklinker1/wxt/tree/main/templates/solid) |
|
||||
| TypeScript |
|
||||
| ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| <Icon name="TypeScript" /> [`vanilla`](https://github.com/wxt-dev/wxt/tree/main/templates/vanilla) |
|
||||
| <Icon name="Vue" /> [`vue`](https://github.com/wxt-dev/wxt/tree/main/templates/vue) |
|
||||
| <Icon name="React" /> [`react`](https://github.com/wxt-dev/wxt/tree/main/templates/react) |
|
||||
| <Icon name="Svelte" /> [`svelte`](https://github.com/wxt-dev/wxt/tree/main/templates/svelte) |
|
||||
| <Icon name="Solid" icon="https://www.solidjs.com/img/favicons/favicon-32x32.png" /> [`solid`](https://github.com/wxt-dev/wxt/tree/main/templates/solid) |
|
||||
|
||||
> All templates are in TypeScript. WXT does not support JS at this time.
|
||||
|
||||
@@ -104,6 +98,22 @@ Finally, add scripts to your `package.json`:
|
||||
}
|
||||
```
|
||||
|
||||
## Migrate an Existing Project
|
||||
|
||||
Before starting the migration, it is recommended to run `pnpx wxt@latest init` to see what a basic project looks like. Once you have an understanding of how WXT projects are structured, you're ready to convert the project, using the initialized project as a reference.
|
||||
|
||||
Migrating a project to WXT comes down to a few steps:
|
||||
|
||||
1. Install WXT and remove any old build tools: `pnpm i -D wxt`
|
||||
1. Refactoring/move your entrypoints to the `entrypoints` directory
|
||||
1. Moving public assets to the `public` directory
|
||||
1. Update the dev and build scripts to use WXT
|
||||
1. Ensure project is compatible with Vite, which is used under the hood to bundle your extension
|
||||
|
||||
:::info
|
||||
Since projects vary greatly in setup, [start a discussion on GitHub](https://github.com/wxt-dev/wxt/discussions/new/choose) if you need help migrating your project to WXT.
|
||||
:::
|
||||
|
||||
## Development
|
||||
|
||||
Once you've installed WXT, you can start the development server using the `dev` script.
|
||||
@@ -134,3 +144,4 @@ You're ready to build your web extension!
|
||||
|
||||
- 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
|
||||
- Checkout [example projects](https://github.com/wxt-dev/wxt-examples) to see how to perfom common tasks with WXT
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
WXT is a free and open source framework for building web extensions in an conventional, intuative, and safe way **_for all browsers_**.
|
||||
|
||||
WXT comes with full TypeScript support and auto-imports. Sounds familiar? That's right, **_WXT was based off of Nuxt_** and aims to provide the same greate DX and features.
|
||||
WXT is based of [Nuxt](https://nuxt.com), and aims to provide the same great DX with TypeScript, auto-imports, and an opinionated project structure.
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -16,9 +16,9 @@ wxt zip -b firefox
|
||||
|
||||
Generated ZIP files are stored in the `.output` directory.
|
||||
|
||||
## Setup Automated Submissions
|
||||
## Automation
|
||||
|
||||
To automate submissions, use the [`publish-browser-extension`](https://www.npmjs.com/package/publish-browser-extension) package.
|
||||
To automate releasing updates, 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.
|
||||
@@ -159,7 +159,7 @@ Ensure that you have a `README.md` or `SOURCE_CODE_REVIEW.md` file with the abov
|
||||
🚧 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:
|
||||
🚧 WXT does not currently support automated publishing for Safari. Safari extensions require a native MacOS or iOS app wrapper, which WXT cannot create at this time. 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
|
||||
|
||||
|
||||
@@ -20,10 +20,19 @@ And more. All [`wxt/client`](/config.md#wxtclient) APIs can be used without impo
|
||||
|
||||
In addition WXT APIs, default and named exports from inside the following directories can be used without listing them in imports.
|
||||
|
||||
- `<srcDir>/components/**/*`
|
||||
- `<srcDir>/composables/**/*`
|
||||
- `<srcDir>/hooks/**/*`
|
||||
- `<srcDir>/utils/**/*`
|
||||
- `<srcDir>/components/*`
|
||||
- `<srcDir>/composables/*`
|
||||
- `<srcDir>/hooks/*`
|
||||
- `<srcDir>/utils/*`
|
||||
|
||||
To add auto-imports from subdirectories, like `utils/api/some-file.ts`, re-export them from the base directory:
|
||||
|
||||
```ts
|
||||
// utils/index.ts
|
||||
export * from './api/some-file.ts';
|
||||
```
|
||||
|
||||
Alternatively, you could add the directory to the list of auto-import directories in your config file.
|
||||
|
||||
## TypeScript
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ For MV2, the background is added as a script to the background page. For MV3, th
|
||||
<EntrypointPatterns
|
||||
:patterns="[
|
||||
['background.ts', 'background.js'],
|
||||
['background/index.ts', 'background.js'],
|
||||
]"
|
||||
/>
|
||||
|
||||
@@ -32,6 +33,10 @@ export default defineBackground({
|
||||
persistent: undefined | true | false,
|
||||
type: undefined | 'module',
|
||||
|
||||
// Set include/exclude if the background should be removed from some builds
|
||||
include: undefined | string[],
|
||||
exclude: undefined | string[],
|
||||
|
||||
// Executed when background is loaded
|
||||
main() {
|
||||
// ...
|
||||
|
||||
@@ -13,15 +13,16 @@
|
||||
|
||||
## Definition
|
||||
|
||||
Plain old HTML file.
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Title</title>
|
||||
<!-- Set include/exclude if the page should be removed from some builds -->
|
||||
<meta name="manifest.include" content="['chrome', ...]" />
|
||||
<meta name="manifest.exclude" content="['chrome', ...]" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- ... -->
|
||||
|
||||
@@ -2,16 +2,16 @@
|
||||
|
||||
[Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/content_scripts/) • [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.
|
||||
When creating content script entrypoints, they are automatically included in the `manifest.json` along with any CSS files they import.
|
||||
|
||||
## Filenames
|
||||
|
||||
<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'],
|
||||
['content.(ts|tsx)', 'content-scripts/content.js'],
|
||||
['content/index.(ts|tsx)', 'content-scripts/content.js'],
|
||||
['<name>.content.(ts|tsx)', 'content-scripts/<name>.js'],
|
||||
['<name>.content/index.(ts|tsx)', 'content-scripts/<name>.js'],
|
||||
]"
|
||||
/>
|
||||
|
||||
@@ -30,7 +30,11 @@ export default defineContentScript({
|
||||
matchOriginAsFallback: undefined | true | false,
|
||||
world: undefined | 'ISOLATED' | 'MAIN',
|
||||
|
||||
main() {
|
||||
// Set include/exclude if the background should be removed from some builds
|
||||
include: undefined | string[],
|
||||
exclude: undefined | string[],
|
||||
|
||||
main(ctx) {
|
||||
// Executed when content script is loaded
|
||||
},
|
||||
});
|
||||
@@ -59,7 +63,7 @@ import './style.css';
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ['*://google.com/*', '*://duckduckgo.com/*'],
|
||||
main() {
|
||||
main(ctx) {
|
||||
// ...
|
||||
},
|
||||
});
|
||||
@@ -79,3 +83,40 @@ Any styles imported in your content script will be added to that content script'
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Context
|
||||
|
||||
Old content scripts are not automatically stopped when an extension updates and restarts. Often, this leads to "Invalidated context" errors in production when a content script from an old version of your extension tries to use a web extension API. Since it's not connected to the latest version of your extension, the browser decides to throw an error.
|
||||
|
||||
WXT provides a utility for managing this process: `ContentScriptContext`. An instance of this class is provided to you automatically inside the `main` function of your content script.
|
||||
|
||||
```ts
|
||||
export default defineContentScript({
|
||||
// ...
|
||||
main(ctx: ContentScriptContext) {
|
||||
// Add custom listeners for stopping work
|
||||
ctx.onInvalidated(() => {
|
||||
// ...
|
||||
});
|
||||
|
||||
// Stop fetch requests
|
||||
fetch('...url', { signal: ctx.signal });
|
||||
|
||||
// Timeout utilities
|
||||
ctx.setTimeout(() => {
|
||||
// ...
|
||||
}, 5e3);
|
||||
ctx.setInterval(() => {
|
||||
// ...
|
||||
}, 60e3);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
The class extends [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) and provides other utilities for stopping a content script's logic once it becomes invalidated.
|
||||
|
||||
:::tip
|
||||
When working with content scripts, **you should always use the `ctx` object to stop any async or future work.**
|
||||
|
||||
This prevents old content scripts from interfering with new content scripts, and prevents error messages from the console in production.
|
||||
:::
|
||||
|
||||
@@ -16,12 +16,15 @@
|
||||
Plain old HTML file.
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Title</title>
|
||||
<!-- Set include/exclude if the page should be removed from some builds -->
|
||||
<meta name="manifest.include" content="['chrome', ...]" />
|
||||
<meta name="manifest.exclude" content="['chrome', ...]" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- ... -->
|
||||
|
||||
@@ -14,9 +14,11 @@ And that's it! Your extension now supports Chrome, Firefox, Safari, Edge, and ot
|
||||
The `browser` variable is available globally via [auto-imports](/guide/auto-imports.md), or it can be imported manually.
|
||||
|
||||
```ts
|
||||
import browser from 'webextension-polyfill';
|
||||
import { browser } from 'wxt/browser';
|
||||
```
|
||||
|
||||
The `wxt/browser` module exports a customized version of `webextension-polyfill`'s browser with improved typing.
|
||||
|
||||
### Example
|
||||
|
||||
Let's save the date the extension was installed. Just like `chrome`, some APIs require the permission is added to your manifest before the API is defined. Here, we need to add the `storage` permission to your manifest.
|
||||
@@ -51,6 +53,13 @@ Follow [Chrome's message passing guide](https://developer.chrome.com/docs/extens
|
||||
|
||||
Here's a basic request/response example:
|
||||
|
||||
```ts
|
||||
// popup/main.ts
|
||||
const res = await browser.runtime.sendMessage('ping');
|
||||
|
||||
console.log('res'); // "pong"
|
||||
```
|
||||
|
||||
```ts
|
||||
// background.ts
|
||||
export default defineBackground(() => {
|
||||
@@ -66,13 +75,6 @@ export default defineBackground(() => {
|
||||
});
|
||||
```
|
||||
|
||||
```ts
|
||||
// popup/main.ts
|
||||
const res = await browser.runtime.sendMessage('ping');
|
||||
|
||||
console.log('res'); // "pong"
|
||||
```
|
||||
|
||||
There are a number of message passing libraries you can use to improve the message passing experience.
|
||||
|
||||
Here are some that are compatible with WXT (because they are based off `webextension-polyfill` as well):
|
||||
|
||||
@@ -13,15 +13,16 @@
|
||||
|
||||
## Definition
|
||||
|
||||
Plain old HTML file.
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Title</title>
|
||||
<!-- Set include/exclude if the page should be removed from some builds -->
|
||||
<meta name="manifest.include" content="['chrome', ...]" />
|
||||
<meta name="manifest.exclude" content="['chrome', ...]" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- ... -->
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
The manifest.json is generated at build-time based on files in your `entrypoints` directory and your `wxt.config.ts`.
|
||||
|
||||
## Customization
|
||||
## Confiuration
|
||||
|
||||
While entrypoints are generated and added to the manifest at build-time, you can customize or add to your `manifest.json` in the config file.
|
||||
|
||||
@@ -47,9 +47,9 @@ The [manifest's `version` and `version_name`](https://developer.chrome.com/docs/
|
||||
}
|
||||
```
|
||||
|
||||
### `icons`
|
||||
## `icons`
|
||||
|
||||
The [manifest's `icons`](https://developer.chrome.com/docs/extensions/mv3/manifest/icons/) property needs to be set in the config file. The files should be added to WXT's [`public` directory](/get-started/assets#public-directory).
|
||||
By default, WXT will discover icons in your [`public` directory](/get-started/assets#public-directory) and use them for the [manifest's `icons`](https://developer.chrome.com/docs/extensions/mv3/manifest/icons/).
|
||||
|
||||
```
|
||||
public/
|
||||
@@ -60,21 +60,27 @@ public/
|
||||
└─ icon-128.png
|
||||
```
|
||||
|
||||
Icon files need to match the following regex to be automatically included in the manifest. Most design software can output icons in one of these formats
|
||||
|
||||
<<< @/../src/core/utils/manifest.ts#snippet
|
||||
|
||||
If you prefer to use filenames in a different format, you can add the icons manually in your `wxt.config.ts` file:
|
||||
|
||||
```ts
|
||||
export default defineConfig({
|
||||
manifest: {
|
||||
icons: {
|
||||
16: '/icon-16.png',
|
||||
24: '/icon-24.png',
|
||||
48: '/icon-48.png',
|
||||
96: '/icon-96.png',
|
||||
128: '/icon-128.png',
|
||||
16: '/extension-icon-16.png',
|
||||
24: '/extension-icon-24.png',
|
||||
48: '/extension-icon-48.png',
|
||||
96: '/extension-icon-96.png',
|
||||
128: '/extension-icon-128.png',
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Permissions
|
||||
## `permissions`
|
||||
|
||||
[Permissions](https://developer.chrome.com/docs/extensions/reference/permissions/) must be listed in the manifest config.
|
||||
|
||||
@@ -86,7 +92,7 @@ export default defineConfig({
|
||||
});
|
||||
```
|
||||
|
||||
### Localization
|
||||
## Localization
|
||||
|
||||
Similar to the icon, the [`_locales` directory](https://developer.chrome.com/docs/extensions/reference/i18n/) should be placed inside the the WXT's [`public` directory](/get-started/assets#public-directory).
|
||||
|
||||
|
||||
@@ -13,15 +13,16 @@
|
||||
|
||||
## Definition
|
||||
|
||||
Plain old HTML file.
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Title</title>
|
||||
<!-- Set include/exclude if the page should be removed from some builds -->
|
||||
<meta name="manifest.include" content="['chrome', ...]" />
|
||||
<meta name="manifest.exclude" content="['chrome', ...]" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- ... -->
|
||||
|
||||
@@ -13,10 +13,8 @@
|
||||
|
||||
## Definition
|
||||
|
||||
Plain old HTML file.
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
@@ -25,6 +23,9 @@ Plain old HTML file.
|
||||
<meta name="manifest.open_in_tab" content="true|false" />
|
||||
<meta name="manifest.chrome_style" content="true|false" />
|
||||
<meta name="manifest.browser_style" content="true|false" />
|
||||
<!-- Set include/exclude if the page should be removed from some builds -->
|
||||
<meta name="manifest.include" content="['chrome', ...]" />
|
||||
<meta name="manifest.exclude" content="['chrome', ...]" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- ... -->
|
||||
|
||||
@@ -13,10 +13,8 @@
|
||||
|
||||
## Definition
|
||||
|
||||
Plain old HTML file.
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
@@ -31,6 +29,9 @@ Plain old HTML file.
|
||||
}"
|
||||
/>
|
||||
<meta name="manifest.type" content="page_action|browser_action" />
|
||||
<!-- Set include/exclude if the page should be removed from some builds -->
|
||||
<meta name="manifest.include" content="['chrome', ...]" />
|
||||
<meta name="manifest.exclude" content="['chrome', ...]" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- ... -->
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# Remote Code
|
||||
|
||||
WXT will automatically download and bundle imports with the `url:` prefix so the extension does not depend of remote code, [a requirement from Google for MV3](https://developer.chrome.com/docs/extensions/migrating/improve-security/#remove-remote-code).
|
||||
|
||||
## Google Analytics
|
||||
|
||||
For example, you can import google analytics:
|
||||
|
||||
```ts
|
||||
// utils/google-analytics.ts
|
||||
import 'url:https://www.googletagmanager.com/gtag/js?id=G-XXXXXX';
|
||||
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
// NOTE: This line is different from Google's documentation
|
||||
window.gtag = function () {
|
||||
dataLayer.push(arguments);
|
||||
};
|
||||
gtag('js', new Date());
|
||||
gtag('config', 'G-XXXXXX');
|
||||
```
|
||||
|
||||
Then you can import this in your HTML files to enable Google Analytics:
|
||||
|
||||
```ts
|
||||
// popup/main.ts
|
||||
import '~/utils/google-analytics';
|
||||
|
||||
gtag('event', 'event_name', {
|
||||
key: 'value',
|
||||
});
|
||||
```
|
||||
@@ -19,15 +19,16 @@ Firefox does not support sandboxed pages.
|
||||
|
||||
## Definition
|
||||
|
||||
Plain old HTML file.
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Title</title>
|
||||
<!-- Set include/exclude if the page should be removed from some builds -->
|
||||
<meta name="manifest.include" content="['chrome', ...]" />
|
||||
<meta name="manifest.exclude" content="['chrome', ...]" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- ... -->
|
||||
|
||||
@@ -19,15 +19,16 @@ Firefox does not support sidepanel pages.
|
||||
|
||||
## Definition
|
||||
|
||||
Plain old HTML file.
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Title</title>
|
||||
<!-- Set include/exclude if the page should be removed from some builds -->
|
||||
<meta name="manifest.include" content="['chrome', ...]" />
|
||||
<meta name="manifest.exclude" content="['chrome', ...]" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- ... -->
|
||||
|
||||
@@ -29,15 +29,16 @@ console.log(url); // "chrome-extension://<id>/<name>.html"
|
||||
|
||||
## Definition
|
||||
|
||||
Plain old HTML file.
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Title</title>
|
||||
<!-- Set include/exclude if the page should be removed from some builds -->
|
||||
<meta name="manifest.include" content="['chrome', ...]" />
|
||||
<meta name="manifest.exclude" content="['chrome', ...]" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- ... -->
|
||||
|
||||
@@ -6,8 +6,8 @@ TypeScript files that are built, but are not included in the manifest.
|
||||
|
||||
<EntrypointPatterns
|
||||
:patterns="[
|
||||
['<name>.tsx?', '<name>.js'],
|
||||
['<name>/index.tsx?', '<name>.js'],
|
||||
['<name>.(ts|tsx)', '<name>.js'],
|
||||
['<name>/index.(ts|tsx)', '<name>.js'],
|
||||
]"
|
||||
/>
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
# Vite
|
||||
|
||||
Under the hood, WXT uses Vite to bundle your web extension.
|
||||
|
||||
## Basic Vite Configuration
|
||||
|
||||
All of Vite's config can be customized by setting the `vite` configuration in your `wxt.config.ts` file.
|
||||
|
||||
```ts
|
||||
import { defineConfig } from 'wxt';
|
||||
|
||||
export default defineConfig({
|
||||
vite: () => ({
|
||||
// Same as `defineConfig({ ... })` inside vite.config.ts
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
## Using Plugins
|
||||
|
||||
Plugins can be passed into the `vite` configuration in you `wxt.config.ts` file, just like any other option.
|
||||
|
||||
All plugins should work in WXT, but it is worth pointing out that since WXT orchestrates multiple vite builds to bundle an extension, plugins will be executed multiple times if necessary.
|
||||
|
||||
```ts
|
||||
import { defineConfig } from 'wxt';
|
||||
|
||||
export default defineConfig({
|
||||
vite: () => ({
|
||||
plugins: [
|
||||
// ...
|
||||
],
|
||||
}),
|
||||
});
|
||||
```
|
||||
@@ -46,12 +46,14 @@ features:
|
||||
- icon: 🖍️
|
||||
title: Bootstrap a New Project
|
||||
details: Comes with starter templates for all major frontend frameworks.
|
||||
- icon: 📏
|
||||
title: Bundle Analysis
|
||||
details: 'Tools for analyizing the final extension bundle.'
|
||||
- icon: 🤖
|
||||
title: Automated Publishing
|
||||
details: 'TODO: Automatically zip, upload, and release extensions.'
|
||||
- icon: 📏
|
||||
title: Bundle Analysis
|
||||
details: 'TODO: Tools for analyizing the final extension bundle.'
|
||||
---
|
||||
|
||||
<UsingWxtSection />
|
||||
<ClientOnly>
|
||||
<UsingWxtSection />
|
||||
</ClientOnly>
|
||||
|
||||
|
Before Width: | Height: | Size: 487 KiB After Width: | Height: | Size: 472 KiB |
@@ -2,60 +2,88 @@ import { describe, it, expect } from 'vitest';
|
||||
import { TestProject } from '../utils';
|
||||
|
||||
describe('Auto Imports', () => {
|
||||
it('should output types for entrypoint paths', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile(
|
||||
'entrypoints/background.ts',
|
||||
'export default defineBackground(() => {})',
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/overlay.content.ts',
|
||||
'export default defineContentScript(() => {})',
|
||||
);
|
||||
project.addFile('entrypoints/popup.html', '<html></html>');
|
||||
describe('imports: { ... }', () => {
|
||||
it('should generate a declaration file, imports.d.ts, for auto-imports', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/popup.html', `<html></html>`);
|
||||
|
||||
await project.build();
|
||||
await project.build();
|
||||
|
||||
expect(await project.serializeFile('.wxt/types/paths.d.ts'))
|
||||
.toMatchInlineSnapshot(`
|
||||
".wxt/types/paths.d.ts
|
||||
expect(await project.serializeFile('.wxt/types/imports.d.ts'))
|
||||
.toMatchInlineSnapshot(`
|
||||
".wxt/types/imports.d.ts
|
||||
----------------------------------------
|
||||
// Generated by wxt
|
||||
export {}
|
||||
declare global {
|
||||
const ContentScriptContext: typeof import('wxt/client')['ContentScriptContext']
|
||||
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']
|
||||
}
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should include auto-imports in the project', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/popup.html', `<html></html>`);
|
||||
|
||||
await project.build();
|
||||
|
||||
expect(await project.serializeFile('.wxt/wxt.d.ts'))
|
||||
.toMatchInlineSnapshot(`
|
||||
".wxt/wxt.d.ts
|
||||
----------------------------------------
|
||||
// Generated by wxt
|
||||
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;
|
||||
}
|
||||
}
|
||||
/// <reference types=\\"vite/client\\" />
|
||||
/// <reference types=\\"./types/imports.d.ts\\" />
|
||||
/// <reference types=\\"./types/paths.d.ts\\" />
|
||||
/// <reference types=\\"./types/i18n.d.ts\\" />
|
||||
/// <reference types=\\"./types/globals.d.ts\\" />
|
||||
"
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
it('should make some client utils auto-importable', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/popup.html', `<html></html>`);
|
||||
describe('imports: false', () => {
|
||||
it('should not generate a imports.d.ts file', async () => {
|
||||
const project = new TestProject();
|
||||
project.setConfigFileConfig({
|
||||
imports: false,
|
||||
});
|
||||
project.addFile('entrypoints/popup.html', `<html></html>`);
|
||||
|
||||
await project.build();
|
||||
await project.build();
|
||||
|
||||
expect(await project.serializeFile('.wxt/types/imports.d.ts'))
|
||||
.toMatchInlineSnapshot(`
|
||||
".wxt/types/imports.d.ts
|
||||
expect(await project.fileExists('.wxt/types/imports.d.ts')).toBe(false);
|
||||
});
|
||||
|
||||
it('should not include imports.d.ts in the type references', async () => {
|
||||
const project = new TestProject();
|
||||
project.setConfigFileConfig({
|
||||
imports: false,
|
||||
});
|
||||
project.addFile('entrypoints/popup.html', `<html></html>`);
|
||||
|
||||
await project.build();
|
||||
|
||||
expect(
|
||||
await project.serializeFile('.wxt/wxt.d.ts'),
|
||||
).toMatchInlineSnapshot(
|
||||
`
|
||||
".wxt/wxt.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']
|
||||
}
|
||||
/// <reference types=\\"vite/client\\" />
|
||||
/// <reference types=\\"./types/paths.d.ts\\" />
|
||||
/// <reference types=\\"./types/i18n.d.ts\\" />
|
||||
/// <reference types=\\"./types/globals.d.ts\\" />
|
||||
"
|
||||
`);
|
||||
`,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -133,6 +133,66 @@ describe('Manifest Content', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('icons', () => {
|
||||
it('should auto-discover icons with the correct name', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('public/icon-16.png');
|
||||
project.addFile('public/icon/32.jpeg');
|
||||
project.addFile('public/icon@48w.jpg');
|
||||
project.addFile('public/icon-64x64.gif');
|
||||
project.addFile('public/icon@96.bmp');
|
||||
project.addFile('public/icon/128x128.ico');
|
||||
|
||||
await project.build();
|
||||
const manifest = await project.getOutputManifest();
|
||||
|
||||
expect(manifest.icons).toEqual({
|
||||
'16': 'icon-16.png',
|
||||
'32': 'icon/32.jpeg',
|
||||
'48': 'icon@48w.jpg',
|
||||
'64': 'icon-64x64.gif',
|
||||
'96': 'icon@96.bmp',
|
||||
'128': 'icon/128x128.ico',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return undefined when no icons are found', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('public/logo.png');
|
||||
project.addFile('public/icon.jpeg');
|
||||
|
||||
await project.build();
|
||||
const manifest = await project.getOutputManifest();
|
||||
|
||||
expect(manifest.icons).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should allow icons to be overwritten from the wxt.config.ts file', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('public/icon-16.png');
|
||||
project.addFile('public/icon-32.png');
|
||||
project.addFile('public/logo-16.png');
|
||||
project.addFile('public/logo-32.png');
|
||||
project.addFile('public/logo-48.png');
|
||||
|
||||
const icons = {
|
||||
'16': 'logo-16.png',
|
||||
'32': 'logo-32.png',
|
||||
'48': 'logo-48.png',
|
||||
};
|
||||
project.setConfigFileConfig({
|
||||
manifest: {
|
||||
icons,
|
||||
},
|
||||
});
|
||||
|
||||
await project.build();
|
||||
const manifest = await project.getOutputManifest();
|
||||
|
||||
expect(manifest.icons).toEqual(icons);
|
||||
});
|
||||
});
|
||||
|
||||
it('should group content scripts and styles together based on their matches and run_at', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile(
|
||||
@@ -206,4 +266,69 @@ describe('Manifest Content', () => {
|
||||
js: ['content-scripts/one.js'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should add to any content scripts declared in wxt.config.ts', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile(
|
||||
'entrypoints/one.content/index.ts',
|
||||
`export default defineContentScript({
|
||||
matches: ["*://google.com/*"],
|
||||
main: () => {},
|
||||
})`,
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/two.content/style.css',
|
||||
`body {
|
||||
background-color: red;
|
||||
}`,
|
||||
);
|
||||
project.setConfigFileConfig({
|
||||
manifest: {
|
||||
content_scripts: [
|
||||
{
|
||||
css: ['content-scripts/two.css'],
|
||||
matches: ['*://*.google.com/*'],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
await project.build();
|
||||
|
||||
const manifest = await project.getOutputManifest();
|
||||
|
||||
expect(manifest.content_scripts).toContainEqual({
|
||||
css: ['content-scripts/two.css'],
|
||||
matches: ['*://*.google.com/*'],
|
||||
});
|
||||
expect(manifest.content_scripts).toContainEqual({
|
||||
matches: ['*://google.com/*'],
|
||||
js: ['content-scripts/one.js'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect the transformManifest option', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile(
|
||||
'wxt.config.ts',
|
||||
`import { defineConfig } from 'wxt';
|
||||
|
||||
export default defineConfig({
|
||||
transformManifest(manifest) {
|
||||
manifest.author = "Custom Author"
|
||||
}
|
||||
})`,
|
||||
);
|
||||
|
||||
await project.build();
|
||||
|
||||
const output = await project.serializeFile(
|
||||
'.output/chrome-mv3/manifest.json',
|
||||
);
|
||||
expect(output).toMatchInlineSnapshot(`
|
||||
".output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"author\\":\\"Custom Author\\"}"
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -43,7 +43,12 @@ describe('Output Directory Structure', () => {
|
||||
|
||||
await project.build();
|
||||
|
||||
expect(await project.serializeOutput()).toMatchInlineSnapshot(`
|
||||
expect(
|
||||
await project.serializeOutput([
|
||||
'.output/chrome-mv3/content-scripts/one.js',
|
||||
'.output/chrome-mv3/content-scripts/two.js',
|
||||
]),
|
||||
).toMatchInlineSnapshot(`
|
||||
".output/chrome-mv3/assets/one.css
|
||||
----------------------------------------
|
||||
body{color:#00f}
|
||||
@@ -56,13 +61,11 @@ describe('Output Directory Structure', () => {
|
||||
================================================================================
|
||||
.output/chrome-mv3/content-scripts/one.js
|
||||
----------------------------------------
|
||||
(function(){\\"use strict\\";function i(n){return n}const s=\\"\\",o={matches:[\\"*://*/*\\"],main:()=>{}};function t(n,...e){if(typeof e[0]==\\"string\\"){const c=e.shift();n(\`[wxt] \${c}\`,...e)}else n(\\"[wxt]\\",...e)}var r={debug:(...n)=>t(console.debug,...n),log:(...n)=>t(console.log,...n),warn:(...n)=>t(console.warn,...n),error:(...n)=>t(console.error,...n)};(async()=>{try{await o.main()}catch(n){r.error(\\"The content script crashed on startup!\\",n)}})()})();
|
||||
|
||||
<contents-ignored>
|
||||
================================================================================
|
||||
.output/chrome-mv3/content-scripts/two.js
|
||||
----------------------------------------
|
||||
(function(){\\"use strict\\";function i(n){return n}const s=\\"\\",o={matches:[\\"*://*/*\\"],main:()=>{}};function t(n,...e){if(typeof e[0]==\\"string\\"){const c=e.shift();n(\`[wxt] \${c}\`,...e)}else n(\\"[wxt]\\",...e)}var r={debug:(...n)=>t(console.debug,...n),log:(...n)=>t(console.log,...n),warn:(...n)=>t(console.warn,...n),error:(...n)=>t(console.error,...n)};(async()=>{try{await o.main()}catch(n){r.error(\\"The content script crashed on startup!\\",n)}})()})();
|
||||
|
||||
<contents-ignored>
|
||||
================================================================================
|
||||
.output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
@@ -82,15 +85,84 @@ describe('Output Directory Structure', () => {
|
||||
|
||||
await project.build();
|
||||
|
||||
expect(await project.serializeOutput()).toMatchInlineSnapshot(`
|
||||
expect(
|
||||
await project.serializeOutput([
|
||||
'.output/chrome-mv3/content-scripts/overlay-one.js',
|
||||
]),
|
||||
).toMatchInlineSnapshot(`
|
||||
".output/chrome-mv3/content-scripts/overlay-one.js
|
||||
----------------------------------------
|
||||
(function(){\\"use strict\\";function i(n){return n}const o={matches:[\\"*://*/*\\"],main:()=>{}};function t(n,...e){if(typeof e[0]==\\"string\\"){const c=e.shift();n(\`[wxt] \${c}\`,...e)}else n(\\"[wxt]\\",...e)}var r={debug:(...n)=>t(console.debug,...n),log:(...n)=>t(console.log,...n),warn:(...n)=>t(console.warn,...n),error:(...n)=>t(console.error,...n)};(async()=>{try{await o.main()}catch(n){r.error(\\"The content script crashed on startup!\\",n)}})()})();
|
||||
|
||||
<contents-ignored>
|
||||
================================================================================
|
||||
.output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"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\\"]}]}"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should not include an entrypoint if the target browser is not in the list of included targets', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/options.html', '<html></html>');
|
||||
project.addFile(
|
||||
'entrypoints/background.ts',
|
||||
`
|
||||
export default defineBackground({
|
||||
include: ["chrome"],
|
||||
main() {},
|
||||
})
|
||||
`,
|
||||
);
|
||||
|
||||
await project.build({ browser: 'firefox' });
|
||||
|
||||
expect(await project.fileExists('.output/firefox-mv2/background.js')).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('should not include an entrypoint if the target browser is in the list of excluded targets', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/options.html', '<html></html>');
|
||||
project.addFile(
|
||||
'entrypoints/background.ts',
|
||||
`
|
||||
export default defineBackground({
|
||||
exclude: ["chrome"],
|
||||
main() {},
|
||||
})
|
||||
`,
|
||||
);
|
||||
|
||||
await project.build({ browser: 'chrome' });
|
||||
|
||||
expect(await project.fileExists('.output/firefox-mv2/background.js')).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('should generate a stats file when analyzing the bundle', async () => {
|
||||
const project = new TestProject();
|
||||
project.setConfigFileConfig({
|
||||
analysis: {
|
||||
enabled: true,
|
||||
template: 'sunburst',
|
||||
},
|
||||
});
|
||||
project.addFile(
|
||||
'entrypoints/background.ts',
|
||||
`export default defineBackground(() => {});`,
|
||||
);
|
||||
project.addFile('entrypoints/popup.html', '<html></html>');
|
||||
project.addFile(
|
||||
'entrypoints/overlay.content.html',
|
||||
`export default defineContentScript({
|
||||
matches: [],
|
||||
main() {},
|
||||
});`,
|
||||
);
|
||||
|
||||
await project.build();
|
||||
|
||||
expect(await project.fileExists('stats.html')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import { TestProject } from '../utils';
|
||||
|
||||
describe('Remote Code', () => {
|
||||
it('should download "url:*" modules and include them in the final bundle', async () => {
|
||||
const url = 'https://stats.aklinker1.io/umami.js';
|
||||
const url = 'https://code.jquery.com/jquery-3.7.1.slim.min.js';
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/popup.ts', `import "url:${url}"`);
|
||||
|
||||
@@ -12,7 +12,7 @@ describe('Remote Code', () => {
|
||||
const output = await project.serializeFile('.output/chrome-mv3/popup.js');
|
||||
expect(output).toContain(
|
||||
// Some text that will hopefully be in future versions of this script
|
||||
'umami.disabled',
|
||||
'jQuery v3.7.1',
|
||||
);
|
||||
expect(output).not.toContain(url);
|
||||
expect(
|
||||
|
||||
@@ -0,0 +1,320 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { TestProject } from '../utils';
|
||||
|
||||
describe('TypeScript Project', () => {
|
||||
it('should generate defined constants correctly', async () => {
|
||||
const project = new TestProject();
|
||||
|
||||
await project.build();
|
||||
|
||||
const output = await project.serializeFile('.wxt/types/globals.d.ts');
|
||||
expect(output).toMatchInlineSnapshot(`
|
||||
".wxt/types/globals.d.ts
|
||||
----------------------------------------
|
||||
// Generated by wxt
|
||||
export {}
|
||||
declare global {
|
||||
const __MANIFEST_VERSION__: 2 | 3;
|
||||
const __BROWSER__: string;
|
||||
const __IS_CHROME__: boolean;
|
||||
const __IS_FIREFOX__: boolean;
|
||||
const __IS_SAFARI__: boolean;
|
||||
const __IS_EDGE__: boolean;
|
||||
const __IS_OPERA__: boolean;
|
||||
const __COMMAND__: \\"build\\" | \\"serve\\";
|
||||
const __ENTRYPOINT__: string;
|
||||
}
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should augment the types for browser.runtime.getURL', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile('entrypoints/popup.html');
|
||||
project.addFile('entrypoints/options.html');
|
||||
project.addFile('entrypoints/sandbox.html');
|
||||
|
||||
await project.build();
|
||||
|
||||
const output = await project.serializeFile('.wxt/types/paths.d.ts');
|
||||
expect(output).toMatchInlineSnapshot(`
|
||||
".wxt/types/paths.d.ts
|
||||
----------------------------------------
|
||||
// Generated by wxt
|
||||
import \\"wxt/browser\\";
|
||||
|
||||
declare module \\"wxt/browser\\" {
|
||||
type PublicPath =
|
||||
| \\"/options.html\\"
|
||||
| \\"/popup.html\\"
|
||||
| \\"/sandbox.html\\"
|
||||
export interface WxtRuntime extends Runtime.Static {
|
||||
getURL(path: PublicPath): string;
|
||||
}
|
||||
}
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should augment the types for browser.i18n.getMessage', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile(
|
||||
'public/_locales/en/messages.json',
|
||||
JSON.stringify({
|
||||
prompt_for_name: {
|
||||
message: "What's your name?",
|
||||
description: "Ask for the user's name",
|
||||
},
|
||||
hello: {
|
||||
message: 'Hello, $USER$',
|
||||
description: 'Greet the user',
|
||||
placeholders: {
|
||||
user: {
|
||||
content: '$1',
|
||||
example: 'Cira',
|
||||
},
|
||||
},
|
||||
},
|
||||
bye: {
|
||||
message: 'Goodbye, $USER$. Come back to $OUR_SITE$ soon!',
|
||||
description: 'Say goodbye to the user',
|
||||
placeholders: {
|
||||
our_site: {
|
||||
content: 'Example.com',
|
||||
},
|
||||
user: {
|
||||
content: '$1',
|
||||
example: 'Cira',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
project.setConfigFileConfig({
|
||||
manifest: {
|
||||
default_locale: 'en',
|
||||
},
|
||||
});
|
||||
|
||||
await project.build();
|
||||
|
||||
const output = await project.serializeFile('.wxt/types/i18n.d.ts');
|
||||
expect(output).toMatchInlineSnapshot(`
|
||||
".wxt/types/i18n.d.ts
|
||||
----------------------------------------
|
||||
// Generated by wxt
|
||||
import \\"wxt/browser\\";
|
||||
|
||||
declare module \\"wxt/browser\\" {
|
||||
/**
|
||||
* See https://developer.chrome.com/docs/extensions/reference/i18n/#method-getMessage
|
||||
*/
|
||||
interface GetMessageOptions {
|
||||
/**
|
||||
* See https://developer.chrome.com/docs/extensions/reference/i18n/#method-getMessage
|
||||
*/
|
||||
escapeLt?: boolean
|
||||
}
|
||||
|
||||
export interface WxtI18n extends I18n.Static {
|
||||
/**
|
||||
* The extension or app ID; you might use this string to construct URLs for resources inside the extension. Even unlocalized extensions can use this message.
|
||||
Note: You can't use this message in a manifest file.
|
||||
*
|
||||
* \\"<browser.runtime.id>\\"
|
||||
*/
|
||||
getMessage(
|
||||
messageName: \\"@@extension_id\\",
|
||||
substitutions?: string | string[],
|
||||
options?: GetMessageOptions,
|
||||
): string;
|
||||
/**
|
||||
*
|
||||
*
|
||||
* \\"<browser.i18n.getUiLocale()>\\"
|
||||
*/
|
||||
getMessage(
|
||||
messageName: \\"@@ui_locale\\",
|
||||
substitutions?: string | string[],
|
||||
options?: GetMessageOptions,
|
||||
): string;
|
||||
/**
|
||||
* The text direction for the current locale, either \\"ltr\\" for left-to-right languages such as English or \\"rtl\\" for right-to-left languages such as Japanese.
|
||||
*
|
||||
* \\"<ltr|rtl>\\"
|
||||
*/
|
||||
getMessage(
|
||||
messageName: \\"@@bidi_dir\\",
|
||||
substitutions?: string | string[],
|
||||
options?: GetMessageOptions,
|
||||
): string;
|
||||
/**
|
||||
* If the @@bidi_dir is \\"ltr\\", then this is \\"rtl\\"; otherwise, it's \\"ltr\\".
|
||||
*
|
||||
* \\"<rtl|ltr>\\"
|
||||
*/
|
||||
getMessage(
|
||||
messageName: \\"@@bidi_reversed_dir\\",
|
||||
substitutions?: string | string[],
|
||||
options?: GetMessageOptions,
|
||||
): string;
|
||||
/**
|
||||
* If the @@bidi_dir is \\"ltr\\", then this is \\"left\\"; otherwise, it's \\"right\\".
|
||||
*
|
||||
* \\"<left|right>\\"
|
||||
*/
|
||||
getMessage(
|
||||
messageName: \\"@@bidi_start_edge\\",
|
||||
substitutions?: string | string[],
|
||||
options?: GetMessageOptions,
|
||||
): string;
|
||||
/**
|
||||
* If the @@bidi_dir is \\"ltr\\", then this is \\"right\\"; otherwise, it's \\"left\\".
|
||||
*
|
||||
* \\"<right|left>\\"
|
||||
*/
|
||||
getMessage(
|
||||
messageName: \\"@@bidi_end_edge\\",
|
||||
substitutions?: string | string[],
|
||||
options?: GetMessageOptions,
|
||||
): string;
|
||||
/**
|
||||
* Ask for the user's name
|
||||
*
|
||||
* \\"What's your name?\\"
|
||||
*/
|
||||
getMessage(
|
||||
messageName: \\"prompt_for_name\\",
|
||||
substitutions?: string | string[],
|
||||
options?: GetMessageOptions,
|
||||
): string;
|
||||
/**
|
||||
* Greet the user
|
||||
*
|
||||
* \\"Hello, $USER$\\"
|
||||
*/
|
||||
getMessage(
|
||||
messageName: \\"hello\\",
|
||||
substitutions?: string | string[],
|
||||
options?: GetMessageOptions,
|
||||
): string;
|
||||
/**
|
||||
* Say goodbye to the user
|
||||
*
|
||||
* \\"Goodbye, $USER$. Come back to $OUR_SITE$ soon!\\"
|
||||
*/
|
||||
getMessage(
|
||||
messageName: \\"bye\\",
|
||||
substitutions?: string | string[],
|
||||
options?: GetMessageOptions,
|
||||
): string;
|
||||
}
|
||||
}
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should reference all the required types in a single declaration file', async () => {
|
||||
const project = new TestProject();
|
||||
|
||||
await project.build();
|
||||
|
||||
const output = await project.serializeFile('.wxt/wxt.d.ts');
|
||||
expect(output).toMatchInlineSnapshot(`
|
||||
".wxt/wxt.d.ts
|
||||
----------------------------------------
|
||||
// Generated by wxt
|
||||
/// <reference types=\\"vite/client\\" />
|
||||
/// <reference types=\\"./types/imports.d.ts\\" />
|
||||
/// <reference types=\\"./types/paths.d.ts\\" />
|
||||
/// <reference types=\\"./types/i18n.d.ts\\" />
|
||||
/// <reference types=\\"./types/globals.d.ts\\" />
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should generate a TSConfig file for the project', async () => {
|
||||
const project = new TestProject();
|
||||
|
||||
await project.build();
|
||||
|
||||
const output = await project.serializeFile('.wxt/tsconfig.json');
|
||||
expect(output).toMatchInlineSnapshot(`
|
||||
".wxt/tsconfig.json
|
||||
----------------------------------------
|
||||
{
|
||||
\\"compilerOptions\\": {
|
||||
\\"target\\": \\"ESNext\\",
|
||||
\\"module\\": \\"ESNext\\",
|
||||
\\"moduleResolution\\": \\"Bundler\\",
|
||||
\\"noEmit\\": true,
|
||||
\\"esModuleInterop\\": true,
|
||||
\\"forceConsistentCasingInFileNames\\": true,
|
||||
\\"resolveJsonModule\\": true,
|
||||
\\"strict\\": true,
|
||||
\\"lib\\": [\\"DOM\\", \\"WebWorker\\"],
|
||||
\\"skipLibCheck\\": true,
|
||||
\\"paths\\": {
|
||||
\\"@@\\": [\\"..\\"],
|
||||
\\"@@/*\\": [\\"../*\\"],
|
||||
\\"~~\\": [\\"..\\"],
|
||||
\\"~~/*\\": [\\"../*\\"],
|
||||
\\"@\\": [\\"..\\"],
|
||||
\\"@/*\\": [\\"../*\\"],
|
||||
\\"~\\": [\\"..\\"],
|
||||
\\"~/*\\": [\\"../*\\"]
|
||||
}
|
||||
},
|
||||
\\"include\\": [
|
||||
\\"../**/*\\",
|
||||
\\"./wxt.d.ts\\"
|
||||
],
|
||||
\\"exclude\\": [\\"../.output\\"]
|
||||
}"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should generate correct path aliases for a custom srcDir', async () => {
|
||||
const project = new TestProject();
|
||||
project.setConfigFileConfig({
|
||||
srcDir: 'src',
|
||||
});
|
||||
|
||||
await project.build();
|
||||
|
||||
const output = await project.serializeFile('.wxt/tsconfig.json');
|
||||
expect(output).toMatchInlineSnapshot(`
|
||||
".wxt/tsconfig.json
|
||||
----------------------------------------
|
||||
{
|
||||
\\"compilerOptions\\": {
|
||||
\\"target\\": \\"ESNext\\",
|
||||
\\"module\\": \\"ESNext\\",
|
||||
\\"moduleResolution\\": \\"Bundler\\",
|
||||
\\"noEmit\\": true,
|
||||
\\"esModuleInterop\\": true,
|
||||
\\"forceConsistentCasingInFileNames\\": true,
|
||||
\\"resolveJsonModule\\": true,
|
||||
\\"strict\\": true,
|
||||
\\"lib\\": [\\"DOM\\", \\"WebWorker\\"],
|
||||
\\"skipLibCheck\\": true,
|
||||
\\"paths\\": {
|
||||
\\"@@\\": [\\"..\\"],
|
||||
\\"@@/*\\": [\\"../*\\"],
|
||||
\\"~~\\": [\\"..\\"],
|
||||
\\"~~/*\\": [\\"../*\\"],
|
||||
\\"@\\": [\\"../src\\"],
|
||||
\\"@/*\\": [\\"../src/*\\"],
|
||||
\\"~\\": [\\"../src\\"],
|
||||
\\"~/*\\": [\\"../src/*\\"]
|
||||
}
|
||||
},
|
||||
\\"include\\": [
|
||||
\\"../**/*\\",
|
||||
\\"./wxt.d.ts\\"
|
||||
],
|
||||
\\"exclude\\": [\\"../.output\\"]
|
||||
}"
|
||||
`);
|
||||
});
|
||||
});
|
||||
@@ -10,8 +10,8 @@ export class TestProject {
|
||||
config: UserConfig | undefined;
|
||||
readonly root: string;
|
||||
|
||||
constructor(root = 'e2e/project') {
|
||||
// We can't put each test's project inside e2e/project directly, otherwise the wxt.config.ts
|
||||
constructor(root = 'e2e/dist') {
|
||||
// We can't put each test's project inside e2e/dist directly, otherwise the wxt.config.ts
|
||||
// file is cached and cannot be different between each test. Instead, we add a random ID to the
|
||||
// end to make each test's path unique.
|
||||
const id = Math.random().toString(32).substring(3);
|
||||
@@ -80,9 +80,12 @@ export class TestProject {
|
||||
/**
|
||||
* Read all the files from the test project's `.output` directory and combine them into a string
|
||||
* that can be used in a snapshot.
|
||||
*
|
||||
* Optionally, provide a list of filenames whose content is not printed (because it's inconsistent
|
||||
* or not relevant to a test).
|
||||
*/
|
||||
serializeOutput(): Promise<string> {
|
||||
return this.serializeDir('.output');
|
||||
serializeOutput(ignoreContentsOfFilenames?: string[]): Promise<string> {
|
||||
return this.serializeDir('.output', ignoreContentsOfFilenames);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -95,7 +98,16 @@ export class TestProject {
|
||||
);
|
||||
}
|
||||
|
||||
private async serializeDir(dir: string): Promise<string> {
|
||||
/**
|
||||
* Deeply print the filename and contents of all files in a directory.
|
||||
*
|
||||
* Optionally, provide a list of filenames whose content is not printed (because it's inconsistent
|
||||
* or not relevant to a test).
|
||||
*/
|
||||
private async serializeDir(
|
||||
dir: string,
|
||||
ignoreContentsOfFilenames?: string[],
|
||||
): Promise<string> {
|
||||
const outputFiles = await glob('**/*', {
|
||||
cwd: resolve(this.root, dir),
|
||||
ignore: ['**/node_modules', '**/.output'],
|
||||
@@ -104,19 +116,24 @@ export class TestProject {
|
||||
const fileContents = [];
|
||||
for (const file of outputFiles) {
|
||||
const path = resolve(this.root, dir, file);
|
||||
fileContents.push(await this.serializeFile(path));
|
||||
const isContentIgnored = !!ignoreContentsOfFilenames?.find(
|
||||
(ignoredFile) => normalizePath(path).endsWith(ignoredFile),
|
||||
);
|
||||
fileContents.push(await this.serializeFile(path, isContentIgnored));
|
||||
}
|
||||
return fileContents.join(`\n${''.padEnd(80, '=')}\n`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path An abosolute path to a file or a path relative to the root.
|
||||
* @param ignoreContents An optional boolean that, when true, causes this function to not print
|
||||
* the file contents.
|
||||
*/
|
||||
async serializeFile(path: string): Promise<string> {
|
||||
async serializeFile(path: string, ignoreContents?: boolean): Promise<string> {
|
||||
const absolutePath = resolve(this.root, path);
|
||||
return [
|
||||
normalizePath(relative(this.root, absolutePath)),
|
||||
await fs.readFile(absolutePath),
|
||||
ignoreContents ? '<contents-ignored>' : await fs.readFile(absolutePath),
|
||||
].join(`\n${''.padEnd(40, '-')}\n`);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "wxt",
|
||||
"type": "module",
|
||||
"version": "0.3.1",
|
||||
"version": "0.6.2",
|
||||
"description": "Next gen framework for developing web extensions",
|
||||
"engines": {
|
||||
"node": ">=18.16.0",
|
||||
@@ -9,8 +9,9 @@
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/aklinker1/wxt"
|
||||
"url": "https://github.com/wxt-dev/wxt"
|
||||
},
|
||||
"homepage": "https://wxt.dev",
|
||||
"keywords": [
|
||||
"vite",
|
||||
"chrome",
|
||||
@@ -60,31 +61,35 @@
|
||||
"prepublish": "pnpm -s build",
|
||||
"docs:dev": "vitepress dev docs",
|
||||
"docs:build": "vitepress build docs",
|
||||
"docs:preview": "vitepress preview docs"
|
||||
"docs:preview": "vitepress preview docs",
|
||||
"sync-releases": "pnpx changelogen@latest gh release all"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/webextension-polyfill": "^0.10.0",
|
||||
"@types/webextension-polyfill": "^0.10.1",
|
||||
"@webext-core/fake-browser": "^1.2.2",
|
||||
"@webext-core/match-patterns": "^1.0.1",
|
||||
"async-mutex": "^0.4.0",
|
||||
"c12": "^1.4.2",
|
||||
"cac": "^6.7.14",
|
||||
"consola": "^3.1.0",
|
||||
"fast-glob": "^3.2.12",
|
||||
"filesize": "^10.0.7",
|
||||
"consola": "^3.2.3",
|
||||
"fast-glob": "^3.3.1",
|
||||
"filesize": "^10.0.8",
|
||||
"fs-extra": "^11.1.1",
|
||||
"get-port": "^7.0.0",
|
||||
"giget": "^1.1.2",
|
||||
"jiti": "^1.18.2",
|
||||
"immer": "^10.0.2",
|
||||
"is-wsl": "^3.0.0",
|
||||
"jiti": "^1.19.1",
|
||||
"json5": "^2.2.3",
|
||||
"linkedom": "^0.14.26",
|
||||
"linkedom": "^0.15.1",
|
||||
"minimatch": "^9.0.3",
|
||||
"ora": "^7.0.1",
|
||||
"picocolors": "^1.0.0",
|
||||
"prompts": "^2.4.2",
|
||||
"unimport": "^3.0.8",
|
||||
"vite": "^4.3.9",
|
||||
"vite-tsconfig-paths": "^4.2.0",
|
||||
"web-ext": "^7.6.2",
|
||||
"rollup-plugin-visualizer": "^5.9.2",
|
||||
"unimport": "^3.1.0",
|
||||
"vite": "^4.4.7",
|
||||
"web-ext-run": "^0.1.0",
|
||||
"webextension-polyfill": "^0.10.0",
|
||||
"zip-dir": "^2.0.0"
|
||||
},
|
||||
@@ -92,22 +97,23 @@
|
||||
"@faker-js/faker": "^8.0.2",
|
||||
"@types/fs-extra": "^11.0.1",
|
||||
"@types/lodash.merge": "^4.6.7",
|
||||
"@types/node": "^20.3.1",
|
||||
"@types/node": "^20.4.5",
|
||||
"@types/prompts": "^2.4.4",
|
||||
"@vitest/coverage-v8": "^0.32.2",
|
||||
"@vitest/coverage-v8": "^0.34.1",
|
||||
"execa": "^7.2.0",
|
||||
"lint-staged": "^14.0.0",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"ora": "^6.3.1",
|
||||
"prettier": "^2.8.8",
|
||||
"pretty-quick": "^3.1.3",
|
||||
"simple-git-hooks": "^2.8.1",
|
||||
"tsup": "^7.0.0",
|
||||
"prettier": "^3.0.1",
|
||||
"simple-git-hooks": "^2.9.0",
|
||||
"ts-morph": "^19.0.0",
|
||||
"tsup": "^7.1.0",
|
||||
"tsx": "^3.12.7",
|
||||
"typescript": "^5.1.3",
|
||||
"vitepress": "1.0.0-beta.5",
|
||||
"vitest": "^0.32.4",
|
||||
"typescript": "^5.1.6",
|
||||
"vitepress": "1.0.0-rc.10",
|
||||
"vitest": "^0.34.1",
|
||||
"vitest-mock-extended": "^1.1.4",
|
||||
"vue": "^3.3.4",
|
||||
"webextension-polyfill": "^0.10.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -115,16 +121,16 @@
|
||||
},
|
||||
"packageManager": "pnpm@8.6.3",
|
||||
"simple-git-hooks": {
|
||||
"pre-commit": "pnpm pretty-quick --staged"
|
||||
"pre-commit": "pnpm lint-staged"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*": "prettier --write"
|
||||
},
|
||||
"pnpm": {
|
||||
"peerDependencyRules": {
|
||||
"ignoreMissing": [
|
||||
"@algolia/client-search",
|
||||
"search-insights"
|
||||
],
|
||||
"allowAny": [
|
||||
"node-fetch"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,12 +23,14 @@ await Promise.all([
|
||||
sourcemap: true,
|
||||
dts: true,
|
||||
silent: true,
|
||||
external: ['vite'],
|
||||
}),
|
||||
tsup.build({
|
||||
entry: { cli: 'src/cli/index.ts' },
|
||||
format: ['cjs'],
|
||||
sourcemap: 'inline',
|
||||
silent: true,
|
||||
external: ['vite'],
|
||||
}),
|
||||
tsup.build({
|
||||
entry: { client: 'src/client/index.ts' },
|
||||
@@ -36,6 +38,7 @@ await Promise.all([
|
||||
sourcemap: 'inline',
|
||||
dts: true,
|
||||
silent: true,
|
||||
external: ['vite'],
|
||||
}),
|
||||
tsup.build({
|
||||
entry: { browser: 'src/client/browser.ts' },
|
||||
@@ -43,6 +46,7 @@ await Promise.all([
|
||||
sourcemap: 'inline',
|
||||
dts: true,
|
||||
silent: true,
|
||||
external: ['vite'],
|
||||
}),
|
||||
...virtualEntrypoints.map((entryName) =>
|
||||
tsup.build({
|
||||
@@ -52,7 +56,7 @@ await Promise.all([
|
||||
format: ['esm'],
|
||||
sourcemap: true,
|
||||
silent: true,
|
||||
external: [`virtual:user-${entryName}`],
|
||||
external: [`virtual:user-${entryName}`, 'vite'],
|
||||
}),
|
||||
),
|
||||
tsup.build({
|
||||
@@ -62,6 +66,7 @@ await Promise.all([
|
||||
format: ['esm'],
|
||||
sourcemap: true,
|
||||
silent: true,
|
||||
external: ['vite'],
|
||||
}),
|
||||
tsup.build({
|
||||
entry: {
|
||||
@@ -69,6 +74,7 @@ await Promise.all([
|
||||
},
|
||||
format: ['esm', 'cjs'],
|
||||
silent: true,
|
||||
external: ['vite'],
|
||||
}),
|
||||
]).catch((err) => {
|
||||
spinner.fail();
|
||||
@@ -76,9 +82,13 @@ await Promise.all([
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
spinner.succeed();
|
||||
spinner.clear().stop();
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
const outFiles = await glob(`${outDir}/**`, { absolute: true });
|
||||
await printFileList(consola.log, outDir, outFiles);
|
||||
consola.success(`Finished in ${formatDuration(duration)}`);
|
||||
await printFileList(
|
||||
consola.success,
|
||||
`Built WXT in ${formatDuration(duration)}`,
|
||||
outDir,
|
||||
outFiles,
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
declare module 'web-ext' {
|
||||
declare module 'web-ext-run' {
|
||||
export interface WebExtRunInstance {
|
||||
reloadAllExtensions(): Promise<void>;
|
||||
exit(): Promise<void>;
|
||||
@@ -12,7 +12,7 @@ declare module 'web-ext' {
|
||||
export default webExt;
|
||||
}
|
||||
|
||||
declare module 'web-ext/util/logger' {
|
||||
declare module 'web-ext-run/util/logger' {
|
||||
// https://github.com/mozilla/web-ext/blob/e37e60a2738478f512f1255c537133321f301771/src/util/logger.js#L43
|
||||
export interface IConsoleStream {
|
||||
stopCapturing(): void;
|
||||
@@ -10,16 +10,21 @@ export const build = defineCommand<
|
||||
browser?: wxt.TargetBrowser;
|
||||
mv3?: boolean;
|
||||
mv2?: boolean;
|
||||
debug?: boolean;
|
||||
analyze?: boolean;
|
||||
},
|
||||
]
|
||||
>(async (root, flags) => {
|
||||
const mode = flags.mode ?? 'production';
|
||||
const cliConfig: wxt.InlineConfig = {
|
||||
root,
|
||||
mode,
|
||||
mode: flags.mode,
|
||||
browser: flags.browser,
|
||||
manifestVersion: flags.mv3 ? 3 : flags.mv2 ? 2 : undefined,
|
||||
configFile: flags.config,
|
||||
debug: flags.debug,
|
||||
analysis: {
|
||||
enabled: flags.analyze,
|
||||
},
|
||||
};
|
||||
|
||||
await wxt.build(cliConfig);
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { defineCommand } from '../utils/defineCommand';
|
||||
import * as wxt from '../..';
|
||||
|
||||
export const clean = defineCommand<
|
||||
[
|
||||
root: string | undefined,
|
||||
flags: {
|
||||
debug?: boolean;
|
||||
},
|
||||
]
|
||||
>(wxt.clean);
|
||||
@@ -10,16 +10,17 @@ export const dev = defineCommand<
|
||||
browser?: wxt.TargetBrowser;
|
||||
mv3?: boolean;
|
||||
mv2?: boolean;
|
||||
debug?: boolean;
|
||||
},
|
||||
]
|
||||
>(async (root, flags) => {
|
||||
const mode = flags.mode ?? 'development';
|
||||
const cliConfig: wxt.InlineConfig = {
|
||||
root,
|
||||
mode,
|
||||
mode: flags.mode,
|
||||
browser: flags.browser,
|
||||
manifestVersion: flags.mv3 ? 3 : flags.mv2 ? 2 : undefined,
|
||||
configFile: flags.config,
|
||||
debug: flags.debug,
|
||||
};
|
||||
|
||||
const server = await wxt.createServer(cliConfig);
|
||||
|
||||
@@ -4,3 +4,4 @@ export * from './init';
|
||||
export * from './prepare';
|
||||
export * from './publish';
|
||||
export * from './zip';
|
||||
export * from './clean';
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
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';
|
||||
@@ -9,7 +8,10 @@ import pc from 'picocolors';
|
||||
import { Formatter } from 'picocolors/types';
|
||||
|
||||
export const init = defineCommand<
|
||||
[directory: string | undefined, options: { template?: string; pm?: string }]
|
||||
[
|
||||
directory: string | undefined,
|
||||
options: { template?: string; pm?: string; debug?: boolean },
|
||||
]
|
||||
>(
|
||||
async (userDirectory, flags) => {
|
||||
consola.info('Initalizing new project');
|
||||
@@ -69,7 +71,7 @@ export const init = defineCommand<
|
||||
console.log();
|
||||
consola.log('Next steps:');
|
||||
let step = 0;
|
||||
if (cdPath !== '.') consola.log(` ${++step}.`, pc.cyan(`cd ${cdPath}`));
|
||||
if (cdPath !== '') consola.log(` ${++step}.`, pc.cyan(`cd ${cdPath}`));
|
||||
consola.log(` ${++step}.`, pc.cyan(`${input.packageManager} install`));
|
||||
console.log();
|
||||
},
|
||||
@@ -90,7 +92,7 @@ interface Template {
|
||||
async function listTemplates(): Promise<Template[]> {
|
||||
try {
|
||||
const res = await fetch(
|
||||
'https://api.github.com/repos/aklinker1/wxt/contents/templates',
|
||||
'https://api.github.com/repos/wxt-dev/wxt/contents/templates',
|
||||
{
|
||||
headers: {
|
||||
Accept: 'application/vnd.github+json',
|
||||
@@ -130,10 +132,11 @@ async function cloneProject({
|
||||
template: Template;
|
||||
packageManager: string;
|
||||
}) {
|
||||
const { default: ora } = await import('ora');
|
||||
const spinner = ora('Downloading template').start();
|
||||
try {
|
||||
// 1. Clone repo
|
||||
await downloadTemplate(`gh:aklinker1/wxt/${template.path}`, {
|
||||
await downloadTemplate(`gh:wxt-dev/wxt/${template.path}`, {
|
||||
dir: directory,
|
||||
force: true,
|
||||
});
|
||||
|
||||
@@ -9,12 +9,14 @@ export const prepare = defineCommand<
|
||||
root: string | undefined,
|
||||
flags: {
|
||||
config?: string;
|
||||
debug?: boolean;
|
||||
},
|
||||
]
|
||||
>(async (root, flags) => {
|
||||
const cliConfig: wxt.InlineConfig = {
|
||||
root,
|
||||
configFile: flags.config,
|
||||
debug: flags.debug,
|
||||
};
|
||||
const config = await getInternalConfig(cliConfig, 'build');
|
||||
|
||||
|
||||
@@ -2,7 +2,10 @@ import { consola } from 'consola';
|
||||
import { defineCommand } from '../utils/defineCommand';
|
||||
|
||||
export const publish = defineCommand(
|
||||
async (root: any, { config: configFile }: any) => {
|
||||
async (
|
||||
root: any,
|
||||
{ config: configFile, debug }: { config?: string; debug?: string },
|
||||
) => {
|
||||
consola.warn('wxt publish: Not implemented');
|
||||
},
|
||||
);
|
||||
|
||||
@@ -13,16 +13,17 @@ export const zip = defineCommand<
|
||||
browser?: wxt.TargetBrowser;
|
||||
mv3?: boolean;
|
||||
mv2?: boolean;
|
||||
debug?: boolean;
|
||||
},
|
||||
]
|
||||
>(async (root, flags) => {
|
||||
const mode = flags.mode ?? 'production';
|
||||
const cliConfig: wxt.InlineConfig = {
|
||||
root,
|
||||
mode,
|
||||
mode: flags.mode,
|
||||
browser: flags.browser,
|
||||
manifestVersion: flags.mv3 ? 3 : flags.mv2 ? 2 : undefined,
|
||||
configFile: flags.config,
|
||||
debug: flags.debug,
|
||||
};
|
||||
|
||||
const config = await getInternalConfig(cliConfig, 'build');
|
||||
|
||||
@@ -6,6 +6,8 @@ const cli = cac('wxt');
|
||||
cli.help();
|
||||
cli.version(version);
|
||||
|
||||
cli.option('--debug', 'enable debug mode');
|
||||
|
||||
// DEV
|
||||
cli
|
||||
.command('[root]', 'start dev server')
|
||||
@@ -24,6 +26,7 @@ cli
|
||||
.option('-b, --browser <browser>', 'specify a browser')
|
||||
.option('--mv3', 'target manifest v3')
|
||||
.option('--mv2', 'target manifest v2')
|
||||
.option('--analyze', 'visualize extension bundle')
|
||||
.action(commands.build);
|
||||
|
||||
// ZIP
|
||||
@@ -38,10 +41,16 @@ cli
|
||||
|
||||
// PREPARE
|
||||
cli
|
||||
.command('prepare [root]', 'prepare')
|
||||
.command('prepare [root]', 'prepare typescript project')
|
||||
.option('-c, --config <file>', 'use specified config file')
|
||||
.action(commands.prepare);
|
||||
|
||||
// CLEAN
|
||||
cli
|
||||
.command('clean [root]', 'clean generated files and caches')
|
||||
.alias('cleanup')
|
||||
.action(commands.clean);
|
||||
|
||||
// PUBLISH
|
||||
cli.command('publish [root]', 'publish to stores').action(commands.publish);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { consola } from 'consola';
|
||||
import { LogLevels, consola } from 'consola';
|
||||
import { printHeader } from '../../core/log/printHeader';
|
||||
import { formatDuration } from '../../core/utils/formatDuration';
|
||||
|
||||
@@ -9,6 +9,13 @@ export function defineCommand<TArgs extends any[]>(
|
||||
},
|
||||
) {
|
||||
return async (...args: TArgs) => {
|
||||
// Enable consola's debug mode globally at the start of all commands when the `--debug` flag is
|
||||
// passed
|
||||
const isDebug = !!args.find((arg) => arg?.debug);
|
||||
if (isDebug) {
|
||||
consola.level = LogLevels.debug;
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
try {
|
||||
printHeader();
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import originalBrowser, { Browser, Runtime } from 'webextension-polyfill';
|
||||
import originalBrowser, { Browser, Runtime, I18n } from 'webextension-polyfill';
|
||||
|
||||
export interface AugmentedBrowser extends Browser {
|
||||
runtime: ProjectRuntime;
|
||||
runtime: WxtRuntime;
|
||||
i18n: WxtI18n;
|
||||
}
|
||||
|
||||
export interface ProjectRuntime extends Runtime.Static {
|
||||
export interface WxtRuntime extends Runtime.Static {
|
||||
// Overriden per-project
|
||||
}
|
||||
|
||||
export interface WxtI18n extends I18n.Static {
|
||||
// Overriden per-project
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './defineContentScript';
|
||||
export * from './defineBackground';
|
||||
export * from './mountContentScriptUi';
|
||||
export * from './utils/ContentScriptContext';
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import { browser } from '../browser';
|
||||
import { logger } from './logger';
|
||||
|
||||
/**
|
||||
* Extends [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController).
|
||||
* Used to detect and stop content script code when the script is invalidated.
|
||||
*
|
||||
* It also provides several utilities like `ctx.setTimeout` and `ctx.setInterval` that should be used in
|
||||
* content scripts instead of `window.setTimeout` or `window.setInterval`.
|
||||
*/
|
||||
export class ContentScriptContext extends AbortController {
|
||||
static SCRIPT_STARTED_MESSAGE_TYPE = 'wxt:content-script-started';
|
||||
|
||||
#isTopFrame = window.self === window.top;
|
||||
|
||||
constructor(private readonly contentScriptName: string) {
|
||||
super();
|
||||
|
||||
if (this.#isTopFrame) {
|
||||
this.#stopOldScripts();
|
||||
}
|
||||
this.setTimeout(() => {
|
||||
// Run on next tick so the listener it adds isn't triggered by stopOldScript
|
||||
this.#listenForNewerScripts();
|
||||
});
|
||||
}
|
||||
|
||||
get isInvalid(): boolean {
|
||||
if (browser.runtime.id == null) {
|
||||
this.notifyInvalidated(); // Sets `signal.aborted` to true
|
||||
}
|
||||
return this.signal.aborted;
|
||||
}
|
||||
|
||||
get isValid(): boolean {
|
||||
return !this.isInvalid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a listener that is called when the content script's context is invalidated.
|
||||
*
|
||||
* @returns A function to remove the listener.
|
||||
*
|
||||
* @example
|
||||
* browser.runtime.onMessage.addListener(cb);
|
||||
* const removeInvalidatedListener = ctx.onInvalidated(() => {
|
||||
* browser.runtime.onMessage.removeListener(cb);
|
||||
* })
|
||||
* // ...
|
||||
* removeInvalidatedListener();
|
||||
*/
|
||||
onInvalidated(cb: () => void): () => void {
|
||||
this.signal.addEventListener('abort', cb);
|
||||
return () => this.signal.removeEventListener('abort', cb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around `window.setInterval` that automatically clears the interval when invalidated.
|
||||
*/
|
||||
setInterval(handler: () => void, timeout?: number): number {
|
||||
const id = setInterval(() => {
|
||||
if (this.isValid) handler();
|
||||
}, timeout) as unknown as number;
|
||||
this.onInvalidated(() => clearInterval(id));
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around `window.setTimeout` that automatically clears the interval when invalidated.
|
||||
*/
|
||||
setTimeout(handler: () => void, timeout?: number): number {
|
||||
const id = setTimeout(() => {
|
||||
if (this.isValid) handler();
|
||||
}, timeout) as unknown as number;
|
||||
this.onInvalidated(() => clearTimeout(id));
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around `window.requestAnimationFrame` that automatically cancels the request when
|
||||
* invalidated.
|
||||
*/
|
||||
requestAnimationFrame(callback: FrameRequestCallback): number {
|
||||
const id = requestAnimationFrame((...args) => {
|
||||
if (this.isValid) callback(...args);
|
||||
});
|
||||
|
||||
this.onInvalidated(() => cancelAnimationFrame(id));
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around `window.requestIdleCallback` that automatically cancels the request when
|
||||
* invalidated.
|
||||
*/
|
||||
requestIdleCallback(
|
||||
callback: IdleRequestCallback,
|
||||
options?: IdleRequestOptions,
|
||||
): number {
|
||||
const id = requestIdleCallback((...args) => {
|
||||
if (!this.signal.aborted) callback(...args);
|
||||
}, options);
|
||||
|
||||
this.onInvalidated(() => cancelIdleCallback(id));
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Abort the abort controller and execute all `onInvalidated` listeners.
|
||||
*/
|
||||
notifyInvalidated() {
|
||||
this.abort('Content script context invalidated');
|
||||
logger.debug(
|
||||
`Content script "${this.contentScriptName}" context invalidated`,
|
||||
);
|
||||
}
|
||||
|
||||
#stopOldScripts() {
|
||||
// Use postMessage so it get's sent to all the frames of the page.
|
||||
window.postMessage({
|
||||
event: ContentScriptContext.SCRIPT_STARTED_MESSAGE_TYPE,
|
||||
contentScriptName: this.contentScriptName,
|
||||
});
|
||||
}
|
||||
|
||||
#listenForNewerScripts() {
|
||||
const cb = (event: MessageEvent) => {
|
||||
if (
|
||||
event.data?.type === ContentScriptContext.SCRIPT_STARTED_MESSAGE_TYPE &&
|
||||
event.data?.contentScriptName === this.contentScriptName
|
||||
) {
|
||||
this.notifyInvalidated();
|
||||
}
|
||||
};
|
||||
|
||||
addEventListener('message', cb);
|
||||
this.onInvalidated(() => removeEventListener('message', cb));
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
function print(method: (...args: any[]) => void, ...args: any[]) {
|
||||
if (import.meta.env.MODE === 'production') return;
|
||||
|
||||
if (typeof args[0] === 'string') {
|
||||
const message = args.shift();
|
||||
method(`[wxt] ${message}`, ...args);
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import definition from 'virtual:user-content-script';
|
||||
import { logger } from '../utils/logger';
|
||||
import { ContentScriptContext } from '../utils/ContentScriptContext';
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
await definition.main();
|
||||
const ctx = new ContentScriptContext(__ENTRYPOINT__);
|
||||
|
||||
await definition.main(ctx);
|
||||
} catch (err) {
|
||||
logger.error('The content script crashed on startup!', err);
|
||||
}
|
||||
|
||||
@@ -21,3 +21,4 @@ declare const __DEV_SERVER_PROTOCOL__: string;
|
||||
declare const __DEV_SERVER_HOSTNAME__: string;
|
||||
declare const __DEV_SERVER_PORT__: string;
|
||||
declare const __MANIFEST_VERSION__: 2 | 3;
|
||||
declare const __ENTRYPOINT__: string;
|
||||
|
||||
@@ -11,6 +11,9 @@ import fs from 'fs-extra';
|
||||
import { groupEntrypoints } from './utils/groupEntrypoints';
|
||||
import { formatDuration } from './utils/formatDuration';
|
||||
import { printBuildSummary } from './log/printBuildSummary';
|
||||
import { execSync } from 'node:child_process';
|
||||
import glob from 'fast-glob';
|
||||
import { unnormalizePath } from './utils/paths';
|
||||
|
||||
/**
|
||||
* Builds the extension based on an internal config.
|
||||
@@ -38,14 +41,24 @@ export async function buildInternal(
|
||||
await fs.ensureDir(config.outDir);
|
||||
|
||||
const entrypoints = await findEntrypoints(config);
|
||||
config.logger.debug('Detected entrypoints:', entrypoints);
|
||||
const groups = groupEntrypoints(entrypoints);
|
||||
const { output } = await rebuild(config, groups, undefined);
|
||||
|
||||
// Post-build
|
||||
config.logger.success(
|
||||
await printBuildSummary(
|
||||
config.logger.success,
|
||||
`Built extension in ${formatDuration(Date.now() - startTime)}`,
|
||||
output,
|
||||
config,
|
||||
);
|
||||
await printBuildSummary(output, config);
|
||||
|
||||
if (config.analysis.enabled) {
|
||||
await combineAnalysisStats(config);
|
||||
config.logger.info(
|
||||
`Analysis complete:\n ${pc.gray('└─')} ${pc.yellow('stats.html')}`,
|
||||
);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
@@ -68,6 +81,9 @@ export async function rebuild(
|
||||
publicAssets: [],
|
||||
},
|
||||
): Promise<{ output: BuildOutput; manifest: Manifest.WebExtensionManifest }> {
|
||||
const { default: ora } = await import('ora');
|
||||
const spinner = ora(`Preparing...`).start();
|
||||
|
||||
// Update types directory with new files and types
|
||||
const allEntrypoints = await findEntrypoints(config);
|
||||
await generateTypesDir(allEntrypoints, config).catch((err) => {
|
||||
@@ -77,7 +93,7 @@ export async function rebuild(
|
||||
});
|
||||
|
||||
// Build and merge the outputs
|
||||
const newOutput = await buildEntrypoints(entrypointGroups, config);
|
||||
const newOutput = await buildEntrypoints(entrypointGroups, config, spinner);
|
||||
const mergedOutput: Omit<BuildOutput, 'manifest'> = {
|
||||
steps: [...existingOutput.steps, ...newOutput.steps],
|
||||
publicAssets: [...existingOutput.publicAssets, ...newOutput.publicAssets],
|
||||
@@ -96,6 +112,9 @@ export async function rebuild(
|
||||
// Write manifest
|
||||
await writeManifest(newManifest, finalOutput, config);
|
||||
|
||||
// Stop the spinner and remove it from the CLI output
|
||||
spinner.clear().stop();
|
||||
|
||||
return {
|
||||
output: {
|
||||
manifest: newManifest,
|
||||
@@ -108,3 +127,18 @@ export async function rebuild(
|
||||
manifest: newManifest,
|
||||
};
|
||||
}
|
||||
|
||||
async function combineAnalysisStats(config: InternalConfig): Promise<void> {
|
||||
const unixFiles = await glob(`stats-*.json`, {
|
||||
cwd: config.outDir,
|
||||
absolute: true,
|
||||
});
|
||||
const absolutePaths = unixFiles.map(unnormalizePath);
|
||||
|
||||
execSync(
|
||||
`rollup-plugin-visualizer ${absolutePaths.join(' ')} --template ${
|
||||
config.analysis.template
|
||||
}`,
|
||||
{ cwd: config.root, stdio: 'inherit' },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,13 +9,13 @@ import {
|
||||
import { resolve } from 'path';
|
||||
import { findEntrypoints } from '../findEntrypoints';
|
||||
import fs from 'fs-extra';
|
||||
import { importTsFile } from '../../utils/importTsFile';
|
||||
import { importEntrypointFile } from '../../utils/importEntrypointFile';
|
||||
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);
|
||||
vi.mock('../../utils/importEntrypointFile');
|
||||
const importEntrypointFileMock = vi.mocked(importEntrypointFile);
|
||||
|
||||
vi.mock('fast-glob');
|
||||
const globMock = vi.mocked(glob);
|
||||
@@ -191,13 +191,16 @@ describe('findEntrypoints', () => {
|
||||
matches: ['<all_urls>'],
|
||||
};
|
||||
globMock.mockResolvedValueOnce([path]);
|
||||
importTsFileMock.mockResolvedValue(options);
|
||||
importEntrypointFileMock.mockResolvedValue(options);
|
||||
|
||||
const entrypoints = await findEntrypoints(config);
|
||||
|
||||
expect(entrypoints).toHaveLength(1);
|
||||
expect(entrypoints[0]).toEqual({ ...expected, options });
|
||||
expect(importTsFileMock).toBeCalledWith(expected.inputPath, config);
|
||||
expect(importEntrypointFileMock).toBeCalledWith(
|
||||
expected.inputPath,
|
||||
config,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -211,6 +214,15 @@ describe('findEntrypoints', () => {
|
||||
outputDir: config.outDir,
|
||||
},
|
||||
],
|
||||
[
|
||||
'background/index.ts',
|
||||
{
|
||||
type: 'background',
|
||||
name: 'background',
|
||||
inputPath: resolve(config.entrypointsDir, 'background/index.ts'),
|
||||
outputDir: config.outDir,
|
||||
},
|
||||
],
|
||||
])(
|
||||
'should find and load background entrypoint config from %s',
|
||||
async (path, expected) => {
|
||||
@@ -218,13 +230,16 @@ describe('findEntrypoints', () => {
|
||||
type: 'module',
|
||||
};
|
||||
globMock.mockResolvedValueOnce([path]);
|
||||
importTsFileMock.mockResolvedValue(options);
|
||||
importEntrypointFileMock.mockResolvedValue(options);
|
||||
|
||||
const entrypoints = await findEntrypoints(config);
|
||||
|
||||
expect(entrypoints).toHaveLength(1);
|
||||
expect(entrypoints[0]).toEqual({ ...expected, options });
|
||||
expect(importTsFileMock).toBeCalledWith(expected.inputPath, config);
|
||||
expect(importEntrypointFileMock).toBeCalledWith(
|
||||
expected.inputPath,
|
||||
config,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -255,6 +270,7 @@ describe('findEntrypoints', () => {
|
||||
name: 'sandbox',
|
||||
inputPath: resolve(config.entrypointsDir, 'sandbox.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -264,6 +280,7 @@ describe('findEntrypoints', () => {
|
||||
name: 'sandbox',
|
||||
inputPath: resolve(config.entrypointsDir, 'sandbox/index.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -273,6 +290,7 @@ describe('findEntrypoints', () => {
|
||||
name: 'named',
|
||||
inputPath: resolve(config.entrypointsDir, 'named.sandbox.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -282,6 +300,7 @@ describe('findEntrypoints', () => {
|
||||
name: 'named',
|
||||
inputPath: resolve(config.entrypointsDir, 'named.sandbox/index.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -293,6 +312,7 @@ describe('findEntrypoints', () => {
|
||||
name: 'bookmarks',
|
||||
inputPath: resolve(config.entrypointsDir, 'bookmarks.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -302,6 +322,7 @@ describe('findEntrypoints', () => {
|
||||
name: 'bookmarks',
|
||||
inputPath: resolve(config.entrypointsDir, 'bookmarks/index.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -313,6 +334,7 @@ describe('findEntrypoints', () => {
|
||||
name: 'history',
|
||||
inputPath: resolve(config.entrypointsDir, 'history.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -322,6 +344,7 @@ describe('findEntrypoints', () => {
|
||||
name: 'history',
|
||||
inputPath: resolve(config.entrypointsDir, 'history/index.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -333,6 +356,7 @@ describe('findEntrypoints', () => {
|
||||
name: 'newtab',
|
||||
inputPath: resolve(config.entrypointsDir, 'newtab.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -342,6 +366,7 @@ describe('findEntrypoints', () => {
|
||||
name: 'newtab',
|
||||
inputPath: resolve(config.entrypointsDir, 'newtab/index.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -353,6 +378,7 @@ describe('findEntrypoints', () => {
|
||||
name: 'sidepanel',
|
||||
inputPath: resolve(config.entrypointsDir, 'sidepanel.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -362,6 +388,7 @@ describe('findEntrypoints', () => {
|
||||
name: 'sidepanel',
|
||||
inputPath: resolve(config.entrypointsDir, 'sidepanel/index.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -371,6 +398,7 @@ describe('findEntrypoints', () => {
|
||||
name: 'named',
|
||||
inputPath: resolve(config.entrypointsDir, 'named.sidepanel.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -380,6 +408,7 @@ describe('findEntrypoints', () => {
|
||||
name: 'named',
|
||||
inputPath: resolve(config.entrypointsDir, 'named.sidepanel/index.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -391,6 +420,7 @@ describe('findEntrypoints', () => {
|
||||
name: 'devtools',
|
||||
inputPath: resolve(config.entrypointsDir, 'devtools.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -400,6 +430,7 @@ describe('findEntrypoints', () => {
|
||||
name: 'devtools',
|
||||
inputPath: resolve(config.entrypointsDir, 'devtools/index.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -411,6 +442,7 @@ describe('findEntrypoints', () => {
|
||||
name: 'onboarding',
|
||||
inputPath: resolve(config.entrypointsDir, 'onboarding.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -420,6 +452,7 @@ describe('findEntrypoints', () => {
|
||||
name: 'onboarding',
|
||||
inputPath: resolve(config.entrypointsDir, 'onboarding/index.html'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -431,6 +464,7 @@ describe('findEntrypoints', () => {
|
||||
name: 'injected',
|
||||
inputPath: resolve(config.entrypointsDir, 'injected.ts'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -440,6 +474,7 @@ describe('findEntrypoints', () => {
|
||||
name: 'injected',
|
||||
inputPath: resolve(config.entrypointsDir, 'injected/index.ts'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -451,6 +486,7 @@ describe('findEntrypoints', () => {
|
||||
name: 'iframe',
|
||||
inputPath: resolve(config.entrypointsDir, 'iframe.scss'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -460,6 +496,7 @@ describe('findEntrypoints', () => {
|
||||
name: 'iframe',
|
||||
inputPath: resolve(config.entrypointsDir, 'iframe.css'),
|
||||
outputDir: config.outDir,
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -471,6 +508,7 @@ describe('findEntrypoints', () => {
|
||||
name: 'content',
|
||||
inputPath: resolve(config.entrypointsDir, 'content.css'),
|
||||
outputDir: resolve(config.outDir, 'content-scripts'),
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -480,6 +518,7 @@ describe('findEntrypoints', () => {
|
||||
name: 'overlay',
|
||||
inputPath: resolve(config.entrypointsDir, 'overlay.content.css'),
|
||||
outputDir: resolve(config.outDir, 'content-scripts'),
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -489,6 +528,7 @@ describe('findEntrypoints', () => {
|
||||
name: 'content',
|
||||
inputPath: resolve(config.entrypointsDir, 'content/index.css'),
|
||||
outputDir: resolve(config.outDir, 'content-scripts'),
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -498,6 +538,7 @@ describe('findEntrypoints', () => {
|
||||
name: 'overlay',
|
||||
inputPath: resolve(config.entrypointsDir, 'overlay.content/index.css'),
|
||||
outputDir: resolve(config.outDir, 'content-scripts'),
|
||||
options: {},
|
||||
},
|
||||
],
|
||||
])('should find entrypoint for %s', async (path, expected) => {
|
||||
@@ -521,4 +562,148 @@ describe('findEntrypoints', () => {
|
||||
expectedPaths.join(', '),
|
||||
);
|
||||
});
|
||||
|
||||
describe('include option', () => {
|
||||
it("should filter out the background when include doesn't contain the target browser", async () => {
|
||||
globMock.mockResolvedValueOnce(['background.ts']);
|
||||
importEntrypointFileMock.mockResolvedValue({
|
||||
include: ['not' + config.browser],
|
||||
});
|
||||
|
||||
const entrypoints = await findEntrypoints(config);
|
||||
|
||||
expect(entrypoints).toEqual([]);
|
||||
});
|
||||
|
||||
it("should filter out content scripts when include doesn't contain the target browser", async () => {
|
||||
globMock.mockResolvedValueOnce(['example.content.ts']);
|
||||
importEntrypointFileMock.mockResolvedValue({
|
||||
include: ['not' + config.browser],
|
||||
});
|
||||
|
||||
const entrypoints = await findEntrypoints(config);
|
||||
|
||||
expect(entrypoints).toEqual([]);
|
||||
});
|
||||
|
||||
it("should filter out the popup when include doesn't contain the target browser", async () => {
|
||||
globMock.mockResolvedValueOnce(['popup.html']);
|
||||
readFileMock.mockResolvedValueOnce(
|
||||
`<html>
|
||||
<head>
|
||||
<meta name="manifest.include" content="['${
|
||||
'not' + config.browser
|
||||
}']" />
|
||||
</head>
|
||||
</html>`,
|
||||
);
|
||||
|
||||
const entrypoints = await findEntrypoints(config);
|
||||
|
||||
expect(entrypoints).toEqual([]);
|
||||
});
|
||||
|
||||
it("should filter out the options page when include doesn't contain the target browser", async () => {
|
||||
globMock.mockResolvedValueOnce(['options.html']);
|
||||
readFileMock.mockResolvedValueOnce(
|
||||
`<html>
|
||||
<head>
|
||||
<meta name="manifest.include" content="['${
|
||||
'not' + config.browser
|
||||
}']" />
|
||||
</head>
|
||||
</html>`,
|
||||
);
|
||||
|
||||
const entrypoints = await findEntrypoints(config);
|
||||
|
||||
expect(entrypoints).toEqual([]);
|
||||
});
|
||||
|
||||
it("should filter out an unlisted page when include doesn't contain the target browser", async () => {
|
||||
globMock.mockResolvedValueOnce(['unlisted.html']);
|
||||
readFileMock.mockResolvedValueOnce(
|
||||
`<html>
|
||||
<head>
|
||||
<meta name="manifest.include" content="['${
|
||||
'not' + config.browser
|
||||
}']" />
|
||||
</head>
|
||||
</html>`,
|
||||
);
|
||||
|
||||
const entrypoints = await findEntrypoints(config);
|
||||
|
||||
expect(entrypoints).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('exclude option', () => {
|
||||
it('should filter out the background when exclude contains the target browser', async () => {
|
||||
globMock.mockResolvedValueOnce(['background.ts']);
|
||||
importEntrypointFileMock.mockResolvedValue({
|
||||
exclude: [config.browser],
|
||||
});
|
||||
|
||||
const entrypoints = await findEntrypoints(config);
|
||||
|
||||
expect(entrypoints).toEqual([]);
|
||||
});
|
||||
|
||||
it('should filter out content scripts when exclude contains the target browser', async () => {
|
||||
globMock.mockResolvedValueOnce(['example.content.ts']);
|
||||
importEntrypointFileMock.mockResolvedValue({
|
||||
exclude: [config.browser],
|
||||
});
|
||||
|
||||
const entrypoints = await findEntrypoints(config);
|
||||
|
||||
expect(entrypoints).toEqual([]);
|
||||
});
|
||||
|
||||
it('should filter out the popup when exclude contains the target browser', async () => {
|
||||
globMock.mockResolvedValueOnce(['popup.html']);
|
||||
readFileMock.mockResolvedValueOnce(
|
||||
`<html>
|
||||
<head>
|
||||
<meta name="manifest.exclude" content="['${config.browser}']" />
|
||||
</head>
|
||||
</html>`,
|
||||
);
|
||||
|
||||
const entrypoints = await findEntrypoints(config);
|
||||
|
||||
expect(entrypoints).toEqual([]);
|
||||
});
|
||||
|
||||
it('should filter out the options page when exclude contains the target browser', async () => {
|
||||
globMock.mockResolvedValueOnce(['options.html']);
|
||||
readFileMock.mockResolvedValueOnce(
|
||||
`<html>
|
||||
<head>
|
||||
<meta name="manifest.exclude" content="['${config.browser}']" />
|
||||
</head>
|
||||
</html>`,
|
||||
);
|
||||
|
||||
const entrypoints = await findEntrypoints(config);
|
||||
|
||||
expect(entrypoints).toEqual([]);
|
||||
});
|
||||
|
||||
it('should filter out an unlisted page when exclude contains the target browser', async () => {
|
||||
globMock.mockResolvedValueOnce(['unlisted.html']);
|
||||
readFileMock.mockResolvedValueOnce(
|
||||
`<html>
|
||||
<head>
|
||||
<meta name="manifest.exclude" content="['${config.browser}']" />
|
||||
</head>
|
||||
</html>`,
|
||||
);
|
||||
|
||||
const entrypoints = await findEntrypoints(config);
|
||||
|
||||
expect(entrypoints).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,13 +12,26 @@ import { getEntrypointBundlePath } from '../utils/entrypoints';
|
||||
import fs from 'fs-extra';
|
||||
import { dirname, resolve } from 'path';
|
||||
import { getPublicFiles } from '../utils/public';
|
||||
import { getEntrypointGlobals } from '../utils/globals';
|
||||
import type { Ora } from 'ora';
|
||||
import pc from 'picocolors';
|
||||
|
||||
export async function buildEntrypoints(
|
||||
groups: EntrypointGroup[],
|
||||
config: InternalConfig,
|
||||
spinner: Ora,
|
||||
): Promise<Omit<BuildOutput, 'manifest'>> {
|
||||
const steps: BuildStepOutput[] = [];
|
||||
for (const group of groups) {
|
||||
for (let i = 0; i < groups.length; i++) {
|
||||
const group = groups[i];
|
||||
|
||||
spinner.text =
|
||||
pc.dim(`[${i + 1}/${groups.length}]`) +
|
||||
` ${[group]
|
||||
.flat()
|
||||
.map((e) => e.name)
|
||||
.join(pc.dim(', '))}`;
|
||||
|
||||
const step = Array.isArray(group)
|
||||
? await buildMultipleEntrypoints(group, config)
|
||||
: await buildSingleEntrypoint(group, config);
|
||||
@@ -78,10 +91,17 @@ async function buildSingleEntrypoint(
|
||||
},
|
||||
},
|
||||
},
|
||||
define: {
|
||||
// See https://github.com/aklinker1/vite-plugin-web-extension/issues/96
|
||||
'process.env.NODE_ENV': JSON.stringify(config.mode),
|
||||
},
|
||||
};
|
||||
for (const global of getEntrypointGlobals(config, entrypoint.name)) {
|
||||
libMode.define![global.name] = JSON.stringify(global.value);
|
||||
}
|
||||
const entryConfig = vite.mergeConfig(
|
||||
libMode,
|
||||
config.vite,
|
||||
await config.vite(config.env),
|
||||
) as vite.InlineConfig;
|
||||
|
||||
const result = await vite.build(entryConfig);
|
||||
@@ -116,11 +136,15 @@ async function buildMultipleEntrypoints(
|
||||
},
|
||||
},
|
||||
},
|
||||
define: {},
|
||||
};
|
||||
for (const global of getEntrypointGlobals(config, 'html')) {
|
||||
multiPage.define![global.name] = JSON.stringify(global.value);
|
||||
}
|
||||
|
||||
const entryConfig = vite.mergeConfig(
|
||||
multiPage,
|
||||
config.vite,
|
||||
await config.vite(config.env),
|
||||
) as vite.UserConfig;
|
||||
|
||||
const result = await vite.build(entryConfig);
|
||||
|
||||
@@ -2,9 +2,11 @@ import { relative, resolve } from 'path';
|
||||
import {
|
||||
BackgroundEntrypoint,
|
||||
BackgroundScriptDefintition,
|
||||
BaseEntrypointOptions,
|
||||
ContentScriptDefinition,
|
||||
ContentScriptEntrypoint,
|
||||
Entrypoint,
|
||||
GenericEntrypoint,
|
||||
InternalConfig,
|
||||
OptionsEntrypoint,
|
||||
PopupEntrypoint,
|
||||
@@ -13,7 +15,7 @@ import fs from 'fs-extra';
|
||||
import { minimatch } from 'minimatch';
|
||||
import { parseHTML } from 'linkedom';
|
||||
import JSON5 from 'json5';
|
||||
import { importTsFile } from '../utils/importTsFile';
|
||||
import { importEntrypointFile } from '../utils/importEntrypointFile';
|
||||
import glob from 'fast-glob';
|
||||
import { getEntrypointName } from '../utils/entrypoints';
|
||||
import { VIRTUAL_NOOP_BACKGROUND_MODULE_ID } from '../vite-plugins/noopBackground';
|
||||
@@ -75,12 +77,19 @@ export async function findEntrypoints(
|
||||
path,
|
||||
);
|
||||
break;
|
||||
case 'unlisted-page':
|
||||
entrypoint = await getUnlistedPageEntrypoint(config, path);
|
||||
break;
|
||||
case 'content-script-style':
|
||||
entrypoint = {
|
||||
type,
|
||||
name: getEntrypointName(config.entrypointsDir, path),
|
||||
inputPath: path,
|
||||
outputDir: resolve(config.outDir, CONTENT_SCRIPT_OUT_DIR),
|
||||
options: {
|
||||
include: undefined,
|
||||
exclude: undefined,
|
||||
},
|
||||
};
|
||||
break;
|
||||
default:
|
||||
@@ -89,6 +98,10 @@ export async function findEntrypoints(
|
||||
name: getEntrypointName(config.entrypointsDir, path),
|
||||
inputPath: path,
|
||||
outputDir: config.outDir,
|
||||
options: {
|
||||
include: undefined,
|
||||
exclude: undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -112,7 +125,47 @@ export async function findEntrypoints(
|
||||
await getBackgroundEntrypoint(config, VIRTUAL_NOOP_BACKGROUND_MODULE_ID),
|
||||
);
|
||||
}
|
||||
return entrypoints;
|
||||
|
||||
config.logger.debug('All entrypoints:', entrypoints);
|
||||
const targetEntrypoints = entrypoints.filter((entry) => {
|
||||
const { include, exclude } = entry.options;
|
||||
if (include?.length && exclude?.length) {
|
||||
config.logger.warn(
|
||||
`The ${entry.name} entrypoint lists both include and exclude, but only one can be used per entrypoint. Entrypoint ignored.`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (exclude?.length && !include?.length) {
|
||||
return !exclude.includes(config.browser);
|
||||
}
|
||||
if (include?.length && !exclude?.length) {
|
||||
return include.includes(config.browser);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
config.logger.debug(`${config.browser} entrypoints:`, targetEntrypoints);
|
||||
return targetEntrypoints;
|
||||
}
|
||||
|
||||
function getHtmlBaseOptions(document: Document): BaseEntrypointOptions {
|
||||
const options: BaseEntrypointOptions = {};
|
||||
|
||||
const includeContent = document
|
||||
.querySelector("meta[name='manifest.include']")
|
||||
?.getAttribute('content');
|
||||
if (includeContent) {
|
||||
options.include = JSON5.parse(includeContent);
|
||||
}
|
||||
|
||||
const excludeContent = document
|
||||
.querySelector("meta[name='manifest.exclude']")
|
||||
?.getAttribute('content');
|
||||
if (excludeContent) {
|
||||
options.exclude = JSON5.parse(excludeContent);
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -123,11 +176,11 @@ async function getPopupEntrypoint(
|
||||
config: InternalConfig,
|
||||
path: string,
|
||||
): Promise<PopupEntrypoint> {
|
||||
const options: PopupEntrypoint['options'] = {};
|
||||
|
||||
const content = await fs.readFile(path, 'utf-8');
|
||||
const { document } = parseHTML(content);
|
||||
|
||||
const options: PopupEntrypoint['options'] = getHtmlBaseOptions(document);
|
||||
|
||||
const title = document.querySelector('title');
|
||||
if (title != null) options.defaultTitle = title.textContent ?? undefined;
|
||||
|
||||
@@ -170,11 +223,11 @@ async function getOptionsEntrypoint(
|
||||
config: InternalConfig,
|
||||
path: string,
|
||||
): Promise<OptionsEntrypoint> {
|
||||
const options: OptionsEntrypoint['options'] = {};
|
||||
|
||||
const content = await fs.readFile(path, 'utf-8');
|
||||
const { document } = parseHTML(content);
|
||||
|
||||
const options: OptionsEntrypoint['options'] = getHtmlBaseOptions(document);
|
||||
|
||||
const openInTabContent = document
|
||||
.querySelector("meta[name='manifest.open_in_tab']")
|
||||
?.getAttribute('content');
|
||||
@@ -205,6 +258,26 @@ async function getOptionsEntrypoint(
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path Absolute path to the HTML file.
|
||||
* @param content String contents of the file at the path.
|
||||
*/
|
||||
async function getUnlistedPageEntrypoint(
|
||||
config: InternalConfig,
|
||||
path: string,
|
||||
): Promise<GenericEntrypoint> {
|
||||
const content = await fs.readFile(path, 'utf-8');
|
||||
const { document } = parseHTML(content);
|
||||
|
||||
return {
|
||||
type: 'unlisted-page',
|
||||
name: getEntrypointName(config.entrypointsDir, path),
|
||||
inputPath: path,
|
||||
outputDir: config.outDir,
|
||||
options: getHtmlBaseOptions(document),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path Absolute path to the background's TS file.
|
||||
*/
|
||||
@@ -214,10 +287,8 @@ async function getBackgroundEntrypoint(
|
||||
): Promise<BackgroundEntrypoint> {
|
||||
let options: Omit<BackgroundScriptDefintition, 'main'> = {};
|
||||
if (path !== VIRTUAL_NOOP_BACKGROUND_MODULE_ID) {
|
||||
const defaultExport = await importTsFile<BackgroundScriptDefintition>(
|
||||
path,
|
||||
config,
|
||||
);
|
||||
const defaultExport =
|
||||
await importEntrypointFile<BackgroundScriptDefintition>(path, config);
|
||||
if (defaultExport == null) {
|
||||
throw Error('Background script does not have a default export');
|
||||
}
|
||||
@@ -241,10 +312,8 @@ async function getContentScriptEntrypoint(
|
||||
name: string,
|
||||
path: string,
|
||||
): Promise<ContentScriptEntrypoint> {
|
||||
const { main: _, ...options } = await importTsFile<ContentScriptDefinition>(
|
||||
path,
|
||||
config,
|
||||
);
|
||||
const { main: _, ...options } =
|
||||
await importEntrypointFile<ContentScriptDefinition>(path, config);
|
||||
if (options == null) {
|
||||
throw Error(`Content script ${name} does not have a default export`);
|
||||
}
|
||||
@@ -281,6 +350,7 @@ const PATH_GLOB_TO_TYPE_MAP: Record<string, Entrypoint['type'] | 'ignored'> = {
|
||||
'devtools/index.html': 'devtools',
|
||||
|
||||
'background.ts': 'background',
|
||||
'background/index.ts': 'background',
|
||||
[VIRTUAL_NOOP_BACKGROUND_MODULE_ID]: 'background',
|
||||
|
||||
'content.ts?(x)': 'content-script',
|
||||
@@ -305,8 +375,8 @@ const PATH_GLOB_TO_TYPE_MAP: Record<string, Entrypoint['type'] | 'ignored'> = {
|
||||
[`*.${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',
|
||||
// Don't warn about any files in subdirectories, like CSS or JS entrypoints for HTML files or tests
|
||||
'*/**': 'ignored',
|
||||
};
|
||||
|
||||
const CONTENT_SCRIPT_OUT_DIR = 'content-scripts';
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { createUnimport } from 'unimport';
|
||||
import { UnimportOptions, createUnimport } from 'unimport';
|
||||
import { Entrypoint, InternalConfig } from '../types';
|
||||
import fs from 'fs-extra';
|
||||
import { relative, resolve } from 'path';
|
||||
import { getEntrypointBundlePath } from '../utils/entrypoints';
|
||||
import { getUnimportOptions } from '../utils/auto-imports';
|
||||
import { getGlobals } from '../utils/globals';
|
||||
import { getEntrypointGlobals, getGlobals } from '../utils/globals';
|
||||
import { getPublicFiles } from '../utils/public';
|
||||
import { normalizePath } from '../utils/paths';
|
||||
import path from 'node:path';
|
||||
import { Message, parseI18nMessages } from '../utils/i18n';
|
||||
import { writeFileIfDifferent } from '../utils/fs';
|
||||
|
||||
/**
|
||||
* Generate and write all the files inside the `InternalConfig.typesDir` directory.
|
||||
@@ -18,8 +21,14 @@ export async function generateTypesDir(
|
||||
await fs.ensureDir(config.typesDir);
|
||||
|
||||
const references: string[] = [];
|
||||
references.push(await writeImportsDeclarationFile(config));
|
||||
|
||||
const imports = getUnimportOptions(config);
|
||||
if (imports !== false) {
|
||||
references.push(await writeImportsDeclarationFile(config, imports));
|
||||
}
|
||||
|
||||
references.push(await writePathsDeclarationFile(entrypoints, config));
|
||||
references.push(await writeI18nDeclarationFile(config));
|
||||
references.push(await writeGlobalsDeclarationFile(config));
|
||||
|
||||
const mainReference = await writeMainDeclarationFile(references, config);
|
||||
@@ -28,14 +37,15 @@ export async function generateTypesDir(
|
||||
|
||||
async function writeImportsDeclarationFile(
|
||||
config: InternalConfig,
|
||||
unimportOptions: Partial<UnimportOptions>,
|
||||
): Promise<string> {
|
||||
const filePath = resolve(config.typesDir, 'imports.d.ts');
|
||||
const unimport = createUnimport(getUnimportOptions(config));
|
||||
const unimport = createUnimport(unimportOptions);
|
||||
|
||||
// Load project imports into unimport memory so they are output via generateTypeDeclarations
|
||||
await unimport.scanImportsFromDir(undefined, { cwd: config.srcDir });
|
||||
|
||||
await fs.writeFile(
|
||||
await writeFileIfDifferent(
|
||||
filePath,
|
||||
['// Generated by wxt', await unimport.generateTypeDeclarations()].join(
|
||||
'\n',
|
||||
@@ -70,13 +80,13 @@ import "wxt/browser";
|
||||
declare module "wxt/browser" {
|
||||
type PublicPath =
|
||||
{{ union }}
|
||||
export interface ProjectRuntime extends Runtime.Static {
|
||||
export interface WxtRuntime extends Runtime.Static {
|
||||
getURL(path: PublicPath): string;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
await fs.writeFile(
|
||||
await writeFileIfDifferent(
|
||||
filePath,
|
||||
template.replace('{{ union }}', unions || ' | never'),
|
||||
);
|
||||
@@ -84,12 +94,71 @@ declare module "wxt/browser" {
|
||||
return filePath;
|
||||
}
|
||||
|
||||
async function writeI18nDeclarationFile(
|
||||
config: InternalConfig,
|
||||
): Promise<string> {
|
||||
const filePath = resolve(config.typesDir, 'i18n.d.ts');
|
||||
const defaultLocale = config.manifest.default_locale;
|
||||
const template = `// Generated by wxt
|
||||
import "wxt/browser";
|
||||
|
||||
declare module "wxt/browser" {
|
||||
/**
|
||||
* See https://developer.chrome.com/docs/extensions/reference/i18n/#method-getMessage
|
||||
*/
|
||||
interface GetMessageOptions {
|
||||
/**
|
||||
* See https://developer.chrome.com/docs/extensions/reference/i18n/#method-getMessage
|
||||
*/
|
||||
escapeLt?: boolean
|
||||
}
|
||||
|
||||
export interface WxtI18n extends I18n.Static {
|
||||
{{ overrides }}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
let messages: Message[];
|
||||
if (defaultLocale) {
|
||||
const defaultLocalePath = path.resolve(
|
||||
config.publicDir,
|
||||
'_locales',
|
||||
defaultLocale,
|
||||
'messages.json',
|
||||
);
|
||||
const content = JSON.parse(await fs.readFile(defaultLocalePath, 'utf-8'));
|
||||
messages = parseI18nMessages(content);
|
||||
} else {
|
||||
messages = parseI18nMessages({});
|
||||
}
|
||||
|
||||
const overrides = messages.map((message) => {
|
||||
return ` /**
|
||||
* ${message.description ?? 'No message description.'}
|
||||
*
|
||||
* "${message.message}"
|
||||
*/
|
||||
getMessage(
|
||||
messageName: "${message.name}",
|
||||
substitutions?: string | string[],
|
||||
options?: GetMessageOptions,
|
||||
): string;`;
|
||||
});
|
||||
await writeFileIfDifferent(
|
||||
filePath,
|
||||
template.replace('{{ overrides }}', overrides.join('\n')),
|
||||
);
|
||||
|
||||
return filePath;
|
||||
}
|
||||
|
||||
async function writeGlobalsDeclarationFile(
|
||||
config: InternalConfig,
|
||||
): Promise<string> {
|
||||
const filePath = resolve(config.typesDir, 'globals.d.ts');
|
||||
const globals = getGlobals(config);
|
||||
await fs.writeFile(
|
||||
const globals = [...getGlobals(config), ...getEntrypointGlobals(config, '')];
|
||||
await writeFileIfDifferent(
|
||||
filePath,
|
||||
[
|
||||
'// Generated by wxt',
|
||||
@@ -98,7 +167,6 @@ async function writeGlobalsDeclarationFile(
|
||||
...globals.map((global) => ` const ${global.name}: ${global.type};`),
|
||||
'}',
|
||||
].join('\n') + '\n',
|
||||
'utf-8',
|
||||
);
|
||||
return filePath;
|
||||
}
|
||||
@@ -109,7 +177,7 @@ async function writeMainDeclarationFile(
|
||||
): Promise<string> {
|
||||
const dir = config.wxtDir;
|
||||
const filePath = resolve(dir, 'wxt.d.ts');
|
||||
await fs.writeFile(
|
||||
await writeFileIfDifferent(
|
||||
filePath,
|
||||
[
|
||||
'// Generated by wxt',
|
||||
@@ -128,7 +196,9 @@ async function writeTsConfigFile(
|
||||
config: InternalConfig,
|
||||
) {
|
||||
const dir = config.wxtDir;
|
||||
await fs.writeFile(
|
||||
const rootPath = normalizePath(relative(dir, config.root));
|
||||
const srcPath = normalizePath(relative(dir, config.srcDir));
|
||||
await writeFileIfDifferent(
|
||||
resolve(dir, 'tsconfig.json'),
|
||||
`{
|
||||
"compilerOptions": {
|
||||
@@ -142,16 +212,15 @@ async function writeTsConfigFile(
|
||||
"strict": true,
|
||||
"lib": ["DOM", "WebWorker"],
|
||||
"skipLibCheck": true,
|
||||
"baseUrl": "${normalizePath(relative(dir, config.root))}",
|
||||
"paths": {
|
||||
"@@": ["."],
|
||||
"@@/*": ["./*"],
|
||||
"~~": ["."],
|
||||
"~~/*": ["./*"],
|
||||
"@": ["${normalizePath(relative(config.root, config.srcDir))}"],
|
||||
"@/*": ["${normalizePath(relative(config.root, config.srcDir))}/*"],
|
||||
"~": ["${normalizePath(relative(config.root, config.srcDir))}"],
|
||||
"~/*": ["${normalizePath(relative(config.root, config.srcDir))}/*"]
|
||||
"@@": ["${rootPath}"],
|
||||
"@@/*": ["${rootPath}/*"],
|
||||
"~~": ["${rootPath}"],
|
||||
"~~/*": ["${rootPath}/*"],
|
||||
"@": ["${srcPath}"],
|
||||
"@/*": ["${srcPath}/*"],
|
||||
"~": ["${srcPath}"],
|
||||
"~/*": ["${srcPath}/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import path from 'node:path';
|
||||
import glob from 'fast-glob';
|
||||
import fs from 'fs-extra';
|
||||
import { consola } from 'consola';
|
||||
import pc from 'picocolors';
|
||||
|
||||
/**
|
||||
* Remove generated/temp files from the directory.
|
||||
*
|
||||
* @param root The directory to look for generated/temp files in. Defaults to `process.cwd()`. Can be relative to `process.cwd()` or absolute.
|
||||
*/
|
||||
export async function clean(root = process.cwd()) {
|
||||
consola.info('Cleaning Project');
|
||||
|
||||
const tempDirs = [
|
||||
'node_modules/.vite',
|
||||
'node_modules/.cache',
|
||||
'**/.wxt',
|
||||
'.output/*',
|
||||
];
|
||||
consola.debug('Looking for:', tempDirs.map(pc.cyan).join(', '));
|
||||
const directories = await glob(tempDirs, {
|
||||
cwd: path.resolve(root),
|
||||
absolute: true,
|
||||
onlyDirectories: true,
|
||||
deep: 2,
|
||||
});
|
||||
if (directories.length === 0) {
|
||||
consola.debug('No generated files found.');
|
||||
return;
|
||||
}
|
||||
|
||||
consola.debug(
|
||||
'Found:',
|
||||
directories.map((dir) => pc.cyan(path.relative(root, dir))).join(', '),
|
||||
);
|
||||
for (const directory of directories) {
|
||||
await fs.rm(directory, { force: true, recursive: true });
|
||||
consola.debug('Deleted ' + pc.cyan(path.relative(root, directory)));
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,8 @@ import { BuildOutput, InternalConfig } from '../types';
|
||||
import { printFileList } from './printFileList';
|
||||
|
||||
export async function printBuildSummary(
|
||||
log: (...args: any[]) => void,
|
||||
header: string,
|
||||
output: BuildOutput,
|
||||
config: InternalConfig,
|
||||
) {
|
||||
@@ -18,7 +20,7 @@ export async function printBuildSummary(
|
||||
});
|
||||
|
||||
const files = chunks.map((chunk) => resolve(config.outDir, chunk.fileName));
|
||||
await printFileList(config.logger.log, config.outDir, files);
|
||||
await printFileList(log, header, config.outDir, files);
|
||||
}
|
||||
|
||||
const DEFAULT_SORT_WEIGHT = 100;
|
||||
|
||||
@@ -6,6 +6,7 @@ import { printTable } from './printTable';
|
||||
|
||||
export async function printFileList(
|
||||
log: (message: string) => void,
|
||||
header: string,
|
||||
baseDir: string,
|
||||
files: string[],
|
||||
): Promise<void> {
|
||||
@@ -29,9 +30,9 @@ export async function printFileList(
|
||||
}),
|
||||
);
|
||||
|
||||
printTable(log, fileRows);
|
||||
fileRows.push([`${pc.cyan('Σ Total size:')} ${String(filesize(totalSize))}`]);
|
||||
|
||||
log(`${pc.cyan('Σ Total size:')} ${String(filesize(totalSize))}`);
|
||||
printTable(log, header, fileRows);
|
||||
}
|
||||
|
||||
const DEFAULT_COLOR = pc.blue;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export function printTable(
|
||||
log: (message: string) => void,
|
||||
header: string,
|
||||
rows: string[][],
|
||||
gap = 2,
|
||||
): void {
|
||||
@@ -24,5 +25,5 @@ export function printTable(
|
||||
if (i !== rows.length - 1) str += '\n';
|
||||
});
|
||||
|
||||
log(str);
|
||||
log(`${header}\n${str}`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { InternalConfig } from '../types';
|
||||
import { ExtensionRunner } from './extension-runner';
|
||||
import { createWslRunner } from './wsl';
|
||||
import { createWebExtRunner } from './web-ext';
|
||||
import { createSafariRunner } from './safari';
|
||||
|
||||
export async function createExtensionRunner(
|
||||
config: InternalConfig,
|
||||
): Promise<ExtensionRunner> {
|
||||
if (config.browser === 'safari') return createSafariRunner();
|
||||
|
||||
const { default: isWsl } = await import('is-wsl'); // ESM only, requires dynamic import
|
||||
if (isWsl) return createWslRunner();
|
||||
|
||||
return createWebExtRunner();
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { ExtensionRunner } from './extension-runner';
|
||||
import { relative } from 'node:path';
|
||||
|
||||
/**
|
||||
* The Safari runner just logs a warning message because `web-ext` doesn't work with Safari.
|
||||
*/
|
||||
export function createSafariRunner(): ExtensionRunner {
|
||||
return {
|
||||
async openBrowser(config) {
|
||||
config.logger.warn(
|
||||
`Cannot Safari using web-ext. Load "${relative(
|
||||
process.cwd(),
|
||||
config.outDir,
|
||||
)}" as an unpacked extension manually`,
|
||||
);
|
||||
},
|
||||
async closeBrowser() {
|
||||
// noop
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { WebExtRunInstance } from 'web-ext';
|
||||
import { ExtensionRunner } from './ExtensionRunner';
|
||||
import type { WebExtRunInstance } from 'web-ext-run';
|
||||
import { ExtensionRunner } from './extension-runner';
|
||||
|
||||
/**
|
||||
* Create an `ExtensionRunner` backed by `web-ext`.
|
||||
@@ -9,13 +9,10 @@ export function createWebExtRunner(): ExtensionRunner {
|
||||
|
||||
return {
|
||||
async openBrowser(config) {
|
||||
if (config.browser === 'safari') {
|
||||
config.logger.warn('Cannot open safari automatically.');
|
||||
return;
|
||||
}
|
||||
config.logger.info('Opening browser...');
|
||||
|
||||
// Use the plugin's logger instead of web-ext's built-in one.
|
||||
const webExtLogger = await import('web-ext/util/logger');
|
||||
const webExtLogger = await import('web-ext-run/util/logger');
|
||||
webExtLogger.consoleStream.write = ({ level, msg, name }) => {
|
||||
if (level >= ERROR_LOG_LEVEL) config.logger.error(name, msg);
|
||||
if (level >= WARN_LOG_LEVEL) config.logger.warn(msg);
|
||||
@@ -55,8 +52,10 @@ export function createWebExtRunner(): ExtensionRunner {
|
||||
config.logger.debug('web-ext config:', finalConfig);
|
||||
config.logger.debug('web-ext options:', options);
|
||||
|
||||
const webExt = await import('web-ext');
|
||||
const webExt = await import('web-ext-run');
|
||||
runner = await webExt.default.cmd.run(finalConfig, options);
|
||||
|
||||
config.logger.success('Opened!');
|
||||
},
|
||||
|
||||
async closeBrowser() {
|
||||
@@ -0,0 +1,21 @@
|
||||
import { ExtensionRunner } from './extension-runner';
|
||||
import { relative } from 'node:path';
|
||||
|
||||
/**
|
||||
* The WSL runner just logs a warning message because `web-ext` doesn't work in WSL.
|
||||
*/
|
||||
export function createWslRunner(): ExtensionRunner {
|
||||
return {
|
||||
async openBrowser(config) {
|
||||
config.logger.warn(
|
||||
`Cannot open browser when using WSL. Load "${relative(
|
||||
process.cwd(),
|
||||
config.outDir,
|
||||
)}" as an unpacked extension manually`,
|
||||
);
|
||||
},
|
||||
async closeBrowser() {
|
||||
// noop
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -8,8 +8,8 @@ import * as vite from 'vite';
|
||||
import { Scripting } from 'webextension-polyfill';
|
||||
import { getEntrypointBundlePath } from './utils/entrypoints';
|
||||
import { getContentScriptCssFiles } from './utils/manifest';
|
||||
import { createWebExtRunner } from './runners/createWebExtRunner';
|
||||
import { buildInternal } from './build';
|
||||
import { createExtensionRunner } from './runners';
|
||||
|
||||
export async function getServerInfo(): Promise<ServerInfo> {
|
||||
const { default: getPort, portNumbers } = await import('get-port');
|
||||
@@ -34,10 +34,10 @@ export async function setupServer(
|
||||
serverInfo: ServerInfo,
|
||||
config: InternalConfig,
|
||||
): Promise<WxtDevServer> {
|
||||
const runner = createWebExtRunner();
|
||||
const runner = await createExtensionRunner(config);
|
||||
|
||||
const viteServer = await vite.createServer(
|
||||
vite.mergeConfig(serverInfo, config.vite),
|
||||
vite.mergeConfig(serverInfo, await config.vite(config.env)),
|
||||
);
|
||||
|
||||
const start = async () => {
|
||||
@@ -45,9 +45,7 @@ export async function setupServer(
|
||||
config.logger.success(`Started dev server @ ${serverInfo.origin}`);
|
||||
|
||||
server.currentOutput = await buildInternal(config);
|
||||
config.logger.info('Opening browser...');
|
||||
await runner.openBrowser(config);
|
||||
config.logger.success('Opened!');
|
||||
};
|
||||
|
||||
const reloadExtension = () => {
|
||||
|
||||
@@ -2,61 +2,70 @@ import * as vite from 'vite';
|
||||
import { Manifest, Scripting } from 'webextension-polyfill';
|
||||
import { UnimportOptions } from 'unimport';
|
||||
import { EntrypointGroup } from '.';
|
||||
import { LogLevel } from 'consola';
|
||||
import { ContentScriptContext } from '../../client/utils/ContentScriptContext';
|
||||
import type { PluginVisualizerOptions } from 'rollup-plugin-visualizer';
|
||||
|
||||
export interface InlineConfig {
|
||||
/**
|
||||
* Project root directory.
|
||||
* Your project's root directory containing the `package.json` used to fill out the
|
||||
* `manifest.json`.
|
||||
*
|
||||
* @default
|
||||
* process.cwd()
|
||||
* @default process.cwd()
|
||||
*/
|
||||
root?: string;
|
||||
/**
|
||||
* Directory containing all source code. Set to `"src"` to move all source code to a `src/`
|
||||
* directory.
|
||||
*
|
||||
* @default
|
||||
* "<rootDir>"
|
||||
* @default config.root
|
||||
*/
|
||||
srcDir?: string;
|
||||
/**
|
||||
* Directory containing files that will be copied to the output directory as-is.
|
||||
*
|
||||
* @default
|
||||
* "<rootDir>/publicDir"
|
||||
* @default "${config.root}/public"
|
||||
*/
|
||||
publicDir?: string;
|
||||
/**
|
||||
* @default
|
||||
* "<srcDir>/entrypoints"
|
||||
* @default "${config.srcDir}/entrypoints"
|
||||
*/
|
||||
entrypointsDir?: string;
|
||||
/**
|
||||
* Path to `"wxt.config.ts"` file or false to disable config file discovery.
|
||||
* > Only available when using the JS API. Not available in `wxt.config.ts` files
|
||||
*
|
||||
* @default
|
||||
* "wxt.config.ts"
|
||||
* Path to `wxt.config.ts` file or `false` to disable config file discovery.
|
||||
*
|
||||
* @default "wxt.config.ts"
|
||||
*/
|
||||
configFile?: string | false;
|
||||
/**
|
||||
* ID of the extension for each store. Used for publishing.
|
||||
* Set to `true` to show debug logs. Overriden by the command line `--debug` option.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
storeIds?: {
|
||||
chrome?: string;
|
||||
firefox?: string;
|
||||
edge?: string;
|
||||
};
|
||||
debug?: boolean;
|
||||
/**
|
||||
* Explicitly set a mode to run in. This will override the default mode for each command, and can
|
||||
* be overridden by the command line `--mode` option.
|
||||
*/
|
||||
mode?: string;
|
||||
/**
|
||||
* Customize auto-import options.
|
||||
* Customize auto-import options. Set to `false` to disable auto-imports.
|
||||
*
|
||||
* For example, to add a directory to auto-import from, you can use:
|
||||
*
|
||||
* ```ts
|
||||
* export default defineConfig({
|
||||
* imports: {
|
||||
* dirs: ["some-directory"]
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
imports?: Partial<UnimportOptions>;
|
||||
imports?: Partial<UnimportOptions> | false;
|
||||
/**
|
||||
* Explicitly set a browser to target. This will override the default browser for each command,
|
||||
* Explicitly set a browser to build for. This will override the default browser for each command,
|
||||
* and can be overridden by the command line `--browser` option.
|
||||
*
|
||||
* @default
|
||||
@@ -76,18 +85,21 @@ export interface InlineConfig {
|
||||
*/
|
||||
logger?: Logger;
|
||||
/**
|
||||
* Custom Vite options.
|
||||
* Return custom Vite options from a function. See
|
||||
* <https://vitejs.dev/config/shared-options.html>.
|
||||
*
|
||||
* [`root`](#root), [`configFile`](#configfile), and [`mode`](#mode) should be set in WXT's config
|
||||
* instead of Vite's.
|
||||
*
|
||||
* This is a function because any vite plugins added need to be recreated for each individual
|
||||
* build step, incase they have internal state causing them to fail when reused.
|
||||
*/
|
||||
vite?: Omit<vite.UserConfig, 'root' | 'configFile' | 'mode'>;
|
||||
vite?: (env: ConfigEnv) => WxtViteConfig | Promise<WxtViteConfig>;
|
||||
/**
|
||||
* Customize the `manifest.json` output. Can be an object, promise, or function that returns an
|
||||
* object or promise.
|
||||
*/
|
||||
manifest?: UserManifest | Promise<UserManifest> | UserManifestFn;
|
||||
/**
|
||||
* Custom server options.
|
||||
*/
|
||||
server?: WxtDevServer;
|
||||
/**
|
||||
* Custom runner options. Options set here can be overridden in a `web-ext.config.ts` file.
|
||||
*/
|
||||
@@ -98,12 +110,12 @@ export interface InlineConfig {
|
||||
*
|
||||
* 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"
|
||||
* - `{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"
|
||||
* @default "{name}-{version}-{browser}.zip"
|
||||
*/
|
||||
artifactTemplate?: string;
|
||||
/**
|
||||
@@ -111,22 +123,23 @@ export interface InlineConfig {
|
||||
*
|
||||
* 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"
|
||||
* - `{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"
|
||||
* @default "{name}-{version}-sources.zip"
|
||||
*/
|
||||
sourcesTemplate?: string;
|
||||
/**
|
||||
* Override the artifactTemplate's `{{name}}` template variable. Defaults to the package.json's
|
||||
* 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.
|
||||
* Root directory to ZIP when generating the sources ZIP.
|
||||
*
|
||||
* @default config.root
|
||||
*/
|
||||
sourcesRoot?: string;
|
||||
/**
|
||||
@@ -143,6 +156,37 @@ export interface InlineConfig {
|
||||
*/
|
||||
ignoredSources?: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Transform the final manifest before it's written to the file system. Edit the `manifest`
|
||||
* parameter directly, do not return a new object. Return values are ignored.
|
||||
*
|
||||
* @example
|
||||
* defineConfig({
|
||||
* // Add a CSS-only content script.
|
||||
* transformManifest(manifest) {
|
||||
* manifest.content_scripts.push({
|
||||
* matches: ["*://google.com/*"],
|
||||
* css: ["content-scripts/some-example.css"],
|
||||
* });
|
||||
* }
|
||||
* })
|
||||
*/
|
||||
transformManifest?: (manifest: Manifest.WebExtensionManifest) => void;
|
||||
analysis?: {
|
||||
/**
|
||||
* Explicitly include bundle analysis when running `wxt build`. This can be overridden by the
|
||||
* command line `--analysis` option.
|
||||
*/
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* When running `wxt build --analyze` or setting `analysis.enabled` to true, customize how the
|
||||
* bundle will be visualized. See
|
||||
* [`rollup-plugin-visualizer`](https://github.com/btd/rollup-plugin-visualizer#how-to-use-generated-files)
|
||||
* for more details.
|
||||
*/
|
||||
template?: PluginVisualizerOptions['template'];
|
||||
};
|
||||
}
|
||||
|
||||
export interface WxtInlineViteConfig
|
||||
@@ -208,7 +252,7 @@ export interface WxtDevServer extends vite.ViteDevServer {
|
||||
) => void;
|
||||
}
|
||||
|
||||
export type TargetBrowser = 'chrome' | 'firefox' | 'safari' | 'edge' | 'opera';
|
||||
export type TargetBrowser = string;
|
||||
export type TargetManifestVersion = 2 | 3;
|
||||
|
||||
export type UserConfig = Omit<InlineConfig, 'configFile'>;
|
||||
@@ -221,6 +265,12 @@ export interface Logger {
|
||||
error(...args: any[]): void;
|
||||
fatal(...args: any[]): void;
|
||||
success(...args: any[]): void;
|
||||
level: LogLevel;
|
||||
}
|
||||
|
||||
export interface BaseEntrypointOptions {
|
||||
include?: TargetBrowser[];
|
||||
exclude?: TargetBrowser[];
|
||||
}
|
||||
|
||||
export interface BaseEntrypoint {
|
||||
@@ -250,6 +300,7 @@ export interface BaseEntrypoint {
|
||||
* subdirectory of it.
|
||||
*/
|
||||
outputDir: string;
|
||||
options: BaseEntrypointOptions;
|
||||
}
|
||||
|
||||
export interface GenericEntrypoint extends BaseEntrypoint {
|
||||
@@ -271,12 +322,12 @@ export interface BackgroundEntrypoint extends BaseEntrypoint {
|
||||
options: {
|
||||
persistent?: boolean;
|
||||
type?: 'module';
|
||||
};
|
||||
} & BaseEntrypointOptions;
|
||||
}
|
||||
|
||||
export interface ContentScriptEntrypoint extends BaseEntrypoint {
|
||||
type: 'content-script';
|
||||
options: Omit<ContentScriptDefinition, 'main'>;
|
||||
options: Omit<ContentScriptDefinition, 'main'> & BaseEntrypointOptions;
|
||||
}
|
||||
|
||||
export interface PopupEntrypoint extends BaseEntrypoint {
|
||||
@@ -288,7 +339,7 @@ export interface PopupEntrypoint extends BaseEntrypoint {
|
||||
mv2Key?: 'browser_action' | 'page_action';
|
||||
defaultIcon?: Record<string, string>;
|
||||
defaultTitle?: string;
|
||||
};
|
||||
} & BaseEntrypointOptions;
|
||||
}
|
||||
|
||||
export interface OptionsEntrypoint extends BaseEntrypoint {
|
||||
@@ -297,7 +348,7 @@ export interface OptionsEntrypoint extends BaseEntrypoint {
|
||||
openInTab?: boolean;
|
||||
browserStyle?: boolean;
|
||||
chromeStyle?: boolean;
|
||||
};
|
||||
} & BaseEntrypointOptions;
|
||||
}
|
||||
|
||||
export type Entrypoint =
|
||||
@@ -309,7 +360,7 @@ export type Entrypoint =
|
||||
|
||||
export type OnContentScriptStopped = (cb: () => void) => void;
|
||||
|
||||
export interface ContentScriptDefinition {
|
||||
export interface ContentScriptDefinition extends ExcludableEntrypoint {
|
||||
matches: Manifest.ContentScript['matches'];
|
||||
/**
|
||||
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
|
||||
@@ -354,14 +405,31 @@ export interface ContentScriptDefinition {
|
||||
/**
|
||||
* Main function executed when the content script is loaded.
|
||||
*/
|
||||
main(): void | Promise<void>;
|
||||
main(ctx: ContentScriptContext): void | Promise<void>;
|
||||
}
|
||||
|
||||
export interface BackgroundScriptDefintition {
|
||||
export interface BackgroundScriptDefintition extends ExcludableEntrypoint {
|
||||
type?: 'module';
|
||||
main(): void;
|
||||
}
|
||||
|
||||
export interface ExcludableEntrypoint {
|
||||
/**
|
||||
* List of target browsers to include this entrypoint in. Defaults to being included in all
|
||||
* builds. Cannot be used with `exclude`. You must choose one of the two options.
|
||||
*
|
||||
* @default undefined
|
||||
*/
|
||||
include?: TargetBrowser[];
|
||||
/**
|
||||
* List of target browsers to exclude this entrypoint from. Cannot be used with `include`. You
|
||||
* must choose one of the two options.
|
||||
*
|
||||
* @default undefined
|
||||
*/
|
||||
exclude?: TargetBrowser[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Manifest customization available in the `wxt.config.ts` file. You cannot configure entrypoints
|
||||
* here, they are configured inline.
|
||||
@@ -373,7 +441,6 @@ export type UserManifest = Partial<
|
||||
| 'background'
|
||||
| 'browser_action'
|
||||
| 'chrome_url_overrides'
|
||||
| 'content_scripts'
|
||||
| 'devtools_page'
|
||||
| 'manifest_version'
|
||||
| 'options_page'
|
||||
@@ -417,31 +484,11 @@ export interface ExtensionRunnerConfig {
|
||||
openDevtools?: boolean;
|
||||
/**
|
||||
* List of browser names and the binary that should be used to open the browser.
|
||||
*
|
||||
* @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#chromium-binary
|
||||
* @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#firefox
|
||||
*/
|
||||
binaries?: {
|
||||
/**
|
||||
* @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#chromium-binary
|
||||
*/
|
||||
chrome?: string;
|
||||
/**
|
||||
* @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#chromium-binary
|
||||
*/
|
||||
edge?: string;
|
||||
/**
|
||||
* @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#chromium-binary
|
||||
*/
|
||||
opera?: string;
|
||||
/**
|
||||
* @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#firefox
|
||||
*/
|
||||
firefox?:
|
||||
| 'firefox'
|
||||
| 'beta'
|
||||
| 'nightly'
|
||||
| 'deved'
|
||||
| 'firefoxdeveloperedition'
|
||||
| string;
|
||||
};
|
||||
binaries?: Record<string, string>;
|
||||
/**
|
||||
* @see https://extensionworkshop.com/documentation/develop/web-ext-command-reference/#firefox-profile
|
||||
*/
|
||||
@@ -467,3 +514,8 @@ export interface ExtensionRunnerConfig {
|
||||
*/
|
||||
startUrls?: string[];
|
||||
}
|
||||
|
||||
export type WxtViteConfig = Omit<
|
||||
vite.UserConfig,
|
||||
'root' | 'configFile' | 'mode'
|
||||
>;
|
||||
|
||||