Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6f5fa9f51c | |||
| 977246f9ed | |||
| 602dd42e5c | |||
| ea92a2743b | |||
| 2daa4d6900 | |||
| b5a9d8082d | |||
| 371be99770 | |||
| 09928e8a15 | |||
| 0a26673172 | |||
| 68611ae944 | |||
| 49965e7786 | |||
| fa2b6566d8 | |||
| 2d1c41eff1 | |||
| 10fba3de49 | |||
| aa16014650 | |||
| 0ce7c9d183 | |||
| ac0b28e608 | |||
| 80fed4a451 | |||
| 05ca9985d5 | |||
| 145038c6a2 | |||
| 6e7184d98d | |||
| ee49837804 | |||
| 67f972eab8 | |||
| b29c3c6fdd | |||
| ef6001e0c0 | |||
| 636aa48e95 | |||
| ec66b6172c | |||
| 1edd94e8f4 | |||
| 2efc193b45 | |||
| 2e51e7349d | |||
| 28733df5f9 | |||
| 0110a2bd4e | |||
| afe838a348 | |||
| 0255028ce2 | |||
| 249aa5eccc | |||
| b7c078f4e4 | |||
| f6b6b8857d | |||
| 54eb118f8c | |||
| 75eb3de89e | |||
| e14f81d97a | |||
| cd714ea196 | |||
| 684197dbe3 | |||
| c51f0e055f | |||
| 7f3a1e8d91 | |||
| 37986bf230 | |||
| 13a713cc28 | |||
| a78aada96e | |||
| 3307fc42a0 | |||
| 8be53efd24 | |||
| f02abc5227 | |||
| 2dd7a99937 | |||
| a9af4d3b67 |
|
Before Width: | Height: | Size: 280 KiB |
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: Setup PNPM
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 7
|
||||
version: 8
|
||||
|
||||
- name: Setup NodeJS
|
||||
uses: actions/setup-node@v3
|
||||
@@ -47,4 +47,4 @@ jobs:
|
||||
pnpm publish
|
||||
|
||||
- name: GitHub Release
|
||||
run: pnpx changelogen@latest gh release
|
||||
run: pnpx changelogen@latest gh release --token ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
- name: Setup PNPM
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 7
|
||||
version: 8
|
||||
|
||||
- name: Setup NodeJS
|
||||
uses: actions/setup-node@v3
|
||||
|
||||
@@ -15,3 +15,6 @@ node_modules
|
||||
TODOs.md
|
||||
web-ext.config.js
|
||||
web-ext.config.ts
|
||||
templates/*/pnpm-lock.yaml
|
||||
templates/*/yarn.lock
|
||||
templates/*/package-lock.json
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
.output
|
||||
coverage
|
||||
dist
|
||||
e2e/project
|
||||
.wxt
|
||||
docs/.vitepress/cache
|
||||
|
||||
@@ -1,5 +1,179 @@
|
||||
# Changelog
|
||||
|
||||
## v0.1.6
|
||||
|
||||
[compare changes](https://github.com/aklinker1/wxt/compare/v0.1.5...v0.1.6)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Resolve tsconfig paths in vite ([ea92a27](https://github.com/aklinker1/wxt/commit/ea92a27))
|
||||
- Add logs when a hot reload happens ([977246f](https://github.com/aklinker1/wxt/commit/977246f))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- React and Vue starter templates ([#33](https://github.com/aklinker1/wxt/pull/33))
|
||||
- Svelte template ([#34](https://github.com/aklinker1/wxt/pull/34))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker
|
||||
- Aaron
|
||||
|
||||
## v0.1.5
|
||||
|
||||
[compare changes](https://github.com/aklinker1/wxt/compare/v0.1.4...v0.1.5)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Include `vite/client` types ([371be99](https://github.com/aklinker1/wxt/commit/371be99))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker
|
||||
|
||||
## v0.1.4
|
||||
|
||||
[compare changes](https://github.com/aklinker1/wxt/compare/v0.1.3...v0.1.4)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Fix regression where manifest was not listed first in build summary ([fa2b656](https://github.com/aklinker1/wxt/commit/fa2b656))
|
||||
- Fix config hook implementations for vite plugins ([49965e7](https://github.com/aklinker1/wxt/commit/49965e7))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Update CLI screenshot ([0a26673](https://github.com/aklinker1/wxt/commit/0a26673))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Update prettier ignore ([68611ae](https://github.com/aklinker1/wxt/commit/68611ae))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker
|
||||
|
||||
## v0.1.3
|
||||
|
||||
[compare changes](https://github.com/aklinker1/wxt/compare/v0.1.2...v0.1.3)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Add tsconfig path aliases ([#32](https://github.com/aklinker1/wxt/pull/32))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Merge `manifest` option from both inline and user config ([05ca998](https://github.com/aklinker1/wxt/commit/05ca998))
|
||||
- Cleanup build summary with sourcemaps ([ac0b28e](https://github.com/aklinker1/wxt/commit/ac0b28e))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Create documentation site ([#31](https://github.com/aklinker1/wxt/pull/31))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Upgrade to pnpm 8 ([0ce7c9d](https://github.com/aklinker1/wxt/commit/0ce7c9d))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron
|
||||
- Aaron Klinker
|
||||
|
||||
## v0.1.2
|
||||
|
||||
[compare changes](https://github.com/aklinker1/wxt/compare/v0.1.1...v0.1.2)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Accept a function for `config.manifest` ([ee49837](https://github.com/aklinker1/wxt/commit/ee49837))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Add missing types for `webextension-polyfill` and the `manifest` option ([636aa48](https://github.com/aklinker1/wxt/commit/636aa48))
|
||||
- Only add imports to JS files ([b29c3c6](https://github.com/aklinker1/wxt/commit/b29c3c6))
|
||||
- Generate valid type for `EntrypointPath` when there are no entrypoints ([6e7184d](https://github.com/aklinker1/wxt/commit/6e7184d))
|
||||
|
||||
### 🌊 Types
|
||||
|
||||
- Change `config.vite` to `UserConfig` ([ef6001e](https://github.com/aklinker1/wxt/commit/ef6001e))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker
|
||||
|
||||
## v0.1.1
|
||||
|
||||
[compare changes](https://github.com/aklinker1/wxt/compare/v0.1.0...v0.1.1)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Allow dashes in entrypoint names ([2e51e73](https://github.com/aklinker1/wxt/commit/2e51e73))
|
||||
- Unable to read entrypoint options ([#28](https://github.com/aklinker1/wxt/pull/28))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker
|
||||
|
||||
## v0.1.0
|
||||
|
||||
Initial release of WXT. Full support for production builds and initial toolkit for development:
|
||||
|
||||
- HMR support when HTML page dependencies change
|
||||
- Reload extension when background changes
|
||||
- Reload HTML pages when saving them directly
|
||||
- Re-register and reload tabs when content scripts change
|
||||
|
||||
[compare changes](https://github.com/aklinker1/wxt/compare/v0.0.2...v0.1.0)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Content scripts reloading ([#25](https://github.com/aklinker1/wxt/pull/25))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Update feature list ([0255028](https://github.com/aklinker1/wxt/commit/0255028))
|
||||
|
||||
### 🤖 CI
|
||||
|
||||
- Create github release ([b7c078f](https://github.com/aklinker1/wxt/commit/b7c078f))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker
|
||||
|
||||
## v0.0.2
|
||||
|
||||
[compare changes](https://github.com/aklinker1/wxt/compare/v0.0.1...v0.0.2)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Reload extension when source code is changed ([#17](https://github.com/aklinker1/wxt/pull/17))
|
||||
- Setup background script web socket/reload ([#22](https://github.com/aklinker1/wxt/pull/22))
|
||||
- Reload HTML files individually ([#23](https://github.com/aklinker1/wxt/pull/23))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Output chunks to a chunks directory ([2dd7a99](https://github.com/aklinker1/wxt/commit/2dd7a99))
|
||||
- Remove hash from content script css outputs ([#20](https://github.com/aklinker1/wxt/pull/20))
|
||||
- Overwrite files with the same name when renaming entrypoints in dev mode ([37986bf](https://github.com/aklinker1/wxt/commit/37986bf))
|
||||
- Separate template builds to prevent sharing chunks ([7f3a1e8](https://github.com/aklinker1/wxt/commit/7f3a1e8))
|
||||
- Show Vite warnings and errors ([c51f0e0](https://github.com/aklinker1/wxt/commit/c51f0e0))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Add milestone progress badge to README ([684197d](https://github.com/aklinker1/wxt/commit/684197d))
|
||||
- Fix milestone link in README ([e14f81d](https://github.com/aklinker1/wxt/commit/e14f81d))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Refactor build output type ([#19](https://github.com/aklinker1/wxt/pull/19))
|
||||
- Refactor build outputs to support transpiled templates ([a78aada](https://github.com/aklinker1/wxt/commit/a78aada))
|
||||
- Rename `templates` to `virtual-modules` ([#24](https://github.com/aklinker1/wxt/pull/24))
|
||||
- Update cli screenshot ([54eb118](https://github.com/aklinker1/wxt/commit/54eb118))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker
|
||||
|
||||
## v0.0.1
|
||||
|
||||
[compare changes](https://github.com/aklinker1/wxt/compare/v0.0.0...v0.0.1)
|
||||
@@ -73,4 +247,3 @@
|
||||
### ❤️ Contributors
|
||||
|
||||
- Aaron Klinker
|
||||
- Aaron
|
||||
|
||||
@@ -2,35 +2,28 @@
|
||||
|
||||
<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>
|
||||
|
||||

|
||||
|
||||
> **Warning**
|
||||
>
|
||||
> WXT is not ready for use yet. Production builds work, but dev mode is incomplete.
|
||||
>
|
||||
> See the [Initial Release Candidate](https://github.com/aklinker1/wxt/milestone/1) milestone for more details.
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
- 🌐 Supports all browsers
|
||||
- ✅ Supports both MV2 and MV3
|
||||
- 📂 Directory based entrypoints
|
||||
- ⚡ Dev mode with HMR & auto-reload
|
||||
- 📂 File based entrypoints
|
||||
- 🚔 TypeScript
|
||||
- 🦾 Auto-imports
|
||||
- ⚡ Dev mode with HMR
|
||||
- ⬇️ Download and bundle remote URL imports
|
||||
- 🎨 Frontend framework agnostic: works with Vue, React, Svelte, etc
|
||||
|
||||
### Todo
|
||||
|
||||
- 🤖 Automated publishing
|
||||
- 🖍️ Quickly bootstrap a new project
|
||||
- 📏 Bundle analysis
|
||||
- ⚡ Dev mode with HMR and **_auto-reload_**
|
||||
- 🤖 Automated publishing
|
||||
|
||||
## Get Started
|
||||
|
||||
Checkout the [installation guide](https://wxtjs.dev/get-started) to get started with WXT.
|
||||
Checkout the [installation guide](https://wxt.dev/get-started/installation.html) to get started with WXT.
|
||||
|
||||
## Contributors
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ export default defineContentScript({
|
||||
matches: ['*://*/*'],
|
||||
async main() {
|
||||
console.log(browser.runtime.id);
|
||||
|
||||
logId();
|
||||
mountContentScriptUi();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { defineConfig } from 'vitepress';
|
||||
|
||||
// https://vitepress.dev/reference/site-config
|
||||
export default defineConfig({
|
||||
title: 'WXT',
|
||||
description: 'Next gen framework for developing web extensions',
|
||||
themeConfig: {
|
||||
// https://vitepress.dev/reference/default-theme-config
|
||||
nav: [
|
||||
{ text: 'Get Started', link: '/get-started/installation.md' },
|
||||
{ text: 'Guide', link: '/guide.md' },
|
||||
{ text: 'API', link: '/api.md' },
|
||||
],
|
||||
|
||||
sidebar: {
|
||||
'/get-started/': [
|
||||
{
|
||||
text: 'Guide',
|
||||
items: [
|
||||
{ text: 'Introduction', link: '/get-started/introduction.md' },
|
||||
{ text: 'Installation', link: '/get-started/installation.md' },
|
||||
{ text: 'Configuration', link: '/get-started/configuration.md' },
|
||||
{ text: 'Entrypoints', link: '/get-started/entrypoints.md' },
|
||||
{ text: 'Assets', link: '/get-started/assets.md' },
|
||||
{ text: 'Build Targets', link: '/get-started/build-targets.md' },
|
||||
{ text: 'Publishing', link: '/get-started/publishing.md' },
|
||||
{ text: 'Testing', link: '/get-started/testing.md' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
socialLinks: [{ icon: 'github', link: 'https://github.com/aklinker1/wxt' }],
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
# API Reference
|
||||
|
||||
:::warning 🚧 Under construction
|
||||
This documentation does not exist yet.
|
||||
:::
|
||||
|
After Width: | Height: | Size: 415 KiB |
@@ -0,0 +1,85 @@
|
||||
# Assets
|
||||
|
||||
WXT has two directories for storing assets like CSS, images, or fonts.
|
||||
|
||||
- `<srcDir>/public`: Store files that will be copied into the output directory as-is
|
||||
- `<srcDir>/assets`: Store files that will be processed by Vite during the build process
|
||||
|
||||
## `/public` Directory
|
||||
|
||||
Place static files like the extension icon or `_locales/` directory here. These files will be copied over to the output directory without being transformed by Vite.
|
||||
|
||||
```
|
||||
<srcDir>
|
||||
└─ public/
|
||||
├─ icon-16.png
|
||||
├─ icon-32.png
|
||||
├─ icon-48.png
|
||||
├─ icon-96.png
|
||||
└─ icon-128.png
|
||||
```
|
||||
|
||||
### Example
|
||||
|
||||
You can reference these files by using absolute paths in HTML files or `browser.runtime.getURL` in content scripts.
|
||||
|
||||
:::code-group
|
||||
|
||||
```html [popup.html]
|
||||
<img src="/icon-128.png" />
|
||||
```
|
||||
|
||||
```ts [content.ts]
|
||||
defineContentScript({
|
||||
main() {
|
||||
const image = document.createElement('img');
|
||||
image.src = browser.runtime.getURL('/icon-128.png');
|
||||
document.body.append(image);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
## `/assets` Directory
|
||||
|
||||
Files in the assets directory will be processed by Vite. They are imported in your source code, and will be transformed or renamed in the output directory.
|
||||
|
||||
```
|
||||
<srcDir>
|
||||
└─ assets/
|
||||
├─ style.css
|
||||
└─ illustration.svg
|
||||
```
|
||||
|
||||
### Example
|
||||
|
||||
:::code-group
|
||||
|
||||
```html [popup.html]
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="~/assets/style.css" />
|
||||
<!-- ... -->
|
||||
</head>
|
||||
<body>
|
||||
<img src="~/illustration.svg" />
|
||||
<!-- ... -->
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
```ts [content.ts]
|
||||
import '~/assets/style.css';
|
||||
import illustration from '~/assets/style.svg';
|
||||
|
||||
defineContentScript({
|
||||
main() {
|
||||
const image = document.createElement('img');
|
||||
image.src = illustration;
|
||||
document.body.append(image);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
:::
|
||||
@@ -0,0 +1,46 @@
|
||||
# Build Targets
|
||||
|
||||
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:
|
||||
|
||||
```
|
||||
<root>
|
||||
└─ .output
|
||||
├─ chrome-mv3
|
||||
├─ firefox-mv2
|
||||
├─ edge-mv3
|
||||
└─ ...
|
||||
```
|
||||
|
||||
## Target Browser
|
||||
|
||||
To build for a specific browser, pass the `-b --browser` flag from the CLI:
|
||||
|
||||
```
|
||||
|
||||
wxt --browser firefox
|
||||
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.
|
||||
|
||||
| Browser | Default Manifest Version |
|
||||
| ---------------- | :----------------------: |
|
||||
| `chrome` | 3 |
|
||||
| `firefox` | 2 |
|
||||
| `safari` | 2 |
|
||||
| `edge` | 3 |
|
||||
| Any other string | 3 |
|
||||
|
||||
## Target Manifest Version
|
||||
|
||||
To build for a specific manifest version, pass either the `--mv2` flag or `--mv3` flag from the CLI.
|
||||
|
||||
```sh
|
||||
wxt --mv2
|
||||
wxt build --mv2
|
||||
```
|
||||
|
||||
When the `-b --browser` flag is not passed, it defaults to `chrome`. So here, we're targetting MV2 for Chrome.
|
||||
@@ -0,0 +1,92 @@
|
||||
# Configuration
|
||||
|
||||
WXT's behavior can be configured via the `wxt.config.ts` file. In this file, you can add Vite plugins, change the directory strucutre of your project, and provide permissions or other fields to the `<outdir>/manifest.json`.
|
||||
|
||||
However, since WXT is an opinionated framework, some things cannot be configured.
|
||||
|
||||
## Config File
|
||||
|
||||
To configure WXT, create a `wxt.config.ts` file in your project root. It should have the following contents:
|
||||
|
||||
```ts
|
||||
import { defineConfig } from 'wxt';
|
||||
|
||||
export default defineConfig({
|
||||
// My WXT config
|
||||
});
|
||||
```
|
||||
|
||||
:::info
|
||||
See the [API reference](/api.md) for a full list of options.
|
||||
:::
|
||||
|
||||
## Directory Config
|
||||
|
||||
WXT allows you to edit several directories to your liking:
|
||||
|
||||
- `root` (default: `process.cwd()`) - Root of the WXT project
|
||||
- `srcDir` (default: `<root>`) - Location of all your source code
|
||||
- `entrypointsDir` (default: `<srcDir>/entrypoints`) - Folder containing all the entrypoints.
|
||||
- `publicDir` (default: `<srcDir>/public`) - Folder containing [public assets](/get-started/assets.md)
|
||||
|
||||
### Example
|
||||
|
||||
If you want a `src/` directory to contain all your source code, and you want to rename `entrypoints/` to `entries/`, your config would look like this:
|
||||
|
||||
```ts
|
||||
import { defineConfig } from 'wxt';
|
||||
|
||||
export default defineConfig({
|
||||
srcDir: 'src',
|
||||
entrypointsDir: 'entries',
|
||||
});
|
||||
```
|
||||
|
||||
## Vite Config
|
||||
|
||||
[Vite](https://vitejs.dev/) is the bundler used to build each entrypoint of your extension. Vite can be configured via the `vite` option.
|
||||
|
||||
See [Vite's documentation](https://vitejs.dev/config/) for configuring the bundler.
|
||||
|
||||
## Frontend Frameworks
|
||||
|
||||
Adding a framework like Vue, React, or Svelte is easy!
|
||||
|
||||
In the `wxt.config.ts` file, install and add the framework's Vite plugin to the config.
|
||||
|
||||
:::code-group
|
||||
|
||||
```ts [Vue]
|
||||
import { defineConfig } from 'wxt';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
|
||||
export default defineConfig({
|
||||
vite: {
|
||||
plugins: [vue()],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```ts [React]
|
||||
import { defineConfig } from 'wxt';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
vite: {
|
||||
plugins: [react()],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```ts [Svelte]
|
||||
import { defineConfig } from 'wxt';
|
||||
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
export default defineConfig({
|
||||
vite: {
|
||||
plugins: [svelte()],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
:::
|
||||
@@ -0,0 +1,89 @@
|
||||
# Defining Entrypoints
|
||||
|
||||
Entrypoints are 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` Directory
|
||||
|
||||
In WXT, entrypoints are defined by adding a file to the `entrypoints/` directory.
|
||||
|
||||
For example, a project that looks like this:
|
||||
|
||||
```
|
||||
<root>
|
||||
├─ entrypoints/
|
||||
│ ├─ background.ts
|
||||
│ ├─ content.ts
|
||||
│ ├─ injected.ts
|
||||
│ └─ popup.html
|
||||
└─ wxt.config.ts
|
||||
```
|
||||
|
||||
would result in the following `manifest.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
// ...
|
||||
"manifest_version": 3,
|
||||
"action": {
|
||||
// ...
|
||||
"default_popup": "popup.html"
|
||||
},
|
||||
"background": {
|
||||
// ...
|
||||
"service_worker": "background.js"
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
// ...
|
||||
"js": ["content-scripts/content.js"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
If a file uses a [special name recognized by WXT](/get-started/entrypoints.md), it will be added to the manifest. In this case:
|
||||
|
||||
- `popup.html` → `action.default_popup`
|
||||
- `content.ts` → `content_scripts.*.js`
|
||||
- `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 unlisted and do not show up in the manifest.
|
||||
|
||||
In this case, `injected.ts` gets bundled to `<outdir>/injected.js` and is accessible via `browser.runtime.getURL("/injected.js")`.
|
||||
|
||||
:::info
|
||||
See [`/entrypoints` folder](/get-started/entrypoints.md) documentation for a full list of recognized entrypoint filenames.
|
||||
:::
|
||||
|
||||
## Entrypoint Options
|
||||
|
||||
Some entrypoints, like content scripts, actions, or the background, can recieve additional options.
|
||||
|
||||
In HTML files, options are listed as `meta` tags:
|
||||
|
||||
```html
|
||||
<html>
|
||||
<head>
|
||||
<!-- Defining the popup's "default_icon" field -->
|
||||
<meta name="manifest.default_icon" content="{ '16': '/icon/16.png' }" />
|
||||
</head>
|
||||
</html>
|
||||
```
|
||||
|
||||
In TS files, options are apart of the file's default export:
|
||||
|
||||
```ts
|
||||
export default defineContentScript({
|
||||
matches: ['*://*.google.com/*'],
|
||||
runAt: 'document_start',
|
||||
main() {
|
||||
// ...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
:::info
|
||||
For a full list of entrypoints and each of their options, see the [`/entrypoints` folder](/get-started/entrypoints.md) documentation.
|
||||
:::
|
||||
@@ -0,0 +1,112 @@
|
||||
# Installation
|
||||
|
||||
Bootstrap a new project or start from scratch.
|
||||
|
||||
## Bootstrap Project
|
||||
|
||||
:::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)
|
||||
|
||||
:::
|
||||
|
||||
:::code-group
|
||||
|
||||
```sh [pnpm]
|
||||
pnpx wxt@latest init <project-name>
|
||||
```
|
||||
|
||||
```sh [npm]
|
||||
npx wxt@latest init <project-name>
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
## From Scratch
|
||||
|
||||
Create a new NPM project:
|
||||
|
||||
:::code-group
|
||||
|
||||
```sh [pnpm]
|
||||
pnpm init <project-name>
|
||||
cd <project-name>
|
||||
echo 'shamefully-hoist=true' >> .npmrc
|
||||
```
|
||||
|
||||
```sh [npm]
|
||||
npm init <project-name>
|
||||
cd <project-name>
|
||||
```
|
||||
|
||||
```sh [yarn]
|
||||
yarn init <project-name>
|
||||
cd <project-name>
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
Then install `wxt`:
|
||||
|
||||
:::code-group
|
||||
|
||||
```sh [pnpm]
|
||||
pnpm add wxt
|
||||
```
|
||||
|
||||
```sh [npm]
|
||||
npm i --save wxt
|
||||
```
|
||||
|
||||
```sh [yarn]
|
||||
yarn add wxt
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
Add your first entrypoint:
|
||||
|
||||
```ts
|
||||
// entrypoints/background.ts
|
||||
export default defineBackgroundScript(() => {
|
||||
console.log(`Hello from ${browser.runtime.id}!`);
|
||||
});
|
||||
```
|
||||
|
||||
Finally, add scripts to your `package.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"dev": "wxt", // [!code ++]
|
||||
"dev:firefox": "wxt --browser firefox", // [!code ++]
|
||||
"build": "wxt build", // [!code ++]
|
||||
"build:firefox": "wxt build --browser firefox" // [!code ++]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> You can skip `*:firefox` scripts if you don't want to support Firefox
|
||||
|
||||
## Development
|
||||
|
||||
Once you've installed WXT, you can start the development server using the `dev` script.
|
||||
|
||||
```sh
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
:::tip 🎉 Well done!
|
||||
|
||||
The dev command will build the extension for development, open the browser, and reload the different parts of the extension when you save changes.
|
||||
:::
|
||||
|
||||
## Next Steps
|
||||
|
||||
You're ready to build a out your web extension!
|
||||
|
||||
- Learn how to [add entrypoints](./entrypoints.md) like the popup, background, or content scripts
|
||||
- [Configure WXT](./configuration.md) by creating a `wxt.config.ts` file
|
||||
@@ -0,0 +1,32 @@
|
||||
# Introduction
|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
## Conventions
|
||||
|
||||
WXT is an optionated framework. This helps keep projects consistent and easy to pick up.
|
||||
|
||||
- **Generated manifest**: Based on your project's file structure
|
||||
- **Entrypoint configuration**: Configure entrypoints from the same file they're declare in
|
||||
- **Type-safety is a priority**: Out-of-the-box TypeScript support with improved browser API typing
|
||||
- **Simple output file structure**: Ouptut file paths minimize the path at runtime
|
||||
|
||||
## Development
|
||||
|
||||
WXT's dev server supports modern features like HMR to provide a lighting fast dev mode.
|
||||
|
||||
When changes can't be hot-reloaded, like content scripts or background scripts, they're reloaded individually to prevent reloading the entire extension and slowing down your development cycle.
|
||||
|
||||
## Production-ready
|
||||
|
||||
Production builds are optimized for store review, changing as few files as possible between builds.
|
||||
|
||||
In addition, WXT fully supports Firefox's source code requirements when using a bundler. It will automatically create and upload a ZIP file of your source code.
|
||||
|
||||
:::info
|
||||
See [Publishing](./publishing.md) for more info around production builds.
|
||||
:::
|
||||
@@ -0,0 +1,5 @@
|
||||
# Publishing
|
||||
|
||||
:::warning 🚧 Not implemented yet!
|
||||
For now, manually zip the output directory and upload to stores by hand.
|
||||
:::
|
||||
@@ -0,0 +1,5 @@
|
||||
# Testing
|
||||
|
||||
:::warning 🚧 Testing utils are not implemented yet!
|
||||
Eventually, the plan is to have an integration with Vitest.
|
||||
:::
|
||||
@@ -0,0 +1,5 @@
|
||||
# Guide
|
||||
|
||||
:::warning 🚧 Under construction
|
||||
This documentation does not exist yet.
|
||||
:::
|
||||
@@ -0,0 +1,49 @@
|
||||
---
|
||||
# https://vitepress.dev/reference/default-theme-home-page
|
||||
layout: home
|
||||
titleTemplate: 'Next Generation Web Extension Framework'
|
||||
|
||||
hero:
|
||||
name: WXT
|
||||
text: Next gen framework for web extensions
|
||||
tagline: Powered by Vite, inspired by Nuxt.
|
||||
actions:
|
||||
- theme: brand
|
||||
text: Get Started
|
||||
link: /get-started/installation
|
||||
- theme: alt
|
||||
text: Learn More
|
||||
link: /get-started/introduction
|
||||
|
||||
features:
|
||||
- icon: 🌐
|
||||
title: Supported Browsers
|
||||
details: Chrome, Firefox, Edge, Safari, and any Chromium based browser.
|
||||
- icon: ✅
|
||||
title: MV2 and MV3
|
||||
details: Supports both manifest versions for each browser.
|
||||
- icon: ⚡
|
||||
title: Fast Dev Mode
|
||||
details: HMR for UIs and fast reload for background and content scripts.
|
||||
- icon: 📂
|
||||
title: File Based Entrypoints
|
||||
details: Manifest is generated based on files inside the project.
|
||||
- icon: 🚔
|
||||
title: TypeScript
|
||||
details: Scale projects with full TS support.
|
||||
- icon: 🦾
|
||||
title: Auto-imports
|
||||
details: Nuxt-like auto-imports to speed up development.
|
||||
- icon: ⬇️
|
||||
title: Bundle Remote Code
|
||||
details: Downloads and bundles remote code imported from URLs.
|
||||
- icon: 🎨
|
||||
title: Frontend framework agnostic
|
||||
details: Works with any front-end framework with a Vite plugin.
|
||||
- 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.'
|
||||
---
|
||||
@@ -191,21 +191,18 @@ describe('Manifest Content', () => {
|
||||
expect(manifest.content_scripts).toContainEqual({
|
||||
matches: ['*://duckduckgo.com/*'],
|
||||
run_at: 'document_end',
|
||||
css: [expect.stringContaining('assets/four-')],
|
||||
css: ['assets/four.css'],
|
||||
js: ['content-scripts/four.js'],
|
||||
});
|
||||
expect(manifest.content_scripts).toContainEqual({
|
||||
matches: ['*://google.com/*'],
|
||||
run_at: 'document_end',
|
||||
css: [
|
||||
expect.stringContaining('assets/three-'),
|
||||
expect.stringContaining('assets/two-'),
|
||||
],
|
||||
css: ['assets/three.css', 'assets/two.css'],
|
||||
js: ['content-scripts/three.js', 'content-scripts/two.js'],
|
||||
});
|
||||
expect(manifest.content_scripts).toContainEqual({
|
||||
matches: ['*://google.com/*'],
|
||||
css: [expect.stringContaining('assets/one-')],
|
||||
css: ['assets/one.css'],
|
||||
js: ['content-scripts/one.js'],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -44,33 +44,53 @@ describe('Output Directory Structure', () => {
|
||||
await project.build();
|
||||
|
||||
expect(await project.serializeOutput()).toMatchInlineSnapshot(`
|
||||
".output/chrome-mv3/assets/one-dedc7a05.css
|
||||
".output/chrome-mv3/assets/one.css
|
||||
----------------------------------------
|
||||
body{color:#00f}
|
||||
|
||||
================================================================================
|
||||
.output/chrome-mv3/assets/two-74d94aed.css
|
||||
.output/chrome-mv3/assets/two.css
|
||||
----------------------------------------
|
||||
body{color:red}
|
||||
|
||||
================================================================================
|
||||
.output/chrome-mv3/content-scripts/one.js
|
||||
----------------------------------------
|
||||
(function(){\\"use strict\\";function c(n){return n}const e=\\"\\",t={matches:[\\"*://*/*\\"],main:()=>{}};(async()=>{try{await t.main()}catch(n){console.error(\`The content script crashed on startup!
|
||||
|
||||
\`,n)}})()})();
|
||||
(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)}})()})();
|
||||
|
||||
================================================================================
|
||||
.output/chrome-mv3/content-scripts/two.js
|
||||
----------------------------------------
|
||||
(function(){\\"use strict\\";function c(n){return n}const e=\\"\\",t={matches:[\\"*://*/*\\"],main:()=>{}};(async()=>{try{await t.main()}catch(n){console.error(\`The content script crashed on startup!
|
||||
|
||||
\`,n)}})()})();
|
||||
(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)}})()})();
|
||||
|
||||
================================================================================
|
||||
.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-dedc7a05.css\\",\\"assets/two-74d94aed.css\\"],\\"js\\":[\\"content-scripts/one.js\\",\\"content-scripts/two.js\\"]}]}"
|
||||
{\\"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\\"]}]}"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should allow inputs with invalid JS variable names, like dashes', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile(
|
||||
'entrypoints/overlay-one.content.ts',
|
||||
`export default defineContentScript({
|
||||
matches: ["*://*/*"],
|
||||
main: () => {},
|
||||
})`,
|
||||
);
|
||||
|
||||
await project.build();
|
||||
|
||||
expect(await project.serializeOutput()).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)}})()})();
|
||||
|
||||
================================================================================
|
||||
.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\\"]}]}"
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -55,6 +55,7 @@ export class TestProject {
|
||||
*/
|
||||
addFile(filename: string, content?: string) {
|
||||
this.files.push([filename, content ?? '']);
|
||||
if (filename === 'wxt.config.ts') this.config = {};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -63,15 +64,13 @@ export class TestProject {
|
||||
async build(config: InlineConfig = {}) {
|
||||
if (this.config == null) this.setConfigFileConfig();
|
||||
|
||||
await Promise.all(
|
||||
this.files.map(async (file) => {
|
||||
const [name, content] = file;
|
||||
const filePath = resolve(this.root, name);
|
||||
const fileDir = dirname(filePath);
|
||||
await fs.ensureDir(fileDir);
|
||||
await fs.writeFile(filePath, content ?? '', 'utf-8');
|
||||
}),
|
||||
);
|
||||
for (const file of this.files) {
|
||||
const [name, content] = file;
|
||||
const filePath = resolve(this.root, name);
|
||||
const fileDir = dirname(filePath);
|
||||
await fs.ensureDir(fileDir);
|
||||
await fs.writeFile(filePath, content ?? '', 'utf-8');
|
||||
}
|
||||
execSync('npm i --ignore-scripts', { cwd: this.root });
|
||||
|
||||
await build({ ...config, root: this.root });
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
{
|
||||
"name": "wxt",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"version": "0.1.6",
|
||||
"description": "Next gen framework for developing web extensions",
|
||||
"engines": {
|
||||
"node": ">=18.16.0",
|
||||
"pnpm": ">=8"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/aklinker1/wxt"
|
||||
@@ -22,10 +26,9 @@
|
||||
},
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"dist",
|
||||
"templates"
|
||||
"dist"
|
||||
],
|
||||
"bin": "dist/cli/index.cjs",
|
||||
"bin": "dist/cli.cjs",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
@@ -36,26 +39,30 @@
|
||||
"types": "./dist/index.d.ts"
|
||||
},
|
||||
"./client": {
|
||||
"require": "./dist/client/index.cjs",
|
||||
"import": "./dist/client/index.js",
|
||||
"types": "./dist/client/index.d.ts"
|
||||
"require": "./dist/client.cjs",
|
||||
"import": "./dist/client.js",
|
||||
"types": "./dist/client.d.ts"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"wxt": "tsx src/cli/index.ts",
|
||||
"build": "run-s -s build:*",
|
||||
"build:js": "tsup src/index.ts --sourcemap --dts --format esm,cjs",
|
||||
"build:cli": "tsup src/cli/index.ts -d dist/cli --sourcemap",
|
||||
"build:client": "tsup src/client/index.ts -d dist/client --sourcemap --dts --format esm,cjs",
|
||||
"build": "tsx scripts/build.ts",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --write .",
|
||||
"compile": "tsc --noEmit",
|
||||
"test": "vitest",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"prepare": "simple-git-hooks",
|
||||
"prepublish": "pnpm -s build"
|
||||
"prepublish": "pnpm -s build",
|
||||
"docs:dev": "vitepress dev docs",
|
||||
"docs:build": "vitepress build docs",
|
||||
"docs:preview": "vitepress preview docs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/webextension-polyfill": "^0.10.0",
|
||||
"@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",
|
||||
@@ -69,23 +76,29 @@
|
||||
"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"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^8.0.2",
|
||||
"@types/fs-extra": "^11.0.1",
|
||||
"@types/lodash.merge": "^4.6.7",
|
||||
"@types/node": "^20.3.1",
|
||||
"@types/picomatch": "^2.3.0",
|
||||
"@types/webextension-polyfill": "^0.10.0",
|
||||
"@vitest/coverage-v8": "^0.32.2",
|
||||
"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",
|
||||
"tsx": "^3.12.7",
|
||||
"typescript": "^5.1.3",
|
||||
"vitest": "^0.32.2",
|
||||
"vitepress": "1.0.0-beta.5",
|
||||
"vitest": "^0.32.4",
|
||||
"vitest-mock-extended": "^1.1.4",
|
||||
"webextension-polyfill": "^0.10.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -94,5 +107,16 @@
|
||||
"packageManager": "pnpm@8.6.3",
|
||||
"simple-git-hooks": {
|
||||
"pre-commit": "pnpm pretty-quick --staged"
|
||||
},
|
||||
"pnpm": {
|
||||
"peerDependencyRules": {
|
||||
"ignoreMissing": [
|
||||
"@algolia/client-search",
|
||||
"search-insights"
|
||||
],
|
||||
"allowAny": [
|
||||
"node-fetch"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import tsup from 'tsup';
|
||||
import glob from 'fast-glob';
|
||||
import pc from 'picocolors';
|
||||
import path from 'path';
|
||||
import { formatDuration } from '../src/core/utils/formatDuration';
|
||||
import ora from 'ora';
|
||||
import fs from 'fs-extra';
|
||||
import { consola } from 'consola';
|
||||
|
||||
const spinner = ora('Building WXT').start();
|
||||
|
||||
const startTime = Date.now();
|
||||
const outDir = 'dist';
|
||||
const virtualEntrypoints = ['background', 'content-script'];
|
||||
|
||||
await fs.rm(outDir, { recursive: true, force: true });
|
||||
|
||||
await Promise.all([
|
||||
tsup.build({
|
||||
entry: {
|
||||
index: 'src/index.ts',
|
||||
},
|
||||
format: ['cjs', 'esm'],
|
||||
sourcemap: true,
|
||||
dts: true,
|
||||
silent: true,
|
||||
}),
|
||||
tsup.build({
|
||||
entry: { cli: 'src/cli/index.ts' },
|
||||
format: ['cjs'],
|
||||
sourcemap: 'inline',
|
||||
silent: true,
|
||||
}),
|
||||
tsup.build({
|
||||
entry: { client: 'src/client/index.ts' },
|
||||
format: ['esm'],
|
||||
sourcemap: 'inline',
|
||||
dts: true,
|
||||
silent: true,
|
||||
}),
|
||||
...virtualEntrypoints.map((entryName) =>
|
||||
tsup.build({
|
||||
entry: {
|
||||
[`virtual-modules/${entryName}-entrypoint`]: `src/client/virtual-modules/${entryName}-entrypoint.ts`,
|
||||
},
|
||||
format: ['esm'],
|
||||
sourcemap: true,
|
||||
silent: true,
|
||||
external: [`virtual:user-${entryName}`],
|
||||
}),
|
||||
),
|
||||
tsup.build({
|
||||
entry: {
|
||||
'virtual-modules/reload-html': `src/client/virtual-modules/reload-html.ts`,
|
||||
},
|
||||
format: ['esm'],
|
||||
sourcemap: true,
|
||||
silent: true,
|
||||
}),
|
||||
tsup.build({
|
||||
entry: {
|
||||
'virtual-modules/fake-browser': `src/client/virtual-modules/fake-browser.ts`,
|
||||
},
|
||||
format: ['esm', 'cjs'],
|
||||
silent: true,
|
||||
}),
|
||||
]).catch((err) => {
|
||||
spinner.fail();
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
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),
|
||||
)}`,
|
||||
);
|
||||
});
|
||||
consola.success(`Finished in ${formatDuration(duration)}`);
|
||||
@@ -0,0 +1 @@
|
||||
declare const __TEST_SEED__: number;
|
||||
@@ -23,7 +23,7 @@ export const dev = defineCommand<
|
||||
};
|
||||
|
||||
const server = await wxt.createServer(cliConfig);
|
||||
await server.listen(server.port);
|
||||
await server.start();
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import browser from 'webextension-polyfill';
|
||||
|
||||
/**
|
||||
* https://developer.chrome.com/blog/longer-esw-lifetimes/
|
||||
*/
|
||||
export function keepServiceWorkerAlive() {
|
||||
setInterval(async () => {
|
||||
// Calling an async browser API resets the service worker's timeout
|
||||
await browser.runtime.getPlatformInfo();
|
||||
}, 5e3);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
function print(method: (...args: any[]) => void, ...args: any[]) {
|
||||
if (typeof args[0] === 'string') {
|
||||
const message = args.shift();
|
||||
method(`[wxt] ${message}`, ...args);
|
||||
} else {
|
||||
method('[wxt]', ...args);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around `console` with a "[wxt]" prefix
|
||||
*/
|
||||
export const logger = {
|
||||
debug: (...args: any[]) => print(console.debug, ...args),
|
||||
log: (...args: any[]) => print(console.log, ...args),
|
||||
warn: (...args: any[]) => print(console.warn, ...args),
|
||||
error: (...args: any[]) => print(console.error, ...args),
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import browser, { Manifest } from 'webextension-polyfill';
|
||||
import { logger } from './logger';
|
||||
import { MatchPattern } from '@webext-core/match-patterns';
|
||||
|
||||
export function reloadContentScript(contentScript: Manifest.ContentScript) {
|
||||
const manifest = browser.runtime.getManifest();
|
||||
if (manifest.manifest_version == 2) {
|
||||
void reloadContentScriptMv2(contentScript);
|
||||
} else {
|
||||
void reloadContentScriptMv3(contentScript);
|
||||
}
|
||||
}
|
||||
|
||||
export async function reloadContentScriptMv3(
|
||||
contentScript: Manifest.ContentScript,
|
||||
) {
|
||||
const id = `wxt:${contentScript.js![0]}`;
|
||||
logger.log('Reloading content script:', contentScript);
|
||||
const registered = await browser.scripting.getRegisteredContentScripts();
|
||||
logger.debug('Existing scripts:', registered);
|
||||
|
||||
const existing = registered.find((cs) => cs.id === id);
|
||||
|
||||
if (existing) {
|
||||
logger.debug('Updating content script', existing);
|
||||
await browser.scripting.updateContentScripts([{ ...contentScript, id }]);
|
||||
} else {
|
||||
logger.debug('Registering new content script...');
|
||||
await browser.scripting.registerContentScripts([{ ...contentScript, id }]);
|
||||
}
|
||||
|
||||
const allTabs = await browser.tabs.query({});
|
||||
const matchPatterns = contentScript.matches.map(
|
||||
(match) => new MatchPattern(match),
|
||||
);
|
||||
const matchingTabs = allTabs.filter((tab) => {
|
||||
const url = tab.url;
|
||||
if (!url) return false;
|
||||
return !!matchPatterns.find((pattern) => pattern.includes(url));
|
||||
});
|
||||
await Promise.all(matchingTabs.map((tab) => browser.tabs.reload(tab.id)));
|
||||
}
|
||||
|
||||
export async function reloadContentScriptMv2(
|
||||
contentScript: Manifest.ContentScript,
|
||||
) {
|
||||
throw Error('TODO: reloadContentScriptMv2');
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { logger } from './logger';
|
||||
|
||||
interface WebSocketMessage {
|
||||
type: string;
|
||||
event: string;
|
||||
data?: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to the websocket and listen for messages.
|
||||
*
|
||||
* @param onMessage Optional callback that is called when a message is recieved and we've verified
|
||||
* it's structure is what we expect.
|
||||
*/
|
||||
export function setupWebSocket(
|
||||
onMessage?: (message: WebSocketMessage) => void,
|
||||
) {
|
||||
const serverUrl = `${__DEV_SERVER_PROTOCOL__}//${__DEV_SERVER_HOSTNAME__}:${__DEV_SERVER_PORT__}`;
|
||||
logger.debug('Connecting to dev server @', serverUrl);
|
||||
const ws = new WebSocket(serverUrl, 'vite-hmr');
|
||||
|
||||
ws.addEventListener('open', () => {
|
||||
logger.debug('Connected to dev server');
|
||||
});
|
||||
ws.addEventListener('close', () => {
|
||||
logger.debug('Disconnected from dev server');
|
||||
});
|
||||
ws.addEventListener('error', (event) => {
|
||||
logger.error('Failed to connect to dev server', event);
|
||||
});
|
||||
|
||||
ws.addEventListener('message', (e) => {
|
||||
try {
|
||||
const message = JSON.parse(e.data) as WebSocketMessage;
|
||||
if (message.type === 'custom' && message.event?.startsWith?.('wxt:')) {
|
||||
onMessage?.(message);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('Failed to handle message', err);
|
||||
}
|
||||
});
|
||||
|
||||
return ws;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import definition from 'virtual:user-background';
|
||||
import { setupWebSocket } from '../utils/setupWebSocket';
|
||||
import { logger } from '../utils/logger';
|
||||
import browser from 'webextension-polyfill';
|
||||
import { keepServiceWorkerAlive } from '../utils/keepServiceWorkerAlive';
|
||||
import { reloadContentScript } from '../utils/reloadContentScript';
|
||||
|
||||
if (__COMMAND__ === 'serve') {
|
||||
try {
|
||||
const ws = setupWebSocket((message) => {
|
||||
if (message.event === 'wxt:reload-extension') browser.runtime.reload();
|
||||
if (message.event === 'wxt:reload-content-script' && message.data != null)
|
||||
reloadContentScript(message.data);
|
||||
});
|
||||
|
||||
if (__MANIFEST_VERSION__ === 3) {
|
||||
// Tell the server the background script is loaded and ready to go
|
||||
ws.addEventListener('open', () => {
|
||||
const msg = { type: 'custom', event: 'wxt:background-initialized' };
|
||||
ws.send(JSON.stringify(msg));
|
||||
});
|
||||
|
||||
// Web Socket will disconnect if the service worker is killed
|
||||
keepServiceWorkerAlive();
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('Failed to setup web socket connection with dev server', err);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const res = definition.main();
|
||||
// @ts-expect-error: res shouldn't be a promise, but we're checking it anyways
|
||||
if (res instanceof Promise) {
|
||||
console.warn(
|
||||
"The background's main() function return a promise, but it must be synchonous",
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('The background script crashed on startup!');
|
||||
throw err;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import definition from 'virtual:user-content-script';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
await definition.main();
|
||||
} catch (err) {
|
||||
logger.error('The content script crashed on startup!', err);
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,3 @@
|
||||
import { fakeBrowser as browser } from '@webext-core/fake-browser';
|
||||
|
||||
export default browser;
|
||||
@@ -0,0 +1,20 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
import { logger } from '../utils/logger';
|
||||
import { setupWebSocket } from '../utils/setupWebSocket';
|
||||
|
||||
if (__COMMAND__ === 'serve') {
|
||||
try {
|
||||
setupWebSocket((message) => {
|
||||
if (message.event === 'wxt:reload-page') {
|
||||
// We need to remove the initial slash from the path to compare correctly
|
||||
// "popup.html" === "/popup.html".substring(1)
|
||||
if (message.data === location.pathname.substring(1)) {
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error('Failed to setup web socket connection with dev server', err);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Types required to make the virtual modules happy.
|
||||
|
||||
declare module '*?raw' {
|
||||
const content: any;
|
||||
export default content;
|
||||
}
|
||||
|
||||
declare module 'virtual:user-background' {
|
||||
const definition: import('../../').BackgroundScriptDefintition;
|
||||
export default definition;
|
||||
}
|
||||
|
||||
declare module 'virtual:user-content-script' {
|
||||
const definition: import('../../').ContentScriptDefinition;
|
||||
export default definition;
|
||||
}
|
||||
|
||||
// Globals defined by the vite-plugins/devServerGlobals.ts and utils/globals.ts
|
||||
declare const __COMMAND__: 'build' | 'serve';
|
||||
declare const __DEV_SERVER_PROTOCOL__: string;
|
||||
declare const __DEV_SERVER_HOSTNAME__: string;
|
||||
declare const __DEV_SERVER_PORT__: string;
|
||||
declare const __MANIFEST_VERSION__: 2 | 3;
|
||||
@@ -0,0 +1,106 @@
|
||||
import { Manifest } from 'webextension-polyfill';
|
||||
import { BuildOutput } from './types';
|
||||
import { buildEntrypoints } from './build/buildEntrypoints';
|
||||
import { findEntrypoints } from './build/findEntrypoints';
|
||||
import { generateTypesDir } from './build/generateTypesDir';
|
||||
import { InternalConfig, EntrypointGroup } from './types';
|
||||
import { generateMainfest, writeManifest } from './utils/manifest';
|
||||
import pc from 'picocolors';
|
||||
import * as vite from 'vite';
|
||||
import fs from 'fs-extra';
|
||||
import { groupEntrypoints } from './utils/groupEntrypoints';
|
||||
import { formatDuration } from './utils/formatDuration';
|
||||
import { printBuildSummary } from './log/printBuildSummary';
|
||||
|
||||
/**
|
||||
* Builds the extension based on an internal config.
|
||||
*
|
||||
* This function:
|
||||
* 1. Cleans the output directory
|
||||
* 2. Executes the rebuild function with a blank previous output so everything is built (see
|
||||
* `rebuild` for more details)
|
||||
* 3. Prints the summary
|
||||
*/
|
||||
export async function buildInternal(
|
||||
config: InternalConfig,
|
||||
): Promise<BuildOutput> {
|
||||
const verb = config.command === 'serve' ? 'Pre-rendering' : 'Building';
|
||||
const target = `${config.browser}-mv${config.manifestVersion}`;
|
||||
config.logger.info(
|
||||
`${verb} ${pc.cyan(target)} for ${pc.cyan(config.mode)} with ${pc.green(
|
||||
`Vite ${vite.version}`,
|
||||
)}`,
|
||||
);
|
||||
const startTime = Date.now();
|
||||
|
||||
// Cleanup
|
||||
await fs.rm(config.outDir, { recursive: true, force: true });
|
||||
await fs.ensureDir(config.outDir);
|
||||
|
||||
const entrypoints = await findEntrypoints(config);
|
||||
const groups = groupEntrypoints(entrypoints);
|
||||
const { output } = await rebuild(config, groups);
|
||||
|
||||
// Post-build
|
||||
config.logger.success(
|
||||
`Built extension in ${formatDuration(Date.now() - startTime)}`,
|
||||
);
|
||||
await printBuildSummary(output, config);
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a configuration, list of entrypoints, and an existing, partial output, build the
|
||||
* entrypoints and merge the new output with the existing output.
|
||||
*
|
||||
* This function will:
|
||||
* 1. Generate the .wxt directory's types
|
||||
* 2. Build the `entrypointGroups` (and copies public files)
|
||||
* 3. Generate the latest manifest for all entrypoints
|
||||
* 4. Write the new manifest to the file system
|
||||
*/
|
||||
export async function rebuild(
|
||||
config: InternalConfig,
|
||||
entrypointGroups: EntrypointGroup[],
|
||||
existingOutput: Omit<BuildOutput, 'manifest'> = {
|
||||
steps: [],
|
||||
publicAssets: [],
|
||||
},
|
||||
): Promise<{ output: BuildOutput; manifest: Manifest.WebExtensionManifest }> {
|
||||
// Update types directory with new files and types
|
||||
const allEntrypoints = await findEntrypoints(config);
|
||||
await generateTypesDir(allEntrypoints, config);
|
||||
|
||||
// Build and merge the outputs
|
||||
const newOutput = await buildEntrypoints(entrypointGroups, config);
|
||||
const mergedOutput: Omit<BuildOutput, 'manifest'> = {
|
||||
steps: [...existingOutput.steps, ...newOutput.steps],
|
||||
publicAssets: [...existingOutput.publicAssets, ...newOutput.publicAssets],
|
||||
};
|
||||
|
||||
const newManifest = await generateMainfest(
|
||||
allEntrypoints,
|
||||
mergedOutput,
|
||||
config,
|
||||
);
|
||||
const finalOutput: BuildOutput = {
|
||||
manifest: newManifest,
|
||||
...newOutput,
|
||||
};
|
||||
|
||||
// Write manifest
|
||||
await writeManifest(newManifest, finalOutput, config);
|
||||
|
||||
return {
|
||||
output: {
|
||||
manifest: newManifest,
|
||||
steps: [...existingOutput.steps, ...finalOutput.steps],
|
||||
publicAssets: [
|
||||
...existingOutput.publicAssets,
|
||||
...finalOutput.publicAssets,
|
||||
],
|
||||
},
|
||||
manifest: newManifest,
|
||||
};
|
||||
}
|
||||
@@ -7,10 +7,11 @@ import {
|
||||
PopupEntrypoint,
|
||||
} from '../../types';
|
||||
import { resolve } from 'path';
|
||||
import { FindEntrypointsConfig, findEntrypoints } from '../findEntrypoints';
|
||||
import { findEntrypoints } from '../findEntrypoints';
|
||||
import fs from 'fs-extra';
|
||||
import { importTsFile } from '../../utils/importTsFile';
|
||||
import glob from 'fast-glob';
|
||||
import { fakeInternalConfig } from '../../../testing/fake-objects';
|
||||
|
||||
vi.mock('../../utils/importTsFile');
|
||||
const importTsFileMock = vi.mocked(importTsFile);
|
||||
@@ -24,24 +25,11 @@ const readFileMock = vi.mocked(
|
||||
);
|
||||
|
||||
describe('findEntrypoints', () => {
|
||||
const config: FindEntrypointsConfig = {
|
||||
const config = fakeInternalConfig({
|
||||
root: '/',
|
||||
entrypointsDir: resolve('/src/entrypoints'),
|
||||
outDir: resolve('.output'),
|
||||
logger: {
|
||||
debug: vi.fn(),
|
||||
error: vi.fn(),
|
||||
fatal: (...args) => {
|
||||
throw Error('logger.fatal called with: ' + JSON.stringify(args));
|
||||
},
|
||||
info: vi.fn(),
|
||||
log: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
success: vi.fn(),
|
||||
},
|
||||
command: 'build',
|
||||
mode: 'production',
|
||||
};
|
||||
});
|
||||
|
||||
it.each<[string, string, PopupEntrypoint]>([
|
||||
[
|
||||
@@ -189,7 +177,7 @@ describe('findEntrypoints', () => {
|
||||
|
||||
expect(entrypoints).toHaveLength(1);
|
||||
expect(entrypoints[0]).toEqual({ ...expected, options });
|
||||
expect(importTsFileMock).toBeCalledWith(config.root, expected.inputPath);
|
||||
expect(importTsFileMock).toBeCalledWith(expected.inputPath, config);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -216,7 +204,7 @@ describe('findEntrypoints', () => {
|
||||
|
||||
expect(entrypoints).toHaveLength(1);
|
||||
expect(entrypoints[0]).toEqual({ ...expected, options });
|
||||
expect(importTsFileMock).toBeCalledWith(config.root, expected.inputPath);
|
||||
expect(importTsFileMock).toBeCalledWith(expected.inputPath, config);
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import * as vite from 'vite';
|
||||
import { BuildOutput, Entrypoint, InternalConfig } from '../types';
|
||||
import { groupEntrypoints } from '../utils/groupEntrypoints';
|
||||
import {
|
||||
BuildOutput,
|
||||
BuildStepOutput,
|
||||
Entrypoint,
|
||||
EntrypointGroup,
|
||||
InternalConfig,
|
||||
} from '../types';
|
||||
import * as plugins from '../vite-plugins';
|
||||
import { removeEmptyDirs } from '../utils/removeEmptyDirs';
|
||||
import { getEntrypointBundlePath } from '../utils/entrypoints';
|
||||
@@ -9,25 +14,22 @@ import fs from 'fs-extra';
|
||||
import { dirname, resolve } from 'path';
|
||||
|
||||
export async function buildEntrypoints(
|
||||
entrypoints: Entrypoint[],
|
||||
groups: EntrypointGroup[],
|
||||
config: InternalConfig,
|
||||
): Promise<BuildOutput> {
|
||||
const groups = groupEntrypoints(entrypoints);
|
||||
|
||||
const outputs: BuildOutput[] = [];
|
||||
): Promise<Omit<BuildOutput, 'manifest'>> {
|
||||
const steps: BuildStepOutput[] = [];
|
||||
for (const group of groups) {
|
||||
const output = Array.isArray(group)
|
||||
const step = Array.isArray(group)
|
||||
? await buildMultipleEntrypoints(group, config)
|
||||
: await buildSingleEntrypoint(group, config);
|
||||
outputs.push(output);
|
||||
steps.push(step);
|
||||
}
|
||||
const publicOutput = await copyPublicDirectory(config);
|
||||
outputs.push(publicOutput);
|
||||
const publicAssets = await copyPublicDirectory(config);
|
||||
|
||||
// Remove any empty directories from moving outputs around
|
||||
await removeEmptyDirs(config.outDir);
|
||||
|
||||
return outputs.flat();
|
||||
return { publicAssets, steps };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -36,23 +38,25 @@ export async function buildEntrypoints(
|
||||
async function buildSingleEntrypoint(
|
||||
entrypoint: Entrypoint,
|
||||
config: InternalConfig,
|
||||
): Promise<BuildOutput> {
|
||||
): Promise<BuildStepOutput> {
|
||||
// Should this entrypoint be wrapped by the vite-plugins/virtualEntrypoint plugin?
|
||||
const isVirtual = ['background', 'content-script'].includes(entrypoint.type);
|
||||
const entry = isVirtual
|
||||
? `virtual:wxt-${entrypoint.type}?${entrypoint.inputPath}`
|
||||
: entrypoint.inputPath;
|
||||
|
||||
const libMode: vite.InlineConfig = {
|
||||
const libMode: vite.UserConfig = {
|
||||
build: {
|
||||
lib: {
|
||||
entry,
|
||||
formats: ['iife'],
|
||||
name: entrypoint.name,
|
||||
name: '_',
|
||||
fileName: entrypoint.name,
|
||||
},
|
||||
rollupOptions: {
|
||||
output: {
|
||||
// There's only a single output for this build, so we use the desired bundle path for the
|
||||
// entry output (like "content-scripts/overlay.js")
|
||||
entryFileNames: getEntrypointBundlePath(
|
||||
entrypoint,
|
||||
config.outDir,
|
||||
@@ -61,7 +65,7 @@ async function buildSingleEntrypoint(
|
||||
// Output content script CSS to assets/ with a hash to prevent conflicts. Defaults to
|
||||
// "[name].[ext]" in lib mode, which usually results in "style.css". That means multiple
|
||||
// content scripts with styles would overwrite each other if it weren't changed below.
|
||||
assetFileNames: `assets/${entrypoint.name}-[hash].[ext]`,
|
||||
assetFileNames: `assets/${entrypoint.name}.[ext]`,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -72,7 +76,10 @@ async function buildSingleEntrypoint(
|
||||
) as vite.InlineConfig;
|
||||
|
||||
const result = await vite.build(entryConfig);
|
||||
return getBuildOutput(result);
|
||||
return {
|
||||
entrypoints: entrypoint,
|
||||
chunks: getBuildOutputChunks(result),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -81,8 +88,8 @@ async function buildSingleEntrypoint(
|
||||
async function buildMultipleEntrypoints(
|
||||
entrypoints: Entrypoint[],
|
||||
config: InternalConfig,
|
||||
): Promise<BuildOutput> {
|
||||
const multiPage: vite.InlineConfig = {
|
||||
): Promise<BuildStepOutput> {
|
||||
const multiPage: vite.UserConfig = {
|
||||
plugins: [plugins.multipageMove(entrypoints, config)],
|
||||
build: {
|
||||
rollupOptions: {
|
||||
@@ -90,6 +97,14 @@ async function buildMultipleEntrypoints(
|
||||
input[entry.name] = entry.inputPath;
|
||||
return input;
|
||||
}, {}),
|
||||
output: {
|
||||
// Include a hash to prevent conflicts
|
||||
chunkFileNames: 'chunks/[name]-[hash].js',
|
||||
// Include a hash to prevent conflicts
|
||||
entryFileNames: 'chunks/[name]-[hash].js',
|
||||
// We can't control the "name", so we need a hash to prevent conflicts
|
||||
assetFileNames: 'assets/[name]-[hash].[ext]',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -97,15 +112,18 @@ async function buildMultipleEntrypoints(
|
||||
const entryConfig = vite.mergeConfig(
|
||||
multiPage,
|
||||
config.vite,
|
||||
) as vite.InlineConfig;
|
||||
) as vite.UserConfig;
|
||||
|
||||
const result = await vite.build(entryConfig);
|
||||
return getBuildOutput(result);
|
||||
return {
|
||||
entrypoints,
|
||||
chunks: getBuildOutputChunks(result),
|
||||
};
|
||||
}
|
||||
|
||||
function getBuildOutput(
|
||||
function getBuildOutputChunks(
|
||||
result: Awaited<ReturnType<typeof vite.build>>,
|
||||
): BuildOutput {
|
||||
): BuildStepOutput['chunks'] {
|
||||
if ('on' in result) throw Error('wxt does not support vite watch mode.');
|
||||
if (Array.isArray(result)) return result.flatMap(({ output }) => output);
|
||||
return result.output;
|
||||
@@ -113,19 +131,19 @@ function getBuildOutput(
|
||||
|
||||
async function copyPublicDirectory(
|
||||
config: InternalConfig,
|
||||
): Promise<BuildOutput> {
|
||||
if (!(await fs.exists(config.publicDir))) return [];
|
||||
): Promise<BuildOutput['publicAssets']> {
|
||||
const publicAssets: BuildOutput['publicAssets'] = [];
|
||||
if (!(await fs.exists(config.publicDir))) return publicAssets;
|
||||
|
||||
const files = await glob('**/*', { cwd: config.publicDir });
|
||||
|
||||
const outputs: BuildOutput = [];
|
||||
for (const file of files) {
|
||||
const srcPath = resolve(config.publicDir, file);
|
||||
const outPath = resolve(config.outDir, file);
|
||||
|
||||
await fs.ensureDir(dirname(outPath));
|
||||
await fs.copyFile(srcPath, outPath);
|
||||
outputs.push({
|
||||
publicAssets.push({
|
||||
type: 'asset',
|
||||
fileName: file,
|
||||
name: file,
|
||||
@@ -133,5 +151,6 @@ async function copyPublicDirectory(
|
||||
source: await fs.readFile(srcPath),
|
||||
});
|
||||
}
|
||||
return outputs;
|
||||
|
||||
return publicAssets;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import { getEntrypointName } from '../utils/entrypoints';
|
||||
* Return entrypoints and their configuration by looking through the
|
||||
*/
|
||||
export async function findEntrypoints(
|
||||
config: FindEntrypointsConfig,
|
||||
config: InternalConfig,
|
||||
): Promise<Entrypoint[]> {
|
||||
const relativePaths = await glob('**/*', {
|
||||
cwd: config.entrypointsDir,
|
||||
@@ -103,7 +103,7 @@ export async function findEntrypoints(
|
||||
* @param content String contents of the file at the path.
|
||||
*/
|
||||
async function getPopupEntrypoint(
|
||||
config: FindEntrypointsConfig,
|
||||
config: InternalConfig,
|
||||
path: string,
|
||||
): Promise<PopupEntrypoint> {
|
||||
const options: PopupEntrypoint['options'] = {};
|
||||
@@ -150,7 +150,7 @@ async function getPopupEntrypoint(
|
||||
* @param content String contents of the file at the path.
|
||||
*/
|
||||
async function getOptionsEntrypoint(
|
||||
config: FindEntrypointsConfig,
|
||||
config: InternalConfig,
|
||||
path: string,
|
||||
): Promise<OptionsEntrypoint> {
|
||||
const options: OptionsEntrypoint['options'] = {};
|
||||
@@ -192,11 +192,11 @@ async function getOptionsEntrypoint(
|
||||
* @param path Absolute path to the background's TS file.
|
||||
*/
|
||||
async function getBackgroundEntrypoint(
|
||||
config: FindEntrypointsConfig,
|
||||
config: InternalConfig,
|
||||
path: string,
|
||||
): Promise<BackgroundEntrypoint> {
|
||||
const { main: _, ...options } =
|
||||
await importTsFile<BackgroundScriptDefintition>(config.root, path);
|
||||
await importTsFile<BackgroundScriptDefintition>(path, config);
|
||||
if (options == null) {
|
||||
throw Error('Background script does not have a default export');
|
||||
}
|
||||
@@ -213,13 +213,13 @@ async function getBackgroundEntrypoint(
|
||||
* @param path Absolute path to the content script's TS file.
|
||||
*/
|
||||
async function getContentScriptEntrypoint(
|
||||
config: FindEntrypointsConfig,
|
||||
config: InternalConfig,
|
||||
name: string,
|
||||
path: string,
|
||||
): Promise<ContentScriptEntrypoint> {
|
||||
const { main: _, ...options } = await importTsFile<ContentScriptDefinition>(
|
||||
config.root,
|
||||
path,
|
||||
config,
|
||||
);
|
||||
if (options == null) {
|
||||
throw Error(`Content script ${name} does not have a default export`);
|
||||
@@ -274,8 +274,3 @@ const PATH_GLOB_TO_TYPE_MAP: Record<string, Entrypoint['type'] | 'ignored'> = {
|
||||
// Don't warn about any files in subdirectories, like CSS or JS entrypoints for HTML files
|
||||
'*/*': 'ignored',
|
||||
};
|
||||
|
||||
export type FindEntrypointsConfig = Pick<
|
||||
InternalConfig,
|
||||
'root' | 'entrypointsDir' | 'outDir' | 'logger' | 'mode' | 'command'
|
||||
>;
|
||||
|
||||
@@ -48,22 +48,23 @@ async function writePathsDeclarationFile(
|
||||
config: InternalConfig,
|
||||
): Promise<string> {
|
||||
const filePath = resolve(config.typesDir, 'paths.d.ts');
|
||||
const unions = entrypoints
|
||||
.map((entry) => {
|
||||
const path = getEntrypointBundlePath(
|
||||
entry,
|
||||
config.outDir,
|
||||
entry.inputPath.endsWith('.html') ? '.html' : '.js',
|
||||
);
|
||||
return ` | "/${path}"`;
|
||||
})
|
||||
.sort();
|
||||
|
||||
await fs.writeFile(
|
||||
filePath,
|
||||
[
|
||||
'// Generated by wxt',
|
||||
'type EntrypointPath =',
|
||||
...entrypoints
|
||||
.map((entry) => {
|
||||
const path = getEntrypointBundlePath(
|
||||
entry,
|
||||
config.outDir,
|
||||
entry.inputPath.endsWith('.html') ? '.html' : '.js',
|
||||
);
|
||||
return ` | "/${path}"`;
|
||||
})
|
||||
.sort(),
|
||||
...(unions.length === 0 ? [' never'] : unions),
|
||||
].join('\n') + '\n',
|
||||
);
|
||||
|
||||
@@ -99,6 +100,7 @@ async function writeMainDeclarationFile(
|
||||
filePath,
|
||||
[
|
||||
'// Generated by wxt',
|
||||
`/// <reference types="vite/client" />`,
|
||||
...references.map(
|
||||
(ref) => `/// <reference types="./${relative(dir, ref)}" />`,
|
||||
),
|
||||
@@ -128,7 +130,20 @@ async function writeTsConfigFile(
|
||||
"strict": true,
|
||||
|
||||
/* Completeness */
|
||||
"skipLibCheck": true
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Aliases */
|
||||
"baseUrl": "${relative(dir, config.root)}",
|
||||
"paths": {
|
||||
"@@": ["."],
|
||||
"@@/*": ["./*"],
|
||||
"~~": ["."],
|
||||
"~~/*": ["./*"],
|
||||
"@": ["${relative(config.root, config.srcDir)}"],
|
||||
"@/*": ["${relative(config.root, config.srcDir)}/*"],
|
||||
"~": ["${relative(config.root, config.srcDir)}"],
|
||||
"~/*": ["${relative(config.root, config.srcDir)}/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"${relative(dir, config.root)}/**/*",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import path, { extname, relative, resolve } from 'path';
|
||||
import path, { relative, resolve } from 'path';
|
||||
import { BuildOutput, InternalConfig } from '../types';
|
||||
import { printTable } from './printTable';
|
||||
import pc from 'picocolors';
|
||||
@@ -9,15 +9,12 @@ export async function printBuildSummary(
|
||||
output: BuildOutput,
|
||||
config: InternalConfig,
|
||||
) {
|
||||
const chunks = output.sort((l, r) => {
|
||||
const lWeight =
|
||||
CHUNK_SORT_WEIGHTS[l.fileName] ??
|
||||
CHUNK_SORT_WEIGHTS[extname(l.fileName)] ??
|
||||
DEFAULT_SORT_WEIGHT;
|
||||
const rWeight =
|
||||
CHUNK_SORT_WEIGHTS[r.fileName] ??
|
||||
CHUNK_SORT_WEIGHTS[extname(r.fileName)] ??
|
||||
DEFAULT_SORT_WEIGHT;
|
||||
const chunks = [
|
||||
...output.steps.flatMap((step) => step.chunks),
|
||||
...output.publicAssets,
|
||||
].sort((l, r) => {
|
||||
const lWeight = getChunkSortWeight(l.fileName);
|
||||
const rWeight = getChunkSortWeight(r.fileName);
|
||||
const diff = lWeight - rWeight;
|
||||
if (diff !== 0) return diff;
|
||||
return l.fileName.localeCompare(r.fileName);
|
||||
@@ -31,9 +28,8 @@ export async function printBuildSummary(
|
||||
relative(process.cwd(), config.outDir) + path.sep,
|
||||
chunk.fileName,
|
||||
];
|
||||
const ext = extname(chunk.fileName);
|
||||
const prefix = i === chunks.length - 1 ? ' └─' : ' ├─';
|
||||
const color = CHUNK_COLORS[ext] ?? DEFAULT_COLOR;
|
||||
const color = getChunkColor(chunk.fileName);
|
||||
const stats = await fs.lstat(resolve(config.outDir, chunk.fileName));
|
||||
totalSize += stats.size;
|
||||
const size = String(filesize(stats.size));
|
||||
@@ -55,13 +51,28 @@ const DEFAULT_SORT_WEIGHT = 100;
|
||||
const CHUNK_SORT_WEIGHTS: Record<string, number> = {
|
||||
'manifest.json': 0,
|
||||
'.html': 1,
|
||||
'.js.map': 2,
|
||||
'.js': 2,
|
||||
'.css': 3,
|
||||
};
|
||||
function getChunkSortWeight(filename: string) {
|
||||
return (
|
||||
Object.entries(CHUNK_SORT_WEIGHTS).find(([key]) =>
|
||||
filename.endsWith(key),
|
||||
)?.[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,131 @@
|
||||
import {
|
||||
BuildStepOutput,
|
||||
EntrypointGroup,
|
||||
InternalConfig,
|
||||
WxtDevServer,
|
||||
} from './types';
|
||||
import * as vite from 'vite';
|
||||
import { findOpenPort } from './utils/findOpenPort';
|
||||
import { Manifest } 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 hostname = 'localhost';
|
||||
const origin = `http://${hostname}:${port}`;
|
||||
const serverConfig: vite.InlineConfig = {
|
||||
server: {
|
||||
origin,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
port,
|
||||
hostname,
|
||||
origin,
|
||||
viteServerConfig: serverConfig,
|
||||
};
|
||||
}
|
||||
|
||||
export async function setupServer(
|
||||
serverInfo: ServerInfo,
|
||||
config: InternalConfig,
|
||||
): Promise<WxtDevServer> {
|
||||
const runner = createWebExtRunner();
|
||||
|
||||
const viteServer = await vite.createServer(
|
||||
vite.mergeConfig(serverInfo, config.vite),
|
||||
);
|
||||
|
||||
const start = async () => {
|
||||
await viteServer.listen(server.port);
|
||||
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 = () => {
|
||||
viteServer.ws.send('wxt:reload-extension');
|
||||
};
|
||||
const reloadPage = (path: string) => {
|
||||
// Can't use Vite's built-in "full-reload" event because it doesn't like our paths, it expects
|
||||
// paths ending in "/index.html"
|
||||
viteServer.ws.send('wxt:reload-page', path);
|
||||
};
|
||||
const reloadContentScript = (contentScript: Manifest.ContentScript) => {
|
||||
viteServer.ws.send('wxt:reload-content-script', contentScript);
|
||||
};
|
||||
|
||||
const server: WxtDevServer = {
|
||||
...viteServer,
|
||||
start,
|
||||
currentOutput: {
|
||||
manifest: {
|
||||
manifest_version: 3,
|
||||
name: '',
|
||||
version: '',
|
||||
},
|
||||
publicAssets: [],
|
||||
steps: [],
|
||||
},
|
||||
port: serverInfo.port,
|
||||
hostname: serverInfo.hostname,
|
||||
origin: serverInfo.origin,
|
||||
reloadExtension,
|
||||
reloadPage,
|
||||
reloadContentScript,
|
||||
};
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
/**
|
||||
* From the server, tell the client to reload content scripts from the provided build step outputs.
|
||||
*/
|
||||
export function reloadContentScripts(
|
||||
steps: BuildStepOutput[],
|
||||
config: InternalConfig,
|
||||
server: WxtDevServer,
|
||||
) {
|
||||
if (config.manifestVersion === 3) {
|
||||
steps.forEach((step) => {
|
||||
const entry = step.entrypoints;
|
||||
if (Array.isArray(entry) || entry.type !== 'content-script') return;
|
||||
|
||||
const js = [getEntrypointBundlePath(entry, config.outDir, '.js')];
|
||||
const css = getContentScriptCssFiles([entry], server.currentOutput);
|
||||
|
||||
server.reloadContentScript({
|
||||
js,
|
||||
css,
|
||||
...entry.options,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
server.reloadExtension();
|
||||
}
|
||||
}
|
||||
|
||||
export function reloadHtmlPages(
|
||||
groups: EntrypointGroup[],
|
||||
server: WxtDevServer,
|
||||
config: InternalConfig,
|
||||
) {
|
||||
groups.flat().forEach((entry) => {
|
||||
const path = getEntrypointBundlePath(entry, config.outDir, '.html');
|
||||
server.reloadPage(path);
|
||||
});
|
||||
}
|
||||
|
||||
interface ServerInfo {
|
||||
port: number;
|
||||
hostname: string;
|
||||
origin: string;
|
||||
viteServerConfig: vite.InlineConfig;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as vite from 'vite';
|
||||
import { Manifest } from 'webextension-polyfill';
|
||||
import { UnimportOptions } from 'unimport';
|
||||
import { EntrypointGroup } from '.';
|
||||
|
||||
export interface InlineConfig {
|
||||
root?: string;
|
||||
@@ -18,8 +19,8 @@ export interface InlineConfig {
|
||||
browser?: TargetBrowser;
|
||||
manifestVersion?: TargetManifestVersion;
|
||||
logger?: Logger;
|
||||
vite?: Omit<vite.InlineConfig, 'root' | 'configFile' | 'mode'>;
|
||||
manifest?: UserManifest;
|
||||
vite?: Omit<vite.UserConfig, 'root' | 'configFile' | 'mode'>;
|
||||
manifest?: UserManifest | Promise<UserManifest> | UserManifestFn;
|
||||
server?: WxtDevServer;
|
||||
runner?: ExtensionRunnerConfig;
|
||||
}
|
||||
@@ -29,13 +30,60 @@ export interface WxtInlineViteConfig
|
||||
build?: Omit<vite.BuildOptions, 'outDir'>;
|
||||
}
|
||||
|
||||
export type BuildOutput = (vite.Rollup.OutputChunk | vite.Rollup.OutputAsset)[];
|
||||
export interface BuildOutput {
|
||||
manifest: Manifest.WebExtensionManifest;
|
||||
publicAssets: vite.Rollup.OutputAsset[];
|
||||
steps: BuildStepOutput[];
|
||||
}
|
||||
|
||||
export interface BuildStepOutput {
|
||||
entrypoints: EntrypointGroup;
|
||||
chunks: (vite.Rollup.OutputChunk | vite.Rollup.OutputAsset)[];
|
||||
}
|
||||
|
||||
export interface WxtDevServer extends vite.ViteDevServer {
|
||||
logger: Logger;
|
||||
/**
|
||||
* Ex: `3000`
|
||||
*/
|
||||
port: number;
|
||||
/**
|
||||
* Ex: `"localhost"`
|
||||
*/
|
||||
hostname: string;
|
||||
/**
|
||||
* Ex: `"http://localhost:3000"`
|
||||
*/
|
||||
origin: string;
|
||||
/**
|
||||
* Stores the current build output of the server.
|
||||
*/
|
||||
currentOutput: BuildOutput;
|
||||
/**
|
||||
* Start the server on the first open port.
|
||||
*/
|
||||
start(): Promise<void>;
|
||||
/**
|
||||
* Tell the extension to reload by running `browser.runtime.reload`.
|
||||
*/
|
||||
reloadExtension: () => void;
|
||||
/**
|
||||
* Tell an extension page to reload.
|
||||
*
|
||||
* The path is the bundle path, not the input paths, so if the input paths is
|
||||
* "src/options/index.html", you would pass "options.html" because that's where it is written to
|
||||
* in the dist directory, and where it's available at in the actual extension.
|
||||
*
|
||||
* @example
|
||||
* server.reloadPage("popup.html")
|
||||
* server.reloadPage("sandbox.html")
|
||||
*/
|
||||
reloadPage: (path: string) => void;
|
||||
/**
|
||||
* Tell the extension to restart a content script.
|
||||
*
|
||||
* @param contentScript The manifest definition for a content script
|
||||
*/
|
||||
reloadContentScript: (contentScript: Manifest.ContentScript) => void;
|
||||
}
|
||||
|
||||
export type TargetBrowser = 'chrome' | 'firefox' | 'safari' | 'edge' | 'opera';
|
||||
@@ -91,7 +139,6 @@ export interface GenericEntrypoint extends BaseEntrypoint {
|
||||
| 'sidepanel'
|
||||
| 'devtools'
|
||||
| 'unlisted-page'
|
||||
| 'unlisted-page'
|
||||
| 'unlisted-script';
|
||||
}
|
||||
|
||||
@@ -144,7 +191,7 @@ export interface ContentScriptDefinition {
|
||||
matchAboutBlank?: boolean;
|
||||
matchOriginAsFallback?: boolean;
|
||||
world?: 'ISOLATED' | 'MAIN';
|
||||
main(onStopped: OnContentScriptStopped): void | Promise<void>;
|
||||
main(): void | Promise<void>;
|
||||
}
|
||||
|
||||
export interface BackgroundScriptDefintition {
|
||||
@@ -179,6 +226,23 @@ export type UserManifest = Omit<
|
||||
| 'version_name'
|
||||
>;
|
||||
|
||||
export type UserManifestFn = (
|
||||
env: ConfigEnv,
|
||||
) => UserManifest | Promise<UserManifest>;
|
||||
|
||||
export interface ConfigEnv {
|
||||
mode: string;
|
||||
command: 'build' | 'serve';
|
||||
/**
|
||||
* Browser passed in from the CLI
|
||||
*/
|
||||
browser: TargetBrowser;
|
||||
/**
|
||||
* Manifest version passed in from the CLI
|
||||
*/
|
||||
manifestVersion: 2 | 3;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure how the browser starts up.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { every } from '../arrays';
|
||||
|
||||
describe('Array Utils', () => {
|
||||
describe('every', () => {
|
||||
it('should return true when the array is empty', () => {
|
||||
expect(every([], () => false)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return true when all item predicate's return true", () => {
|
||||
expect(every([1, 1, 1], (item) => item === 1)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false when a single item predicate's return false", () => {
|
||||
expect(every([1, 2, 1], (item) => item === 1)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,251 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { DevModeChange, detectDevChanges } from '../detectDevChanges';
|
||||
import {
|
||||
fakeBackgroundEntrypoint,
|
||||
fakeContentScriptEntrypoint,
|
||||
fakeFile,
|
||||
fakeGenericEntrypoint,
|
||||
fakeManifest,
|
||||
fakeOptionsEntrypoint,
|
||||
fakePopupEntrypoint,
|
||||
fakeRollupOutputAsset,
|
||||
fakeRollupOutputChunk,
|
||||
} from '../../../testing/fake-objects';
|
||||
import { BuildOutput, BuildStepOutput } from '../../types';
|
||||
|
||||
describe('Detect Dev Changes', () => {
|
||||
describe('No changes', () => {
|
||||
it("should return 'no-change' when a build hasn't finished", () => {
|
||||
const actual = detectDevChanges(
|
||||
[['unknown', '/path/to/file.ts']],
|
||||
undefined,
|
||||
);
|
||||
|
||||
expect(actual).toEqual({ type: 'no-change' });
|
||||
});
|
||||
|
||||
it("should return 'no-change' when the changed file isn't used by any of the entrypoints", () => {
|
||||
const change: [string, string] = ['unknown', '/some/path.ts'];
|
||||
const currentOutput: BuildOutput = {
|
||||
manifest: fakeManifest(),
|
||||
publicAssets: [],
|
||||
steps: [
|
||||
{
|
||||
entrypoints: fakeContentScriptEntrypoint(),
|
||||
chunks: [fakeRollupOutputChunk(), fakeRollupOutputChunk()],
|
||||
},
|
||||
{
|
||||
entrypoints: fakeContentScriptEntrypoint(),
|
||||
chunks: [
|
||||
fakeRollupOutputChunk(),
|
||||
fakeRollupOutputChunk(),
|
||||
fakeRollupOutputChunk(),
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const actual = detectDevChanges([change], currentOutput);
|
||||
|
||||
expect(actual).toEqual({ type: 'no-change' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Public Assets', () => {
|
||||
it("should return 'extension-reload' without any groups to rebuild when the changed file is a public asset", () => {
|
||||
const change: [string, string] = [
|
||||
'unknown',
|
||||
'/root/src/public/image.svg',
|
||||
];
|
||||
const asset1 = fakeRollupOutputAsset({
|
||||
fileName: 'image.svg',
|
||||
});
|
||||
const asset2 = fakeRollupOutputAsset({
|
||||
fileName: 'some-other-image.svg',
|
||||
});
|
||||
const currentOutput: BuildOutput = {
|
||||
manifest: fakeManifest(),
|
||||
publicAssets: [asset1, asset2],
|
||||
steps: [],
|
||||
};
|
||||
const expected: DevModeChange = {
|
||||
type: 'extension-reload',
|
||||
rebuildGroups: [],
|
||||
cachedOutput: {
|
||||
...currentOutput,
|
||||
publicAssets: [asset2],
|
||||
},
|
||||
};
|
||||
|
||||
const actual = detectDevChanges([change], currentOutput);
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Background', () => {
|
||||
it("should rebuild the background and reload the extension when the changed file in it's chunks' `moduleIds` field", () => {
|
||||
const changedPath = '/root/utils/shared.ts';
|
||||
const contentScript = fakeContentScriptEntrypoint({
|
||||
inputPath: '/root/overlay.content.ts',
|
||||
});
|
||||
const background = fakeBackgroundEntrypoint({
|
||||
inputPath: '/root/background.ts',
|
||||
});
|
||||
|
||||
const step1: BuildStepOutput = {
|
||||
entrypoints: contentScript,
|
||||
chunks: [
|
||||
fakeRollupOutputChunk({
|
||||
moduleIds: [fakeFile(), fakeFile()],
|
||||
}),
|
||||
],
|
||||
};
|
||||
const step2: BuildStepOutput = {
|
||||
entrypoints: background,
|
||||
chunks: [
|
||||
fakeRollupOutputChunk({
|
||||
moduleIds: [fakeFile(), changedPath, fakeFile()],
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
const currentOutput: BuildOutput = {
|
||||
manifest: fakeManifest(),
|
||||
publicAssets: [],
|
||||
steps: [step1, step2],
|
||||
};
|
||||
const expected: DevModeChange = {
|
||||
type: 'extension-reload',
|
||||
cachedOutput: {
|
||||
...currentOutput,
|
||||
steps: [step1],
|
||||
},
|
||||
rebuildGroups: [background],
|
||||
};
|
||||
|
||||
const actual = detectDevChanges(
|
||||
[['unknown', changedPath]],
|
||||
currentOutput,
|
||||
);
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('HTML Pages', () => {
|
||||
it('should rebuild then reload only the effected pages', async () => {
|
||||
const changedPath = '/root/page1/index.html';
|
||||
const htmlPage1 = fakePopupEntrypoint({
|
||||
inputPath: changedPath,
|
||||
});
|
||||
const htmlPage2 = fakeOptionsEntrypoint({
|
||||
inputPath: '/root/page2.html',
|
||||
});
|
||||
const htmlPage3 = fakeGenericEntrypoint({
|
||||
type: 'sandbox',
|
||||
inputPath: '/root/page3.html',
|
||||
});
|
||||
|
||||
const step1: BuildStepOutput = {
|
||||
entrypoints: [htmlPage1, htmlPage2],
|
||||
chunks: [
|
||||
fakeRollupOutputChunk({
|
||||
moduleIds: [fakeFile(), changedPath],
|
||||
}),
|
||||
],
|
||||
};
|
||||
const step2: BuildStepOutput = {
|
||||
entrypoints: [htmlPage3],
|
||||
chunks: [
|
||||
fakeRollupOutputChunk({
|
||||
moduleIds: [fakeFile(), fakeFile(), fakeFile()],
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
const currentOutput: BuildOutput = {
|
||||
manifest: fakeManifest(),
|
||||
publicAssets: [],
|
||||
steps: [step1, step2],
|
||||
};
|
||||
const expected: DevModeChange = {
|
||||
type: 'html-reload',
|
||||
cachedOutput: {
|
||||
...currentOutput,
|
||||
steps: [step2],
|
||||
},
|
||||
rebuildGroups: [[htmlPage1, htmlPage2]],
|
||||
};
|
||||
|
||||
const actual = detectDevChanges(
|
||||
[['unknown', changedPath]],
|
||||
currentOutput,
|
||||
);
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Content Scripts', () => {
|
||||
it('should rebuild then reload only the effected content scripts', async () => {
|
||||
const changedPath = '/root/utils/shared.ts';
|
||||
const script1 = fakeContentScriptEntrypoint({
|
||||
inputPath: '/root/overlay1.content/index.ts',
|
||||
});
|
||||
const script2 = fakeContentScriptEntrypoint({
|
||||
inputPath: '/root/overlay2.ts',
|
||||
});
|
||||
const script3 = fakeContentScriptEntrypoint({
|
||||
inputPath: '/root/overlay3.content/index.ts',
|
||||
});
|
||||
|
||||
const step1: BuildStepOutput = {
|
||||
entrypoints: script1,
|
||||
chunks: [
|
||||
fakeRollupOutputChunk({
|
||||
moduleIds: [fakeFile(), changedPath],
|
||||
}),
|
||||
],
|
||||
};
|
||||
const step2: BuildStepOutput = {
|
||||
entrypoints: script2,
|
||||
chunks: [
|
||||
fakeRollupOutputChunk({
|
||||
moduleIds: [fakeFile(), fakeFile(), fakeFile()],
|
||||
}),
|
||||
],
|
||||
};
|
||||
const step3: BuildStepOutput = {
|
||||
entrypoints: script3,
|
||||
chunks: [
|
||||
fakeRollupOutputChunk({
|
||||
moduleIds: [changedPath, fakeFile(), fakeFile()],
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
const currentOutput: BuildOutput = {
|
||||
manifest: fakeManifest(),
|
||||
publicAssets: [],
|
||||
steps: [step1, step2, step3],
|
||||
};
|
||||
const expected: DevModeChange = {
|
||||
type: 'content-script-reload',
|
||||
cachedOutput: {
|
||||
...currentOutput,
|
||||
steps: [step2],
|
||||
},
|
||||
changedSteps: [step1, step3],
|
||||
rebuildGroups: [script1, script3],
|
||||
};
|
||||
|
||||
const actual = detectDevChanges(
|
||||
[['unknown', changedPath]],
|
||||
currentOutput,
|
||||
);
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Checks if `predicate` returns truthy for all elements of the array.
|
||||
*/
|
||||
export function every<T>(
|
||||
array: T[],
|
||||
predicate: (item: T, index: number) => boolean,
|
||||
): boolean {
|
||||
for (let i = 0; i < array.length; i++)
|
||||
if (!predicate(array[i], i)) return false;
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
import { BuildOutput, BuildStepOutput, EntrypointGroup } from '../types';
|
||||
import * as vite from 'vite';
|
||||
import { every } from './arrays';
|
||||
|
||||
/**
|
||||
* Compare the changed files vs the build output and determine what kind of reload needs to happen:
|
||||
*
|
||||
* - Do nothing
|
||||
* - CSS or JS file associated with an HTML page is changed - this is handled automatically by the
|
||||
* dev server
|
||||
* - Change isn't used by any of the entrypoints
|
||||
* - Reload Content script
|
||||
* - CSS or JS file associated with a content script
|
||||
* - Background script will be told to reload the content script
|
||||
* - Reload HTML file
|
||||
* - HTML file itself is saved - HMR doesn't handle this because the HTML pages are pre-rendered
|
||||
* - Chrome is OK reloading the page when the HTML file is changed without reloading the whole
|
||||
* extension. Not sure about firefox, this might need to change to an extension reload
|
||||
* - Reload extension
|
||||
* - Background script is changed
|
||||
* - Manifest is different
|
||||
* - Restart browser
|
||||
* - Config file changed (wxt.config.ts, .env, web-ext.config.ts, etc)
|
||||
*/
|
||||
export function detectDevChanges(
|
||||
changedFiles: [event: string, path: string][],
|
||||
currentOutput: BuildOutput | undefined,
|
||||
): DevModeChange {
|
||||
if (currentOutput == null) return { type: 'no-change' };
|
||||
|
||||
const changedSteps = new Set(
|
||||
changedFiles.flatMap((changedFile) =>
|
||||
findEffectedSteps(changedFile, currentOutput),
|
||||
),
|
||||
);
|
||||
if (changedSteps.size === 0) return { type: 'no-change' };
|
||||
|
||||
const unchangedOutput: BuildOutput = {
|
||||
manifest: currentOutput.manifest,
|
||||
steps: [],
|
||||
publicAssets: [],
|
||||
};
|
||||
const changedOutput: BuildOutput = {
|
||||
manifest: currentOutput.manifest,
|
||||
steps: [],
|
||||
publicAssets: [],
|
||||
};
|
||||
|
||||
for (const step of currentOutput.steps) {
|
||||
if (changedSteps.has(step)) {
|
||||
changedOutput.steps.push(step);
|
||||
} else {
|
||||
unchangedOutput.steps.push(step);
|
||||
}
|
||||
}
|
||||
for (const asset of currentOutput.publicAssets) {
|
||||
if (changedSteps.has(asset)) {
|
||||
changedOutput.publicAssets.push(asset);
|
||||
} else {
|
||||
unchangedOutput.publicAssets.push(asset);
|
||||
}
|
||||
}
|
||||
|
||||
const isOnlyHtmlChanges =
|
||||
changedFiles.length > 0 &&
|
||||
every(changedFiles, ([_, file]) => file.endsWith('.html'));
|
||||
if (isOnlyHtmlChanges) {
|
||||
return {
|
||||
type: 'html-reload',
|
||||
cachedOutput: unchangedOutput,
|
||||
rebuildGroups: changedOutput.steps.map((step) => step.entrypoints),
|
||||
};
|
||||
}
|
||||
|
||||
const isOnlyContentScripts =
|
||||
changedOutput.steps.length > 0 &&
|
||||
every(
|
||||
changedOutput.steps.flatMap((step) => step.entrypoints),
|
||||
(entry) => entry.type === 'content-script',
|
||||
);
|
||||
if (isOnlyContentScripts) {
|
||||
return {
|
||||
type: 'content-script-reload',
|
||||
cachedOutput: unchangedOutput,
|
||||
changedSteps: changedOutput.steps,
|
||||
rebuildGroups: changedOutput.steps.map((step) => step.entrypoints),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'extension-reload',
|
||||
cachedOutput: unchangedOutput,
|
||||
rebuildGroups: changedOutput.steps.map((step) => step.entrypoints),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* For a single change, return all the step of the build output that were effected by it.
|
||||
*/
|
||||
function findEffectedSteps(
|
||||
changedFile: [event: string, path: string],
|
||||
currentOutput: BuildOutput,
|
||||
): DetectedChange[] {
|
||||
const changes: DetectedChange[] = [];
|
||||
const changedPath = changedFile[1];
|
||||
|
||||
const isChunkEffected = (
|
||||
chunk: vite.Rollup.OutputChunk | vite.Rollup.OutputAsset,
|
||||
): boolean =>
|
||||
// If it's an HTML file with the same path, is is effected because HTML files need to be pre-rendered
|
||||
// TODO: use bundle path to support `<name>/index.html`?
|
||||
(chunk.type === 'asset' && changedPath.endsWith(chunk.fileName)) ||
|
||||
// If it's a chunk that depends on the changed file, it is effected
|
||||
(chunk.type === 'chunk' && chunk.moduleIds.includes(changedPath));
|
||||
|
||||
for (const step of currentOutput.steps) {
|
||||
const effectedChunk = step.chunks.find((chunk) => isChunkEffected(chunk));
|
||||
if (effectedChunk) changes.push(step);
|
||||
}
|
||||
|
||||
const effectedAsset = currentOutput.publicAssets.find((chunk) =>
|
||||
isChunkEffected(chunk),
|
||||
);
|
||||
if (effectedAsset) changes.push(effectedAsset);
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Contains information about what files changed, what needs rebuilt, and the type of reload that is
|
||||
* required.
|
||||
*/
|
||||
export type DevModeChange =
|
||||
| NoChange
|
||||
| HtmlReload
|
||||
| ExtensionReload
|
||||
| ContentScriptReload;
|
||||
// | BrowserRestart
|
||||
|
||||
interface NoChange {
|
||||
type: 'no-change';
|
||||
}
|
||||
|
||||
interface RebuildChange {
|
||||
/**
|
||||
* The list of entrypoints that need rebuilt.
|
||||
*/
|
||||
rebuildGroups: EntrypointGroup[];
|
||||
/**
|
||||
* The previous output stripped of any files are going to change.
|
||||
*/
|
||||
cachedOutput: BuildOutput;
|
||||
}
|
||||
|
||||
interface HtmlReload extends RebuildChange {
|
||||
type: 'html-reload';
|
||||
}
|
||||
|
||||
interface ExtensionReload extends RebuildChange {
|
||||
type: 'extension-reload';
|
||||
}
|
||||
|
||||
// interface BrowserRestart extends RebuildChange {
|
||||
// type: 'browser-restart';
|
||||
// }
|
||||
|
||||
interface ContentScriptReload extends RebuildChange {
|
||||
type: 'content-script-reload';
|
||||
changedSteps: BuildStepOutput[];
|
||||
}
|
||||
|
||||
/**
|
||||
* When figuring out what needs reloaded, this stores the step that was changed, or the public
|
||||
* directory asset that was changed. It doesn't know what type of change is required yet. Just an
|
||||
* intermediate type.
|
||||
*/
|
||||
type DetectedChange = BuildStepOutput | vite.Rollup.OutputAsset;
|
||||
@@ -1,13 +1,15 @@
|
||||
import {
|
||||
ConfigEnv,
|
||||
ExtensionRunnerConfig,
|
||||
InlineConfig,
|
||||
InternalConfig,
|
||||
UserConfig,
|
||||
UserManifest,
|
||||
UserManifestFn,
|
||||
} from '../types';
|
||||
import path, { resolve } from 'node:path';
|
||||
import * as vite from 'vite';
|
||||
import { consola } from 'consola';
|
||||
import { importTsFile } from './importTsFile';
|
||||
import * as plugins from '../vite-plugins';
|
||||
import { createFsCache } from './createFsCache';
|
||||
import { getGlobals } from './globals';
|
||||
@@ -43,7 +45,6 @@ export async function getInternalConfig(
|
||||
command,
|
||||
logger,
|
||||
vite: config.vite ?? {},
|
||||
manifest: config.manifest ?? {},
|
||||
imports: config.imports ?? {},
|
||||
runnerConfig: await loadConfig<ExtensionRunnerConfig>({
|
||||
name: 'web-ext',
|
||||
@@ -59,10 +60,12 @@ export async function getInternalConfig(
|
||||
mode,
|
||||
};
|
||||
if (config.configFile !== false) {
|
||||
userConfig = await importTsFile<UserConfig>(
|
||||
root,
|
||||
path.resolve(root, config.configFile ?? 'wxt.config.ts'),
|
||||
);
|
||||
const loaded = await loadConfig<UserConfig>({
|
||||
name: 'wxt',
|
||||
cwd: root,
|
||||
rcFile: false,
|
||||
});
|
||||
userConfig = loaded.config ?? {};
|
||||
}
|
||||
|
||||
// Merge inline and user configs
|
||||
@@ -81,6 +84,12 @@ export async function getInternalConfig(
|
||||
const wxtDir = resolve(srcDir, '.wxt');
|
||||
const typesDir = resolve(wxtDir, 'types');
|
||||
|
||||
// Merge manifest sources
|
||||
const env: ConfigEnv = { mode, browser, manifestVersion, command };
|
||||
const userManifest = await resolveManifestConfig(env, userConfig.manifest);
|
||||
const inlineManifest = await resolveManifestConfig(env, config.manifest);
|
||||
const manifest = vite.mergeConfig(userManifest, inlineManifest);
|
||||
|
||||
const finalConfig: InternalConfig = {
|
||||
...merged,
|
||||
srcDir,
|
||||
@@ -89,12 +98,13 @@ export async function getInternalConfig(
|
||||
wxtDir: wxtDir,
|
||||
typesDir,
|
||||
fsCache: createFsCache(wxtDir),
|
||||
manifest,
|
||||
};
|
||||
|
||||
// Customize the default vite config
|
||||
finalConfig.vite.root = root;
|
||||
finalConfig.vite.configFile = false;
|
||||
finalConfig.vite.logLevel = 'silent';
|
||||
finalConfig.vite.logLevel = 'warn';
|
||||
|
||||
finalConfig.vite.build ??= {};
|
||||
finalConfig.vite.build.outDir = outDir;
|
||||
@@ -110,6 +120,9 @@ export async function getInternalConfig(
|
||||
finalConfig.vite.plugins.push(
|
||||
plugins.virtualEntrypoin('content-script', finalConfig),
|
||||
);
|
||||
finalConfig.vite.plugins.push(plugins.devServerGlobals(finalConfig));
|
||||
finalConfig.vite.plugins.push(plugins.tsconfigPaths(finalConfig));
|
||||
finalConfig.vite.plugins.push(plugins.hmrLogger(finalConfig));
|
||||
|
||||
finalConfig.vite.define ??= {};
|
||||
getGlobals(finalConfig).forEach((global) => {
|
||||
@@ -125,5 +138,20 @@ export async function getInternalConfig(
|
||||
*/
|
||||
type InternalConfigNoUserDirs = Omit<
|
||||
InternalConfig,
|
||||
'srcDir' | 'publicDir' | 'entrypointsDir' | 'wxtDir' | 'typesDir' | 'fsCache'
|
||||
| 'srcDir'
|
||||
| 'publicDir'
|
||||
| 'entrypointsDir'
|
||||
| 'wxtDir'
|
||||
| 'typesDir'
|
||||
| 'fsCache'
|
||||
| 'manifest'
|
||||
>;
|
||||
|
||||
async function resolveManifestConfig(
|
||||
env: ConfigEnv,
|
||||
manifest: UserManifest | Promise<UserManifest> | UserManifestFn | undefined,
|
||||
): Promise<UserManifest> {
|
||||
return await (typeof manifest === 'function'
|
||||
? manifest(env)
|
||||
: manifest ?? {});
|
||||
}
|
||||
|
||||
@@ -39,5 +39,10 @@ export function getGlobals(
|
||||
value: config.browser === 'opera',
|
||||
type: `boolean`,
|
||||
},
|
||||
{
|
||||
name: '__COMMAND__',
|
||||
value: config.command,
|
||||
type: `"build" | "serve"`,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,44 +1,67 @@
|
||||
import { consola } from 'consola';
|
||||
import createJITI from 'jiti';
|
||||
import transform from 'jiti/dist/babel';
|
||||
import { InternalConfig } from '../types';
|
||||
import { createUnimport } from 'unimport';
|
||||
import fs from 'fs-extra';
|
||||
import { resolve } from 'path';
|
||||
import { scanExports } from 'unimport';
|
||||
import transform from 'jiti/dist/babel';
|
||||
import { getUnimportOptions } from './auto-imports';
|
||||
|
||||
export async function importTsFile<T>(root: string, path: string): Promise<T> {
|
||||
const clientImports = await scanExports(
|
||||
resolve(root, 'node_modules/wxt/dist/client/index.js'),
|
||||
/**
|
||||
* Get the value from the default export of a `path`.
|
||||
*
|
||||
* It works by:
|
||||
*
|
||||
* 1. Reading the file text
|
||||
* 2. Stripping all imports from it via regex
|
||||
* 3. Auto-import only the client helper functions
|
||||
*
|
||||
* This prevents resolving imports of imports, speeding things up and preventing "xxx is not
|
||||
* defined" errors.
|
||||
*
|
||||
* Downside is that code cannot be executed outside of the main fucntion for the entrypoint,
|
||||
* otherwise you will see "xxx is not defined" errors for any imports used outside of main function.
|
||||
*/
|
||||
export async function importTsFile<T>(
|
||||
path: string,
|
||||
config: InternalConfig,
|
||||
): Promise<T> {
|
||||
config.logger.debug('Loading file metadata:', path);
|
||||
|
||||
const unimport = createUnimport({
|
||||
...getUnimportOptions(config),
|
||||
// Only allow specific imports, not all from the project
|
||||
imports: [{ name: '*', as: 'browser', from: 'webextension-polyfill' }],
|
||||
dirs: [],
|
||||
});
|
||||
await unimport.init();
|
||||
|
||||
const text = await fs.readFile(path, 'utf-8');
|
||||
const textNoImports = text.replace(/import.*[\n;]/gm, '');
|
||||
const { code } = await unimport.injectImports(textNoImports);
|
||||
config.logger.debug(
|
||||
['Text:', text, 'No imports:', textNoImports, 'Code:', code].join('\n'),
|
||||
);
|
||||
|
||||
const jiti = createJITI(__filename, {
|
||||
cache: false,
|
||||
esmResolve: true,
|
||||
interopDefault: true,
|
||||
|
||||
alias: {
|
||||
'webextension-polyfill': resolve(
|
||||
config.root,
|
||||
'node_modules/wxt/dist/virtual-modules/fake-browser.js',
|
||||
),
|
||||
},
|
||||
transform(opts) {
|
||||
// Remove CSS imports from the source code - Jiti can't handle them.
|
||||
opts.source = opts.source.replace(/^import ['"].*\.css['"];?$/gm, '');
|
||||
opts.source = opts.source.replace(
|
||||
/^import\s+.*\s+from ['"]webextension-polyfill['"];?$/gm,
|
||||
'',
|
||||
);
|
||||
|
||||
// Append any wxt/client functions so babel doesn't complain about undefined variables
|
||||
if (opts.filename === path) {
|
||||
// TODO: Only append import if it isn't already imported
|
||||
const imports =
|
||||
clientImports
|
||||
.map((i) => `import { ${i.name} } from "${i.from}";`)
|
||||
.join('\n') + '\n';
|
||||
opts.source = imports + opts.source;
|
||||
}
|
||||
|
||||
// Call the default babel transformer with our modified source code
|
||||
return transform(opts);
|
||||
if (opts.filename === path) return transform({ ...opts, source: code });
|
||||
else return transform(opts);
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
return await jiti(path);
|
||||
} catch (err) {
|
||||
consola.error(`Failed to import file: ${path}`);
|
||||
config.logger.error(err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ export async function writeManifest(
|
||||
await fs.ensureDir(config.outDir);
|
||||
await fs.writeFile(resolve(config.outDir, 'manifest.json'), str, 'utf-8');
|
||||
|
||||
output.unshift({
|
||||
output.publicAssets.unshift({
|
||||
type: 'asset',
|
||||
fileName: 'manifest.json',
|
||||
name: 'manifest',
|
||||
@@ -43,7 +43,7 @@ export async function writeManifest(
|
||||
*/
|
||||
export async function generateMainfest(
|
||||
entrypoints: Entrypoint[],
|
||||
buildOutput: BuildOutput,
|
||||
buildOutput: Omit<BuildOutput, 'manifest'>,
|
||||
config: InternalConfig,
|
||||
): Promise<Manifest.WebExtensionManifest> {
|
||||
const pkg = await getPackageJson(config);
|
||||
@@ -65,6 +65,7 @@ export async function generateMainfest(
|
||||
addEntrypoints(manifest, entrypoints, buildOutput, config);
|
||||
|
||||
if (config.command === 'serve') addDevModeCsp(manifest, config);
|
||||
if (config.command === 'serve') addDevModePermissions(manifest, config);
|
||||
|
||||
return manifest;
|
||||
}
|
||||
@@ -100,7 +101,7 @@ function simplifyVersion(versionName: string): string {
|
||||
function addEntrypoints(
|
||||
manifest: Manifest.WebExtensionManifest,
|
||||
entrypoints: Entrypoint[],
|
||||
buildOutput: BuildOutput,
|
||||
buildOutput: Omit<BuildOutput, 'manifest'>,
|
||||
config: InternalConfig,
|
||||
): void {
|
||||
const entriesByType = entrypoints.reduce<
|
||||
@@ -270,24 +271,23 @@ function addEntrypoints(
|
||||
}
|
||||
|
||||
if (contentScripts?.length) {
|
||||
if (config.command === 'serve') {
|
||||
const permissionsKey =
|
||||
config.manifestVersion === 2 ? 'permissions' : 'host_permissions';
|
||||
const hostPermissions = new Set<string>(manifest[permissionsKey] ?? []);
|
||||
// Don't add content scripts to the manifest in dev mode for MV3 - they're managed and reloaded
|
||||
// at runtime
|
||||
if (config.command === 'serve' && config.manifestVersion === 3) {
|
||||
const hostPermissions = new Set<string>(manifest.host_permissions ?? []);
|
||||
contentScripts.forEach((script) => {
|
||||
script.options.matches.forEach((matchPattern) => {
|
||||
hostPermissions.add(matchPattern);
|
||||
});
|
||||
});
|
||||
manifest[permissionsKey] = Array.from(hostPermissions).sort();
|
||||
hostPermissions.forEach((permission) =>
|
||||
addHostPermission(manifest, permission),
|
||||
);
|
||||
} else {
|
||||
const hashToEntrypointsMap = contentScripts.reduce((map, script) => {
|
||||
const hash = JSON.stringify(script.options);
|
||||
if (!map.has(hash)) {
|
||||
map.set(hash, [script]);
|
||||
} else {
|
||||
map.get(hash)?.push(script);
|
||||
}
|
||||
if (map.has(hash)) map.get(hash)?.push(script);
|
||||
else map.set(hash, [script]);
|
||||
return map;
|
||||
}, new Map<string, ContentScriptEntrypoint[]>());
|
||||
|
||||
@@ -316,13 +316,9 @@ function addDevModeCsp(
|
||||
const allowedCsp = config.server?.origin ?? 'http://localhost:*';
|
||||
|
||||
if (manifest.manifest_version === 3) {
|
||||
manifest.host_permissions ??= [];
|
||||
if (!manifest.host_permissions.includes(permission))
|
||||
manifest.host_permissions.push(permission);
|
||||
addHostPermission(manifest, permission);
|
||||
} else {
|
||||
manifest.permissions ??= [];
|
||||
if (!manifest.permissions.includes(permission))
|
||||
manifest.permissions.push(permission);
|
||||
addPermission(manifest, permission);
|
||||
}
|
||||
|
||||
const csp = new ContentSecurityPolicy(
|
||||
@@ -345,20 +341,33 @@ function addDevModeCsp(
|
||||
}
|
||||
}
|
||||
|
||||
function addDevModePermissions(
|
||||
manifest: Manifest.WebExtensionManifest,
|
||||
config: InternalConfig,
|
||||
) {
|
||||
// For reloading the page
|
||||
addPermission(manifest, 'tabs');
|
||||
|
||||
// For registering content scripts
|
||||
if (config.manifestVersion === 3) addPermission(manifest, 'scripting');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the bundle paths to CSS files associated with a list of content scripts, or undefined if
|
||||
* there is no associated CSS.
|
||||
*/
|
||||
function getContentScriptCssFiles(
|
||||
export function getContentScriptCssFiles(
|
||||
contentScripts: ContentScriptEntrypoint[],
|
||||
buildOutput: BuildOutput,
|
||||
buildOutput: Omit<BuildOutput, 'manifest'>,
|
||||
): string[] | undefined {
|
||||
const css: string[] = [];
|
||||
|
||||
const allChunks = buildOutput.steps.flatMap((step) => step.chunks);
|
||||
|
||||
contentScripts.forEach((script) => {
|
||||
const cssRegex = new RegExp(`^assets/${script.name}-[a-f0-9]{8}.css$`);
|
||||
const relatedCss = buildOutput.find((chunk) =>
|
||||
chunk.fileName.match(cssRegex),
|
||||
// TODO: optimize and remove loop with a map
|
||||
const relatedCss = allChunks.find(
|
||||
(chunk) => chunk.fileName === `assets/${script.name}.css`,
|
||||
);
|
||||
if (relatedCss) css.push(relatedCss.fileName);
|
||||
});
|
||||
@@ -366,3 +375,21 @@ function getContentScriptCssFiles(
|
||||
if (css.length > 0) return css;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function addPermission(
|
||||
manifest: Manifest.WebExtensionManifest,
|
||||
permission: string,
|
||||
): void {
|
||||
manifest.permissions ??= [];
|
||||
if (manifest.permissions.includes(permission)) return;
|
||||
manifest.permissions.push(permission);
|
||||
}
|
||||
|
||||
function addHostPermission(
|
||||
manifest: Manifest.WebExtensionManifest,
|
||||
hostPermission: string,
|
||||
): void {
|
||||
manifest.host_permissions ??= [];
|
||||
if (manifest.host_permissions.includes(hostPermission)) return;
|
||||
manifest.host_permissions.push(hostPermission);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,18 @@ export function devHtmlPrerender(config: InternalConfig): vite.Plugin {
|
||||
return {
|
||||
apply: 'build',
|
||||
name: 'wxt:dev-html-prerender',
|
||||
config() {
|
||||
return {
|
||||
resolve: {
|
||||
alias: {
|
||||
'@wxt/reload-html': resolve(
|
||||
config.root,
|
||||
'node_modules/wxt/dist/virtual-modules/reload-html.js',
|
||||
),
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
async transform(html, id) {
|
||||
const server = config.server;
|
||||
if (config.command !== 'serve' || server == null || !id.endsWith('.html'))
|
||||
@@ -43,6 +55,12 @@ export function devHtmlPrerender(config: InternalConfig): vite.Plugin {
|
||||
pointToDevServer('script[type=module]', 'src');
|
||||
pointToDevServer('link[rel=stylesheet]', 'href');
|
||||
|
||||
// Add a script to add page reloading
|
||||
const reloader = document.createElement('script');
|
||||
reloader.src = '@wxt/reload-html';
|
||||
reloader.type = 'module';
|
||||
document.head.appendChild(reloader);
|
||||
|
||||
const newHtml = document.toString();
|
||||
config.logger.debug('Transformed ' + id);
|
||||
config.logger.debug('Old HTML:\n' + html);
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Plugin } from 'vite';
|
||||
import { InternalConfig } from '../types';
|
||||
|
||||
/**
|
||||
* Defines global constants about the dev server. Helps scripts connect to the server's web socket.
|
||||
*/
|
||||
export function devServerGlobals(internalConfig: InternalConfig): Plugin {
|
||||
return {
|
||||
name: 'wxt:dev-server-globals',
|
||||
config() {
|
||||
if (internalConfig.server == null || internalConfig.command == 'build')
|
||||
return;
|
||||
|
||||
return {
|
||||
define: {
|
||||
__DEV_SERVER_PROTOCOL__: JSON.stringify('ws:'),
|
||||
__DEV_SERVER_HOSTNAME__: JSON.stringify(
|
||||
internalConfig.server.hostname,
|
||||
),
|
||||
__DEV_SERVER_PORT__: JSON.stringify(internalConfig.server.port),
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
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)),
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
export * from './download';
|
||||
export * from './unimport';
|
||||
export * from './multipageMove';
|
||||
export * from './devHtmlPrerender';
|
||||
export * from './devServerGlobals';
|
||||
export * from './download';
|
||||
export * from './multipageMove';
|
||||
export * from './unimport';
|
||||
export * from './virtualEntrypoint';
|
||||
export * from './tsconfigPaths';
|
||||
export * from './hmrLogger';
|
||||
|
||||
@@ -53,7 +53,7 @@ export function multipageMove(
|
||||
const oldAbsPath = resolve(config.outDir, oldBundlePath);
|
||||
const newAbsPath = resolve(config.outDir, newBundlePath);
|
||||
await ensureDir(dirname(newAbsPath));
|
||||
await fs.move(oldAbsPath, newAbsPath);
|
||||
await fs.move(oldAbsPath, newAbsPath, { overwrite: true });
|
||||
|
||||
const renamedChunk = {
|
||||
...bundle[oldBundlePath],
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { InternalConfig } from '../types';
|
||||
import paths from 'vite-tsconfig-paths';
|
||||
|
||||
export function tsconfigPaths(config: InternalConfig) {
|
||||
const fn: typeof paths =
|
||||
typeof paths === 'function' ? paths : (paths as any).default;
|
||||
return fn({
|
||||
root: config.root,
|
||||
});
|
||||
}
|
||||
@@ -2,6 +2,16 @@ import { createUnimport } from 'unimport';
|
||||
import { InternalConfig } from '../types';
|
||||
import { getUnimportOptions } from '../utils/auto-imports';
|
||||
import { Plugin } from 'vite';
|
||||
import { extname } from 'path';
|
||||
|
||||
const ENABLED_EXTENSIONS: Record<string, boolean | undefined> = {
|
||||
'.js': true,
|
||||
'.jsx': true,
|
||||
'.ts': true,
|
||||
'.tsx': true,
|
||||
'.vue': true,
|
||||
'.svelte': true,
|
||||
};
|
||||
|
||||
/**
|
||||
* Inject any global imports defined by unimport
|
||||
@@ -16,7 +26,8 @@ export function unimport(config: InternalConfig): Plugin {
|
||||
await unimport.scanImportsFromDir(undefined, { cwd: config.srcDir });
|
||||
},
|
||||
async transform(code, id) {
|
||||
return unimport.injectImports(code, id);
|
||||
const ext = extname(id);
|
||||
if (ENABLED_EXTENSIONS[ext]) return unimport.injectImports(code, id);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -29,10 +29,13 @@ export function virtualEntrypoin(
|
||||
|
||||
const inputPath = id.replace(resolvedVirtualId, '');
|
||||
const template = await fs.readFile(
|
||||
resolve(config.root, `node_modules/wxt/templates/virtual-${type}.ts`),
|
||||
resolve(
|
||||
config.root,
|
||||
`node_modules/wxt/dist/virtual-modules/${type}-entrypoint.js`,
|
||||
),
|
||||
'utf-8',
|
||||
);
|
||||
return template.replaceAll('{{moduleId}}', inputPath);
|
||||
return template.replace(`virtual:user-${type}`, inputPath);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
import {
|
||||
BuildOutput,
|
||||
WxtDevServer,
|
||||
InlineConfig,
|
||||
InternalConfig,
|
||||
} from './core/types';
|
||||
import { BuildOutput, WxtDevServer, InlineConfig } from './core/types';
|
||||
import { getInternalConfig } from './core/utils/getInternalConfig';
|
||||
import { findEntrypoints } from './core/build/findEntrypoints';
|
||||
import { buildEntrypoints } from './core/build/buildEntrypoints';
|
||||
import { generateMainfest, writeManifest } from './core/utils/manifest';
|
||||
import { printBuildSummary } from './core/log/printBuildSummary';
|
||||
import fs from 'fs-extra';
|
||||
import { generateTypesDir } from './core/build/generateTypesDir';
|
||||
import pc from 'picocolors';
|
||||
import * as vite from 'vite';
|
||||
import { findOpenPort } from './core/utils/findOpenPort';
|
||||
import { formatDuration } from './core/utils/formatDuration';
|
||||
import { createWebExtRunner } from './core/runners/createWebExtRunner';
|
||||
import { detectDevChanges } from './core/utils/detectDevChanges';
|
||||
import { Mutex } from 'async-mutex';
|
||||
import { consola } from 'consola';
|
||||
import { relative } from 'node:path';
|
||||
import { getEntrypointOutputFile } from './core/utils/entrypoints';
|
||||
import { buildInternal, rebuild } from './core/build';
|
||||
import {
|
||||
getServerInfo,
|
||||
reloadContentScripts,
|
||||
reloadHtmlPages,
|
||||
setupServer,
|
||||
} from './core/server';
|
||||
|
||||
export { version } from '../package.json';
|
||||
export * from './core/types/external';
|
||||
@@ -33,77 +31,79 @@ export async function build(config: InlineConfig): Promise<BuildOutput> {
|
||||
export async function createServer(
|
||||
config?: InlineConfig,
|
||||
): Promise<WxtDevServer> {
|
||||
const port = await findOpenPort(3000, 3010);
|
||||
const hostname = 'localhost';
|
||||
const origin = `http://${hostname}:${port}`;
|
||||
const serverConfig: vite.InlineConfig = {
|
||||
server: {
|
||||
origin,
|
||||
},
|
||||
const serverInfo = await getServerInfo();
|
||||
|
||||
const getLatestInternalConfig = () => {
|
||||
const viteConfig: vite.InlineConfig = vite.mergeConfig(
|
||||
serverInfo.viteServerConfig,
|
||||
config?.vite ?? {},
|
||||
);
|
||||
return getInternalConfig({ ...config, vite: viteConfig }, 'serve');
|
||||
};
|
||||
const internalConfig = await getInternalConfig(
|
||||
vite.mergeConfig(serverConfig, config ?? {}),
|
||||
'serve',
|
||||
);
|
||||
const runner = createWebExtRunner();
|
||||
|
||||
const viteServer = await vite.createServer(internalConfig.vite);
|
||||
const server: WxtDevServer = {
|
||||
...viteServer,
|
||||
async listen(port, isRestart) {
|
||||
const res = await viteServer.listen(port, isRestart);
|
||||
|
||||
if (!isRestart) {
|
||||
internalConfig.logger.success(`Started dev server @ ${origin}`);
|
||||
|
||||
internalConfig.logger.info('Opening browser...');
|
||||
await runner.openBrowser(internalConfig);
|
||||
internalConfig.logger.success('Opened!');
|
||||
}
|
||||
|
||||
return res;
|
||||
},
|
||||
logger: internalConfig.logger,
|
||||
port,
|
||||
hostname,
|
||||
origin,
|
||||
};
|
||||
internalConfig.logger.info('Created dev server');
|
||||
|
||||
let internalConfig = await getLatestInternalConfig();
|
||||
const server = await setupServer(serverInfo, internalConfig);
|
||||
internalConfig.server = server;
|
||||
await buildInternal(internalConfig);
|
||||
|
||||
const fileChangedMutex = new Mutex();
|
||||
const changeQueue: Array<[string, string]> = [];
|
||||
|
||||
server.ws.on('wxt:background-initialized', () => {
|
||||
// Register content scripts for the first time since they're not listed in the manifest
|
||||
reloadContentScripts(server.currentOutput.steps, internalConfig, server);
|
||||
});
|
||||
|
||||
server.watcher.on('all', async (event, path, _stats) => {
|
||||
if (path.startsWith(internalConfig.outBaseDir)) return;
|
||||
changeQueue.push([event, path]);
|
||||
|
||||
await fileChangedMutex.runExclusive(async () => {
|
||||
const fileChanges = changeQueue.splice(0, changeQueue.length);
|
||||
const changes = detectDevChanges(fileChanges, server.currentOutput);
|
||||
|
||||
if (changes.type === 'no-change') return;
|
||||
|
||||
// Log the entrypoints that were effected
|
||||
internalConfig.logger.info(
|
||||
`Changed: ${Array.from(new Set(fileChanges.map((change) => change[1])))
|
||||
.map((file) => pc.dim(relative(internalConfig.root, file)))
|
||||
.join(', ')}`,
|
||||
);
|
||||
const rebuiltNames = changes.rebuildGroups
|
||||
.flat()
|
||||
.map((entry) => {
|
||||
return pc.cyan(
|
||||
relative(internalConfig.outDir, getEntrypointOutputFile(entry, '')),
|
||||
);
|
||||
})
|
||||
.join(pc.dim(', '));
|
||||
|
||||
// Get latest config and Rebuild groups with changes
|
||||
internalConfig = await getLatestInternalConfig();
|
||||
internalConfig.server = server;
|
||||
const { output: newOutput } = await rebuild(
|
||||
internalConfig,
|
||||
// TODO: this excludes new entrypoints, so they're not built until the dev command is restarted
|
||||
changes.rebuildGroups,
|
||||
changes.cachedOutput,
|
||||
);
|
||||
server.currentOutput = newOutput;
|
||||
|
||||
// Perform reloads
|
||||
switch (changes.type) {
|
||||
case 'extension-reload':
|
||||
server.reloadExtension();
|
||||
break;
|
||||
case 'html-reload':
|
||||
reloadHtmlPages(changes.rebuildGroups, server, internalConfig);
|
||||
break;
|
||||
case 'content-script-reload':
|
||||
reloadContentScripts(changes.changedSteps, internalConfig, server);
|
||||
break;
|
||||
}
|
||||
consola.success(`Reloaded: ${rebuiltNames}`);
|
||||
});
|
||||
});
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
async function buildInternal(config: InternalConfig): Promise<BuildOutput> {
|
||||
const verb = config.command === 'serve' ? 'Pre-rendering' : 'Building';
|
||||
const target = `${config.browser}-mv${config.manifestVersion}`;
|
||||
config.logger.info(
|
||||
`${verb} ${pc.cyan(target)} for ${pc.cyan(config.mode)} with ${pc.green(
|
||||
`Vite ${vite.version}`,
|
||||
)}`,
|
||||
);
|
||||
const startTime = Date.now();
|
||||
|
||||
// Cleanup
|
||||
await fs.rm(config.outDir, { recursive: true, force: true });
|
||||
await fs.ensureDir(config.outDir);
|
||||
|
||||
// Build
|
||||
const entrypoints = await findEntrypoints(config);
|
||||
await generateTypesDir(entrypoints, config);
|
||||
const output = await buildEntrypoints(entrypoints, config);
|
||||
|
||||
// Write manifest
|
||||
const manifest = await generateMainfest(entrypoints, output, config);
|
||||
await writeManifest(manifest, output, config);
|
||||
|
||||
// Post-build
|
||||
config.logger.success(
|
||||
`Built extension in ${formatDuration(Date.now() - startTime)}`,
|
||||
);
|
||||
await printBuildSummary(output, config);
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
/**
|
||||
* SHOULD ONLY BE IMPORTED IN TESTS.
|
||||
*/
|
||||
import { resolve } from 'path';
|
||||
import {
|
||||
BackgroundEntrypoint,
|
||||
ContentScriptEntrypoint,
|
||||
GenericEntrypoint,
|
||||
OptionsEntrypoint,
|
||||
PopupEntrypoint,
|
||||
} from '..';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import merge from 'lodash.merge';
|
||||
import { Rollup } from 'vite';
|
||||
import { Manifest } from 'webextension-polyfill';
|
||||
import { FsCache, InternalConfig, WxtDevServer } from '../core/types';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
|
||||
faker.seed(__TEST_SEED__);
|
||||
|
||||
type DeepPartial<T> = { [key in keyof T]+?: Partial<T[key]> };
|
||||
function fakeObjectCreator<T>(base: () => T) {
|
||||
return (overrides?: DeepPartial<T>): T => merge(base(), overrides);
|
||||
}
|
||||
|
||||
export function fakeFileName(): string {
|
||||
return faker.string.alphanumeric() + '.' + faker.string.alpha({ length: 3 });
|
||||
}
|
||||
|
||||
export function fakeFile(root = process.cwd()): string {
|
||||
return resolve(root, fakeFileName());
|
||||
}
|
||||
|
||||
export function fakeDir(root = process.cwd()): string {
|
||||
return resolve(root, faker.string.alphanumeric());
|
||||
}
|
||||
|
||||
export const fakeContentScriptEntrypoint =
|
||||
fakeObjectCreator<ContentScriptEntrypoint>(() => ({
|
||||
type: 'content-script',
|
||||
inputPath: fakeFile('src'),
|
||||
name: faker.string.alpha(),
|
||||
options: {
|
||||
matches: [],
|
||||
matchAboutBlank: faker.helpers.arrayElement([true, false, undefined]),
|
||||
matchOriginAsFallback: faker.helpers.arrayElement([
|
||||
true,
|
||||
false,
|
||||
undefined,
|
||||
]),
|
||||
runAt: faker.helpers.arrayElement([
|
||||
'document_start',
|
||||
'document_end',
|
||||
'document_idle',
|
||||
undefined,
|
||||
]),
|
||||
},
|
||||
outputDir: fakeDir('.output'),
|
||||
}));
|
||||
|
||||
export const fakeBackgroundEntrypoint = fakeObjectCreator<BackgroundEntrypoint>(
|
||||
() => ({
|
||||
type: 'background',
|
||||
inputPath: fakeFile('src'),
|
||||
name: faker.string.alpha(),
|
||||
options: {
|
||||
persistent: faker.helpers.arrayElement([true, false, undefined]),
|
||||
type: faker.helpers.maybe(() => 'module'),
|
||||
},
|
||||
outputDir: fakeDir('.output'),
|
||||
}),
|
||||
);
|
||||
|
||||
export const fakeUnlistedScriptEntrypoint =
|
||||
fakeObjectCreator<GenericEntrypoint>(() => ({
|
||||
type: 'unlisted-script',
|
||||
inputPath: fakeFile('src'),
|
||||
name: faker.string.alpha(),
|
||||
outputDir: fakeDir('.output'),
|
||||
}));
|
||||
|
||||
export const fakeOptionsEntrypoint = fakeObjectCreator<OptionsEntrypoint>(
|
||||
() => ({
|
||||
type: 'options',
|
||||
inputPath: fakeFile('src'),
|
||||
name: faker.string.alpha(),
|
||||
outputDir: fakeDir('.output'),
|
||||
options: {
|
||||
browserStyle: faker.helpers.arrayElement([true, false, undefined]),
|
||||
chromeStyle: faker.helpers.arrayElement([true, false, undefined]),
|
||||
openInTab: faker.helpers.arrayElement([true, false, undefined]),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
export const fakePopupEntrypoint = fakeObjectCreator<PopupEntrypoint>(() => ({
|
||||
type: 'popup',
|
||||
inputPath: fakeFile('src'),
|
||||
name: faker.string.alpha(),
|
||||
outputDir: fakeDir('.output'),
|
||||
options: {
|
||||
defaultTitle: faker.helpers.arrayElement([
|
||||
faker.person.fullName(),
|
||||
undefined,
|
||||
]),
|
||||
defaultIcon: faker.helpers.arrayElement([
|
||||
{
|
||||
'16': 'icon/16.png',
|
||||
'24': 'icon/24.png',
|
||||
'64': 'icon/64.png',
|
||||
},
|
||||
]),
|
||||
mv2Key: faker.helpers.arrayElement([
|
||||
'browser_action',
|
||||
'page_action',
|
||||
undefined,
|
||||
]),
|
||||
},
|
||||
}));
|
||||
|
||||
export const fakeGenericEntrypoint = fakeObjectCreator<GenericEntrypoint>(
|
||||
() => ({
|
||||
type: faker.helpers.arrayElement([
|
||||
'sandbox',
|
||||
'bookmarks',
|
||||
'history',
|
||||
'newtab',
|
||||
'sidepanel',
|
||||
'devtools',
|
||||
'unlisted-page',
|
||||
'unlisted-script',
|
||||
]),
|
||||
inputPath: fakeFile('src'),
|
||||
name: faker.string.alpha(),
|
||||
outputDir: fakeDir('.output'),
|
||||
}),
|
||||
);
|
||||
|
||||
export const fakeRollupOutputChunk = fakeObjectCreator<Rollup.OutputChunk>(
|
||||
() => ({
|
||||
type: 'chunk',
|
||||
code: '',
|
||||
dynamicImports: [],
|
||||
exports: [],
|
||||
facadeModuleId: faker.helpers.arrayElement([null, fakeFile()]),
|
||||
fileName: faker.string.alphanumeric(),
|
||||
implicitlyLoadedBefore: [],
|
||||
importedBindings: {},
|
||||
imports: [],
|
||||
isDynamicEntry: faker.datatype.boolean(),
|
||||
isEntry: faker.datatype.boolean(),
|
||||
isImplicitEntry: faker.datatype.boolean(),
|
||||
map: null,
|
||||
moduleIds: [],
|
||||
modules: {},
|
||||
name: faker.string.alpha(),
|
||||
referencedFiles: [],
|
||||
viteMetadata: {
|
||||
importedAssets: new Set(),
|
||||
importedCss: new Set(),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
export const fakeRollupOutputAsset = fakeObjectCreator<Rollup.OutputAsset>(
|
||||
() => ({
|
||||
type: 'asset',
|
||||
fileName: fakeFileName(),
|
||||
name: faker.string.alpha(),
|
||||
needsCodeReference: faker.datatype.boolean(),
|
||||
source: '',
|
||||
}),
|
||||
);
|
||||
|
||||
export function fakeRollupOutput(): Rollup.OutputAsset | Rollup.OutputChunk {
|
||||
return faker.helpers.arrayElement([
|
||||
fakeRollupOutputAsset(),
|
||||
fakeRollupOutputChunk(),
|
||||
]);
|
||||
}
|
||||
|
||||
export const fakeManifest = fakeObjectCreator<Manifest.WebExtensionManifest>(
|
||||
() => ({
|
||||
manifest_version: faker.helpers.arrayElement([2, 3]),
|
||||
name: faker.string.alphanumeric(),
|
||||
version: `${faker.number.int()}.${faker.number.int()}.${faker.number.int()}`,
|
||||
}),
|
||||
);
|
||||
|
||||
export function fakeArray<T>(createItem: () => T, count = 3): T[] {
|
||||
const array: T[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
array.push(createItem());
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
export const fakeInternalConfig = fakeObjectCreator<InternalConfig>(() => ({
|
||||
browser: faker.helpers.arrayElement(['chrome', 'firefox']),
|
||||
command: faker.helpers.arrayElement(['build', 'serve']),
|
||||
entrypointsDir: fakeDir(),
|
||||
fsCache: mock<FsCache>(),
|
||||
imports: {},
|
||||
logger: mock(),
|
||||
manifest: fakeManifest(),
|
||||
manifestVersion: faker.helpers.arrayElement([2, 3]),
|
||||
mode: faker.helpers.arrayElement(['development', 'production']),
|
||||
outBaseDir: fakeDir(),
|
||||
outDir: fakeDir(),
|
||||
publicDir: fakeDir(),
|
||||
root: fakeDir(),
|
||||
runnerConfig: {
|
||||
config: {},
|
||||
},
|
||||
srcDir: fakeDir(),
|
||||
storeIds: {},
|
||||
typesDir: fakeDir(),
|
||||
vite: {},
|
||||
wxtDir: fakeDir(),
|
||||
server: mock<WxtDevServer>(),
|
||||
}));
|
||||
@@ -0,0 +1,5 @@
|
||||
# WXT Templates
|
||||
|
||||
To add or change a template, open the template's folder (`template/<name>`) in your editor directly. Then, it's a normal NPM project.
|
||||
|
||||
Note that you should use `npm` instead of `pnpm` in the template directories so that it doesn't complict with the repo's PNPM workspace. Templates are standalone and should not depend on the local state of `wxt`.
|
||||
@@ -0,0 +1,7 @@
|
||||
# WXT + Vue 3
|
||||
|
||||
This template should help get you started developing with Vue 3 in WXT.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
|
||||
@@ -0,0 +1,23 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.output
|
||||
artifacts
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
@@ -0,0 +1,3 @@
|
||||
export default defineBackgroundScript(() => {
|
||||
console.log('Hello background!', { id: browser.runtime.id });
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
#root {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: filter 300ms;
|
||||
}
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #646cffaa);
|
||||
}
|
||||
.logo.react:hover {
|
||||
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||
}
|
||||
|
||||
@keyframes logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
a:nth-of-type(2) .logo {
|
||||
animation: logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { useState } from 'react';
|
||||
import reactLogo from '../../assets/react.svg';
|
||||
import wxtLogo from '/wxt.svg';
|
||||
import './App.css';
|
||||
|
||||
function App() {
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<a href="https://wxt.dev" target="_blank">
|
||||
<img src={wxtLogo} className="logo" alt="WXT logo" />
|
||||
</a>
|
||||
<a href="https://react.dev" target="_blank">
|
||||
<img src={reactLogo} className="logo react" alt="React logo" />
|
||||
</a>
|
||||
</div>
|
||||
<h1>WXT + React</h1>
|
||||
<div className="card">
|
||||
<button onClick={() => setCount((count) => count + 1)}>
|
||||
count is {count}
|
||||
</button>
|
||||
<p>
|
||||
Edit <code>src/App.tsx</code> and save to test HMR
|
||||
</p>
|
||||
</div>
|
||||
<p className="read-the-docs">
|
||||
Click on the WXT and React logos to learn more
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,69 @@
|
||||
:root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<!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.type" content="browser_action" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="./main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App.tsx';
|
||||
import './index.css';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "wxt-starter",
|
||||
"description": "manifest.json description",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "wxt",
|
||||
"dev:firefox": "wxt -b firefox",
|
||||
"build": "wxt build",
|
||||
"build:firefox": "wxt build -b firefox",
|
||||
"compile": "tsc --noEmit",
|
||||
"prepare": "wxt prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.14",
|
||||
"@types/react-dom": "^18.2.6",
|
||||
"@vitejs/plugin-react": "^4.0.3",
|
||||
"typescript": "^5.1.6",
|
||||
"wxt": "^0.1.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "./.wxt/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowImportingTsExtensions": true,
|
||||
"jsx": "react-jsx"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { defineConfig } from 'wxt';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
// See https://wxt.dev/config.html
|
||||
export default defineConfig({
|
||||
vite: {
|
||||
plugins: [react()],
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["svelte.svelte-vscode"]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
# Svelte + Vite
|
||||
|
||||
This template should help get you started developing with Svelte in WXT.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
[VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode).
|
||||
@@ -0,0 +1,23 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.output
|
||||
artifacts
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "wxt-starter",
|
||||
"description": "manifest.json description",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"dev": "wxt",
|
||||
"dev:firefox": "wxt -b firefox",
|
||||
"build": "wxt build",
|
||||
"build:firefox": "wxt build -b firefox",
|
||||
"check": "svelte-check --tsconfig ./tsconfig.json",
|
||||
"prepare": "wxt prepare"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^2.4.2",
|
||||
"@tsconfig/svelte": "^5.0.0",
|
||||
"svelte": "^4.0.5",
|
||||
"svelte-check": "^3.4.6",
|
||||
"tslib": "^2.6.0",
|
||||
"typescript": "^5.1.6",
|
||||
"wxt": "^0.1.5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="26.6" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 308"><path fill="#FF3E00" d="M239.682 40.707C211.113-.182 154.69-12.301 113.895 13.69L42.247 59.356a82.198 82.198 0 0 0-37.135 55.056a86.566 86.566 0 0 0 8.536 55.576a82.425 82.425 0 0 0-12.296 30.719a87.596 87.596 0 0 0 14.964 66.244c28.574 40.893 84.997 53.007 125.787 27.016l71.648-45.664a82.182 82.182 0 0 0 37.135-55.057a86.601 86.601 0 0 0-8.53-55.577a82.409 82.409 0 0 0 12.29-30.718a87.573 87.573 0 0 0-14.963-66.244"></path><path fill="#FFF" d="M106.889 270.841c-23.102 6.007-47.497-3.036-61.103-22.648a52.685 52.685 0 0 1-9.003-39.85a49.978 49.978 0 0 1 1.713-6.693l1.35-4.115l3.671 2.697a92.447 92.447 0 0 0 28.036 14.007l2.663.808l-.245 2.659a16.067 16.067 0 0 0 2.89 10.656a17.143 17.143 0 0 0 18.397 6.828a15.786 15.786 0 0 0 4.403-1.935l71.67-45.672a14.922 14.922 0 0 0 6.734-9.977a15.923 15.923 0 0 0-2.713-12.011a17.156 17.156 0 0 0-18.404-6.832a15.78 15.78 0 0 0-4.396 1.933l-27.35 17.434a52.298 52.298 0 0 1-14.553 6.391c-23.101 6.007-47.497-3.036-61.101-22.649a52.681 52.681 0 0 1-9.004-39.849a49.428 49.428 0 0 1 22.34-33.114l71.664-45.677a52.218 52.218 0 0 1 14.563-6.398c23.101-6.007 47.497 3.036 61.101 22.648a52.685 52.685 0 0 1 9.004 39.85a50.559 50.559 0 0 1-1.713 6.692l-1.35 4.116l-3.67-2.693a92.373 92.373 0 0 0-28.037-14.013l-2.664-.809l.246-2.658a16.099 16.099 0 0 0-2.89-10.656a17.143 17.143 0 0 0-18.398-6.828a15.786 15.786 0 0 0-4.402 1.935l-71.67 45.674a14.898 14.898 0 0 0-6.73 9.975a15.9 15.9 0 0 0 2.709 12.012a17.156 17.156 0 0 0 18.404 6.832a15.841 15.841 0 0 0 4.402-1.935l27.345-17.427a52.147 52.147 0 0 1 14.552-6.397c23.101-6.006 47.497 3.037 61.102 22.65a52.681 52.681 0 0 1 9.003 39.848a49.453 49.453 0 0 1-22.34 33.12l-71.664 45.673a52.218 52.218 0 0 1-14.563 6.398"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1,3 @@
|
||||
export default defineBackgroundScript(() => {
|
||||
console.log('Hello background!', { id: browser.runtime.id });
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
<script lang="ts">
|
||||
import svelteLogo from '../../assets/svelte.svg'
|
||||
import Counter from '../../lib/Counter.svelte'
|
||||
</script>
|
||||
|
||||
<main>
|
||||
<div>
|
||||
<a href="https://wxt.dev" target="_blank" rel="noreferrer">
|
||||
<img src="/wxt.svg" class="logo" alt="WXT Logo" />
|
||||
</a>
|
||||
<a href="https://svelte.dev" target="_blank" rel="noreferrer">
|
||||
<img src={svelteLogo} class="logo svelte" alt="Svelte Logo" />
|
||||
</a>
|
||||
</div>
|
||||
<h1>WXT 2 + Svelte</h1>
|
||||
|
||||
<div class="card">
|
||||
<Counter />
|
||||
</div>
|
||||
|
||||
<p class="read-the-docs">
|
||||
Click on the WXT and Svelte logos to learn more
|
||||
</p>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: filter 300ms;
|
||||
}
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #646cffaa);
|
||||
}
|
||||
.logo.svelte:hover {
|
||||
filter: drop-shadow(0 0 2em #ff3e00aa);
|
||||
}
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,80 @@
|
||||
:root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<!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.type" content="browser_action" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="./main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,8 @@
|
||||
import './app.css';
|
||||
import App from './App.svelte';
|
||||
|
||||
const app = new App({
|
||||
target: document.getElementById('app')!,
|
||||
});
|
||||
|
||||
export default app;
|
||||
@@ -0,0 +1,10 @@
|
||||
<script lang="ts">
|
||||
let count: number = 0
|
||||
const increment = () => {
|
||||
count += 1
|
||||
}
|
||||
</script>
|
||||
|
||||
<button on:click={increment}>
|
||||
count is {count}
|
||||
</button>
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "./src/.wxt/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"useDefineForClassFields": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/// <reference types="svelte" />
|
||||
@@ -0,0 +1,17 @@
|
||||
import { defineConfig } from 'wxt';
|
||||
import { svelte, vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
// See https://wxt.dev/config.html
|
||||
export default defineConfig({
|
||||
srcDir: 'src',
|
||||
vite: {
|
||||
logLevel: 'info',
|
||||
plugins: [
|
||||
svelte({
|
||||
// Using a svelte.config.js file causes a segmentation fault when importing the file
|
||||
configFile: false,
|
||||
preprocess: [vitePreprocess()],
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
// Types required to make templates happy.
|
||||
|
||||
declare module '*?raw' {
|
||||
const content: any;
|
||||
export default content;
|
||||
}
|
||||
|
||||
declare module '{{moduleId}}' {
|
||||
const variable: any;
|
||||
export default variable;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.output
|
||||
artifacts
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="32" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path fill="#007ACC" d="M0 128v128h256V0H0z"></path><path fill="#FFF" d="m56.612 128.85l-.081 10.483h33.32v94.68h23.568v-94.68h33.321v-10.28c0-5.69-.122-10.444-.284-10.566c-.122-.162-20.4-.244-44.983-.203l-44.74.122l-.121 10.443Zm149.955-10.742c6.501 1.625 11.459 4.51 16.01 9.224c2.357 2.52 5.851 7.111 6.136 8.208c.08.325-11.053 7.802-17.798 11.988c-.244.162-1.22-.894-2.317-2.52c-3.291-4.795-6.745-6.867-12.028-7.233c-7.76-.528-12.759 3.535-12.718 10.321c0 1.992.284 3.17 1.097 4.795c1.707 3.536 4.876 5.649 14.832 9.956c18.326 7.883 26.168 13.084 31.045 20.48c5.445 8.249 6.664 21.415 2.966 31.208c-4.063 10.646-14.14 17.879-28.323 20.276c-4.388.772-14.79.65-19.504-.203c-10.28-1.828-20.033-6.908-26.047-13.572c-2.357-2.6-6.949-9.387-6.664-9.874c.122-.163 1.178-.813 2.356-1.504c1.138-.65 5.446-3.129 9.509-5.485l7.355-4.267l1.544 2.276c2.154 3.29 6.867 7.801 9.712 9.305c8.167 4.307 19.383 3.698 24.909-1.26c2.357-2.153 3.332-4.388 3.332-7.68c0-2.966-.366-4.266-1.91-6.501c-1.99-2.845-6.054-5.242-17.595-10.24c-13.206-5.69-18.895-9.224-24.096-14.832c-3.007-3.25-5.852-8.452-7.03-12.8c-.975-3.617-1.22-12.678-.447-16.335c2.723-12.76 12.353-21.659 26.25-24.3c4.51-.853 14.994-.528 19.424.569Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,9 @@
|
||||
export function setupCounter(element: HTMLButtonElement) {
|
||||
let counter = 0;
|
||||
const setCounter = (count: number) => {
|
||||
counter = count;
|
||||
element.innerHTML = `count is ${counter}`;
|
||||
};
|
||||
element.addEventListener('click', () => setCounter(counter + 1));
|
||||
setCounter(0);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export default defineBackgroundScript(() => {
|
||||
console.log('Hello background!', { id: browser.runtime.id });
|
||||
});
|
||||