Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5fa5fd01cb | |||
| de16423e02 | |||
| aa4c0449e9 | |||
| 9d00eb2466 | |||
| 9ac756fb43 | |||
| f195aa429c | |||
| ca20a210ea | |||
| 547fee0e0e | |||
| 54b18cc66e | |||
| d8c190365a | |||
| b625f41919 | |||
| 236a135ac2 | |||
| dca43efe0d | |||
| eedb136e13 | |||
| c71b5e12f2 | |||
| 495c5c8e4c | |||
| 63c4359d83 | |||
| efc3230674 | |||
| 69e840b984 | |||
| 2fb5a542d3 | |||
| 84657e035a | |||
| 6f5bf89645 | |||
| 90fa6bfc40 | |||
| 992452cdae | |||
| 5c7e041f7f | |||
| 3db5cec2ca | |||
| 375a2a6640 | |||
| 77cbfc1985 | |||
| 5b48ae99f4 | |||
| c1ea9ba4a3 | |||
| f28a29e118 |
@@ -7,7 +7,8 @@ on:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
wxt:
|
||||
name: WXT
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -45,4 +46,50 @@ jobs:
|
||||
working-directory: demo
|
||||
|
||||
- name: Tests
|
||||
run: pnpm test:coverage
|
||||
run: pnpm test:coverage --reporter=default --reporter=hanging-process
|
||||
|
||||
project-templates:
|
||||
name: Project Templates
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup PNPM
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
|
||||
- name: Setup NodeJS
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Validate Vanilla
|
||||
working-directory: templates/vanilla
|
||||
run: |
|
||||
npm i
|
||||
npm run build
|
||||
npm run compile
|
||||
|
||||
- name: Validate Vue
|
||||
working-directory: templates/vue
|
||||
run: |
|
||||
npm i
|
||||
npm run build
|
||||
npm run compile
|
||||
|
||||
- name: Validate React
|
||||
working-directory: templates/react
|
||||
run: |
|
||||
npm i
|
||||
npm run build
|
||||
npm run compile
|
||||
|
||||
- name: Validate Svelte
|
||||
working-directory: templates/svelte
|
||||
run: |
|
||||
npm i
|
||||
npm run build
|
||||
npm run check
|
||||
|
||||
@@ -4,3 +4,4 @@ dist
|
||||
e2e/project
|
||||
.wxt
|
||||
docs/.vitepress/cache
|
||||
pnpm-lock.yaml
|
||||
|
||||
+108
-1
@@ -1,5 +1,113 @@
|
||||
# Changelog
|
||||
|
||||
## v0.2.5
|
||||
|
||||
[compare changes](https://github.com/aklinker1/wxt/compare/v0.2.4...v0.2.5)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Auto-import from subdirectories ([547fee0](https://github.com/aklinker1/wxt/commit/547fee0))
|
||||
- Include background script in dev mode if user doesn't define one ([ca20a21](https://github.com/aklinker1/wxt/commit/ca20a21))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Don't crash when generating types in dev mode ([d8c1903](https://github.com/aklinker1/wxt/commit/d8c1903))
|
||||
- Properly load entrypoints that reference `import.meta` ([54b18cc](https://github.com/aklinker1/wxt/commit/54b18cc))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Update templates to wxt@0.2 ([9d00eb2](https://github.com/aklinker1/wxt/commit/9d00eb2))
|
||||
|
||||
### 🤖 CI
|
||||
|
||||
- Validate project templates ([9ac756f](https://github.com/aklinker1/wxt/commit/9ac756f))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker
|
||||
|
||||
## v0.2.4
|
||||
|
||||
[compare changes](https://github.com/aklinker1/wxt/compare/v0.2.3...v0.2.4)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Add `wxt zip` command ([#47](https://github.com/aklinker1/wxt/pull/47))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron
|
||||
|
||||
## v0.2.3
|
||||
|
||||
[compare changes](https://github.com/aklinker1/wxt/compare/v0.2.2...v0.2.3)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Correctly lookup open port ([#45](https://github.com/aklinker1/wxt/pull/45))
|
||||
- Read boolean maniest options from meta tags correctly ([495c5c8](https://github.com/aklinker1/wxt/commit/495c5c8))
|
||||
- Some fields cannot be overridden from `config.manifest` ([#46](https://github.com/aklinker1/wxt/pull/46))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker
|
||||
|
||||
## v0.2.2
|
||||
|
||||
[compare changes](https://github.com/aklinker1/wxt/compare/v0.2.1...v0.2.2)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Register content scripts correctly in dev mode ([2fb5a54](https://github.com/aklinker1/wxt/commit/2fb5a54))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker
|
||||
|
||||
## v0.2.1
|
||||
|
||||
[compare changes](https://github.com/aklinker1/wxt/compare/v0.2.0...v0.2.1)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Support all content script options ([6f5bf89](https://github.com/aklinker1/wxt/commit/6f5bf89))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Remove HMR log ([90fa6bf](https://github.com/aklinker1/wxt/commit/90fa6bf))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker
|
||||
|
||||
## v0.2.0
|
||||
|
||||
[compare changes](https://github.com/aklinker1/wxt/compare/v0.1.6...v0.2.0)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- ⚠️ Rename `defineBackgroundScript` to `defineBackground` ([5b48ae9](https://github.com/aklinker1/wxt/commit/5b48ae9))
|
||||
- Recongize unnamed content scripts (`content.ts` and `content/index.ts`) ([3db5cec](https://github.com/aklinker1/wxt/commit/3db5cec))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Update templates ([f28a29e](https://github.com/aklinker1/wxt/commit/f28a29e))
|
||||
- Add docs for each type of entrypoint ([77cbfc1](https://github.com/aklinker1/wxt/commit/77cbfc1))
|
||||
- Add inline JSDoc for public types ([375a2a6](https://github.com/aklinker1/wxt/commit/375a2a6))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Run `wxt prepare` on `postinstall` ([c1ea9ba](https://github.com/aklinker1/wxt/commit/c1ea9ba))
|
||||
- Don't format lockfile ([5c7e041](https://github.com/aklinker1/wxt/commit/5c7e041))
|
||||
|
||||
#### ⚠️ Breaking Changes
|
||||
|
||||
- ⚠️ Rename `defineBackgroundScript` to `defineBackground` ([5b48ae9](https://github.com/aklinker1/wxt/commit/5b48ae9))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker
|
||||
|
||||
## v0.1.6
|
||||
|
||||
[compare changes](https://github.com/aklinker1/wxt/compare/v0.1.5...v0.1.6)
|
||||
@@ -75,7 +183,6 @@
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron
|
||||
- Aaron Klinker
|
||||
|
||||
## v0.1.2
|
||||
|
||||
+3
-2
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "Demo",
|
||||
"name": "WXT Demo",
|
||||
"version": "1.0.0",
|
||||
"description": "Demo extension for WXT",
|
||||
"type": "module",
|
||||
@@ -11,7 +11,8 @@
|
||||
"build:all:chrome-mv2": "wxt build --mv2",
|
||||
"build:all:firefox-mv3": "wxt build -b firefox --mv3",
|
||||
"build:all:firefox-mv2": "wxt build -b firefox",
|
||||
"prepare": "pnpm -w build && wxt prepare"
|
||||
"zip": "pnpm -w build && wxt zip",
|
||||
"postinstall": "pnpm -w build && wxt prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"webextension-polyfill": "^0.10.0"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export default defineBackgroundScript(() => {
|
||||
export default defineBackground(() => {
|
||||
console.log(browser.runtime.id);
|
||||
logId();
|
||||
console.log({
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<script lang="ts" setup>
|
||||
defineProps<{
|
||||
name: string;
|
||||
icon?: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<img
|
||||
:src="`https://raw.githubusercontent.com/PKief/vscode-material-icon-theme/main/icons/${
|
||||
name?.toLowerCase() ?? icon
|
||||
}.svg`"
|
||||
:alt="`${name} Logo`"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
img {
|
||||
display: inline;
|
||||
transform: translateY(5px);
|
||||
margin-right: 8px;
|
||||
width: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -4,18 +4,29 @@ import { defineConfig } from 'vitepress';
|
||||
export default defineConfig({
|
||||
title: 'WXT',
|
||||
description: 'Next gen framework for developing web extensions',
|
||||
lastUpdated: true,
|
||||
|
||||
themeConfig: {
|
||||
// https://vitepress.dev/reference/default-theme-config
|
||||
// logo: '/logo.svg',
|
||||
editLink: {
|
||||
pattern: 'https://github.com/aklinker1/wxt/edit/main/docs/:path',
|
||||
},
|
||||
search: {
|
||||
provider: 'local',
|
||||
},
|
||||
|
||||
nav: [
|
||||
{ text: 'Get Started', link: '/get-started/installation.md' },
|
||||
{ text: 'Guide', link: '/guide.md' },
|
||||
{ text: 'Guide', link: '/guide/auto-imports.md' },
|
||||
{ text: 'Config', link: '/config.md' },
|
||||
{ text: 'API', link: '/api.md' },
|
||||
],
|
||||
|
||||
sidebar: {
|
||||
'/get-started/': [
|
||||
{
|
||||
text: 'Guide',
|
||||
text: 'Get Started',
|
||||
items: [
|
||||
{ text: 'Introduction', link: '/get-started/introduction.md' },
|
||||
{ text: 'Installation', link: '/get-started/installation.md' },
|
||||
@@ -28,6 +39,32 @@ export default defineConfig({
|
||||
],
|
||||
},
|
||||
],
|
||||
'/guide/': [
|
||||
{
|
||||
text: 'Guide',
|
||||
items: [
|
||||
{ text: 'Auto-imports', link: '/guide/auto-imports.md' },
|
||||
{ text: 'Manifest.json', link: '/guide/manifest.md' },
|
||||
{ text: 'Extension APIs', link: '/guide/extension-apis.md' },
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Entrypoints',
|
||||
items: [
|
||||
{ text: 'Background', link: '/guide/background.md' },
|
||||
{ text: 'Bookmarks', link: '/guide/bookmarks.md' },
|
||||
{ text: 'Content Scripts', link: '/guide/content-scripts.md' },
|
||||
{ text: 'Devtools', link: '/guide/devtools.md' },
|
||||
{ text: 'History', link: '/guide/history.md' },
|
||||
{ text: 'Newtab', link: '/guide/newtab.md' },
|
||||
{ text: 'Options', link: '/guide/options.md' },
|
||||
{ text: 'Popup', link: '/guide/popup.md' },
|
||||
{ text: 'Sandbox', link: '/guide/sandbox.md' },
|
||||
{ text: 'Unlisted Pages', link: '/guide/unlisted-pages.md' },
|
||||
{ text: 'Unlisted Scripts', link: '/guide/unlisted-scripts.md' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
socialLinks: [{ icon: 'github', link: 'https://github.com/aklinker1/wxt' }],
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import DefaultTheme from 'vitepress/theme';
|
||||
import Icon from '../components/Icon.vue';
|
||||
|
||||
export default {
|
||||
extends: DefaultTheme,
|
||||
enhanceApp(ctx) {
|
||||
ctx.app.component('Icon', Icon);
|
||||
},
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
# Guide
|
||||
# Config
|
||||
|
||||
:::warning 🚧 Under construction
|
||||
This documentation does not exist yet.
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
You can build an extension for any combination of browser and manifest verison. Different browsers and manifest versions support different APIs and entrypoints, so be sure to check that your extension functions as expected for each target.
|
||||
|
||||
Separate build targets are to separate output directories:
|
||||
Separate build targets are written to their own output directories:
|
||||
|
||||
```
|
||||
<root>
|
||||
@@ -24,7 +24,7 @@ wxt build --browser firefox
|
||||
|
||||
```
|
||||
|
||||
By default, it will build for `chrome`. When excluding the [`--mv2` or `--mv3` flags](#target-manifest-version), it will default to the commonly accepted version used with that browser.
|
||||
By default, it will build for `chrome`. When excluding the [`--mv2` or `--mv3` flags](#target-manifest-version), it will default to the commonly accepted manifest version used with that browser.
|
||||
|
||||
| Browser | Default Manifest Version |
|
||||
| ---------------- | :----------------------: |
|
||||
|
||||
@@ -6,10 +6,7 @@ Bootstrap a new project or start from scratch.
|
||||
|
||||
:::warning 🚧 The `wxt init` command is not implemented yet.
|
||||
|
||||
See [From Scratch](#from-scratch) or reference one of the templates on GitHub:
|
||||
|
||||
- [Vue](https://github.com/aklinker1/wxt/tree/main/templates/vue)
|
||||
- [React](https://github.com/aklinker1/wxt/tree/main/templates/react)
|
||||
See [From Scratch](#from-scratch) or reference one of the templates below.
|
||||
|
||||
:::
|
||||
|
||||
@@ -25,6 +22,17 @@ 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) |
|
||||
|
||||
> All templates are in TypeScript. WXT does not support JS at this time.
|
||||
|
||||
## From Scratch
|
||||
|
||||
Create a new NPM project:
|
||||
@@ -71,7 +79,7 @@ Add your first entrypoint:
|
||||
|
||||
```ts
|
||||
// entrypoints/background.ts
|
||||
export default defineBackgroundScript(() => {
|
||||
export default defineBackground(() => {
|
||||
console.log(`Hello from ${browser.runtime.id}!`);
|
||||
});
|
||||
```
|
||||
@@ -84,7 +92,8 @@ Finally, add scripts to your `package.json`:
|
||||
"dev": "wxt", // [!code ++]
|
||||
"dev:firefox": "wxt --browser firefox", // [!code ++]
|
||||
"build": "wxt build", // [!code ++]
|
||||
"build:firefox": "wxt build --browser firefox" // [!code ++]
|
||||
"build:firefox": "wxt build --browser firefox", // [!code ++]
|
||||
"postinstall": "wxt prepare" // [!code ++]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
# Auto-imports
|
||||
|
||||
WXT uses the same tool as Nuxt for auto-imports, [`unimport`](https://github.com/unjs/unimport).
|
||||
|
||||
:::info Testing
|
||||
To setup your test environment for auto-imports, see [Testing](/get-started/testing.md).
|
||||
:::
|
||||
|
||||
## WXT Auto-imports
|
||||
|
||||
Some WXT APIs can be used without importing them:
|
||||
|
||||
- [`browser`](/config.md#browser) from `webextension-polyfill`
|
||||
- [`defineContentScript`](/config.md#defiencontentscript) from `wxt/client`
|
||||
- [`defineBackground`](/config.md#definebackgroundscript) from `wxt/client`
|
||||
|
||||
And more. All [`wxt/client`](/config.md#wxtclient) APIs can be used without imports.
|
||||
|
||||
## Project Auto-imports
|
||||
|
||||
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/**/*`
|
||||
|
||||
## TypeScript
|
||||
|
||||
For TypeScript to work, you need to run the `wxt prepare` command. This will ensure types are generated for auto-imports.
|
||||
|
||||
This should be added to your `postinstall` script:
|
||||
|
||||
```json
|
||||
// package.json
|
||||
{
|
||||
"scripts": {
|
||||
"postinstall": "wxt prepare" // [!code ++]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Customization
|
||||
|
||||
You can override the default auto-import behavior in your `wxt.config.ts` file.
|
||||
|
||||
See [`unimport`'s documentation](https://github.com/unjs/unimport#configurations) for a complete list of options.
|
||||
|
||||
```ts
|
||||
import { defineConfig } from 'wxt';
|
||||
|
||||
export default defineConfig({
|
||||
imports: {
|
||||
// Add auto-imports for vue fuctions like createApp, ref, computed, watch, toRaw, etc...
|
||||
preset: ['vue'],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
To disable auto-imports, set `imports: false`
|
||||
|
||||
```ts
|
||||
export default defineConfig({
|
||||
imports: false,
|
||||
});
|
||||
```
|
||||
@@ -0,0 +1,38 @@
|
||||
# Background
|
||||
|
||||
[Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/manifest/background/) • [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/background)
|
||||
|
||||
For MV2, the background is added as a script to the background page. For MV3, the background becomes a service worker.
|
||||
|
||||
## Filenames
|
||||
|
||||
`entrypoints/background.ts` is the only recoginzed filename for the background script.
|
||||
|
||||
## Definition
|
||||
|
||||
:::warning
|
||||
The main function of the background **_CANNOT BE ASYNC_**. Event listeners must be added syncronously on background startup. If your main function returns a promise, WXT will log an error.
|
||||
:::
|
||||
|
||||
```ts
|
||||
export default defineBackground(() => {
|
||||
// Executed when background is loaded
|
||||
});
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```ts
|
||||
export default defineBackground({
|
||||
// Set manifest options
|
||||
persistent: undefined | true | false,
|
||||
type: undefined | 'module',
|
||||
|
||||
// Executed when background is loaded
|
||||
main() {
|
||||
// ...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
> All manifest options default to `undefined`.
|
||||
@@ -0,0 +1,26 @@
|
||||
# Bookmarks
|
||||
|
||||
[Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/override/) • [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/chrome_url_overrides)
|
||||
|
||||
## Filenames
|
||||
|
||||
- `entrypoints/bookmarks.html`
|
||||
- `entrypoints/bookmarks/index.html`
|
||||
|
||||
## Definition
|
||||
|
||||
Plain old HTML file.
|
||||
|
||||
```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>
|
||||
</head>
|
||||
<body>
|
||||
<!-- ... -->
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
@@ -0,0 +1,60 @@
|
||||
# Content Scripts
|
||||
|
||||
[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)
|
||||
|
||||
## Filenames
|
||||
|
||||
When a filename matches the pattern below, it is added as a content script in the `manifest.json`.
|
||||
|
||||
- `entrypoints/content.tsx?`
|
||||
- `entrypoints/<name>.content.tsx?`
|
||||
- `entrypoints/content/index.tsx?`
|
||||
- `entrypoints/<name>.content/index.tsx?`
|
||||
|
||||
## Definition
|
||||
|
||||
```ts
|
||||
export default defineContentScript({
|
||||
// Set manifest options
|
||||
matches: ['*://google.com/*', '*://duckduckgo.com/*'],
|
||||
excludeMatches: undefined | [],
|
||||
includeGlobs: undefined | [],
|
||||
excludeGlobs: undefined | [],
|
||||
allFrames: undefined | [],
|
||||
runAt: undefined | 'document_start' | 'document_end' | 'document_idle',
|
||||
matchAboutBlank: undefined | true | false,
|
||||
matchOriginAsFallback: undefined | true | false,
|
||||
world: undefined | 'ISOLATED' | 'MAIN',
|
||||
|
||||
main() {
|
||||
// Executed when content script is loaded
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
> All manifest options default to `undefined`.
|
||||
|
||||
## CSS
|
||||
|
||||
To include CSS with your content script, import the CSS file at the top of your entrypoint:
|
||||
|
||||
```
|
||||
|
||||
<srcDir>
|
||||
└─ entrypoints/
|
||||
└─ overlay.content/
|
||||
├─ index.ts
|
||||
└─ style.css
|
||||
```
|
||||
|
||||
```ts
|
||||
// entrypoints/overlay.content/index.ts
|
||||
import './style.css';
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ['*://google.com/*', '*://duckduckgo.com/*'],
|
||||
main() {
|
||||
// ...
|
||||
},
|
||||
});
|
||||
```
|
||||
@@ -0,0 +1,26 @@
|
||||
# Devtools
|
||||
|
||||
[Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/devtools/) • [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/devtools_page)
|
||||
|
||||
## Filenames
|
||||
|
||||
- `entrypoints/devtools.html`
|
||||
- `entrypoints/devtools/index.html`
|
||||
|
||||
## Definition
|
||||
|
||||
Plain old HTML file.
|
||||
|
||||
```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>
|
||||
</head>
|
||||
<body>
|
||||
<!-- ... -->
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
@@ -0,0 +1,94 @@
|
||||
# Extension APIs
|
||||
|
||||
WXT is built on top of [`webextension-polyfill`](https://www.npmjs.com/package/webextension-polyfill), which uses the standard `browser` global.
|
||||
|
||||
If you're used to Chrome's `chrome` global, its a simple switch:
|
||||
|
||||
1. Replace `chrome` with `browser`
|
||||
2. [Replace callbacks with async/await](https://developer.chrome.com/docs/extensions/mv3/promises/)
|
||||
|
||||
And that's it! Your extension now supports Chrome, Firefox, Safari, Edge, and other Chromium browsers.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
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';
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
```ts
|
||||
// wxt.config.ts
|
||||
import { defineConfig } from 'wxt';
|
||||
|
||||
export default defineConfig({
|
||||
manifest: {
|
||||
permissions: ['storage'],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Then we can use `browser.storage` to save the install date to local storage.
|
||||
|
||||
```ts
|
||||
// background.ts
|
||||
export default defineBackground(() => {
|
||||
browser.runtime.onInstall.addEventListener(({ reason }) => {
|
||||
if (reason === 'install') {
|
||||
browser.storage.local.setItem({ installDate: Date.now() });
|
||||
}
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Messaging
|
||||
|
||||
Follow [Chrome's message passing guide](https://developer.chrome.com/docs/extensions/mv3/messaging/) to understand how message passing works in web extensions. In Google's examples, just replace `chrome` with `browser`, and it will work in WXT.
|
||||
|
||||
Here's a basic request/response example:
|
||||
|
||||
```ts
|
||||
// background.ts
|
||||
export default defineBackground(() => {
|
||||
browser.runtime.onMessage.addEventListener(
|
||||
(message, sender, sendResponse) => {
|
||||
console.log(message); // "ping"
|
||||
|
||||
// Wait 1 second and respond with "pong"
|
||||
setTimeout(() => sendResponse('pong'), 1000);
|
||||
return true;
|
||||
},
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
```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):
|
||||
|
||||
- [`@webext-core/messaging`](https://webext-core.aklinker1.io/guide/proxy-service/) - "A light-weight, type-safe wrapper around the `browser.runtime` messaging APIs"
|
||||
- [`@webext-core/proxy-service`](https://webext-core.aklinker1.io/guide/messaging/) - "Create TRPC-like services that can be called from anywhere but run in the background"
|
||||
- [`webext-bridge`](https://github.com/zikaari/webext-bridge) - "Messaging in Web Extensions made super easy. Out of the box."
|
||||
|
||||
## Browser Differences
|
||||
|
||||
Some APIs are only available on certain browsers or manifest versions. You will have to check if an API exists at runtime if it is not in the `browser` standard.
|
||||
|
||||
```ts
|
||||
if ('session' in browser.storage) {
|
||||
// Do something with the non-standard session storage API
|
||||
}
|
||||
```
|
||||
|
||||
> If you're using TypeScript, knowing what is non-standard is easy! APIs that are not typed are non-standard.
|
||||
@@ -0,0 +1,26 @@
|
||||
# History
|
||||
|
||||
[Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/override/) • [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/chrome_url_overrides)
|
||||
|
||||
## Filenames
|
||||
|
||||
- `entrypoints/history.html`
|
||||
- `entrypoints/history/index.html`
|
||||
|
||||
## Definition
|
||||
|
||||
Plain old HTML file.
|
||||
|
||||
```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>
|
||||
</head>
|
||||
<body>
|
||||
<!-- ... -->
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
@@ -0,0 +1,114 @@
|
||||
# Manifest.json
|
||||
|
||||
The manifest.json is generated at build-time based on files in your `entrypoints` directory and your `wxt.config.ts`.
|
||||
|
||||
## Customization
|
||||
|
||||
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.
|
||||
|
||||
```ts
|
||||
import { defineConfig } from 'wxt';
|
||||
|
||||
export default defineConfig({
|
||||
manifest: {
|
||||
host_permissions: ['*://google.com/*'],
|
||||
content_security_policy: {
|
||||
// ...
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## `name`
|
||||
|
||||
If not provided via the `manifest` config, the [manifest's `name`](https://developer.chrome.com/docs/extensions/mv3/manifest/name/) defaults to your package.json's `name` property.
|
||||
|
||||
## `version` and `version_name`
|
||||
|
||||
The [manifest's `version` and `version_name`](https://developer.chrome.com/docs/extensions/mv3/manifest/version/) fields are based on your package.json's `version` property.
|
||||
|
||||
- `version_name` is the exact string listed in your package.json
|
||||
- `version` is the string cleaned up, with any invalid suffixes removed
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
// package.json
|
||||
{
|
||||
"version": "1.3.0-alpha2"
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
// .output/<dir>/manifest.json
|
||||
{
|
||||
"version": "1.3.0",
|
||||
"version_name": "1.3.0-alpha2"
|
||||
}
|
||||
```
|
||||
|
||||
### `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).
|
||||
|
||||
```
|
||||
public/
|
||||
├─ icon-16.png
|
||||
├─ icon-24.png
|
||||
├─ icon-48.png
|
||||
├─ icon-96.png
|
||||
└─ icon-128.png
|
||||
```
|
||||
|
||||
```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',
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Permissions
|
||||
|
||||
[Permissions](https://developer.chrome.com/docs/extensions/reference/permissions/) must be listed in the manifest config.
|
||||
|
||||
```ts
|
||||
export default defineConfig({
|
||||
manifest: {
|
||||
permissions: ['storage', 'tabs'],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### 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).
|
||||
|
||||
```
|
||||
public/
|
||||
└─ _locales/
|
||||
├─ en/
|
||||
│ └─ messages.json
|
||||
├─ es/
|
||||
│ └─ messages.json
|
||||
└─ ko/
|
||||
└─ messages.json
|
||||
```
|
||||
|
||||
Then you'll need to explicitly override the `name` and `description` properties in your config for them to be localized.
|
||||
|
||||
```ts
|
||||
export default defineConfig({
|
||||
manifest: {
|
||||
name: '__MSG_extName__',
|
||||
description: '__MSG_extDescription__',
|
||||
default_locale: 'en',
|
||||
},
|
||||
});
|
||||
```
|
||||
@@ -0,0 +1,26 @@
|
||||
# Newtab
|
||||
|
||||
[Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/override/) • [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/chrome_url_overrides)
|
||||
|
||||
## Filenames
|
||||
|
||||
- `entrypoints/newtab.html`
|
||||
- `entrypoints/newtab/index.html`
|
||||
|
||||
## Definition
|
||||
|
||||
Plain old HTML file.
|
||||
|
||||
```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>
|
||||
</head>
|
||||
<body>
|
||||
<!-- ... -->
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
@@ -0,0 +1,31 @@
|
||||
# Options
|
||||
|
||||
[Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/options/) • [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/options_ui)
|
||||
|
||||
## Filenames
|
||||
|
||||
- `entrypoints/options.html`
|
||||
- `entrypoints/options/index.html`
|
||||
|
||||
## Definition
|
||||
|
||||
Plain old HTML file.
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Options Title</title>
|
||||
<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" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- ... -->
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
> All manifest options default to `undefined` when the `meta` tag is not present.
|
||||
@@ -0,0 +1,37 @@
|
||||
# Popup
|
||||
|
||||
[Chrome Docs](https://developer.chrome.com/docs/extensions/reference/action/) • [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/action)
|
||||
|
||||
## Filenames
|
||||
|
||||
- `entrypoints/popup.html`
|
||||
- `entrypoints/popup/index.html`
|
||||
|
||||
## Definition
|
||||
|
||||
Plain old HTML file.
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Default Popup Title</title>
|
||||
<meta
|
||||
name="manifest.default_icon"
|
||||
content="{
|
||||
16: '/icon-16.png',
|
||||
24: '/icon-24.png',
|
||||
...
|
||||
}"
|
||||
/>
|
||||
<meta name="manifest.type" content="page_action|browser_action" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- ... -->
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
> All manifest options default to `undefined` when the `meta` tag is not present.
|
||||
@@ -0,0 +1,32 @@
|
||||
# Sandbox
|
||||
|
||||
[Chrome Docs](https://developer.chrome.com/docs/extensions/mv3/manifest/sandbox/)
|
||||
|
||||
:::tip Chromium Only
|
||||
Firefox does not support sandboxed pages.
|
||||
:::
|
||||
|
||||
## Filenames
|
||||
|
||||
- `entrypoints/sandbox.html`
|
||||
- `entrypoints/<name>.sandbox.html`
|
||||
- `entrypoints/sandbox/index.html`
|
||||
- `entrypoints/<name>.sandbox/index.html`
|
||||
|
||||
## Definition
|
||||
|
||||
Plain old HTML file.
|
||||
|
||||
```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>
|
||||
</head>
|
||||
<body>
|
||||
<!-- ... -->
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
@@ -0,0 +1,32 @@
|
||||
# Side Panel
|
||||
|
||||
[Chrome Docs](https://developer.chrome.com/docs/extensions/reference/sidePanel/)
|
||||
|
||||
:::tip Chromium Only
|
||||
Firefox does not support sandboxed pages.
|
||||
:::
|
||||
|
||||
## Filenames
|
||||
|
||||
- `entrypoints/sidepanel.html`
|
||||
- `entrypoints/<name>.sidepanel.html`
|
||||
- `entrypoints/sidepanel/index.html`
|
||||
- `entrypoints/<name>.sidepanel/index.html`
|
||||
|
||||
## Definition
|
||||
|
||||
Plain old HTML file.
|
||||
|
||||
```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>
|
||||
</head>
|
||||
<body>
|
||||
<!-- ... -->
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
@@ -0,0 +1,42 @@
|
||||
# Unlisted Pages
|
||||
|
||||
HTML pages that are built by Vite, but are not included in the manifest.
|
||||
|
||||
### Examples
|
||||
|
||||
- Onboarding
|
||||
- Dashboard
|
||||
- FAQ
|
||||
- Help
|
||||
- Changelog
|
||||
|
||||
## Filenames
|
||||
|
||||
- `entrypoints/<name>.html`
|
||||
- `entrypoints/<name>/index.html`
|
||||
|
||||
Pages are accessible at `'/<name>.html'`:
|
||||
|
||||
```ts
|
||||
const url = browser.runtime.getURL('/<name>.html');
|
||||
|
||||
console.log(url); // "chrome-extension://<id>/<name>.html"
|
||||
```
|
||||
|
||||
## Definition
|
||||
|
||||
Plain old HTML file.
|
||||
|
||||
```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>
|
||||
</head>
|
||||
<body>
|
||||
<!-- ... -->
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
@@ -0,0 +1,16 @@
|
||||
# Unlisted Scripts
|
||||
|
||||
TypeScript files that are built, but are not included in the manifest.
|
||||
|
||||
## Filenames
|
||||
|
||||
- `entrypoints/<name>.tsx?`
|
||||
- `entrypoints/<name>/index.tsx?`
|
||||
|
||||
## Definition
|
||||
|
||||
Unlike the background and content scripts, you can define this script's logic in the main scope.
|
||||
|
||||
```ts
|
||||
// Code goes here
|
||||
```
|
||||
@@ -6,7 +6,7 @@ describe('Auto Imports', () => {
|
||||
const project = new TestProject();
|
||||
project.addFile(
|
||||
'entrypoints/background.ts',
|
||||
'export default defineBackgroundScript(() => {})',
|
||||
'export default defineBackground(() => {})',
|
||||
);
|
||||
project.addFile(
|
||||
'entrypoints/overlay.content.ts',
|
||||
@@ -43,7 +43,7 @@ describe('Auto Imports', () => {
|
||||
export {}
|
||||
declare global {
|
||||
const browser: typeof import('webextension-polyfill')
|
||||
const defineBackgroundScript: typeof import('wxt/client')['defineBackgroundScript']
|
||||
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']
|
||||
|
||||
@@ -57,7 +57,7 @@ describe('Manifest Content', () => {
|
||||
const optionsContent = `
|
||||
<html>
|
||||
<head>
|
||||
<meta name="manifest.open_in_tab" content="true">
|
||||
<meta name="manifest.open_in_tab" content="false">
|
||||
<meta name="manifest.chrome_style" content="true">
|
||||
<meta name="manifest.browser_style" content="true">
|
||||
</head>
|
||||
@@ -72,7 +72,7 @@ describe('Manifest Content', () => {
|
||||
const manifest = await project.getOutputManifest();
|
||||
|
||||
expect(manifest.options_ui).toEqual({
|
||||
open_in_tab: true,
|
||||
open_in_tab: false,
|
||||
chrome_style: true,
|
||||
page: 'options.html',
|
||||
});
|
||||
@@ -88,7 +88,7 @@ describe('Manifest Content', () => {
|
||||
);
|
||||
|
||||
expect(manifest.options_ui).toEqual({
|
||||
open_in_tab: true,
|
||||
open_in_tab: false,
|
||||
browser_style: true,
|
||||
page: 'options.html',
|
||||
});
|
||||
@@ -97,7 +97,7 @@ describe('Manifest Content', () => {
|
||||
|
||||
describe('background', () => {
|
||||
const backgroundContent = `
|
||||
export default defineBackgroundScript({
|
||||
export default defineBackground({
|
||||
persistent: true,
|
||||
type: "module",
|
||||
main: () => {},
|
||||
@@ -149,7 +149,7 @@ describe('Manifest Content', () => {
|
||||
`import "./style.css";
|
||||
export default defineContentScript({
|
||||
matches: ["*://google.com/*"],
|
||||
run_at: "document_end",
|
||||
runAt: "document_end",
|
||||
main: () => {},
|
||||
})`,
|
||||
);
|
||||
@@ -162,7 +162,7 @@ describe('Manifest Content', () => {
|
||||
`import "./style.css";
|
||||
export default defineContentScript({
|
||||
matches: ["*://google.com/*"],
|
||||
run_at: "document_end",
|
||||
runAt: "document_end",
|
||||
main: () => {},
|
||||
})`,
|
||||
);
|
||||
@@ -175,7 +175,7 @@ describe('Manifest Content', () => {
|
||||
`import "./style.css";
|
||||
export default defineContentScript({
|
||||
matches: ["*://duckduckgo.com/*"],
|
||||
run_at: "document_end",
|
||||
runAt: "document_end",
|
||||
main: () => {},
|
||||
})`,
|
||||
);
|
||||
|
||||
@@ -13,7 +13,7 @@ describe('Output Directory Structure', () => {
|
||||
expect(await project.serializeOutput()).toMatchInlineSnapshot(`
|
||||
".output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\"}"
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\"}"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -66,7 +66,7 @@ describe('Output Directory Structure', () => {
|
||||
================================================================================
|
||||
.output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"content_scripts\\":[{\\"matches\\":[\\"*://*/*\\"],\\"css\\":[\\"assets/one.css\\",\\"assets/two.css\\"],\\"js\\":[\\"content-scripts/one.js\\",\\"content-scripts/two.js\\"]}]}"
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"content_scripts\\":[{\\"matches\\":[\\"*://*/*\\"],\\"css\\":[\\"assets/one.css\\",\\"assets/two.css\\"],\\"js\\":[\\"content-scripts/one.js\\",\\"content-scripts/two.js\\"]}]}"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -90,7 +90,7 @@ describe('Output Directory Structure', () => {
|
||||
================================================================================
|
||||
.output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"content_scripts\\":[{\\"matches\\":[\\"*://*/*\\"],\\"js\\":[\\"content-scripts/overlay-one.js\\"]}]}"
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"content_scripts\\":[{\\"matches\\":[\\"*://*/*\\"],\\"js\\":[\\"content-scripts/overlay-one.js\\"]}]}"
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ describe('User Config', () => {
|
||||
});
|
||||
project.addFile(
|
||||
'src/entrypoints/background.ts',
|
||||
`export default defineBackgroundScript(
|
||||
`export default defineBackground(
|
||||
() => console.log('Hello background'),
|
||||
);`,
|
||||
);
|
||||
@@ -27,7 +27,7 @@ describe('User Config', () => {
|
||||
================================================================================
|
||||
.output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"background\\":{\\"service_worker\\":\\"background.js\\"}}"
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"background\\":{\\"service_worker\\":\\"background.js\\"}}"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -38,7 +38,7 @@ describe('User Config', () => {
|
||||
});
|
||||
project.addFile(
|
||||
'entries/background.ts',
|
||||
`export default defineBackgroundScript(() => console.log('Hello background'));`,
|
||||
`export default defineBackground(() => console.log('Hello background'));`,
|
||||
);
|
||||
|
||||
await project.build();
|
||||
@@ -52,7 +52,7 @@ describe('User Config', () => {
|
||||
================================================================================
|
||||
.output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"background\\":{\\"service_worker\\":\\"background.js\\"}}"
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"background\\":{\\"service_worker\\":\\"background.js\\"}}"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -80,7 +80,7 @@ describe('User Config', () => {
|
||||
expect(output).toMatchInlineSnapshot(`
|
||||
".output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"example_customization\\":[\\"production\\",\\"chrome\\",\\"3\\",\\"build\\"]}"
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"example_customization\\":[\\"production\\",\\"chrome\\",\\"3\\",\\"build\\"]}"
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
+5
-2
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "wxt",
|
||||
"type": "module",
|
||||
"version": "0.1.6",
|
||||
"version": "0.2.5",
|
||||
"description": "Next gen framework for developing web extensions",
|
||||
"engines": {
|
||||
"node": ">=18.16.0",
|
||||
@@ -69,16 +69,19 @@
|
||||
"fast-glob": "^3.2.12",
|
||||
"filesize": "^10.0.7",
|
||||
"fs-extra": "^11.1.1",
|
||||
"get-port": "^7.0.0",
|
||||
"jiti": "^1.18.2",
|
||||
"json5": "^2.2.3",
|
||||
"linkedom": "^0.14.26",
|
||||
"minimatch": "^9.0.3",
|
||||
"picocolors": "^1.0.0",
|
||||
"picomatch": "^2.3.1",
|
||||
"unimport": "^3.0.8",
|
||||
"vite": "^4.3.9",
|
||||
"vite-tsconfig-paths": "^4.2.0",
|
||||
"web-ext": "^7.6.2",
|
||||
"webextension-polyfill": "^0.10.0"
|
||||
"webextension-polyfill": "^0.10.0",
|
||||
"zip-dir": "^2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^8.0.2",
|
||||
|
||||
Generated
+1328
-3819
File diff suppressed because it is too large
Load Diff
+3
-16
@@ -1,7 +1,6 @@
|
||||
import tsup from 'tsup';
|
||||
import glob from 'fast-glob';
|
||||
import pc from 'picocolors';
|
||||
import path from 'path';
|
||||
import { printFileList } from '../src/core/log/printFileList';
|
||||
import { formatDuration } from '../src/core/utils/formatDuration';
|
||||
import ora from 'ora';
|
||||
import fs from 'fs-extra';
|
||||
@@ -73,18 +72,6 @@ await Promise.all([
|
||||
spinner.succeed();
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
const outFiles = await glob(`${outDir}/**`);
|
||||
outFiles.forEach((file, i) => {
|
||||
const color = file.endsWith('.map')
|
||||
? pc.dim
|
||||
: file.endsWith('.d.ts')
|
||||
? pc.blue
|
||||
: pc.cyan;
|
||||
const prefix = i === outFiles.length - 1 ? ' └─' : ' ├─';
|
||||
console.log(
|
||||
`${pc.gray(prefix)} ${pc.dim(outDir + path.sep)}${color(
|
||||
path.relative(outDir, file),
|
||||
)}`,
|
||||
);
|
||||
});
|
||||
const outFiles = await glob(`${outDir}/**`, { absolute: true });
|
||||
await printFileList(consola.log, outDir, outFiles);
|
||||
consola.success(`Finished in ${formatDuration(duration)}`);
|
||||
|
||||
Vendored
+16
@@ -0,0 +1,16 @@
|
||||
declare module 'zip-dir' {
|
||||
// Represents the options object for zipdir function
|
||||
interface ZipDirOptions {
|
||||
saveTo?: string;
|
||||
filter?: (path: string, stat: import('fs').Stats) => boolean;
|
||||
each?: (path: string) => void;
|
||||
}
|
||||
|
||||
function zipdir(
|
||||
dirPath: string,
|
||||
options?: ZipDirOptions,
|
||||
callback?: (error: Error | null, buffer: Buffer) => void,
|
||||
): Promise<Buffer>;
|
||||
|
||||
export = zipdir;
|
||||
}
|
||||
@@ -3,3 +3,4 @@ export * from './dev';
|
||||
export * from './init';
|
||||
export * from './prepare';
|
||||
export * from './publish';
|
||||
export * from './zip';
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import * as wxt from '../..';
|
||||
import { buildInternal } from '../../core/build';
|
||||
import { getInternalConfig } from '../../core/utils/getInternalConfig';
|
||||
import { zipExtension } from '../../core/zip';
|
||||
import { defineCommand } from '../utils/defineCommand';
|
||||
|
||||
export const zip = defineCommand<
|
||||
[
|
||||
root: string | undefined,
|
||||
flags: {
|
||||
mode?: string;
|
||||
config?: string;
|
||||
browser?: wxt.TargetBrowser;
|
||||
mv3?: boolean;
|
||||
mv2?: boolean;
|
||||
},
|
||||
]
|
||||
>(async (root, flags) => {
|
||||
const mode = flags.mode ?? 'production';
|
||||
const cliConfig: wxt.InlineConfig = {
|
||||
root,
|
||||
mode,
|
||||
browser: flags.browser,
|
||||
manifestVersion: flags.mv3 ? 3 : flags.mv2 ? 2 : undefined,
|
||||
configFile: flags.config,
|
||||
};
|
||||
|
||||
const config = await getInternalConfig(cliConfig, 'build');
|
||||
const output = await buildInternal(config);
|
||||
await zipExtension(config, output);
|
||||
});
|
||||
@@ -28,6 +28,16 @@ cli
|
||||
.option('--mv2', 'target manifest v2')
|
||||
.action(commands.build);
|
||||
|
||||
// ZIP
|
||||
cli
|
||||
.command('zip [root]', 'build for production and zip output')
|
||||
.option('-c, --config <file>', 'use specified config file')
|
||||
.option('-m, --mode <mode>', 'set env mode')
|
||||
.option('-b, --browser <browser>', 'specify a browser')
|
||||
.option('--mv3', 'target manifest v3')
|
||||
.option('--mv2', 'target manifest v2')
|
||||
.action(commands.zip);
|
||||
|
||||
// PREPARE
|
||||
cli
|
||||
.command('prepare [root]', 'prepare')
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { BackgroundScriptDefintition } from '..';
|
||||
|
||||
export function defineBackgroundScript(
|
||||
main: () => void,
|
||||
): BackgroundScriptDefintition;
|
||||
export function defineBackgroundScript(
|
||||
export function defineBackground(main: () => void): BackgroundScriptDefintition;
|
||||
export function defineBackground(
|
||||
definition: BackgroundScriptDefintition,
|
||||
): BackgroundScriptDefintition;
|
||||
export function defineBackgroundScript(
|
||||
export function defineBackground(
|
||||
arg: (() => void) | BackgroundScriptDefintition,
|
||||
): BackgroundScriptDefintition {
|
||||
if (typeof arg === 'function') return { main: arg };
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
export * from './defineContentScript';
|
||||
export * from './defineBackgroundScript';
|
||||
export * from './defineBackground';
|
||||
export * from './mountContentScriptUi';
|
||||
|
||||
+6
-2
@@ -39,7 +39,7 @@ export async function buildInternal(
|
||||
|
||||
const entrypoints = await findEntrypoints(config);
|
||||
const groups = groupEntrypoints(entrypoints);
|
||||
const { output } = await rebuild(config, groups);
|
||||
const { output } = await rebuild(config, groups, undefined);
|
||||
|
||||
// Post-build
|
||||
config.logger.success(
|
||||
@@ -70,7 +70,11 @@ export async function rebuild(
|
||||
): Promise<{ output: BuildOutput; manifest: Manifest.WebExtensionManifest }> {
|
||||
// Update types directory with new files and types
|
||||
const allEntrypoints = await findEntrypoints(config);
|
||||
await generateTypesDir(allEntrypoints, config);
|
||||
await generateTypesDir(allEntrypoints, config).catch((err) => {
|
||||
config.logger.warn('Failed to update .wxt directory:', err);
|
||||
// Throw the error if doing a regular build, don't for dev mode.
|
||||
if (config.command === 'build') throw err;
|
||||
});
|
||||
|
||||
// Build and merge the outputs
|
||||
const newOutput = await buildEntrypoints(entrypointGroups, config);
|
||||
|
||||
@@ -29,6 +29,7 @@ describe('findEntrypoints', () => {
|
||||
root: '/',
|
||||
entrypointsDir: resolve('/src/entrypoints'),
|
||||
outDir: resolve('.output'),
|
||||
command: 'build',
|
||||
});
|
||||
|
||||
it.each<[string, string, PopupEntrypoint]>([
|
||||
@@ -137,6 +138,15 @@ describe('findEntrypoints', () => {
|
||||
);
|
||||
|
||||
it.each<[string, Omit<ContentScriptEntrypoint, 'options'>]>([
|
||||
[
|
||||
'content.ts',
|
||||
{
|
||||
type: 'content-script',
|
||||
name: 'content',
|
||||
inputPath: resolve(config.entrypointsDir, 'content.ts'),
|
||||
outputDir: resolve(config.outDir, 'content-scripts'),
|
||||
},
|
||||
],
|
||||
[
|
||||
'overlay.content.ts',
|
||||
{
|
||||
@@ -146,6 +156,15 @@ describe('findEntrypoints', () => {
|
||||
outputDir: resolve(config.outDir, 'content-scripts'),
|
||||
},
|
||||
],
|
||||
[
|
||||
'content/index.ts',
|
||||
{
|
||||
type: 'content-script',
|
||||
name: 'content',
|
||||
inputPath: resolve(config.entrypointsDir, 'content/index.ts'),
|
||||
outputDir: resolve(config.outDir, 'content-scripts'),
|
||||
},
|
||||
],
|
||||
[
|
||||
'overlay.content/index.ts',
|
||||
{
|
||||
@@ -192,10 +211,10 @@ describe('findEntrypoints', () => {
|
||||
},
|
||||
],
|
||||
])(
|
||||
'should find and load content script entrypoint config from %s',
|
||||
'should find and load background entrypoint config from %s',
|
||||
async (path, expected) => {
|
||||
const options: ContentScriptEntrypoint['options'] = {
|
||||
matches: ['<all_urls>'],
|
||||
const options: BackgroundEntrypoint['options'] = {
|
||||
type: 'module',
|
||||
};
|
||||
globMock.mockResolvedValueOnce([path]);
|
||||
importTsFileMock.mockResolvedValue(options);
|
||||
@@ -208,6 +227,24 @@ describe('findEntrypoints', () => {
|
||||
},
|
||||
);
|
||||
|
||||
it("should include a virtual background script so dev reloading works when there isn't a background entrypoint defined by the user", async () => {
|
||||
globMock.mockResolvedValueOnce([]);
|
||||
|
||||
const entrypoints = await findEntrypoints({
|
||||
...config,
|
||||
command: 'serve',
|
||||
});
|
||||
|
||||
expect(entrypoints).toHaveLength(1);
|
||||
expect(entrypoints[0]).toEqual({
|
||||
type: 'background',
|
||||
inputPath: 'virtual:user-background',
|
||||
name: 'background',
|
||||
options: {},
|
||||
outputDir: config.outDir,
|
||||
});
|
||||
});
|
||||
|
||||
it.each<[string, GenericEntrypoint]>([
|
||||
// Sandbox
|
||||
[
|
||||
|
||||
@@ -16,9 +16,10 @@ import JSON5 from 'json5';
|
||||
import { importTsFile } from '../utils/importTsFile';
|
||||
import glob from 'fast-glob';
|
||||
import { getEntrypointName } from '../utils/entrypoints';
|
||||
import { VIRTUAL_NOOP_BACKGROUND_MODULE_ID } from '../vite-plugins/noopBackground';
|
||||
|
||||
/**
|
||||
* Return entrypoints and their configuration by looking through the
|
||||
* Return entrypoints and their configuration by looking through the project's files.
|
||||
*/
|
||||
export async function findEntrypoints(
|
||||
config: InternalConfig,
|
||||
@@ -33,6 +34,7 @@ export async function findEntrypoints(
|
||||
const existingNames: Record<string, Entrypoint | undefined> = {};
|
||||
|
||||
const entrypoints: Entrypoint[] = [];
|
||||
let hasBackground = false;
|
||||
await Promise.all(
|
||||
relativePaths.map(async (relativePath) => {
|
||||
const path = resolve(config.entrypointsDir, relativePath);
|
||||
@@ -63,11 +65,12 @@ export async function findEntrypoints(
|
||||
break;
|
||||
case 'background':
|
||||
entrypoint = await getBackgroundEntrypoint(config, path);
|
||||
hasBackground = true;
|
||||
break;
|
||||
case 'content-script':
|
||||
entrypoint = await getContentScriptEntrypoint(
|
||||
config,
|
||||
relativePath.split('.', 2)[0],
|
||||
getEntrypointName(config.entrypointsDir, path),
|
||||
path,
|
||||
);
|
||||
break;
|
||||
@@ -95,6 +98,11 @@ export async function findEntrypoints(
|
||||
existingNames[entrypoint.name] = entrypoint;
|
||||
}),
|
||||
);
|
||||
if (config.command === 'serve' && !hasBackground) {
|
||||
entrypoints.push(
|
||||
await getBackgroundEntrypoint(config, VIRTUAL_NOOP_BACKGROUND_MODULE_ID),
|
||||
);
|
||||
}
|
||||
return entrypoints;
|
||||
}
|
||||
|
||||
@@ -162,21 +170,21 @@ async function getOptionsEntrypoint(
|
||||
.querySelector("meta[name='manifest.open_in_tab']")
|
||||
?.getAttribute('content');
|
||||
if (openInTabContent) {
|
||||
options.openInTab = Boolean(openInTabContent);
|
||||
options.openInTab = openInTabContent === 'true';
|
||||
}
|
||||
|
||||
const chromeStyleContent = document
|
||||
.querySelector("meta[name='manifest.chrome_style']")
|
||||
?.getAttribute('content');
|
||||
if (chromeStyleContent) {
|
||||
options.chromeStyle = Boolean(chromeStyleContent);
|
||||
options.chromeStyle = chromeStyleContent === 'true';
|
||||
}
|
||||
|
||||
const browserStyleContent = document
|
||||
.querySelector("meta[name='manifest.browser_style']")
|
||||
?.getAttribute('content');
|
||||
if (browserStyleContent) {
|
||||
options.browserStyle = Boolean(browserStyleContent);
|
||||
options.browserStyle = browserStyleContent === 'true';
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -195,10 +203,17 @@ async function getBackgroundEntrypoint(
|
||||
config: InternalConfig,
|
||||
path: string,
|
||||
): Promise<BackgroundEntrypoint> {
|
||||
const { main: _, ...options } =
|
||||
await importTsFile<BackgroundScriptDefintition>(path, config);
|
||||
if (options == null) {
|
||||
throw Error('Background script does not have a default export');
|
||||
let options: Omit<BackgroundScriptDefintition, 'main'> = {};
|
||||
if (path !== VIRTUAL_NOOP_BACKGROUND_MODULE_ID) {
|
||||
const defaultExport = await importTsFile<BackgroundScriptDefintition>(
|
||||
path,
|
||||
config,
|
||||
);
|
||||
if (defaultExport == null) {
|
||||
throw Error('Background script does not have a default export');
|
||||
}
|
||||
const { main: _, ...moduleOptions } = defaultExport;
|
||||
options = moduleOptions;
|
||||
}
|
||||
return {
|
||||
type: 'background',
|
||||
@@ -257,7 +272,10 @@ const PATH_GLOB_TO_TYPE_MAP: Record<string, Entrypoint['type'] | 'ignored'> = {
|
||||
'devtools/index.html': 'devtools',
|
||||
|
||||
'background.ts': 'background',
|
||||
[VIRTUAL_NOOP_BACKGROUND_MODULE_ID]: 'background',
|
||||
|
||||
'content.ts?(x)': 'content-script',
|
||||
'content/index.ts?(x)': 'content-script',
|
||||
'*.content.ts?(x)': 'content-script',
|
||||
'*.content/index.ts?(x)': 'content-script',
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import path, { relative, resolve } from 'path';
|
||||
import { resolve } from 'path';
|
||||
import { BuildOutput, InternalConfig } from '../types';
|
||||
import { printTable } from './printTable';
|
||||
import pc from 'picocolors';
|
||||
import fs from 'fs-extra';
|
||||
import { filesize } from 'filesize';
|
||||
import { printFileList } from './printFileList';
|
||||
|
||||
export async function printBuildSummary(
|
||||
output: BuildOutput,
|
||||
@@ -20,31 +17,8 @@ export async function printBuildSummary(
|
||||
return l.fileName.localeCompare(r.fileName);
|
||||
});
|
||||
|
||||
let totalSize = 0;
|
||||
|
||||
const chunkRows: string[][] = await Promise.all(
|
||||
chunks.map(async (chunk, i) => {
|
||||
const file = [
|
||||
relative(process.cwd(), config.outDir) + path.sep,
|
||||
chunk.fileName,
|
||||
];
|
||||
const prefix = i === chunks.length - 1 ? ' └─' : ' ├─';
|
||||
const color = getChunkColor(chunk.fileName);
|
||||
const stats = await fs.lstat(resolve(config.outDir, chunk.fileName));
|
||||
totalSize += stats.size;
|
||||
const size = String(filesize(stats.size));
|
||||
return [
|
||||
`${pc.gray(prefix)} ${pc.dim(file[0])}${color(file[1])}`,
|
||||
pc.dim(size),
|
||||
];
|
||||
}),
|
||||
);
|
||||
|
||||
printTable(config.logger.log, chunkRows);
|
||||
|
||||
config.logger.log(
|
||||
`${pc.cyan('Σ Total size:')} ${String(filesize(totalSize))}`,
|
||||
);
|
||||
const files = chunks.map((chunk) => resolve(config.outDir, chunk.fileName));
|
||||
await printFileList(config.logger.log, config.outDir, files);
|
||||
}
|
||||
|
||||
const DEFAULT_SORT_WEIGHT = 100;
|
||||
@@ -62,17 +36,3 @@ function getChunkSortWeight(filename: string) {
|
||||
)?.[1] ?? DEFAULT_SORT_WEIGHT
|
||||
);
|
||||
}
|
||||
|
||||
const DEFAULT_COLOR = pc.blue;
|
||||
const CHUNK_COLORS: Record<string, (text: string) => string> = {
|
||||
'.js.map': pc.gray,
|
||||
'.html': pc.green,
|
||||
'.css': pc.magenta,
|
||||
'.js': pc.cyan,
|
||||
};
|
||||
function getChunkColor(filename: string) {
|
||||
return (
|
||||
Object.entries(CHUNK_COLORS).find(([key]) => filename.endsWith(key))?.[1] ??
|
||||
DEFAULT_COLOR
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import path from 'node:path';
|
||||
import pc from 'picocolors';
|
||||
import fs from 'fs-extra';
|
||||
import { filesize } from 'filesize';
|
||||
import { printTable } from './printTable';
|
||||
|
||||
export async function printFileList(
|
||||
log: (message: string) => void,
|
||||
baseDir: string,
|
||||
files: string[],
|
||||
): Promise<void> {
|
||||
let totalSize = 0;
|
||||
|
||||
const fileRows: string[][] = await Promise.all(
|
||||
files.map(async (file, i) => {
|
||||
const parts = [
|
||||
path.relative(process.cwd(), baseDir) + path.sep,
|
||||
path.relative(baseDir, file),
|
||||
];
|
||||
const prefix = i === files.length - 1 ? ' └─' : ' ├─';
|
||||
const color = getChunkColor(file);
|
||||
const stats = await fs.lstat(file);
|
||||
totalSize += stats.size;
|
||||
const size = String(filesize(stats.size));
|
||||
return [
|
||||
`${pc.gray(prefix)} ${pc.dim(parts[0])}${color(parts[1])}`,
|
||||
pc.dim(size),
|
||||
];
|
||||
}),
|
||||
);
|
||||
|
||||
printTable(log, fileRows);
|
||||
|
||||
log(`${pc.cyan('Σ Total size:')} ${String(filesize(totalSize))}`);
|
||||
}
|
||||
|
||||
const DEFAULT_COLOR = pc.blue;
|
||||
const CHUNK_COLORS: Record<string, (text: string) => string> = {
|
||||
'.js.map': pc.gray,
|
||||
'.html': pc.green,
|
||||
'.css': pc.magenta,
|
||||
'.js': pc.cyan,
|
||||
'.zip': pc.yellow,
|
||||
};
|
||||
function getChunkColor(filename: string) {
|
||||
return (
|
||||
Object.entries(CHUNK_COLORS).find(([key]) => filename.endsWith(key))?.[1] ??
|
||||
DEFAULT_COLOR
|
||||
);
|
||||
}
|
||||
+7
-5
@@ -5,15 +5,15 @@ import {
|
||||
WxtDevServer,
|
||||
} from './types';
|
||||
import * as vite from 'vite';
|
||||
import { findOpenPort } from './utils/findOpenPort';
|
||||
import { Manifest } from 'webextension-polyfill';
|
||||
import { Scripting } from 'webextension-polyfill';
|
||||
import { getEntrypointBundlePath } from './utils/entrypoints';
|
||||
import { getContentScriptCssFiles } from './utils/manifest';
|
||||
import { createWebExtRunner } from './runners/createWebExtRunner';
|
||||
import { buildInternal } from './build';
|
||||
|
||||
export async function getServerInfo(): Promise<ServerInfo> {
|
||||
const port = await findOpenPort(3000, 3010);
|
||||
const { default: getPort, portNumbers } = await import('get-port');
|
||||
const port = await getPort({ port: portNumbers(3000, 3010) });
|
||||
const hostname = 'localhost';
|
||||
const origin = `http://${hostname}:${port}`;
|
||||
const serverConfig: vite.InlineConfig = {
|
||||
@@ -58,7 +58,9 @@ export async function setupServer(
|
||||
// paths ending in "/index.html"
|
||||
viteServer.ws.send('wxt:reload-page', path);
|
||||
};
|
||||
const reloadContentScript = (contentScript: Manifest.ContentScript) => {
|
||||
const reloadContentScript = (
|
||||
contentScript: Omit<Scripting.RegisteredContentScript, 'id'>,
|
||||
) => {
|
||||
viteServer.ws.send('wxt:reload-content-script', contentScript);
|
||||
};
|
||||
|
||||
@@ -102,9 +104,9 @@ export function reloadContentScripts(
|
||||
const css = getContentScriptCssFiles([entry], server.currentOutput);
|
||||
|
||||
server.reloadContentScript({
|
||||
...entry.options,
|
||||
js,
|
||||
css,
|
||||
...entry.options,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
|
||||
+186
-28
@@ -1,28 +1,148 @@
|
||||
import * as vite from 'vite';
|
||||
import { Manifest } from 'webextension-polyfill';
|
||||
import { Manifest, Scripting } from 'webextension-polyfill';
|
||||
import { UnimportOptions } from 'unimport';
|
||||
import { EntrypointGroup } from '.';
|
||||
|
||||
export interface InlineConfig {
|
||||
/**
|
||||
* Project root directory.
|
||||
*
|
||||
* @default
|
||||
* process.cwd()
|
||||
*/
|
||||
root?: string;
|
||||
/**
|
||||
* Directory containing all source code. Set to `"src"` to move all source code to a `src/`
|
||||
* directory.
|
||||
*
|
||||
* @default
|
||||
* "<root>"
|
||||
*/
|
||||
srcDir?: string;
|
||||
/**
|
||||
* Directory containing files that will be copied to the output directory as-is.
|
||||
*
|
||||
* @default
|
||||
* "<srcDir>/publicDir"
|
||||
*/
|
||||
publicDir?: string;
|
||||
/**
|
||||
* @default
|
||||
* "<srcDir>/entrypoints"
|
||||
*/
|
||||
entrypointsDir?: string;
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
storeIds?: {
|
||||
chrome?: string;
|
||||
firefox?: string;
|
||||
edge?: string;
|
||||
};
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
imports?: Partial<UnimportOptions>;
|
||||
/**
|
||||
* Explicitly set a browser to target. This will override the default browser for each command,
|
||||
* and can be overridden by the command line `--browser` option.
|
||||
*
|
||||
* @default
|
||||
* "chrome"
|
||||
*/
|
||||
browser?: TargetBrowser;
|
||||
/**
|
||||
* Explicitly set a manifest version to target. This will override the default manifest version
|
||||
* for each command, and can be overridden by the command line `--mv2` or `--mv3` option.
|
||||
*/
|
||||
manifestVersion?: TargetManifestVersion;
|
||||
/**
|
||||
* Override the logger used.
|
||||
*
|
||||
* @default
|
||||
* consola
|
||||
*/
|
||||
logger?: Logger;
|
||||
/**
|
||||
* Custom Vite options.
|
||||
*/
|
||||
vite?: Omit<vite.UserConfig, 'root' | 'configFile' | 'mode'>;
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
runner?: ExtensionRunnerConfig;
|
||||
zip?: {
|
||||
/**
|
||||
* Configure the filename output when zipping files.
|
||||
*
|
||||
* Available template variables:
|
||||
*
|
||||
* - `{{name}}` - The project's name converted to kebab-case
|
||||
* - `{{version}} - The version_name or version from the manifest
|
||||
* - `{{browser}} - The target browser from the `--browser` CLI flag
|
||||
* - `{{manifestVersion}}` - Either "2" or "3"
|
||||
*
|
||||
* @default "{{name}}-{{version}}-{{browser}}.zip"
|
||||
*/
|
||||
artifactTemplate?: string;
|
||||
/**
|
||||
* Configure the filename output when zipping files.
|
||||
*
|
||||
* Available template variables:
|
||||
*
|
||||
* - `{{name}}` - The project's name converted to kebab-case
|
||||
* - `{{version}} - The version_name or version from the manifest
|
||||
* - `{{browser}} - The target browser from the `--browser` CLI flag
|
||||
* - `{{manifestVersion}}` - Either "2" or "3"
|
||||
*
|
||||
* @default "{{name}}-{{version}}-sources.zip"
|
||||
*/
|
||||
sourcesTemplate?: string;
|
||||
/**
|
||||
* Override the artifactTemplate's `{{name}}` template variable. Defaults to the package.json's
|
||||
* name, or if that doesn't exist, the current working directories name.
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* Root directory to ZIP. The ZIP can be uploaded to the Firefox Addon Store as your source
|
||||
* code. Defaults to the `config.root` directory.
|
||||
*/
|
||||
sourcesRoot?: string;
|
||||
/**
|
||||
* [Minimatch](https://www.npmjs.com/package/minimatch) patterns of files to exclude when
|
||||
* creating a ZIP of all your source code for Firfox. Patterns are relative to your
|
||||
* `config.zip.sourcesRoot`.
|
||||
*
|
||||
* Hidden files, node_modules, and tests are ignored by default.
|
||||
*
|
||||
* @example
|
||||
* [
|
||||
* "coverage", // Ignore the coverage directory in the `sourcesRoot`
|
||||
* ]
|
||||
*/
|
||||
ignoredSources?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface WxtInlineViteConfig
|
||||
@@ -83,7 +203,9 @@ export interface WxtDevServer extends vite.ViteDevServer {
|
||||
*
|
||||
* @param contentScript The manifest definition for a content script
|
||||
*/
|
||||
reloadContentScript: (contentScript: Manifest.ContentScript) => void;
|
||||
reloadContentScript: (
|
||||
contentScript: Omit<Scripting.RegisteredContentScript, 'id'>,
|
||||
) => void;
|
||||
}
|
||||
|
||||
export type TargetBrowser = 'chrome' | 'firefox' | 'safari' | 'edge' | 'opera';
|
||||
@@ -186,11 +308,50 @@ export type Entrypoint =
|
||||
export type OnContentScriptStopped = (cb: () => void) => void;
|
||||
|
||||
export interface ContentScriptDefinition {
|
||||
matches: string[];
|
||||
runAt?: 'document_start' | 'document_end' | 'document_idle';
|
||||
matchAboutBlank?: boolean;
|
||||
matches: Manifest.ContentScript['matches'];
|
||||
/**
|
||||
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
|
||||
* @default "documentIdle"
|
||||
*/
|
||||
runAt?: Manifest.ContentScript['run_at'];
|
||||
/**
|
||||
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
|
||||
* @default false
|
||||
*/
|
||||
matchAboutBlank?: Manifest.ContentScript['match_about_blank'];
|
||||
/**
|
||||
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
|
||||
* @default []
|
||||
*/
|
||||
excludeMatches?: Manifest.ContentScript['exclude_matches'];
|
||||
/**
|
||||
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
|
||||
* @default []
|
||||
*/
|
||||
includeGlobs?: Manifest.ContentScript['include_globs'];
|
||||
/**
|
||||
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
|
||||
* @default []
|
||||
*/
|
||||
excludeGlobs?: Manifest.ContentScript['exclude_globs'];
|
||||
/**
|
||||
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
|
||||
* @default false
|
||||
*/
|
||||
allFrames?: Manifest.ContentScript['all_frames'];
|
||||
/**
|
||||
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
|
||||
* @default false
|
||||
*/
|
||||
matchOriginAsFallback?: boolean;
|
||||
/**
|
||||
* See https://developer.chrome.com/docs/extensions/mv3/content_scripts/
|
||||
* @default "ISOLATED"
|
||||
*/
|
||||
world?: 'ISOLATED' | 'MAIN';
|
||||
/**
|
||||
* Main function executed when the content script is loaded.
|
||||
*/
|
||||
main(): void | Promise<void>;
|
||||
}
|
||||
|
||||
@@ -200,30 +361,27 @@ export interface BackgroundScriptDefintition {
|
||||
}
|
||||
|
||||
/**
|
||||
* Manifest customization available in the `wxt.config.ts` file. Any missing fields like "name"
|
||||
* and "version" are managed automatically, and don't need to be listed here.
|
||||
* Manifest customization available in the `wxt.config.ts` file. You cannot configure entrypoints
|
||||
* here, they are configured inline.
|
||||
*/
|
||||
export type UserManifest = Omit<
|
||||
Manifest.WebExtensionManifest,
|
||||
| 'action'
|
||||
| 'background'
|
||||
| 'browser_action'
|
||||
| 'chrome_url_overrides'
|
||||
| 'content_scripts'
|
||||
| 'description'
|
||||
| 'devtools_page'
|
||||
| 'manifest_version'
|
||||
| 'name'
|
||||
| 'options_page'
|
||||
| 'options_ui'
|
||||
| 'sandbox'
|
||||
| 'page_action'
|
||||
| 'popup'
|
||||
| 'short_name'
|
||||
| 'sidepanel'
|
||||
| 'sidebar_action'
|
||||
| 'version'
|
||||
| 'version_name'
|
||||
export type UserManifest = Partial<
|
||||
Omit<
|
||||
Manifest.WebExtensionManifest,
|
||||
| 'action'
|
||||
| 'background'
|
||||
| 'browser_action'
|
||||
| 'chrome_url_overrides'
|
||||
| 'content_scripts'
|
||||
| 'devtools_page'
|
||||
| 'manifest_version'
|
||||
| 'options_page'
|
||||
| 'options_ui'
|
||||
| 'sandbox'
|
||||
| 'page_action'
|
||||
| 'popup'
|
||||
| 'sidepanel'
|
||||
| 'sidebar_action'
|
||||
>
|
||||
>;
|
||||
|
||||
export type UserManifestFn = (
|
||||
|
||||
@@ -36,6 +36,13 @@ export interface InternalConfig {
|
||||
fsCache: FsCache;
|
||||
server?: WxtDevServer;
|
||||
runnerConfig: ResolvedConfig<ExtensionRunnerConfig>;
|
||||
zip: {
|
||||
name?: string;
|
||||
artifactTemplate: string;
|
||||
sourcesTemplate: string;
|
||||
ignoredSources: string[];
|
||||
sourcesRoot: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type EntrypointGroup = Entrypoint | Entrypoint[];
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { hashContentScriptOptions } from '../content-scripts';
|
||||
|
||||
describe('Content Script Utils', () => {
|
||||
describe('hashContentScriptOptions', () => {
|
||||
it('should return a string containing all the options with defaults applied', () => {
|
||||
const hash = hashContentScriptOptions({ matches: [] });
|
||||
|
||||
expect(hash).toMatchInlineSnapshot(
|
||||
'"[[\\"allFrames\\",false],[\\"excludeGlobs\\",[]],[\\"excludeMatches\\",[]],[\\"includeGlobs\\",[]],[\\"matchAboutBlank\\",false],[\\"matches\\",[]],[\\"matchOriginAsFallback\\",false],[\\"runAt\\",\\"document_idle\\"],[\\"world\\",\\"ISOLATED\\"]]"',
|
||||
);
|
||||
});
|
||||
|
||||
it('should be consistent regardless of the object ordering and default values', () => {
|
||||
const hash1 = hashContentScriptOptions({
|
||||
allFrames: true,
|
||||
matches: ['*://google.com/*', '*://duckduckgo.com/*'],
|
||||
matchAboutBlank: false,
|
||||
});
|
||||
const hash2 = hashContentScriptOptions({
|
||||
matches: ['*://duckduckgo.com/*', '*://google.com/*'],
|
||||
allFrames: true,
|
||||
});
|
||||
|
||||
expect(hash1).toBe(hash2);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { kebabCaseAlphanumeric, removeImportStatements } from '../strings';
|
||||
|
||||
describe('String utils', () => {
|
||||
describe('kebabCaseAlphanumeric', () => {
|
||||
it.each([
|
||||
['HELLO', 'hello'],
|
||||
['Hello, World!', 'hello-world'],
|
||||
['hello123', 'hello123'],
|
||||
['Hello World This Is A Test', 'hello-world-this-is-a-test'],
|
||||
['Hello World', 'hello-world'],
|
||||
['hello-world', 'hello-world'], // Ensure hyphens are preserved
|
||||
])('should convert "%s" to "%s"', (input, expected) => {
|
||||
expect(kebabCaseAlphanumeric(input)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeImportStatements', () => {
|
||||
it('should remove all import formats', () => {
|
||||
const imports = `
|
||||
import { registerGithubService, createGithubApi } from "@/utils/github";
|
||||
import {
|
||||
registerGithubService,
|
||||
createGithubApi
|
||||
} from "@/utils/github";
|
||||
import{ registerGithubService, createGithubApi }from "@/utils/github";
|
||||
import GitHub from "@/utils/github";
|
||||
import "@/utils/github";
|
||||
import '@/utils/github';
|
||||
import"@/utils/github"
|
||||
import'@/utils/github';
|
||||
`;
|
||||
expect(removeImportStatements(imports).trim()).toEqual('');
|
||||
});
|
||||
|
||||
it('should not remove import.meta or inline import statements', () => {
|
||||
const imports = `
|
||||
import.meta.env.DEV
|
||||
const a = await import("example");
|
||||
import("example");
|
||||
`;
|
||||
expect(removeImportStatements(imports)).toEqual(imports);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -13,7 +13,7 @@ export function getUnimportOptions(
|
||||
],
|
||||
presets: [{ package: 'wxt/client' }],
|
||||
warn: config.logger.warn,
|
||||
dirs: ['components', 'composables', 'hooks', 'utils'],
|
||||
dirs: ['./components/*', './composables/*', './hooks/*', './utils/*'],
|
||||
};
|
||||
|
||||
return mergeConfig(
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Manifest } from 'webextension-polyfill';
|
||||
import { ContentScriptEntrypoint } from '../types';
|
||||
|
||||
/**
|
||||
* Returns a unique and consistent string hash based on a content scripts options.
|
||||
*
|
||||
* It is able to recognize default values,
|
||||
*/
|
||||
export function hashContentScriptOptions(
|
||||
options: ContentScriptEntrypoint['options'],
|
||||
): string {
|
||||
const withDefaults: ContentScriptEntrypoint['options'] = {
|
||||
excludeGlobs: [],
|
||||
excludeMatches: [],
|
||||
includeGlobs: [],
|
||||
matchAboutBlank: false,
|
||||
matchOriginAsFallback: false,
|
||||
runAt: 'document_idle',
|
||||
allFrames: false,
|
||||
world: 'ISOLATED',
|
||||
// TODO: strip undefined fields from options object to improve content script grouping.
|
||||
...options,
|
||||
};
|
||||
return JSON.stringify(
|
||||
Object.entries(withDefaults)
|
||||
// Sort any arrays so their values are consistent
|
||||
.map<[string, unknown]>(([key, value]) => {
|
||||
if (Array.isArray(value)) return [key, value.sort()];
|
||||
else return [key, value];
|
||||
})
|
||||
// Sort all the fields alphabetically
|
||||
.sort((l, r) => l[0].localeCompare(r[0])),
|
||||
);
|
||||
}
|
||||
|
||||
export function mapWxtOptionsToContentScript(
|
||||
options: ContentScriptEntrypoint['options'],
|
||||
): Omit<Manifest.ContentScript, 'js' | 'css'> {
|
||||
return {
|
||||
matches: options.matches,
|
||||
all_frames: options.allFrames,
|
||||
match_about_blank: options.matchAboutBlank,
|
||||
exclude_globs: options.excludeGlobs,
|
||||
exclude_matches: options.excludeMatches,
|
||||
include_globs: options.includeGlobs,
|
||||
run_at: options.runAt,
|
||||
|
||||
// @ts-expect-error: untyped chrome options
|
||||
match_origin_as_fallback: options.matchOriginAsFallback,
|
||||
world: options.world,
|
||||
};
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import net from 'node:net';
|
||||
|
||||
/**
|
||||
* Finds the first open port in a range of ports.
|
||||
*/
|
||||
export function findOpenPort(
|
||||
startPort: number,
|
||||
endPort: number,
|
||||
): Promise<number> {
|
||||
return findOpenPortRecursive(startPort, startPort, endPort);
|
||||
}
|
||||
|
||||
function findOpenPortRecursive(
|
||||
port: number,
|
||||
startPort: number,
|
||||
endPort: number,
|
||||
): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (port > endPort)
|
||||
return reject(
|
||||
Error(`Could not find open port between ${startPort}-${endPort}`),
|
||||
);
|
||||
const server = net.createServer();
|
||||
|
||||
server.listen(port, () => {
|
||||
server.once('close', () => resolve(port));
|
||||
server.close();
|
||||
});
|
||||
server.on('error', () =>
|
||||
resolve(findOpenPortRecursive(port + 1, startPort, endPort)),
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -99,6 +99,26 @@ export async function getInternalConfig(
|
||||
typesDir,
|
||||
fsCache: createFsCache(wxtDir),
|
||||
manifest,
|
||||
zip: {
|
||||
sourcesTemplate: '{{name}}-{{version}}-sources.zip',
|
||||
artifactTemplate: '{{name}}-{{version}}-{{browser}}.zip',
|
||||
sourcesRoot: root,
|
||||
...userConfig.zip,
|
||||
...config.zip,
|
||||
ignoredSources: [
|
||||
'**/node_modules',
|
||||
// WXT files
|
||||
'**/web-ext.config.ts',
|
||||
// Hidden files
|
||||
'**/.*',
|
||||
// Tests
|
||||
'**/__tests__/**',
|
||||
'**/*.+(test|spec).?(c|m)+(j|t)s?(x)',
|
||||
// User config
|
||||
...(userConfig.zip?.ignoredSources ?? []),
|
||||
...(config.zip?.ignoredSources ?? []),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
// Customize the default vite config
|
||||
@@ -122,7 +142,7 @@ export async function getInternalConfig(
|
||||
);
|
||||
finalConfig.vite.plugins.push(plugins.devServerGlobals(finalConfig));
|
||||
finalConfig.vite.plugins.push(plugins.tsconfigPaths(finalConfig));
|
||||
finalConfig.vite.plugins.push(plugins.hmrLogger(finalConfig));
|
||||
finalConfig.vite.plugins.push(plugins.noopBackground());
|
||||
|
||||
finalConfig.vite.define ??= {};
|
||||
getGlobals(finalConfig).forEach((global) => {
|
||||
@@ -145,6 +165,7 @@ type InternalConfigNoUserDirs = Omit<
|
||||
| 'typesDir'
|
||||
| 'fsCache'
|
||||
| 'manifest'
|
||||
| 'zip'
|
||||
>;
|
||||
|
||||
async function resolveManifestConfig(
|
||||
|
||||
@@ -5,6 +5,7 @@ import fs from 'fs-extra';
|
||||
import { resolve } from 'path';
|
||||
import transform from 'jiti/dist/babel';
|
||||
import { getUnimportOptions } from './auto-imports';
|
||||
import { removeImportStatements } from './strings';
|
||||
|
||||
/**
|
||||
* Get the value from the default export of a `path`.
|
||||
@@ -36,7 +37,7 @@ export async function importTsFile<T>(
|
||||
await unimport.init();
|
||||
|
||||
const text = await fs.readFile(path, 'utf-8');
|
||||
const textNoImports = text.replace(/import.*[\n;]/gm, '');
|
||||
const textNoImports = removeImportStatements(text);
|
||||
const { code } = await unimport.injectImports(textNoImports);
|
||||
config.logger.debug(
|
||||
['Text:', text, 'No imports:', textNoImports, 'Code:', code].join('\n'),
|
||||
|
||||
+34
-25
@@ -12,6 +12,11 @@ import fs from 'fs-extra';
|
||||
import { resolve } from 'path';
|
||||
import { getEntrypointBundlePath } from './entrypoints';
|
||||
import { ContentSecurityPolicy } from './ContentSecurityPolicy';
|
||||
import {
|
||||
hashContentScriptOptions,
|
||||
mapWxtOptionsToContentScript,
|
||||
} from './content-scripts';
|
||||
import { getPackageJson } from './package';
|
||||
|
||||
/**
|
||||
* Writes the manifest to the output directory and the build output.
|
||||
@@ -47,36 +52,41 @@ export async function generateMainfest(
|
||||
config: InternalConfig,
|
||||
): Promise<Manifest.WebExtensionManifest> {
|
||||
const pkg = await getPackageJson(config);
|
||||
if (pkg.version == null)
|
||||
throw Error('package.json does not include a version');
|
||||
if (pkg.name == null) throw Error('package.json does not include a name');
|
||||
if (pkg.description == null)
|
||||
throw Error('package.json does not include a description');
|
||||
|
||||
const manifest: Manifest.WebExtensionManifest = {
|
||||
manifest_version: config.manifestVersion,
|
||||
name: pkg.name,
|
||||
short_name: pkg.shortName,
|
||||
version: simplifyVersion(pkg.version),
|
||||
version_name: config.browser === 'firefox' ? undefined : pkg.version,
|
||||
...config.manifest,
|
||||
};
|
||||
const manifest: Manifest.WebExtensionManifest = Object.assign(
|
||||
{
|
||||
manifest_version: config.manifestVersion,
|
||||
name: pkg?.name,
|
||||
description: pkg?.description,
|
||||
version: pkg?.version && simplifyVersion(pkg.version),
|
||||
// Only add the version name to chromium and if the user hasn't specified a custom version.
|
||||
version_name:
|
||||
config.browser !== 'firefox' && !config.manifest.version
|
||||
? pkg?.version
|
||||
: undefined,
|
||||
short_name: pkg?.shortName,
|
||||
},
|
||||
config.manifest,
|
||||
);
|
||||
|
||||
addEntrypoints(manifest, entrypoints, buildOutput, config);
|
||||
|
||||
if (config.command === 'serve') addDevModeCsp(manifest, config);
|
||||
if (config.command === 'serve') addDevModePermissions(manifest, config);
|
||||
|
||||
return manifest;
|
||||
}
|
||||
// TODO: transform manifest here.
|
||||
|
||||
/**
|
||||
* Read the package.json from the current directory.
|
||||
*
|
||||
* TODO: look in root and up directories until it's found
|
||||
*/
|
||||
async function getPackageJson(config: InternalConfig): Promise<any> {
|
||||
return await fs.readJson(resolve(config.root, 'package.json'));
|
||||
if (manifest.name == null)
|
||||
throw Error(
|
||||
"Manifest 'name' is missing. Either:\n1. Set the name in your <root>/package.json\n2. Set a name via the manifest option in your wxt.config.ts",
|
||||
);
|
||||
if (manifest.version == null) {
|
||||
throw Error(
|
||||
"Manifest 'version' is missing. Either:\n1. Add a version in your <root>/package.json\n2. Pass the version via the manifest option in your wxt.config.ts",
|
||||
);
|
||||
}
|
||||
|
||||
return manifest;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -85,7 +95,6 @@ async function getPackageJson(config: InternalConfig): Promise<any> {
|
||||
*/
|
||||
function simplifyVersion(versionName: string): string {
|
||||
// Regex adapted from here: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/version#version_format
|
||||
|
||||
const version = /^((0|[1-9][0-9]{0,8})([.](0|[1-9][0-9]{0,8})){0,3}).*$/.exec(
|
||||
versionName,
|
||||
)?.[1];
|
||||
@@ -285,7 +294,7 @@ function addEntrypoints(
|
||||
);
|
||||
} else {
|
||||
const hashToEntrypointsMap = contentScripts.reduce((map, script) => {
|
||||
const hash = JSON.stringify(script.options);
|
||||
const hash = hashContentScriptOptions(script.options);
|
||||
if (map.has(hash)) map.get(hash)?.push(script);
|
||||
else map.set(hash, [script]);
|
||||
return map;
|
||||
@@ -293,7 +302,7 @@ function addEntrypoints(
|
||||
|
||||
manifest.content_scripts = Array.from(hashToEntrypointsMap.entries()).map(
|
||||
([, scripts]) => ({
|
||||
...scripts[0].options,
|
||||
...mapWxtOptionsToContentScript(scripts[0].options),
|
||||
// TOOD: Sorting css and js arrays here so we get consistent test results... but we
|
||||
// shouldn't have to. Where is the inconsistency coming from?
|
||||
css: getContentScriptCssFiles(scripts, buildOutput)?.sort(),
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { resolve } from 'node:path';
|
||||
import { InternalConfig } from '../types';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
/**
|
||||
* Read the project's package.json.
|
||||
*
|
||||
* TODO: look in root and up directories until it's found
|
||||
*/
|
||||
export async function getPackageJson(
|
||||
config: InternalConfig,
|
||||
): Promise<Partial<Record<string, any>> | undefined> {
|
||||
const file = resolve(config.root, 'package.json');
|
||||
try {
|
||||
return await fs.readJson(file);
|
||||
} catch (err) {
|
||||
config.logger.debug(
|
||||
`Failed to read package.json at: ${file}. Returning undefined.`,
|
||||
);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
export function kebabCaseAlphanumeric(str: string): string {
|
||||
return str
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9-\s]/g, '') // Remove all non-alphanumeric, non-hyphen characters
|
||||
.replace(/\s+/g, '-'); // Replace spaces with hyphens
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes import statements from the top of a file. Keeps import.meta and inline, async `import()`
|
||||
* calls.
|
||||
*/
|
||||
export function removeImportStatements(text: string): string {
|
||||
return text.replace(
|
||||
/(import\s?[{\w][\s\S]*?from\s?["'][\s\S]*?["'];?|import\s?["'][\s\S]*?["'];?)/gm,
|
||||
'',
|
||||
);
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { Plugin } from 'vite';
|
||||
import { InternalConfig } from '../types';
|
||||
import { relative } from 'path';
|
||||
import pc from 'picocolors';
|
||||
|
||||
/**
|
||||
* Log when HMR changes are fired
|
||||
*/
|
||||
export function hmrLogger(config: InternalConfig): Plugin {
|
||||
return {
|
||||
name: 'wxt:hmr-logger',
|
||||
apply: 'serve',
|
||||
handleHotUpdate(ctx) {
|
||||
if (
|
||||
ctx.file.startsWith(config.srcDir) &&
|
||||
!ctx.file.startsWith(config.wxtDir) &&
|
||||
!ctx.file.endsWith('.html')
|
||||
) {
|
||||
config.logger.info(
|
||||
'Hot reload: ' + pc.dim(relative(process.cwd(), ctx.file)),
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -5,4 +5,4 @@ export * from './multipageMove';
|
||||
export * from './unimport';
|
||||
export * from './virtualEntrypoint';
|
||||
export * from './tsconfigPaths';
|
||||
export * from './hmrLogger';
|
||||
export * from './noopBackground';
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Plugin } from 'vite';
|
||||
|
||||
/**
|
||||
* In dev mode, if there's not a background script listed, we need to add one.
|
||||
*
|
||||
* This define's a virtual module that is basically just a noop.
|
||||
*/
|
||||
export function noopBackground(): Plugin {
|
||||
const virtualModuleId = VIRTUAL_NOOP_BACKGROUND_MODULE_ID;
|
||||
const resolvedVirtualModuleId = '\0' + virtualModuleId;
|
||||
return {
|
||||
name: 'wxt:noop-background',
|
||||
resolveId(id) {
|
||||
if (id === virtualModuleId) return resolvedVirtualModuleId;
|
||||
},
|
||||
load(id) {
|
||||
if (id === resolvedVirtualModuleId) {
|
||||
return `import { defineBackground } from 'wxt/client';\nexport default defineBackground(() => void 0)`;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const VIRTUAL_NOOP_BACKGROUND_MODULE_ID = 'virtual:user-background';
|
||||
@@ -0,0 +1,75 @@
|
||||
import { BuildOutput, InternalConfig } from './types';
|
||||
import zipdir from 'zip-dir';
|
||||
import { dirname, relative, resolve } from 'node:path';
|
||||
import fs from 'fs-extra';
|
||||
import { kebabCaseAlphanumeric } from './utils/strings';
|
||||
import { getPackageJson } from './utils/package';
|
||||
import { minimatch } from 'minimatch';
|
||||
import { formatDuration } from './utils/formatDuration';
|
||||
import { printFileList } from './log/printFileList';
|
||||
|
||||
/**
|
||||
* Zip the extension for distribution. Does not build, just zips the output that should exist for
|
||||
* the given config.
|
||||
*
|
||||
* Returns a list of ZIP files that were created.
|
||||
*/
|
||||
export async function zipExtension(
|
||||
config: InternalConfig,
|
||||
buildOutput: BuildOutput,
|
||||
): Promise<string[]> {
|
||||
const start = Date.now();
|
||||
config.logger.info('Zipping extension...');
|
||||
const zipFiles: string[] = [];
|
||||
|
||||
const projectName =
|
||||
config.zip.name ??
|
||||
kebabCaseAlphanumeric(
|
||||
(await getPackageJson(config))?.name || dirname(process.cwd()),
|
||||
);
|
||||
const applyTemplate = (template: string): string =>
|
||||
template
|
||||
.replaceAll('{{name}}', projectName)
|
||||
.replaceAll('{{browser}}', config.browser)
|
||||
.replaceAll(
|
||||
'{{version}}',
|
||||
buildOutput.manifest.version_name ?? buildOutput.manifest.version,
|
||||
)
|
||||
.replaceAll('{{manifestVersion}}', `mv${config.manifestVersion}`);
|
||||
|
||||
await fs.ensureDir(config.outBaseDir);
|
||||
|
||||
// ZIP output directory
|
||||
|
||||
const outZipFilename = applyTemplate(config.zip.artifactTemplate);
|
||||
const outZipPath = resolve(config.outBaseDir, outZipFilename);
|
||||
await zipdir(config.outDir, {
|
||||
saveTo: outZipPath,
|
||||
});
|
||||
zipFiles.push(outZipPath);
|
||||
|
||||
// ZIP sources for Firefox
|
||||
|
||||
if (config.browser === 'firefox') {
|
||||
const sourcesZipFilename = applyTemplate(config.zip.sourcesTemplate);
|
||||
const sourcesZipPath = resolve(config.outBaseDir, sourcesZipFilename);
|
||||
await zipdir(config.zip.sourcesRoot, {
|
||||
saveTo: sourcesZipPath,
|
||||
filter(path) {
|
||||
const relativePath = relative(config.zip.sourcesRoot, path);
|
||||
const matchedPattern = config.zip.ignoredSources.find((pattern) =>
|
||||
minimatch(relativePath, pattern),
|
||||
);
|
||||
return matchedPattern == null;
|
||||
},
|
||||
});
|
||||
zipFiles.push(sourcesZipPath);
|
||||
}
|
||||
|
||||
config.logger.success(
|
||||
`Zipped extension in ${formatDuration(Date.now() - start)}`,
|
||||
);
|
||||
await printFileList(config.logger.log, config.outBaseDir, zipFiles);
|
||||
|
||||
return zipFiles;
|
||||
}
|
||||
+2
-1
@@ -59,8 +59,9 @@ export async function createServer(
|
||||
|
||||
await fileChangedMutex.runExclusive(async () => {
|
||||
const fileChanges = changeQueue.splice(0, changeQueue.length);
|
||||
const changes = detectDevChanges(fileChanges, server.currentOutput);
|
||||
if (fileChanges.length === 0) return;
|
||||
|
||||
const changes = detectDevChanges(fileChanges, server.currentOutput);
|
||||
if (changes.type === 'no-change') return;
|
||||
|
||||
// Log the entrypoints that were effected
|
||||
|
||||
@@ -218,4 +218,11 @@ export const fakeInternalConfig = fakeObjectCreator<InternalConfig>(() => ({
|
||||
vite: {},
|
||||
wxtDir: fakeDir(),
|
||||
server: mock<WxtDevServer>(),
|
||||
zip: {
|
||||
artifactTemplate: '{{name}}-{{version}}.zip',
|
||||
ignoredSources: [],
|
||||
sourcesRoot: fakeDir(),
|
||||
sourcesTemplate: '{{name}}-sources.zip',
|
||||
name: faker.person.firstName().toLowerCase(),
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export default defineBackgroundScript(() => {
|
||||
export default defineBackground(() => {
|
||||
console.log('Hello background!', { id: browser.runtime.id });
|
||||
});
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"build": "wxt build",
|
||||
"build:firefox": "wxt build -b firefox",
|
||||
"compile": "tsc --noEmit",
|
||||
"prepare": "wxt prepare"
|
||||
"postinstall": "wxt prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
@@ -21,6 +21,6 @@
|
||||
"@types/react-dom": "^18.2.6",
|
||||
"@vitejs/plugin-react": "^4.0.3",
|
||||
"typescript": "^5.1.6",
|
||||
"wxt": "^0.1.3"
|
||||
"wxt": "^0.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"build": "wxt build",
|
||||
"build:firefox": "wxt build -b firefox",
|
||||
"check": "svelte-check --tsconfig ./tsconfig.json",
|
||||
"prepare": "wxt prepare"
|
||||
"postinstall": "wxt prepare"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^2.4.2",
|
||||
@@ -18,6 +18,6 @@
|
||||
"svelte-check": "^3.4.6",
|
||||
"tslib": "^2.6.0",
|
||||
"typescript": "^5.1.6",
|
||||
"wxt": "^0.1.5"
|
||||
"wxt": "^0.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export default defineBackgroundScript(() => {
|
||||
export default defineBackground(() => {
|
||||
console.log('Hello background!', { id: browser.runtime.id });
|
||||
});
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export default defineBackgroundScript(() => {
|
||||
export default defineBackground(() => {
|
||||
console.log('Hello background!', { id: browser.runtime.id });
|
||||
});
|
||||
|
||||
@@ -10,10 +10,10 @@
|
||||
"build": "wxt build",
|
||||
"build:firefox": "wxt build -b firefox",
|
||||
"compile": "tsc --noEmit",
|
||||
"prepare": "wxt prepare"
|
||||
"postinstall": "wxt prepare"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.1.6",
|
||||
"wxt": "^0.1.3"
|
||||
"wxt": "^0.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export default defineBackgroundScript(() => {
|
||||
export default defineBackground(() => {
|
||||
console.log('Hello background!', { id: browser.runtime.id });
|
||||
});
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"build": "wxt build",
|
||||
"build:firefox": "wxt build -b firefox",
|
||||
"compile": "vue-tsc --noEmit",
|
||||
"prepare": "wxt prepare"
|
||||
"postinstall": "wxt prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.3.4"
|
||||
@@ -19,6 +19,6 @@
|
||||
"@vitejs/plugin-vue": "^4.2.3",
|
||||
"typescript": "^5.1.6",
|
||||
"vue-tsc": "^1.8.4",
|
||||
"wxt": "^0.1.3"
|
||||
"wxt": "^0.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user