Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ba41691296 | |||
| 8786aa91b3 | |||
| 0623d10653 | |||
| 85174d4994 | |||
| 4e2dd5d618 | |||
| 1a39a0d153 | |||
| f922648dd7 | |||
| e6142e3608 | |||
| ed73451fd0 | |||
| 8a25a2f60f | |||
| 397e9a8b45 | |||
| 7cef7680de | |||
| 0c57375758 | |||
| 2c6af84165 | |||
| 58e1371701 | |||
| 0d8e7463b8 | |||
| 77eeacaf0f | |||
| e80c134150 | |||
| 03c8ab4d6f | |||
| df13b9705c | |||
| 762ba0080d | |||
| 317b1b6dcc | |||
| 93175a6477 | |||
| 60d6707b11 | |||
| 97cbda3dab | |||
| 1611c1dba6 | |||
| b59252284a | |||
| 742b99657a | |||
| e2997a43e0 | |||
| 21dead60fd |
@@ -0,0 +1,27 @@
|
||||
name: Publish Docs
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: Docker Image Tag
|
||||
required: true
|
||||
default: latest
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
registry: https://${{ secrets.DOCKER_REGISTRY_HOSTNAME }}
|
||||
username: ${{ secrets.DOCKER_REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}
|
||||
- run: pnpm docs:build
|
||||
- run: docker build docs/.vitepress -t ${{ secrets.DOCKER_REGISTRY_HOSTNAME }}/wxt/docs:${{ github.event.inputs.tag || 'latest' }}
|
||||
- run: docker push ${{ secrets.DOCKER_REGISTRY_HOSTNAME }}/wxt/docs:${{ github.event.inputs.tag || 'latest' }}
|
||||
- run: curl -X POST -i ${{ secrets.UPDATE_DOCS_WEBHOOK }}
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
matrix:
|
||||
template:
|
||||
- react
|
||||
- solid
|
||||
# - solid
|
||||
- svelte
|
||||
- vanilla
|
||||
- vue
|
||||
@@ -66,11 +66,11 @@ jobs:
|
||||
working-directory: templates/${{ matrix.template }}
|
||||
- run: npm i -D ../../wxt-*.tgz
|
||||
working-directory: templates/${{ matrix.template }}
|
||||
- run: npm run compile
|
||||
- run: pnpm compile
|
||||
if: matrix.template != 'svelte'
|
||||
working-directory: templates/${{ matrix.template }}
|
||||
- run: npm run check
|
||||
- run: pnpm check
|
||||
if: matrix.template == 'svelte'
|
||||
working-directory: templates/${{ matrix.template }}
|
||||
- run: npm run build
|
||||
- run: pnpm build
|
||||
working-directory: templates/${{ matrix.template }}
|
||||
|
||||
@@ -1,5 +1,88 @@
|
||||
# Changelog
|
||||
|
||||
## v0.11.1
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.11.0...v0.11.1)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Add util for detecting URL changes in content scripts ([#264](https://github.com/wxt-dev/wxt/pull/264))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Upgrade templates to `wxt@^0.11.0` ([#263](https://github.com/wxt-dev/wxt/pull/263))
|
||||
|
||||
## v0.11.0
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.10.4...v0.11.0)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- ⚠️ Vite 5 support ([#261](https://github.com/wxt-dev/wxt/pull/261))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Adds tl;dv to homepage ([#260](https://github.com/wxt-dev/wxt/pull/260))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Speed up CI using `pnpm` instead of `npm` ([#259](https://github.com/wxt-dev/wxt/pull/259))
|
||||
- Abstract vite from WXT's core logic ([#242](https://github.com/wxt-dev/wxt/pull/242))
|
||||
|
||||
#### ⚠️ Breaking Changes
|
||||
|
||||
- You will need to update any other Vite plugins to a version that supports Vite 5 ([#261](https://github.com/wxt-dev/wxt/pull/261))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Ítalo Brasil ([@italodeverdade](http://github.com/italodeverdade))
|
||||
|
||||
## v0.10.4
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.10.3...v0.10.4)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Add config to customize `outDir` ([#258](https://github.com/wxt-dev/wxt/pull/258))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Add Doozy to homepage ([#249](https://github.com/wxt-dev/wxt/pull/249))
|
||||
- Update sidepanel availability ([#250](https://github.com/wxt-dev/wxt/pull/250))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- **deps-dev:** Bump prettier from 3.0.3 to 3.1.0 ([#254](https://github.com/wxt-dev/wxt/pull/254))
|
||||
- **deps-dev:** Bump @types/lodash.merge from 4.6.8 to 4.6.9 ([#255](https://github.com/wxt-dev/wxt/pull/255))
|
||||
- **deps-dev:** Bump tsx from 3.14.0 to 4.6.1 ([#252](https://github.com/wxt-dev/wxt/pull/252))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- 冯不游
|
||||
|
||||
## v0.10.3
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.10.2...v0.10.3)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- **auto-imports:** Don't add imports to `node_module` dependencies ([#247](https://github.com/wxt-dev/wxt/pull/247))
|
||||
|
||||
### 📖 Documentation
|
||||
|
||||
- Fix typo ([317b1b6](https://github.com/wxt-dev/wxt/commit/317b1b6))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Trigger docs upgrade via webhook ([742b996](https://github.com/wxt-dev/wxt/commit/742b996))
|
||||
- Use `normalize-path` instead of `vite.normalizePath` ([#244](https://github.com/wxt-dev/wxt/pull/244))
|
||||
- Use `defu` for merging some config objects ([#243](https://github.com/wxt-dev/wxt/pull/243))
|
||||
|
||||
### 🤖 CI
|
||||
|
||||
- Publish docs on push to main ([1611c1d](https://github.com/wxt-dev/wxt/commit/1611c1d))
|
||||
- Only print response headers from docs webhook ([97cbda3](https://github.com/wxt-dev/wxt/commit/97cbda3))
|
||||
|
||||
## v0.10.2
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/v0.10.1...v0.10.2)
|
||||
|
||||
+1
-1
@@ -86,7 +86,7 @@ Note that templates are hardcoded to a specific version of `wxt` from NPM, they
|
||||
|
||||
```diff
|
||||
"devDependencies": {
|
||||
"typescript": "^5.1.6",
|
||||
"typescript": "^5.3.2",
|
||||
"vite-plugin-solid": "^2.7.0",
|
||||
- "wxt": "^0.8.0"
|
||||
+ "wxt": "../.."
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
export default defineContentScript({
|
||||
// Site that uses HTML5 history
|
||||
matches: ['*://*.crunchyroll.com/*'],
|
||||
|
||||
main(ctx) {
|
||||
ctx.addEventListener(window, 'wxt:locationchange', ({ newUrl, oldUrl }) => {
|
||||
console.log('Location changed:', newUrl.href, oldUrl.href);
|
||||
});
|
||||
console.log('Watching for location change...');
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Sidebar</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Example</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,2 @@
|
||||
FROM lipanski/docker-static-website:latest
|
||||
COPY dist .
|
||||
@@ -10,6 +10,8 @@ const chromeExtensionIds = [
|
||||
'mgmdkjcljneegjfajchedjpdhbadklcf', // Anime Skip Player
|
||||
'bfbnagnphiehemkdgmmficmjfddgfhpl', // UltraWideo
|
||||
'elfaihghhjjoknimpccccmkioofjjfkf', // StayFree - Website Blocker & Web Analytics
|
||||
'okifoaikfmpfcamplcfjkpdnhfodpkil', // Doozy: Ai Made Easy
|
||||
'lknmjhcajhfbbglglccadlfdjbaiifig' // tl;dv - Record, Transcribe & ChatGPT for Google Meet
|
||||
];
|
||||
|
||||
const { data } = useListExtensionDetails(chromeExtensionIds);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# Side Panel
|
||||
|
||||
[Chrome Docs](https://developer.chrome.com/docs/extensions/reference/sidePanel/)
|
||||
[Chrome Docs](https://developer.chrome.com/docs/extensions/reference/sidePanel/) • [Firefox Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/user_interface/Sidebars)
|
||||
|
||||
:::tip Chromium Only
|
||||
Firefox does not support sidepanel pages.
|
||||
:::warning
|
||||
Chrome added support for sidepanels in Manifest V3, they are not available in Manfiest V2.
|
||||
:::
|
||||
|
||||
## Filenames
|
||||
|
||||
@@ -49,16 +49,16 @@ describe('Auto Imports', () => {
|
||||
|
||||
expect(await project.serializeFile('.wxt/wxt.d.ts'))
|
||||
.toMatchInlineSnapshot(`
|
||||
".wxt/wxt.d.ts
|
||||
----------------------------------------
|
||||
// Generated by wxt
|
||||
/// <reference types=\\"vite/client\\" />
|
||||
/// <reference types=\\"./types/imports.d.ts\\" />
|
||||
/// <reference types=\\"./types/paths.d.ts\\" />
|
||||
/// <reference types=\\"./types/i18n.d.ts\\" />
|
||||
/// <reference types=\\"./types/globals.d.ts\\" />
|
||||
"
|
||||
`);
|
||||
".wxt/wxt.d.ts
|
||||
----------------------------------------
|
||||
// Generated by wxt
|
||||
/// <reference types="vite/client" />
|
||||
/// <reference types="./types/imports.d.ts" />
|
||||
/// <reference types="./types/paths.d.ts" />
|
||||
/// <reference types="./types/i18n.d.ts" />
|
||||
/// <reference types="./types/globals.d.ts" />
|
||||
"
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -91,10 +91,10 @@ describe('Auto Imports', () => {
|
||||
".wxt/wxt.d.ts
|
||||
----------------------------------------
|
||||
// Generated by wxt
|
||||
/// <reference types=\\"vite/client\\" />
|
||||
/// <reference types=\\"./types/paths.d.ts\\" />
|
||||
/// <reference types=\\"./types/i18n.d.ts\\" />
|
||||
/// <reference types=\\"./types/globals.d.ts\\" />
|
||||
/// <reference types="vite/client" />
|
||||
/// <reference types="./types/paths.d.ts" />
|
||||
/// <reference types="./types/i18n.d.ts" />
|
||||
/// <reference types="./types/globals.d.ts" />
|
||||
"
|
||||
`,
|
||||
);
|
||||
|
||||
@@ -371,7 +371,7 @@ describe('Manifest Content', () => {
|
||||
.toMatchInlineSnapshot(`
|
||||
".output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"content_scripts\\":[{\\"matches\\":[\\"https://*.google.com/*\\"],\\"css\\":[\\"content-scripts/content.css\\"],\\"js\\":[\\"content-scripts/content.js\\"]}]}"
|
||||
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","content_scripts":[{"matches":["https://*.google.com/*"],"css":["content-scripts/content.css"],"js":["content-scripts/content.js"]}]}"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -398,7 +398,7 @@ describe('Manifest Content', () => {
|
||||
.toMatchInlineSnapshot(`
|
||||
".output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"content_scripts\\":[{\\"matches\\":[\\"https://*.google.com/*\\"],\\"css\\":[\\"content-scripts/content.css\\"],\\"js\\":[\\"content-scripts/content.js\\"]}]}"
|
||||
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","content_scripts":[{"matches":["https://*.google.com/*"],"css":["content-scripts/content.css"],"js":["content-scripts/content.js"]}]}"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -425,7 +425,7 @@ describe('Manifest Content', () => {
|
||||
.toMatchInlineSnapshot(`
|
||||
".output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"content_scripts\\":[{\\"matches\\":[\\"https://*.google.com/*\\"],\\"js\\":[\\"content-scripts/content.js\\"]}]}"
|
||||
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","content_scripts":[{"matches":["https://*.google.com/*"],"js":["content-scripts/content.js"]}]}"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -454,7 +454,7 @@ describe('Manifest Content', () => {
|
||||
.toMatchInlineSnapshot(`
|
||||
".output/chrome-mv2/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":2,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"content_scripts\\":[{\\"matches\\":[\\"https://*.google.com/*\\"],\\"js\\":[\\"content-scripts/content.js\\"]}],\\"web_accessible_resources\\":[\\"content-scripts/content.css\\"]}"
|
||||
{"manifest_version":2,"name":"E2E Extension","description":"Example description","version":"0.0.0","content_scripts":[{"matches":["https://*.google.com/*"],"js":["content-scripts/content.js"]}],"web_accessible_resources":["content-scripts/content.css"]}"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -483,7 +483,7 @@ describe('Manifest Content', () => {
|
||||
.toMatchInlineSnapshot(`
|
||||
".output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"content_scripts\\":[{\\"matches\\":[\\"https://*.google.com/*\\"],\\"js\\":[\\"content-scripts/content.js\\"]}],\\"web_accessible_resources\\":[{\\"resources\\":[\\"content-scripts/content.css\\"],\\"matches\\":[\\"https://*.google.com/*\\"]}]}"
|
||||
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","content_scripts":[{"matches":["https://*.google.com/*"],"js":["content-scripts/content.js"]}],"web_accessible_resources":[{"resources":["content-scripts/content.css"],"matches":["https://*.google.com/*"]}]}"
|
||||
`);
|
||||
});
|
||||
});
|
||||
@@ -518,7 +518,7 @@ describe('Manifest Content', () => {
|
||||
.toMatchInlineSnapshot(`
|
||||
".output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"web_accessible_resources\\":[{\\"resources\\":[\\"one.png\\"],\\"matches\\":[\\"https://one.com/*\\"]},{\\"resources\\":[\\"content-scripts/content.css\\"],\\"matches\\":[\\"https://*.google.com/*\\"]}],\\"content_scripts\\":[{\\"matches\\":[\\"https://*.google.com/*\\"],\\"js\\":[\\"content-scripts/content.js\\"]}]}"
|
||||
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","web_accessible_resources":[{"resources":["one.png"],"matches":["https://one.com/*"]},{"resources":["content-scripts/content.css"],"matches":["https://*.google.com/*"]}],"content_scripts":[{"matches":["https://*.google.com/*"],"js":["content-scripts/content.js"]}]}"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -543,7 +543,7 @@ describe('Manifest Content', () => {
|
||||
expect(output).toMatchInlineSnapshot(`
|
||||
".output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"author\\":\\"Custom Author\\"}"
|
||||
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","author":"Custom Author"}"
|
||||
`);
|
||||
});
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ describe('Output Directory Structure', () => {
|
||||
expect(await project.serializeOutput()).toMatchInlineSnapshot(`
|
||||
".output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\"}"
|
||||
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0"}"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -69,7 +69,7 @@ describe('Output Directory Structure', () => {
|
||||
================================================================================
|
||||
.output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"content_scripts\\":[{\\"matches\\":[\\"*://*/*\\"],\\"css\\":[\\"content-scripts/one.css\\",\\"content-scripts/two.css\\"],\\"js\\":[\\"content-scripts/one.js\\",\\"content-scripts/two.js\\"]}]}"
|
||||
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","content_scripts":[{"matches":["*://*/*"],"css":["content-scripts/one.css","content-scripts/two.css"],"js":["content-scripts/one.js","content-scripts/two.js"]}]}"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -96,7 +96,7 @@ describe('Output Directory Structure', () => {
|
||||
================================================================================
|
||||
.output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"content_scripts\\":[{\\"matches\\":[\\"*://*/*\\"],\\"js\\":[\\"content-scripts/overlay-one.js\\"]}]}"
|
||||
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","content_scripts":[{"matches":["*://*/*"],"js":["content-scripts/overlay-one.js"]}]}"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -197,7 +197,7 @@ describe('Output Directory Structure', () => {
|
||||
.toMatchInlineSnapshot(`
|
||||
".output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"background\\":{\\"service_worker\\":\\"background.js\\"},\\"content_scripts\\":[{\\"matches\\":[\\"*://*.google.com/*\\"],\\"js\\":[\\"content-scripts/content.js\\"]},{\\"matches\\":[\\"*://*.duckduckgo.com/*\\"],\\"js\\":[\\"content-scripts/named.js\\"]}]}"
|
||||
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","background":{"service_worker":"background.js"},"content_scripts":[{"matches":["*://*.google.com/*"],"js":["content-scripts/content.js"]},{"matches":["*://*.duckduckgo.com/*"],"js":["content-scripts/named.js"]}]}"
|
||||
`);
|
||||
expect(await project.fileExists('.output/chrome-mv3/background.js'));
|
||||
expect(
|
||||
@@ -208,4 +208,17 @@ describe('Output Directory Structure', () => {
|
||||
);
|
||||
expect(await project.fileExists('.output/chrome-mv3/unlisted.js'));
|
||||
});
|
||||
|
||||
it("should output to a custom directory when overriding 'outDir'", async () => {
|
||||
const project = new TestProject();
|
||||
project.setConfigFileConfig({
|
||||
outDir: 'dist',
|
||||
});
|
||||
|
||||
await project.build();
|
||||
|
||||
expect(await project.fileExists('dist/chrome-mv3/manifest.json')).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -37,7 +37,7 @@ describe('React', () => {
|
||||
.toMatchInlineSnapshot(`
|
||||
".output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"content_scripts\\":[{\\"matches\\":\\"<all_urls>\\",\\"js\\":[\\"content-scripts/demo.js\\"]}]}"
|
||||
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","content_scripts":[{"matches":"<all_urls>","js":["content-scripts/demo.js"]}]}"
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,7 +21,7 @@ describe('TypeScript Project', () => {
|
||||
const __IS_SAFARI__: boolean;
|
||||
const __IS_EDGE__: boolean;
|
||||
const __IS_OPERA__: boolean;
|
||||
const __COMMAND__: \\"build\\" | \\"serve\\";
|
||||
const __COMMAND__: "build" | "serve";
|
||||
const __ENTRYPOINT__: string;
|
||||
}
|
||||
"
|
||||
@@ -41,13 +41,13 @@ describe('TypeScript Project', () => {
|
||||
".wxt/types/paths.d.ts
|
||||
----------------------------------------
|
||||
// Generated by wxt
|
||||
import \\"wxt/browser\\";
|
||||
import "wxt/browser";
|
||||
|
||||
declare module \\"wxt/browser\\" {
|
||||
declare module "wxt/browser" {
|
||||
export type PublicPath =
|
||||
| \\"/options.html\\"
|
||||
| \\"/popup.html\\"
|
||||
| \\"/sandbox.html\\"
|
||||
| "/options.html"
|
||||
| "/popup.html"
|
||||
| "/sandbox.html"
|
||||
export interface WxtRuntime extends Runtime.Static {
|
||||
getURL(path: PublicPath): string;
|
||||
}
|
||||
@@ -103,9 +103,9 @@ describe('TypeScript Project', () => {
|
||||
".wxt/types/i18n.d.ts
|
||||
----------------------------------------
|
||||
// Generated by wxt
|
||||
import \\"wxt/browser\\";
|
||||
import "wxt/browser";
|
||||
|
||||
declare module \\"wxt/browser\\" {
|
||||
declare module "wxt/browser" {
|
||||
/**
|
||||
* See https://developer.chrome.com/docs/extensions/reference/i18n/#method-getMessage
|
||||
*/
|
||||
@@ -121,90 +121,90 @@ describe('TypeScript Project', () => {
|
||||
* The extension or app ID; you might use this string to construct URLs for resources inside the extension. Even unlocalized extensions can use this message.
|
||||
Note: You can't use this message in a manifest file.
|
||||
*
|
||||
* \\"<browser.runtime.id>\\"
|
||||
* "<browser.runtime.id>"
|
||||
*/
|
||||
getMessage(
|
||||
messageName: \\"@@extension_id\\",
|
||||
messageName: "@@extension_id",
|
||||
substitutions?: string | string[],
|
||||
options?: GetMessageOptions,
|
||||
): string;
|
||||
/**
|
||||
*
|
||||
*
|
||||
* \\"<browser.i18n.getUiLocale()>\\"
|
||||
* "<browser.i18n.getUiLocale()>"
|
||||
*/
|
||||
getMessage(
|
||||
messageName: \\"@@ui_locale\\",
|
||||
messageName: "@@ui_locale",
|
||||
substitutions?: string | string[],
|
||||
options?: GetMessageOptions,
|
||||
): string;
|
||||
/**
|
||||
* The text direction for the current locale, either \\"ltr\\" for left-to-right languages such as English or \\"rtl\\" for right-to-left languages such as Japanese.
|
||||
* The text direction for the current locale, either "ltr" for left-to-right languages such as English or "rtl" for right-to-left languages such as Japanese.
|
||||
*
|
||||
* \\"<ltr|rtl>\\"
|
||||
* "<ltr|rtl>"
|
||||
*/
|
||||
getMessage(
|
||||
messageName: \\"@@bidi_dir\\",
|
||||
messageName: "@@bidi_dir",
|
||||
substitutions?: string | string[],
|
||||
options?: GetMessageOptions,
|
||||
): string;
|
||||
/**
|
||||
* If the @@bidi_dir is \\"ltr\\", then this is \\"rtl\\"; otherwise, it's \\"ltr\\".
|
||||
* If the @@bidi_dir is "ltr", then this is "rtl"; otherwise, it's "ltr".
|
||||
*
|
||||
* \\"<rtl|ltr>\\"
|
||||
* "<rtl|ltr>"
|
||||
*/
|
||||
getMessage(
|
||||
messageName: \\"@@bidi_reversed_dir\\",
|
||||
messageName: "@@bidi_reversed_dir",
|
||||
substitutions?: string | string[],
|
||||
options?: GetMessageOptions,
|
||||
): string;
|
||||
/**
|
||||
* If the @@bidi_dir is \\"ltr\\", then this is \\"left\\"; otherwise, it's \\"right\\".
|
||||
* If the @@bidi_dir is "ltr", then this is "left"; otherwise, it's "right".
|
||||
*
|
||||
* \\"<left|right>\\"
|
||||
* "<left|right>"
|
||||
*/
|
||||
getMessage(
|
||||
messageName: \\"@@bidi_start_edge\\",
|
||||
messageName: "@@bidi_start_edge",
|
||||
substitutions?: string | string[],
|
||||
options?: GetMessageOptions,
|
||||
): string;
|
||||
/**
|
||||
* If the @@bidi_dir is \\"ltr\\", then this is \\"right\\"; otherwise, it's \\"left\\".
|
||||
* If the @@bidi_dir is "ltr", then this is "right"; otherwise, it's "left".
|
||||
*
|
||||
* \\"<right|left>\\"
|
||||
* "<right|left>"
|
||||
*/
|
||||
getMessage(
|
||||
messageName: \\"@@bidi_end_edge\\",
|
||||
messageName: "@@bidi_end_edge",
|
||||
substitutions?: string | string[],
|
||||
options?: GetMessageOptions,
|
||||
): string;
|
||||
/**
|
||||
* Ask for the user's name
|
||||
*
|
||||
* \\"What's your name?\\"
|
||||
* "What's your name?"
|
||||
*/
|
||||
getMessage(
|
||||
messageName: \\"prompt_for_name\\",
|
||||
messageName: "prompt_for_name",
|
||||
substitutions?: string | string[],
|
||||
options?: GetMessageOptions,
|
||||
): string;
|
||||
/**
|
||||
* Greet the user
|
||||
*
|
||||
* \\"Hello, $USER$\\"
|
||||
* "Hello, $USER$"
|
||||
*/
|
||||
getMessage(
|
||||
messageName: \\"hello\\",
|
||||
messageName: "hello",
|
||||
substitutions?: string | string[],
|
||||
options?: GetMessageOptions,
|
||||
): string;
|
||||
/**
|
||||
* Say goodbye to the user
|
||||
*
|
||||
* \\"Goodbye, $USER$. Come back to $OUR_SITE$ soon!\\"
|
||||
* "Goodbye, $USER$. Come back to $OUR_SITE$ soon!"
|
||||
*/
|
||||
getMessage(
|
||||
messageName: \\"bye\\",
|
||||
messageName: "bye",
|
||||
substitutions?: string | string[],
|
||||
options?: GetMessageOptions,
|
||||
): string;
|
||||
@@ -224,11 +224,11 @@ describe('TypeScript Project', () => {
|
||||
".wxt/wxt.d.ts
|
||||
----------------------------------------
|
||||
// Generated by wxt
|
||||
/// <reference types=\\"vite/client\\" />
|
||||
/// <reference types=\\"./types/imports.d.ts\\" />
|
||||
/// <reference types=\\"./types/paths.d.ts\\" />
|
||||
/// <reference types=\\"./types/i18n.d.ts\\" />
|
||||
/// <reference types=\\"./types/globals.d.ts\\" />
|
||||
/// <reference types="vite/client" />
|
||||
/// <reference types="./types/imports.d.ts" />
|
||||
/// <reference types="./types/paths.d.ts" />
|
||||
/// <reference types="./types/i18n.d.ts" />
|
||||
/// <reference types="./types/globals.d.ts" />
|
||||
"
|
||||
`);
|
||||
});
|
||||
@@ -243,32 +243,32 @@ describe('TypeScript Project', () => {
|
||||
".wxt/tsconfig.json
|
||||
----------------------------------------
|
||||
{
|
||||
\\"compilerOptions\\": {
|
||||
\\"target\\": \\"ESNext\\",
|
||||
\\"module\\": \\"ESNext\\",
|
||||
\\"moduleResolution\\": \\"Bundler\\",
|
||||
\\"noEmit\\": true,
|
||||
\\"esModuleInterop\\": true,
|
||||
\\"forceConsistentCasingInFileNames\\": true,
|
||||
\\"resolveJsonModule\\": true,
|
||||
\\"strict\\": true,
|
||||
\\"skipLibCheck\\": true,
|
||||
\\"paths\\": {
|
||||
\\"@\\": [\\"..\\"],
|
||||
\\"@/*\\": [\\"../*\\"],
|
||||
\\"~\\": [\\"..\\"],
|
||||
\\"~/*\\": [\\"../*\\"],
|
||||
\\"@@\\": [\\"..\\"],
|
||||
\\"@@/*\\": [\\"../*\\"],
|
||||
\\"~~\\": [\\"..\\"],
|
||||
\\"~~/*\\": [\\"../*\\"]
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"paths": {
|
||||
"@": [".."],
|
||||
"@/*": ["../*"],
|
||||
"~": [".."],
|
||||
"~/*": ["../*"],
|
||||
"@@": [".."],
|
||||
"@@/*": ["../*"],
|
||||
"~~": [".."],
|
||||
"~~/*": ["../*"]
|
||||
}
|
||||
},
|
||||
\\"include\\": [
|
||||
\\"../**/*\\",
|
||||
\\"./wxt.d.ts\\"
|
||||
"include": [
|
||||
"../**/*",
|
||||
"./wxt.d.ts"
|
||||
],
|
||||
\\"exclude\\": [\\"../.output\\"]
|
||||
"exclude": ["../.output"]
|
||||
}"
|
||||
`);
|
||||
});
|
||||
@@ -286,32 +286,32 @@ describe('TypeScript Project', () => {
|
||||
".wxt/tsconfig.json
|
||||
----------------------------------------
|
||||
{
|
||||
\\"compilerOptions\\": {
|
||||
\\"target\\": \\"ESNext\\",
|
||||
\\"module\\": \\"ESNext\\",
|
||||
\\"moduleResolution\\": \\"Bundler\\",
|
||||
\\"noEmit\\": true,
|
||||
\\"esModuleInterop\\": true,
|
||||
\\"forceConsistentCasingInFileNames\\": true,
|
||||
\\"resolveJsonModule\\": true,
|
||||
\\"strict\\": true,
|
||||
\\"skipLibCheck\\": true,
|
||||
\\"paths\\": {
|
||||
\\"@\\": [\\"../src\\"],
|
||||
\\"@/*\\": [\\"../src/*\\"],
|
||||
\\"~\\": [\\"../src\\"],
|
||||
\\"~/*\\": [\\"../src/*\\"],
|
||||
\\"@@\\": [\\"..\\"],
|
||||
\\"@@/*\\": [\\"../*\\"],
|
||||
\\"~~\\": [\\"..\\"],
|
||||
\\"~~/*\\": [\\"../*\\"]
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"paths": {
|
||||
"@": ["../src"],
|
||||
"@/*": ["../src/*"],
|
||||
"~": ["../src"],
|
||||
"~/*": ["../src/*"],
|
||||
"@@": [".."],
|
||||
"@@/*": ["../*"],
|
||||
"~~": [".."],
|
||||
"~~/*": ["../*"]
|
||||
}
|
||||
},
|
||||
\\"include\\": [
|
||||
\\"../**/*\\",
|
||||
\\"./wxt.d.ts\\"
|
||||
"include": [
|
||||
"../**/*",
|
||||
"./wxt.d.ts"
|
||||
],
|
||||
\\"exclude\\": [\\"../.output\\"]
|
||||
"exclude": ["../.output"]
|
||||
}"
|
||||
`);
|
||||
});
|
||||
@@ -333,34 +333,34 @@ describe('TypeScript Project', () => {
|
||||
".wxt/tsconfig.json
|
||||
----------------------------------------
|
||||
{
|
||||
\\"compilerOptions\\": {
|
||||
\\"target\\": \\"ESNext\\",
|
||||
\\"module\\": \\"ESNext\\",
|
||||
\\"moduleResolution\\": \\"Bundler\\",
|
||||
\\"noEmit\\": true,
|
||||
\\"esModuleInterop\\": true,
|
||||
\\"forceConsistentCasingInFileNames\\": true,
|
||||
\\"resolveJsonModule\\": true,
|
||||
\\"strict\\": true,
|
||||
\\"skipLibCheck\\": true,
|
||||
\\"paths\\": {
|
||||
\\"example\\": [\\"../example\\"],
|
||||
\\"example/*\\": [\\"../example/*\\"],
|
||||
\\"@\\": [\\"../src\\"],
|
||||
\\"@/*\\": [\\"../src/*\\"],
|
||||
\\"~\\": [\\"../src\\"],
|
||||
\\"~/*\\": [\\"../src/*\\"],
|
||||
\\"@@\\": [\\"..\\"],
|
||||
\\"@@/*\\": [\\"../*\\"],
|
||||
\\"~~\\": [\\"..\\"],
|
||||
\\"~~/*\\": [\\"../*\\"]
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"paths": {
|
||||
"example": ["../example"],
|
||||
"example/*": ["../example/*"],
|
||||
"@": ["../src"],
|
||||
"@/*": ["../src/*"],
|
||||
"~": ["../src"],
|
||||
"~/*": ["../src/*"],
|
||||
"@@": [".."],
|
||||
"@@/*": ["../*"],
|
||||
"~~": [".."],
|
||||
"~~/*": ["../*"]
|
||||
}
|
||||
},
|
||||
\\"include\\": [
|
||||
\\"../**/*\\",
|
||||
\\"./wxt.d.ts\\"
|
||||
"include": [
|
||||
"../**/*",
|
||||
"./wxt.d.ts"
|
||||
],
|
||||
\\"exclude\\": [\\"../.output\\"]
|
||||
"exclude": ["../.output"]
|
||||
}"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -29,7 +29,7 @@ describe('User Config', () => {
|
||||
================================================================================
|
||||
.output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"background\\":{\\"service_worker\\":\\"background.js\\"}}"
|
||||
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","background":{"service_worker":"background.js"}}"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -55,7 +55,7 @@ describe('User Config', () => {
|
||||
================================================================================
|
||||
.output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"background\\":{\\"service_worker\\":\\"background.js\\"}}"
|
||||
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","background":{"service_worker":"background.js"}}"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -83,7 +83,7 @@ describe('User Config', () => {
|
||||
expect(output).toMatchInlineSnapshot(`
|
||||
".output/chrome-mv3/manifest.json
|
||||
----------------------------------------
|
||||
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"description\\":\\"Example description\\",\\"version\\":\\"0.0.0\\",\\"example_customization\\":[\\"production\\",\\"chrome\\",\\"3\\",\\"build\\"]}"
|
||||
{"manifest_version":3,"name":"E2E Extension","description":"Example description","version":"0.0.0","example_customization":["3","build","production","chrome"]}"
|
||||
`);
|
||||
});
|
||||
|
||||
|
||||
+3
-1
@@ -77,7 +77,9 @@ export class TestProject {
|
||||
await fs.writeFile(filePath, content ?? '', 'utf-8');
|
||||
}
|
||||
|
||||
await execaCommand('npm i --ignore-scripts', { cwd: this.root });
|
||||
await execaCommand('pnpm --ignore-workspace i --ignore-scripts', {
|
||||
cwd: this.root,
|
||||
});
|
||||
await build({ ...config, root: this.root });
|
||||
}
|
||||
|
||||
|
||||
+23
-19
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "wxt",
|
||||
"type": "module",
|
||||
"version": "0.10.2",
|
||||
"version": "0.11.1",
|
||||
"description": "Next gen framework for developing web extensions",
|
||||
"engines": {
|
||||
"node": ">=18",
|
||||
@@ -87,7 +87,9 @@
|
||||
"async-mutex": "^0.4.0",
|
||||
"c12": "^1.5.1",
|
||||
"cac": "^6.7.14",
|
||||
"chokidar": "^3.5.3",
|
||||
"consola": "^3.2.3",
|
||||
"defu": "^6.1.3",
|
||||
"esbuild": "^0.19.5",
|
||||
"fast-glob": "^3.3.1",
|
||||
"filesize": "^10.0.8",
|
||||
@@ -100,42 +102,44 @@
|
||||
"json5": "^2.2.3",
|
||||
"linkedom": "^0.16.1",
|
||||
"minimatch": "^9.0.3",
|
||||
"normalize-path": "^3.0.0",
|
||||
"ora": "^7.0.1",
|
||||
"picocolors": "^1.0.0",
|
||||
"prompts": "^2.4.2",
|
||||
"rollup-plugin-visualizer": "^5.9.2",
|
||||
"unimport": "^3.4.0",
|
||||
"unstorage": "^1.9.0",
|
||||
"vite": "^4.0.0 || ^5.0.0-0",
|
||||
"vite": "^5.0.0",
|
||||
"web-ext-run": "^0.1.0",
|
||||
"webextension-polyfill": "^0.10.0",
|
||||
"zip-dir": "^2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^8.2.0",
|
||||
"@types/fs-extra": "^11.0.3",
|
||||
"@types/lodash.merge": "^4.6.8",
|
||||
"@types/node": "^20.8.10",
|
||||
"@types/prompts": "^2.4.7",
|
||||
"@vitest/coverage-v8": "^0.34.6",
|
||||
"@faker-js/faker": "^8.3.1",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/lodash.merge": "^4.6.9",
|
||||
"@types/node": "^20.10.3",
|
||||
"@types/normalize-path": "^3.0.2",
|
||||
"@types/prompts": "^2.4.9",
|
||||
"@vitest/coverage-v8": "^1.0.1",
|
||||
"execa": "^8.0.1",
|
||||
"happy-dom": "^12.4.0",
|
||||
"lint-staged": "^15.0.2",
|
||||
"happy-dom": "^12.10.3",
|
||||
"lint-staged": "^15.2.0",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"p-map": "^6.0.0",
|
||||
"prettier": "^3.0.1",
|
||||
"p-map": "^7.0.0",
|
||||
"prettier": "^3.1.0",
|
||||
"simple-git-hooks": "^2.9.0",
|
||||
"tsup": "^7.2.0",
|
||||
"tsx": "^3.12.7",
|
||||
"typedoc": "^0.25.3",
|
||||
"tsup": "^8.0.1",
|
||||
"tsx": "^4.6.2",
|
||||
"typedoc": "^0.25.4",
|
||||
"typedoc-plugin-markdown": "4.0.0-next.23",
|
||||
"typedoc-vitepress-theme": "1.0.0-next.3",
|
||||
"typescript": "^5.2.2",
|
||||
"vitepress": "1.0.0-rc.24",
|
||||
"vitest": "^0.34.6",
|
||||
"typescript": "^5.3.2",
|
||||
"vitepress": "1.0.0-rc.31",
|
||||
"vitest": "^1.0.0",
|
||||
"vitest-mock-extended": "^1.3.1",
|
||||
"vue": "^3.3.7"
|
||||
"vue": "^3.3.10"
|
||||
},
|
||||
"packageManager": "pnpm@8.6.3",
|
||||
"simple-git-hooks": {
|
||||
|
||||
Generated
+1017
-347
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,8 @@
|
||||
import { ContentScriptDefinition } from '~/types';
|
||||
import { browser } from '~/browser';
|
||||
import { logger } from '~/client/utils/logger';
|
||||
import { WxtLocationChangeEvent, getUniqueEventName } from './custom-events';
|
||||
import { createLocationWatcher } from './location-watcher';
|
||||
|
||||
/**
|
||||
* Implements [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController).
|
||||
@@ -14,6 +16,7 @@ export class ContentScriptContext implements AbortController {
|
||||
|
||||
#isTopFrame = window.self === window.top;
|
||||
#abortController: AbortController;
|
||||
#locationWatcher = createLocationWatcher(this);
|
||||
|
||||
constructor(
|
||||
private readonly contentScriptName: string,
|
||||
@@ -137,23 +140,42 @@ export class ContentScriptContext implements AbortController {
|
||||
/**
|
||||
* Call `target.addEventListener` and remove the event listener when the context is invalidated.
|
||||
*
|
||||
* Includes additional events useful for content scripts:
|
||||
*
|
||||
* - `"wxt:locationchange"` - Triggered when HTML5 history mode is used to change URL. Content
|
||||
* scripts are not reloaded when navigating this way, so this can be used to reset the content
|
||||
* script state on URL change, or run custom code.
|
||||
*
|
||||
* @example
|
||||
* ctx.addEventListener(window, "mousemove", () => {
|
||||
* // ...
|
||||
* });
|
||||
* ctx.addEventListener(document, "visibilitychange", () => {
|
||||
* // ...
|
||||
* });
|
||||
* ctx.addEventListener(document, "wxt:locationchange", () => {
|
||||
* // ...
|
||||
* });
|
||||
*/
|
||||
addEventListener(
|
||||
target: any,
|
||||
type: string,
|
||||
handler: (event: Event) => void,
|
||||
addEventListener<
|
||||
TTarget extends EventTarget,
|
||||
TType extends keyof WxtContentScriptEventMap,
|
||||
>(
|
||||
target: TTarget,
|
||||
type: TType,
|
||||
handler: (event: WxtContentScriptEventMap[TType]) => void,
|
||||
options?: AddEventListenerOptions,
|
||||
) {
|
||||
target.addEventListener?.(type, handler, options);
|
||||
this.onInvalidated(
|
||||
() => target.removeEventListener?.(type, handler, options),
|
||||
if (type === 'wxt:locationchange') {
|
||||
// Start the location watcher when adding the event for the first time
|
||||
if (this.isValid) this.#locationWatcher.run();
|
||||
}
|
||||
|
||||
target.addEventListener?.(
|
||||
type.startsWith('wxt:') ? getUniqueEventName(type) : type,
|
||||
// @ts-expect-error: Event don't match, but that's OK, EventTarget doesn't allow custom types in the callback
|
||||
handler,
|
||||
{
|
||||
...options,
|
||||
signal: this.signal,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -193,3 +215,7 @@ export class ContentScriptContext implements AbortController {
|
||||
this.onInvalidated(() => removeEventListener('message', cb));
|
||||
}
|
||||
}
|
||||
|
||||
interface WxtContentScriptEventMap extends WindowEventMap {
|
||||
'wxt:locationchange': WxtLocationChangeEvent;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { browser } from '~/browser';
|
||||
|
||||
export class WxtLocationChangeEvent extends Event {
|
||||
static EVENT_NAME = getUniqueEventName('wxt:locationchange');
|
||||
|
||||
constructor(
|
||||
readonly newUrl: URL,
|
||||
readonly oldUrl: URL,
|
||||
) {
|
||||
super(WxtLocationChangeEvent.EVENT_NAME, {});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an event name unique to the extension and content script that's running.
|
||||
*/
|
||||
export function getUniqueEventName(eventName: string): string {
|
||||
// During the build process, __ENTRYPOINT__ is not defined when importing entrypoints to get their
|
||||
// metadata.
|
||||
const entrypointName =
|
||||
typeof __ENTRYPOINT__ === 'undefined' ? 'build' : __ENTRYPOINT__;
|
||||
|
||||
return `${browser.runtime.id}:${entrypointName}:${eventName}`;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { ContentScriptContext } from '.';
|
||||
import { WxtLocationChangeEvent } from './custom-events';
|
||||
|
||||
/**
|
||||
* Create a util that watches for URL changes, dispatching the custom event when detected. Stops
|
||||
* watching when content script is invalidated.
|
||||
*/
|
||||
export function createLocationWatcher(ctx: ContentScriptContext) {
|
||||
let interval: number | undefined;
|
||||
let oldUrl: URL;
|
||||
|
||||
return {
|
||||
/**
|
||||
* Ensure the location watcher is actively looking for URL changes. If it's already watching,
|
||||
* this is a noop.
|
||||
*/
|
||||
run() {
|
||||
if (interval != null) return;
|
||||
|
||||
oldUrl = new URL(location.href);
|
||||
interval = ctx.setInterval(() => {
|
||||
let newUrl = new URL(location.href);
|
||||
if (newUrl.href !== oldUrl.href) {
|
||||
window.dispatchEvent(new WxtLocationChangeEvent(newUrl, oldUrl));
|
||||
oldUrl = newUrl;
|
||||
}
|
||||
}, 1e3);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
import type * as vite from 'vite';
|
||||
import {
|
||||
BuildStepOutput,
|
||||
Entrypoint,
|
||||
InlineConfig,
|
||||
InternalConfig,
|
||||
UserConfig,
|
||||
WxtBuilder,
|
||||
WxtBuilderServer,
|
||||
} from '~/types';
|
||||
import * as wxtPlugins from './plugins';
|
||||
import { getEntrypointBundlePath } from '~/core/utils/entrypoints';
|
||||
|
||||
export async function craeteViteBuilder(
|
||||
inlineConfig: InlineConfig,
|
||||
userConfig: UserConfig,
|
||||
wxtConfig: Omit<InternalConfig, 'builder'>,
|
||||
): Promise<WxtBuilder> {
|
||||
const vite = await import('vite');
|
||||
|
||||
/**
|
||||
* Returns the base vite config shared by all builds based on the inline and user config.
|
||||
*/
|
||||
const getBaseConfig = async () => {
|
||||
const resolvedInlineConfig =
|
||||
(await inlineConfig.vite?.(wxtConfig.env)) ?? {};
|
||||
const resolvedUserConfig = (await userConfig.vite?.(wxtConfig.env)) ?? {};
|
||||
|
||||
const config: vite.InlineConfig = vite.mergeConfig(
|
||||
resolvedUserConfig,
|
||||
resolvedInlineConfig,
|
||||
);
|
||||
|
||||
config.root = wxtConfig.root;
|
||||
config.configFile = false;
|
||||
config.logLevel = 'warn';
|
||||
config.mode = wxtConfig.mode;
|
||||
|
||||
config.build ??= {};
|
||||
config.build.outDir = wxtConfig.outDir;
|
||||
config.build.emptyOutDir = false;
|
||||
|
||||
config.plugins ??= [];
|
||||
config.plugins.push(
|
||||
wxtPlugins.download(wxtConfig),
|
||||
wxtPlugins.devHtmlPrerender(wxtConfig),
|
||||
wxtPlugins.unimport(wxtConfig),
|
||||
wxtPlugins.virtualEntrypoint('background', wxtConfig),
|
||||
wxtPlugins.virtualEntrypoint('content-script', wxtConfig),
|
||||
wxtPlugins.virtualEntrypoint('unlisted-script', wxtConfig),
|
||||
wxtPlugins.devServerGlobals(wxtConfig),
|
||||
wxtPlugins.tsconfigPaths(wxtConfig),
|
||||
wxtPlugins.noopBackground(),
|
||||
wxtPlugins.globals(wxtConfig),
|
||||
wxtPlugins.excludeBrowserPolyfill(wxtConfig),
|
||||
);
|
||||
if (wxtConfig.analysis.enabled) {
|
||||
config.plugins.push(wxtPlugins.bundleAnalysis());
|
||||
}
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the basic config for building an entrypoint in [lib mode](https://vitejs.dev/guide/build.html#library-mode).
|
||||
*/
|
||||
const getLibModeConfig = (entrypoint: Entrypoint): vite.InlineConfig => {
|
||||
const isVirtual = [
|
||||
'background',
|
||||
'content-script',
|
||||
'unlisted-script',
|
||||
].includes(entrypoint.type);
|
||||
const entry = isVirtual
|
||||
? `virtual:wxt-${entrypoint.type}?${entrypoint.inputPath}`
|
||||
: entrypoint.inputPath;
|
||||
|
||||
const plugins: NonNullable<vite.UserConfig['plugins']> = [
|
||||
wxtPlugins.entrypointGroupGlobals(entrypoint),
|
||||
];
|
||||
if (
|
||||
entrypoint.type === 'content-script-style' ||
|
||||
entrypoint.type === 'unlisted-style'
|
||||
) {
|
||||
plugins.push(wxtPlugins.cssEntrypoints(entrypoint, wxtConfig));
|
||||
}
|
||||
|
||||
const libMode: vite.UserConfig = {
|
||||
mode: wxtConfig.mode,
|
||||
plugins,
|
||||
build: {
|
||||
lib: {
|
||||
entry,
|
||||
formats: ['iife'],
|
||||
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,
|
||||
wxtConfig.outDir,
|
||||
'.js',
|
||||
),
|
||||
// Output content script CSS to `content-scripts/`, but all other scripts are written to
|
||||
// `assets/`.
|
||||
assetFileNames: ({ name }) => {
|
||||
if (
|
||||
entrypoint.type === 'content-script' &&
|
||||
name?.endsWith('css')
|
||||
) {
|
||||
return `content-scripts/${entrypoint.name}.[ext]`;
|
||||
} else {
|
||||
return `assets/${entrypoint.name}.[ext]`;
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
define: {
|
||||
// See https://github.com/aklinker1/vite-plugin-web-extension/issues/96
|
||||
'process.env.NODE_ENV': JSON.stringify(wxtConfig.mode),
|
||||
},
|
||||
};
|
||||
return libMode;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the basic config for building multiple entrypoints in [multi-page mode](https://vitejs.dev/guide/build.html#multi-page-app).
|
||||
*/
|
||||
const getMultiPageConfig = (entrypoints: Entrypoint[]): vite.InlineConfig => {
|
||||
return {
|
||||
mode: wxtConfig.mode,
|
||||
plugins: [
|
||||
wxtPlugins.multipageMove(entrypoints, wxtConfig),
|
||||
wxtPlugins.entrypointGroupGlobals(entrypoints),
|
||||
],
|
||||
build: {
|
||||
rollupOptions: {
|
||||
input: entrypoints.reduce<Record<string, string>>((input, entry) => {
|
||||
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]',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the basic config for building a sinlge CSS entrypoint in [multi-page mode](https://vitejs.dev/guide/build.html#multi-page-app).
|
||||
*/
|
||||
const getCssConfig = (entrypoint: Entrypoint): vite.InlineConfig => {
|
||||
return {
|
||||
mode: wxtConfig.mode,
|
||||
plugins: [wxtPlugins.entrypointGroupGlobals(entrypoint)],
|
||||
build: {
|
||||
rollupOptions: {
|
||||
input: {
|
||||
[entrypoint.name]: entrypoint.inputPath,
|
||||
},
|
||||
output: {
|
||||
assetFileNames: () => {
|
||||
if (entrypoint.type === 'content-script-style') {
|
||||
return `content-scripts/${entrypoint.name}.[ext]`;
|
||||
} else {
|
||||
return `assets/${entrypoint.name}.[ext]`;
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
name: 'Vite',
|
||||
version: vite.version,
|
||||
async build(group) {
|
||||
let entryConfig;
|
||||
if (Array.isArray(group)) entryConfig = getMultiPageConfig(group);
|
||||
else if (group.inputPath.endsWith('.css'))
|
||||
entryConfig = getCssConfig(group);
|
||||
else entryConfig = getLibModeConfig(group);
|
||||
|
||||
const buildConfig = vite.mergeConfig(await getBaseConfig(), entryConfig);
|
||||
const result = await vite.build(buildConfig);
|
||||
return {
|
||||
entrypoints: group,
|
||||
chunks: getBuildOutputChunks(result),
|
||||
};
|
||||
},
|
||||
async createServer(info) {
|
||||
const serverConfig: vite.InlineConfig = {
|
||||
server: {
|
||||
port: info.port,
|
||||
strictPort: true,
|
||||
host: info.hostname,
|
||||
origin: info.origin,
|
||||
},
|
||||
};
|
||||
const baseConfig = await getBaseConfig();
|
||||
const viteServer = await vite.createServer(
|
||||
vite.mergeConfig(baseConfig, serverConfig),
|
||||
);
|
||||
|
||||
const server: WxtBuilderServer = {
|
||||
async listen() {
|
||||
await viteServer.listen(info.port);
|
||||
},
|
||||
transformHtml(...args) {
|
||||
return viteServer.transformIndexHtml(...args);
|
||||
},
|
||||
ws: {
|
||||
send(message, payload) {
|
||||
return viteServer.ws.send(message, payload);
|
||||
},
|
||||
on(message, cb) {
|
||||
viteServer.ws.on(message, cb);
|
||||
},
|
||||
},
|
||||
watcher: viteServer.watcher,
|
||||
};
|
||||
|
||||
return server;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getBuildOutputChunks(
|
||||
result: Awaited<ReturnType<typeof vite.build>>,
|
||||
): 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;
|
||||
}
|
||||
+1
-1
@@ -14,7 +14,7 @@ import { getEntrypointBundlePath } from '~/core/utils/entrypoints';
|
||||
*/
|
||||
export function cssEntrypoints(
|
||||
entrypoint: Entrypoint,
|
||||
config: InternalConfig,
|
||||
config: Omit<InternalConfig, 'builder'>,
|
||||
): vite.Plugin {
|
||||
return {
|
||||
name: 'wxt:css-entrypoint',
|
||||
+5
-7
@@ -2,7 +2,7 @@ import * as vite from 'vite';
|
||||
import { InternalConfig } from '~/types';
|
||||
import { getEntrypointName } from '~/core/utils/entrypoints';
|
||||
import { parseHTML } from 'linkedom';
|
||||
import { dirname, isAbsolute, relative, resolve } from 'path';
|
||||
import { dirname, isAbsolute, relative, resolve } from 'node:path';
|
||||
|
||||
// Cache the preamble script for all devHtmlPrerender plugins, not just one
|
||||
let reactRefreshPreamble = '';
|
||||
@@ -10,7 +10,9 @@ let reactRefreshPreamble = '';
|
||||
/**
|
||||
* Pre-renders the HTML entrypoints when building the extension to connect to the dev server.
|
||||
*/
|
||||
export function devHtmlPrerender(config: InternalConfig): vite.PluginOption {
|
||||
export function devHtmlPrerender(
|
||||
config: Omit<InternalConfig, 'builder'>,
|
||||
): vite.PluginOption {
|
||||
const htmlReloadId = '@wxt/reload-html';
|
||||
const resolvedHtmlReloadId = resolve(
|
||||
config.root,
|
||||
@@ -86,11 +88,7 @@ export function devHtmlPrerender(config: InternalConfig): vite.PluginOption {
|
||||
const originalUrl = `${server.origin}${ctx.path}`;
|
||||
const name = getEntrypointName(config.entrypointsDir, ctx.filename);
|
||||
const url = `${server.origin}/${name}.html`;
|
||||
const serverHtml = await server.transformIndexHtml(
|
||||
url,
|
||||
html,
|
||||
originalUrl,
|
||||
);
|
||||
const serverHtml = await server.transformHtml(url, html, originalUrl);
|
||||
const { document } = parseHTML(serverHtml);
|
||||
|
||||
// React pages include a preamble as an unsafe-inline type="module" script to enable fast refresh, as shown here:
|
||||
+6
-7
@@ -4,20 +4,19 @@ 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 {
|
||||
export function devServerGlobals(
|
||||
config: Omit<InternalConfig, 'builder'>,
|
||||
): Plugin {
|
||||
return {
|
||||
name: 'wxt:dev-server-globals',
|
||||
config() {
|
||||
if (internalConfig.server == null || internalConfig.command == 'build')
|
||||
return;
|
||||
if (config.server == null || config.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),
|
||||
__DEV_SERVER_HOSTNAME__: JSON.stringify(config.server.hostname),
|
||||
__DEV_SERVER_PORT__: JSON.stringify(config.server.port),
|
||||
},
|
||||
};
|
||||
},
|
||||
@@ -9,7 +9,7 @@ import { fetchCached } from '~/core/utils/network';
|
||||
* @example
|
||||
* import "url:https://google-tagmanager.com/gtag?id=XYZ";
|
||||
*/
|
||||
export function download(config: InternalConfig): Plugin {
|
||||
export function download(config: Omit<InternalConfig, 'builder'>): Plugin {
|
||||
return {
|
||||
name: 'wxt:download',
|
||||
resolveId(id) {
|
||||
@@ -0,0 +1,24 @@
|
||||
import * as vite from 'vite';
|
||||
import { EntrypointGroup } from '~/types';
|
||||
import { getEntrypointGlobals } from '~/core/utils/globals';
|
||||
|
||||
/**
|
||||
* Define a set of global variables specific to an entrypoint.
|
||||
*/
|
||||
export function entrypointGroupGlobals(
|
||||
entrypointGroup: EntrypointGroup,
|
||||
): vite.PluginOption {
|
||||
return {
|
||||
name: 'wxt:entrypoint-group-globals',
|
||||
config() {
|
||||
const define: vite.InlineConfig['define'] = {};
|
||||
let name = Array.isArray(entrypointGroup) ? 'html' : entrypointGroup.name;
|
||||
for (const global of getEntrypointGlobals(name)) {
|
||||
define[global.name] = JSON.stringify(global.value);
|
||||
}
|
||||
return {
|
||||
define,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
+3
-1
@@ -6,7 +6,9 @@ import * as vite from 'vite';
|
||||
* `webextension-polyfill` module to a virtual module and exporting the `chrome` global from the
|
||||
* virtual module.
|
||||
*/
|
||||
export function excludeBrowserPolyfill(config: InternalConfig): vite.Plugin {
|
||||
export function excludeBrowserPolyfill(
|
||||
config: Omit<InternalConfig, 'builder'>,
|
||||
): vite.Plugin {
|
||||
const virtualId = 'virtual:wxt-webextension-polyfill-disabled';
|
||||
|
||||
return {
|
||||
@@ -1,8 +1,10 @@
|
||||
import * as vite from 'vite';
|
||||
import { InternalConfig } from '~/types';
|
||||
import { getGlobals } from '../utils/globals';
|
||||
import { getGlobals } from '~/core/utils/globals';
|
||||
|
||||
export function globals(config: InternalConfig): vite.PluginOption {
|
||||
export function globals(
|
||||
config: Omit<InternalConfig, 'builder'>,
|
||||
): vite.PluginOption {
|
||||
return {
|
||||
name: 'wxt:globals',
|
||||
config() {
|
||||
@@ -12,3 +12,4 @@ export * from './globals';
|
||||
export * from './webextensionPolyfillAlias';
|
||||
export * from './webextensionPolyfillInlineDeps';
|
||||
export * from './excludeBrowserPolyfill';
|
||||
export * from './entrypointGroupGlobals';
|
||||
+26
-2
@@ -1,6 +1,6 @@
|
||||
import * as vite from 'vite';
|
||||
import { Entrypoint, InternalConfig } from '~/types';
|
||||
import { dirname, extname, resolve } from 'node:path';
|
||||
import { dirname, extname, resolve, join } from 'node:path';
|
||||
import { getEntrypointBundlePath } from '~/core/utils/entrypoints';
|
||||
import fs, { ensureDir } from 'fs-extra';
|
||||
import { normalizePath } from '~/core/utils/paths';
|
||||
@@ -20,7 +20,7 @@ import { normalizePath } from '~/core/utils/paths';
|
||||
*/
|
||||
export function multipageMove(
|
||||
entrypoints: Entrypoint[],
|
||||
config: InternalConfig,
|
||||
config: Omit<InternalConfig, 'builder'>,
|
||||
): vite.Plugin {
|
||||
return {
|
||||
name: 'wxt:multipage-move',
|
||||
@@ -67,6 +67,30 @@ export function multipageMove(
|
||||
delete bundle[oldBundlePath];
|
||||
bundle[newBundlePath] = renamedChunk;
|
||||
}
|
||||
|
||||
// Remove directories that were created
|
||||
// TODO: Optimize and only delete old path directories
|
||||
removeEmptyDirs(config.outDir);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively remove all directories that are empty/
|
||||
*/
|
||||
export async function removeEmptyDirs(dir: string): Promise<void> {
|
||||
const files = await fs.readdir(dir);
|
||||
for (const file of files) {
|
||||
const filePath = join(dir, file);
|
||||
const stats = await fs.stat(filePath);
|
||||
if (stats.isDirectory()) {
|
||||
await removeEmptyDirs(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.rmdir(dir);
|
||||
} catch {
|
||||
// noop on failure - this means the directory was not empty.
|
||||
}
|
||||
}
|
||||
+1
-2
@@ -1,4 +1,5 @@
|
||||
import { Plugin } from 'vite';
|
||||
import { VIRTUAL_NOOP_BACKGROUND_MODULE_ID } from '~/core/utils/constants';
|
||||
|
||||
/**
|
||||
* In dev mode, if there's not a background script listed, we need to add one.
|
||||
@@ -20,5 +21,3 @@ export function noopBackground(): Plugin {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const VIRTUAL_NOOP_BACKGROUND_MODULE_ID = 'virtual:user-background';
|
||||
+3
-1
@@ -1,7 +1,9 @@
|
||||
import { InternalConfig } from '~/types';
|
||||
import * as vite from 'vite';
|
||||
|
||||
export function tsconfigPaths(config: InternalConfig): vite.Plugin {
|
||||
export function tsconfigPaths(
|
||||
config: Omit<InternalConfig, 'builder'>,
|
||||
): vite.Plugin {
|
||||
return {
|
||||
name: 'wxt:aliases',
|
||||
async config() {
|
||||
@@ -4,19 +4,21 @@ import { getUnimportOptions } from '~/core/utils/unimport';
|
||||
import * as vite 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,
|
||||
};
|
||||
const ENABLED_EXTENSIONS = new Set([
|
||||
'.js',
|
||||
'.jsx',
|
||||
'.ts',
|
||||
'.tsx',
|
||||
'.vue',
|
||||
'.svelte',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Inject any global imports defined by unimport
|
||||
*/
|
||||
export function unimport(config: InternalConfig): vite.PluginOption {
|
||||
export function unimport(
|
||||
config: Omit<InternalConfig, 'builder'>,
|
||||
): vite.PluginOption {
|
||||
const options = getUnimportOptions(config);
|
||||
if (options === false) return [];
|
||||
|
||||
@@ -28,8 +30,13 @@ export function unimport(config: InternalConfig): vite.PluginOption {
|
||||
await unimport.scanImportsFromDir(undefined, { cwd: config.srcDir });
|
||||
},
|
||||
async transform(code, id) {
|
||||
const ext = extname(id);
|
||||
if (ENABLED_EXTENSIONS[ext]) return unimport.injectImports(code, id);
|
||||
// Don't transform dependencies
|
||||
if (id.includes('node_modules')) return;
|
||||
|
||||
// Don't transform non-js files
|
||||
if (!ENABLED_EXTENSIONS.has(extname(id))) return;
|
||||
|
||||
return unimport.injectImports(code, id);
|
||||
},
|
||||
};
|
||||
}
|
||||
+1
-1
@@ -9,7 +9,7 @@ import { normalizePath } from '~/core/utils/paths';
|
||||
*/
|
||||
export function virtualEntrypoint(
|
||||
type: Entrypoint['type'],
|
||||
config: InternalConfig,
|
||||
config: Omit<InternalConfig, 'builder'>,
|
||||
): Plugin {
|
||||
const virtualId = `virtual:wxt-${type}?`;
|
||||
const resolvedVirtualId = `\0${virtualId}`;
|
||||
+1
-1
@@ -8,7 +8,7 @@ import { InternalConfig } from '~/types';
|
||||
* This should only be used during tests.
|
||||
*/
|
||||
export function webextensionPolyfillAlias(
|
||||
config: InternalConfig,
|
||||
config: Omit<InternalConfig, 'builder'>,
|
||||
): vite.PluginOption {
|
||||
return {
|
||||
name: 'wxt:webextension-polyfill-test-alias',
|
||||
+96
-114
@@ -3,10 +3,9 @@ import {
|
||||
EntrypointGroup,
|
||||
InlineConfig,
|
||||
InternalConfig,
|
||||
ServerInfo,
|
||||
WxtDevServer,
|
||||
} from '~/types';
|
||||
import * as vite from 'vite';
|
||||
import type { Scripting } from 'webextension-polyfill';
|
||||
import {
|
||||
getEntrypointBundlePath,
|
||||
getEntrypointOutputFile,
|
||||
@@ -38,35 +37,103 @@ import { relative } from 'node:path';
|
||||
* await server.start();
|
||||
*/
|
||||
export async function createServer(
|
||||
config?: InlineConfig,
|
||||
inlineConfig?: InlineConfig,
|
||||
): Promise<WxtDevServer> {
|
||||
const serverInfo = await getServerInfo();
|
||||
|
||||
const getLatestInternalConfig = async () => {
|
||||
return getInternalConfig(
|
||||
{
|
||||
...config,
|
||||
vite: () => serverInfo.viteServerConfig,
|
||||
},
|
||||
'serve',
|
||||
);
|
||||
const port = await getPort();
|
||||
const hostname = 'localhost';
|
||||
const origin = `http://${hostname}:${port}`;
|
||||
const serverInfo: ServerInfo = {
|
||||
port,
|
||||
hostname,
|
||||
origin,
|
||||
};
|
||||
|
||||
let internalConfig = await getLatestInternalConfig();
|
||||
const server = await setupServer(serverInfo, internalConfig);
|
||||
internalConfig.server = server;
|
||||
// Server instance must be created first so its reference can be added to the internal config used
|
||||
// to pre-render entrypoints
|
||||
const server: WxtDevServer = {
|
||||
...serverInfo,
|
||||
watcher: undefined as any, // Filled out later down below
|
||||
ws: undefined as any, // Filled out later down below
|
||||
currentOutput: undefined as any, // Filled out later down below
|
||||
async start() {
|
||||
await builderServer.listen();
|
||||
config.logger.success(`Started dev server @ ${serverInfo.origin}`);
|
||||
|
||||
// Build after starting the dev server so it can be used to transform HTML files
|
||||
server.currentOutput = await internalBuild(config);
|
||||
|
||||
// Open browser after everything is ready to go.
|
||||
await runner.openBrowser(config);
|
||||
},
|
||||
transformHtml(url, html, originalUrl) {
|
||||
return builderServer.transformHtml(url, html, originalUrl);
|
||||
},
|
||||
reloadContentScript(contentScript) {
|
||||
server.ws.send('wxt:reload-content-script', contentScript);
|
||||
},
|
||||
reloadPage(path) {
|
||||
server.ws.send('wxt:reload-page', path);
|
||||
},
|
||||
reloadExtension() {
|
||||
server.ws.send('wxt:reload-extension');
|
||||
},
|
||||
};
|
||||
|
||||
const getLatestConfig = () =>
|
||||
getInternalConfig(inlineConfig ?? {}, 'serve', server);
|
||||
let config = await getLatestConfig();
|
||||
|
||||
const [runner, builderServer] = await Promise.all([
|
||||
createExtensionRunner(config),
|
||||
config.builder.createServer(server),
|
||||
]);
|
||||
|
||||
server.watcher = builderServer.watcher;
|
||||
server.ws = builderServer.ws;
|
||||
|
||||
// Register content scripts for the first time after the background starts up since they're not
|
||||
// listed in the manifest
|
||||
server.ws.on('wxt:background-initialized', () => {
|
||||
reloadContentScripts(server.currentOutput.steps, config, server);
|
||||
});
|
||||
|
||||
// Listen for file changes and reload different parts of the extension accordingly
|
||||
const reloadOnChange = createFileReloader({
|
||||
server,
|
||||
getLatestConfig,
|
||||
updateConfig(newConfig) {
|
||||
config = newConfig;
|
||||
},
|
||||
});
|
||||
server.watcher.on('all', reloadOnChange);
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
async function getPort(): Promise<number> {
|
||||
const { default: getPort, portNumbers } = await import('get-port');
|
||||
return await getPort({ port: portNumbers(3000, 3010) });
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a function responsible for reloading different parts of the extension when a file
|
||||
* changes.
|
||||
*/
|
||||
function createFileReloader(options: {
|
||||
server: WxtDevServer;
|
||||
getLatestConfig: () => Promise<InternalConfig>;
|
||||
updateConfig: (config: InternalConfig) => void;
|
||||
}) {
|
||||
const { server, getLatestConfig, updateConfig } = options;
|
||||
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);
|
||||
});
|
||||
return async (event: string, path: string) => {
|
||||
const config = await getLatestConfig();
|
||||
updateConfig(config);
|
||||
|
||||
server.watcher.on('all', async (event, path, _stats) => {
|
||||
// Here, "path" is a non-normalized path (ie: C:\\users\\... instead of C:/users/...)
|
||||
if (path.startsWith(internalConfig.outBaseDir)) return;
|
||||
if (path.startsWith(config.outBaseDir)) return;
|
||||
changeQueue.push([event, path]);
|
||||
|
||||
await fileChangedMutex.runExclusive(async () => {
|
||||
@@ -77,25 +144,23 @@ export async function createServer(
|
||||
if (changes.type === 'no-change') return;
|
||||
|
||||
// Log the entrypoints that were effected
|
||||
internalConfig.logger.info(
|
||||
config.logger.info(
|
||||
`Changed: ${Array.from(new Set(fileChanges.map((change) => change[1])))
|
||||
.map((file) => pc.dim(relative(internalConfig.root, file)))
|
||||
.map((file) => pc.dim(relative(config.root, file)))
|
||||
.join(', ')}`,
|
||||
);
|
||||
const rebuiltNames = changes.rebuildGroups
|
||||
.flat()
|
||||
.map((entry) => {
|
||||
return pc.cyan(
|
||||
relative(internalConfig.outDir, getEntrypointOutputFile(entry, '')),
|
||||
relative(config.outDir, getEntrypointOutputFile(entry, '')),
|
||||
);
|
||||
})
|
||||
.join(pc.dim(', '));
|
||||
|
||||
// Get latest config and Rebuild groups with changes
|
||||
internalConfig = await getLatestInternalConfig();
|
||||
internalConfig.server = server;
|
||||
// Rebuild entrypoints on change
|
||||
const { output: newOutput } = await rebuild(
|
||||
internalConfig,
|
||||
config,
|
||||
// TODO: this excludes new entrypoints, so they're not built until the dev command is restarted
|
||||
changes.rebuildGroups,
|
||||
changes.cachedOutput,
|
||||
@@ -108,91 +173,15 @@ export async function createServer(
|
||||
server.reloadExtension();
|
||||
break;
|
||||
case 'html-reload':
|
||||
reloadHtmlPages(changes.rebuildGroups, server, internalConfig);
|
||||
reloadHtmlPages(changes.rebuildGroups, server, config);
|
||||
break;
|
||||
case 'content-script-reload':
|
||||
reloadContentScripts(changes.changedSteps, internalConfig, server);
|
||||
reloadContentScripts(changes.changedSteps, config, server);
|
||||
break;
|
||||
}
|
||||
consola.success(`Reloaded: ${rebuiltNames}`);
|
||||
});
|
||||
});
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
async function getServerInfo(): Promise<ServerInfo> {
|
||||
const { default: getPort, portNumbers } = await import('get-port');
|
||||
const port = await getPort({ port: portNumbers(3000, 3010) });
|
||||
const hostname = 'localhost';
|
||||
const origin = `http://${hostname}:${port}`;
|
||||
const serverConfig: vite.InlineConfig = {
|
||||
server: {
|
||||
origin,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
port,
|
||||
hostname,
|
||||
origin,
|
||||
viteServerConfig: serverConfig,
|
||||
};
|
||||
}
|
||||
|
||||
async function setupServer(
|
||||
serverInfo: ServerInfo,
|
||||
config: InternalConfig,
|
||||
): Promise<WxtDevServer> {
|
||||
const runner = await createExtensionRunner(config);
|
||||
|
||||
const viteServer = await vite.createServer(
|
||||
vite.mergeConfig(serverInfo, await config.vite(config.env)),
|
||||
);
|
||||
|
||||
const start = async () => {
|
||||
await viteServer.listen(server.port);
|
||||
config.logger.success(`Started dev server @ ${serverInfo.origin}`);
|
||||
|
||||
server.currentOutput = await internalBuild(config);
|
||||
await runner.openBrowser(config);
|
||||
};
|
||||
|
||||
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: Omit<Scripting.RegisteredContentScript, 'id'>,
|
||||
) => {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -244,10 +233,3 @@ function reloadHtmlPages(
|
||||
server.reloadPage(path);
|
||||
});
|
||||
}
|
||||
|
||||
interface ServerInfo {
|
||||
port: number;
|
||||
hostname: string;
|
||||
origin: string;
|
||||
viteServerConfig: vite.InlineConfig;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ describe('Content Script Utils', () => {
|
||||
);
|
||||
|
||||
expect(hash).toMatchInlineSnapshot(
|
||||
'"[[\\"all_frames\\",false],[\\"exclude_globs\\",[]],[\\"exclude_matches\\",[]],[\\"include_globs\\",[]],[\\"match_about_blank\\",false],[\\"match_origin_as_fallback\\",false],[\\"matches\\",[]],[\\"run_at\\",\\"document_idle\\"],[\\"world\\",\\"ISOLATED\\"]]"',
|
||||
`"[["all_frames",false],["exclude_globs",[]],["exclude_matches",[]],["include_globs",[]],["match_about_blank",false],["match_origin_as_fallback",false],["matches",[]],["run_at","document_idle"],["world","ISOLATED"]]"`,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { normalizePath } from '../paths';
|
||||
|
||||
describe('Path Utils', () => {
|
||||
describe('normalizePath', () => {
|
||||
it.each([
|
||||
// Relative paths
|
||||
['../test.sh', '../test.sh'],
|
||||
['..\\test.sh', '../test.sh'],
|
||||
['test.png', 'test.png'],
|
||||
// Absolute paths
|
||||
['C:\\\\path\\to\\file', 'C:/path/to/file'],
|
||||
['/path/to/file', '/path/to/file'],
|
||||
// Strip trailing slash
|
||||
['C:\\\\path\\to\\folder\\', 'C:/path/to/folder'],
|
||||
['/path/to/folder/', '/path/to/folder'],
|
||||
// Dedupe slashes
|
||||
['path\\\\\\file', 'path/file'],
|
||||
['path//file', 'path/file'],
|
||||
])('should normalize "%s" to "%s"', (input, expected) => {
|
||||
expect(normalizePath(input)).toBe(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -8,8 +8,8 @@ import {
|
||||
fakeManifest,
|
||||
fakeOptionsEntrypoint,
|
||||
fakePopupEntrypoint,
|
||||
fakeRollupOutputAsset,
|
||||
fakeRollupOutputChunk,
|
||||
fakeOutputAsset,
|
||||
fakeOutputChunk,
|
||||
} from '~/core/utils/testing/fake-objects';
|
||||
import { BuildOutput, BuildStepOutput } from '~/types';
|
||||
|
||||
@@ -32,15 +32,11 @@ describe('Detect Dev Changes', () => {
|
||||
steps: [
|
||||
{
|
||||
entrypoints: fakeContentScriptEntrypoint(),
|
||||
chunks: [fakeRollupOutputChunk(), fakeRollupOutputChunk()],
|
||||
chunks: [fakeOutputChunk(), fakeOutputChunk()],
|
||||
},
|
||||
{
|
||||
entrypoints: fakeContentScriptEntrypoint(),
|
||||
chunks: [
|
||||
fakeRollupOutputChunk(),
|
||||
fakeRollupOutputChunk(),
|
||||
fakeRollupOutputChunk(),
|
||||
],
|
||||
chunks: [fakeOutputChunk(), fakeOutputChunk(), fakeOutputChunk()],
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -57,10 +53,10 @@ describe('Detect Dev Changes', () => {
|
||||
'unknown',
|
||||
'/root/src/public/image.svg',
|
||||
];
|
||||
const asset1 = fakeRollupOutputAsset({
|
||||
const asset1 = fakeOutputAsset({
|
||||
fileName: 'image.svg',
|
||||
});
|
||||
const asset2 = fakeRollupOutputAsset({
|
||||
const asset2 = fakeOutputAsset({
|
||||
fileName: 'some-other-image.svg',
|
||||
});
|
||||
const currentOutput: BuildOutput = {
|
||||
@@ -96,7 +92,7 @@ describe('Detect Dev Changes', () => {
|
||||
const step1: BuildStepOutput = {
|
||||
entrypoints: contentScript,
|
||||
chunks: [
|
||||
fakeRollupOutputChunk({
|
||||
fakeOutputChunk({
|
||||
moduleIds: [fakeFile(), fakeFile()],
|
||||
}),
|
||||
],
|
||||
@@ -104,7 +100,7 @@ describe('Detect Dev Changes', () => {
|
||||
const step2: BuildStepOutput = {
|
||||
entrypoints: background,
|
||||
chunks: [
|
||||
fakeRollupOutputChunk({
|
||||
fakeOutputChunk({
|
||||
moduleIds: [fakeFile(), changedPath, fakeFile()],
|
||||
}),
|
||||
],
|
||||
@@ -150,7 +146,7 @@ describe('Detect Dev Changes', () => {
|
||||
const step1: BuildStepOutput = {
|
||||
entrypoints: [htmlPage1, htmlPage2],
|
||||
chunks: [
|
||||
fakeRollupOutputChunk({
|
||||
fakeOutputChunk({
|
||||
moduleIds: [fakeFile(), changedPath],
|
||||
}),
|
||||
],
|
||||
@@ -158,7 +154,7 @@ describe('Detect Dev Changes', () => {
|
||||
const step2: BuildStepOutput = {
|
||||
entrypoints: [htmlPage3],
|
||||
chunks: [
|
||||
fakeRollupOutputChunk({
|
||||
fakeOutputChunk({
|
||||
moduleIds: [fakeFile(), fakeFile(), fakeFile()],
|
||||
}),
|
||||
],
|
||||
@@ -203,7 +199,7 @@ describe('Detect Dev Changes', () => {
|
||||
const step1: BuildStepOutput = {
|
||||
entrypoints: script1,
|
||||
chunks: [
|
||||
fakeRollupOutputChunk({
|
||||
fakeOutputChunk({
|
||||
moduleIds: [fakeFile(), changedPath],
|
||||
}),
|
||||
],
|
||||
@@ -211,7 +207,7 @@ describe('Detect Dev Changes', () => {
|
||||
const step2: BuildStepOutput = {
|
||||
entrypoints: script2,
|
||||
chunks: [
|
||||
fakeRollupOutputChunk({
|
||||
fakeOutputChunk({
|
||||
moduleIds: [fakeFile(), fakeFile(), fakeFile()],
|
||||
}),
|
||||
],
|
||||
@@ -219,7 +215,7 @@ describe('Detect Dev Changes', () => {
|
||||
const step3: BuildStepOutput = {
|
||||
entrypoints: script3,
|
||||
chunks: [
|
||||
fakeRollupOutputChunk({
|
||||
fakeOutputChunk({
|
||||
moduleIds: [changedPath, fakeFile(), fakeFile()],
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
import * as vite from 'vite';
|
||||
import {
|
||||
BuildOutput,
|
||||
BuildStepOutput,
|
||||
Entrypoint,
|
||||
EntrypointGroup,
|
||||
InternalConfig,
|
||||
} from '~/types';
|
||||
import * as wxtPlugins from '~/core/vite-plugins';
|
||||
import { removeEmptyDirs } from '~/core/utils/fs';
|
||||
import { getPublicFiles } from '~/core/utils/fs';
|
||||
import { getEntrypointBundlePath } from '~/core/utils/entrypoints';
|
||||
import fs from 'fs-extra';
|
||||
import { dirname, resolve } from 'path';
|
||||
import { getEntrypointGlobals } from '~/core/utils/globals';
|
||||
import type { Ora } from 'ora';
|
||||
import pc from 'picocolors';
|
||||
|
||||
@@ -24,155 +18,18 @@ export async function buildEntrypoints(
|
||||
const steps: BuildStepOutput[] = [];
|
||||
for (let i = 0; i < groups.length; i++) {
|
||||
const group = groups[i];
|
||||
|
||||
spinner.text =
|
||||
pc.dim(`[${i + 1}/${groups.length}]`) +
|
||||
` ${[group]
|
||||
.flat()
|
||||
.map((e) => e.name)
|
||||
.join(pc.dim(', '))}`;
|
||||
|
||||
const step = Array.isArray(group)
|
||||
? await buildMultipleEntrypoints(group, config)
|
||||
: await buildSingleEntrypoint(group, config);
|
||||
steps.push(step);
|
||||
const groupNames = [group]
|
||||
.flat()
|
||||
.map((e) => e.name)
|
||||
.join(pc.dim(', '));
|
||||
spinner.text = pc.dim(`[${i + 1}/${groups.length}]`) + ` ${groupNames}`;
|
||||
steps.push(await config.builder.build(group));
|
||||
}
|
||||
const publicAssets = await copyPublicDirectory(config);
|
||||
|
||||
// Remove any empty directories from moving outputs around
|
||||
await removeEmptyDirs(config.outDir);
|
||||
|
||||
return { publicAssets, steps };
|
||||
}
|
||||
|
||||
/**
|
||||
* Use Vite's lib mode + IIFE format to bundle the entrypoint to a single file.
|
||||
*/
|
||||
async function buildSingleEntrypoint(
|
||||
entrypoint: Entrypoint,
|
||||
config: InternalConfig,
|
||||
): Promise<BuildStepOutput> {
|
||||
// Should this entrypoint be wrapped by the vite-plugins/virtualEntrypoint plugin?
|
||||
const isVirtual = [
|
||||
'background',
|
||||
'content-script',
|
||||
'unlisted-script',
|
||||
].includes(entrypoint.type);
|
||||
const entry = isVirtual
|
||||
? `virtual:wxt-${entrypoint.type}?${entrypoint.inputPath}`
|
||||
: entrypoint.inputPath;
|
||||
|
||||
const plugins: NonNullable<vite.UserConfig['plugins']> = [];
|
||||
if (
|
||||
entrypoint.type === 'content-script-style' ||
|
||||
entrypoint.type === 'unlisted-style'
|
||||
) {
|
||||
plugins.push(wxtPlugins.cssEntrypoints(entrypoint, config));
|
||||
}
|
||||
|
||||
const libMode: vite.UserConfig = {
|
||||
mode: config.mode,
|
||||
plugins,
|
||||
build: {
|
||||
lib: {
|
||||
entry,
|
||||
formats: ['iife'],
|
||||
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,
|
||||
'.js',
|
||||
),
|
||||
// Output content script CSS to `content-scripts/`, but all other scripts are written to
|
||||
// `assets/`.
|
||||
assetFileNames: ({ name }) => {
|
||||
if (entrypoint.type === 'content-script' && name?.endsWith('css')) {
|
||||
return `content-scripts/${entrypoint.name}.[ext]`;
|
||||
} else {
|
||||
return `assets/${entrypoint.name}.[ext]`;
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
define: {
|
||||
// See https://github.com/aklinker1/vite-plugin-web-extension/issues/96
|
||||
'process.env.NODE_ENV': JSON.stringify(config.mode),
|
||||
},
|
||||
};
|
||||
for (const global of getEntrypointGlobals(config, entrypoint.name)) {
|
||||
libMode.define![global.name] = JSON.stringify(global.value);
|
||||
}
|
||||
const entryConfig = vite.mergeConfig(
|
||||
libMode,
|
||||
await config.vite(config.env),
|
||||
) as vite.InlineConfig;
|
||||
|
||||
const result = await vite.build(entryConfig);
|
||||
return {
|
||||
entrypoints: entrypoint,
|
||||
chunks: getBuildOutputChunks(result),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Use Vite's multipage build to bundle all the entrypoints in a single step.
|
||||
*/
|
||||
async function buildMultipleEntrypoints(
|
||||
entrypoints: Entrypoint[],
|
||||
config: InternalConfig,
|
||||
): Promise<BuildStepOutput> {
|
||||
const multiPage: vite.UserConfig = {
|
||||
mode: config.mode,
|
||||
plugins: [wxtPlugins.multipageMove(entrypoints, config)],
|
||||
build: {
|
||||
rollupOptions: {
|
||||
input: entrypoints.reduce<Record<string, string>>((input, entry) => {
|
||||
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]',
|
||||
},
|
||||
},
|
||||
},
|
||||
define: {},
|
||||
};
|
||||
for (const global of getEntrypointGlobals(config, 'html')) {
|
||||
multiPage.define![global.name] = JSON.stringify(global.value);
|
||||
}
|
||||
|
||||
const entryConfig = vite.mergeConfig(
|
||||
multiPage,
|
||||
await config.vite(config.env),
|
||||
) as vite.UserConfig;
|
||||
|
||||
const result = await vite.build(entryConfig);
|
||||
return {
|
||||
entrypoints,
|
||||
chunks: getBuildOutputChunks(result),
|
||||
};
|
||||
}
|
||||
|
||||
function getBuildOutputChunks(
|
||||
result: Awaited<ReturnType<typeof vite.build>>,
|
||||
): 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;
|
||||
}
|
||||
|
||||
async function copyPublicDirectory(
|
||||
config: InternalConfig,
|
||||
): Promise<BuildOutput['publicAssets']> {
|
||||
@@ -189,9 +46,6 @@ async function copyPublicDirectory(
|
||||
publicAssets.push({
|
||||
type: 'asset',
|
||||
fileName: file,
|
||||
name: file,
|
||||
needsCodeReference: false,
|
||||
source: await fs.readFile(srcPath),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { BuildOutput, BuildStepOutput, EntrypointGroup } from '~/types';
|
||||
import * as vite from 'vite';
|
||||
import {
|
||||
BuildOutput,
|
||||
BuildStepOutput,
|
||||
EntrypointGroup,
|
||||
OutputAsset,
|
||||
OutputFile,
|
||||
} from '~/types';
|
||||
import { every } from '~/core/utils/arrays';
|
||||
import { normalizePath } from '~/core/utils/paths';
|
||||
|
||||
@@ -105,9 +110,7 @@ function findEffectedSteps(
|
||||
const changes: DetectedChange[] = [];
|
||||
const changedPath = normalizePath(changedFile[1]);
|
||||
|
||||
const isChunkEffected = (
|
||||
chunk: vite.Rollup.OutputChunk | vite.Rollup.OutputAsset,
|
||||
): boolean =>
|
||||
const isChunkEffected = (chunk: OutputFile): boolean =>
|
||||
// If it's an HTML file with the same path, is is effected because HTML files need to be pre-rendered
|
||||
// fileName is normalized, relative bundle path
|
||||
(chunk.type === 'asset' && changedPath.endsWith(chunk.fileName)) ||
|
||||
@@ -176,4 +179,4 @@ interface ContentScriptReload extends RebuildChange {
|
||||
* 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;
|
||||
type DetectedChange = BuildStepOutput | OutputAsset;
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
getEntrypointName,
|
||||
resolvePerBrowserOption,
|
||||
} from '~/core/utils/entrypoints';
|
||||
import { VIRTUAL_NOOP_BACKGROUND_MODULE_ID } from '~/core/vite-plugins/noopBackground';
|
||||
import { VIRTUAL_NOOP_BACKGROUND_MODULE_ID } from '~/core/utils/constants';
|
||||
import { CSS_EXTENSIONS_PATTERN } from '~/core/utils/paths';
|
||||
|
||||
/**
|
||||
|
||||
@@ -156,7 +156,7 @@ async function writeGlobalsDeclarationFile(
|
||||
config: InternalConfig,
|
||||
): Promise<string> {
|
||||
const filePath = resolve(config.typesDir, 'globals.d.ts');
|
||||
const globals = [...getGlobals(config), ...getEntrypointGlobals(config, '')];
|
||||
const globals = [...getGlobals(config), ...getEntrypointGlobals('')];
|
||||
await writeFileIfDifferent(
|
||||
filePath,
|
||||
[
|
||||
|
||||
@@ -6,14 +6,15 @@ import {
|
||||
ConfigEnv,
|
||||
UserManifestFn,
|
||||
UserManifest,
|
||||
WxtViteConfig,
|
||||
ExtensionRunnerConfig,
|
||||
WxtDevServer,
|
||||
} from '~/types';
|
||||
import path from 'node:path';
|
||||
import * as vite from 'vite';
|
||||
import { createFsCache } from '~/core/utils/cache';
|
||||
import consola, { LogLevels } from 'consola';
|
||||
import * as plugins from '~/core/vite-plugins';
|
||||
import { craeteViteBuilder } from '~/core/builders/vite';
|
||||
import defu from 'defu';
|
||||
import { NullablyRequired } from '../types';
|
||||
|
||||
/**
|
||||
* Given an inline config, discover the config file if necessary, merge the results, resolve any
|
||||
@@ -25,6 +26,7 @@ import * as plugins from '~/core/vite-plugins';
|
||||
export async function getInternalConfig(
|
||||
inlineConfig: InlineConfig,
|
||||
command: 'build' | 'serve',
|
||||
server?: WxtDevServer,
|
||||
): Promise<InternalConfig> {
|
||||
// Load user config
|
||||
|
||||
@@ -69,7 +71,7 @@ export async function getInternalConfig(
|
||||
);
|
||||
const publicDir = path.resolve(srcDir, mergedConfig.publicDir ?? 'public');
|
||||
const typesDir = path.resolve(wxtDir, 'types');
|
||||
const outBaseDir = path.resolve(root, '.output');
|
||||
const outBaseDir = path.resolve(root, mergedConfig.outDir ?? '.output');
|
||||
const outDir = path.resolve(outBaseDir, `${browser}-mv${manifestVersion}`);
|
||||
|
||||
const runnerConfig = await loadConfig<ExtensionRunnerConfig>({
|
||||
@@ -91,7 +93,7 @@ export async function getInternalConfig(
|
||||
}).map(([key, value]) => [key, path.resolve(root, value)]),
|
||||
);
|
||||
|
||||
const finalConfig: InternalConfig = {
|
||||
const finalConfig: Omit<InternalConfig, 'builder'> = {
|
||||
browser,
|
||||
command,
|
||||
debug,
|
||||
@@ -110,7 +112,6 @@ export async function getInternalConfig(
|
||||
runnerConfig,
|
||||
srcDir,
|
||||
typesDir,
|
||||
vite: () => ({}), // Real value added after this object is initialized.
|
||||
wxtDir,
|
||||
zip: resolveInternalZipConfig(root, mergedConfig),
|
||||
transformManifest(manifest) {
|
||||
@@ -127,12 +128,19 @@ export async function getInternalConfig(
|
||||
includeBrowserPolyfill:
|
||||
mergedConfig.experimental?.includeBrowserPolyfill ?? true,
|
||||
},
|
||||
server,
|
||||
};
|
||||
|
||||
finalConfig.vite = (env) =>
|
||||
resolveInternalViteConfig(env, mergedConfig, finalConfig);
|
||||
const builder = await craeteViteBuilder(
|
||||
inlineConfig,
|
||||
userConfig,
|
||||
finalConfig,
|
||||
);
|
||||
|
||||
return finalConfig;
|
||||
return {
|
||||
...finalConfig,
|
||||
builder,
|
||||
};
|
||||
}
|
||||
|
||||
async function resolveManifestConfig(
|
||||
@@ -150,35 +158,27 @@ async function resolveManifestConfig(
|
||||
function mergeInlineConfig(
|
||||
inlineConfig: InlineConfig,
|
||||
userConfig: UserConfig,
|
||||
): InlineConfig {
|
||||
): NullablyRequired<InlineConfig> {
|
||||
let imports: InlineConfig['imports'];
|
||||
if (inlineConfig.imports === false || userConfig.imports === false) {
|
||||
imports = false;
|
||||
} else if (userConfig.imports == null && inlineConfig.imports == null) {
|
||||
imports = undefined;
|
||||
} else {
|
||||
imports = vite.mergeConfig(
|
||||
userConfig.imports ?? {},
|
||||
inlineConfig.imports ?? {},
|
||||
);
|
||||
imports = defu(inlineConfig.imports ?? {}, userConfig.imports ?? {});
|
||||
}
|
||||
const manifest: UserManifestFn = async (env) => {
|
||||
const user = await resolveManifestConfig(env, userConfig.manifest);
|
||||
const inline = await resolveManifestConfig(env, inlineConfig.manifest);
|
||||
return vite.mergeConfig(user, inline);
|
||||
return defu(inline, user);
|
||||
};
|
||||
const viteConfig = async (env: ConfigEnv): Promise<WxtViteConfig> => {
|
||||
const user = await userConfig.vite?.(env);
|
||||
const inline = await inlineConfig.vite?.(env);
|
||||
return vite.mergeConfig(user ?? {}, inline ?? {});
|
||||
};
|
||||
const runner: InlineConfig['runner'] = vite.mergeConfig(
|
||||
userConfig.runner ?? {},
|
||||
const runner: InlineConfig['runner'] = defu(
|
||||
inlineConfig.runner ?? {},
|
||||
userConfig.runner ?? {},
|
||||
);
|
||||
const zip: InlineConfig['zip'] = vite.mergeConfig(
|
||||
userConfig.zip ?? {},
|
||||
const zip: InlineConfig['zip'] = defu(
|
||||
inlineConfig.zip ?? {},
|
||||
userConfig.zip ?? {},
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -195,7 +195,7 @@ function mergeInlineConfig(
|
||||
publicDir: inlineConfig.publicDir ?? userConfig.publicDir,
|
||||
runner,
|
||||
srcDir: inlineConfig.srcDir ?? userConfig.srcDir,
|
||||
vite: viteConfig,
|
||||
outDir: inlineConfig.outDir ?? userConfig.outDir,
|
||||
zip,
|
||||
analysis: {
|
||||
enabled: inlineConfig.analysis?.enabled ?? userConfig.analysis?.enabled,
|
||||
@@ -210,14 +210,17 @@ function mergeInlineConfig(
|
||||
...userConfig.experimental,
|
||||
...inlineConfig.experimental,
|
||||
},
|
||||
vite: undefined,
|
||||
transformManifest: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveInternalZipConfig(
|
||||
root: string,
|
||||
mergedConfig: InlineConfig,
|
||||
): InternalConfig['zip'] {
|
||||
): NullablyRequired<InternalConfig['zip']> {
|
||||
return {
|
||||
name: undefined,
|
||||
sourcesTemplate: '{{name}}-{{version}}-sources.zip',
|
||||
artifactTemplate: '{{name}}-{{version}}-{{browser}}.zip',
|
||||
sourcesRoot: root,
|
||||
@@ -236,46 +239,3 @@ function resolveInternalZipConfig(
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
async function resolveInternalViteConfig(
|
||||
env: ConfigEnv,
|
||||
mergedConfig: InlineConfig,
|
||||
finalConfig: InternalConfig,
|
||||
) {
|
||||
const internalVite: vite.InlineConfig =
|
||||
(await mergedConfig.vite?.(env)) ?? {};
|
||||
|
||||
internalVite.root = finalConfig.root;
|
||||
internalVite.configFile = false;
|
||||
internalVite.logLevel = 'warn';
|
||||
internalVite.mode = env.mode;
|
||||
|
||||
internalVite.build ??= {};
|
||||
internalVite.build.outDir = finalConfig.outDir;
|
||||
internalVite.build.emptyOutDir = false;
|
||||
|
||||
internalVite.plugins ??= [];
|
||||
internalVite.plugins.push(plugins.download(finalConfig));
|
||||
internalVite.plugins.push(plugins.devHtmlPrerender(finalConfig));
|
||||
internalVite.plugins.push(plugins.unimport(finalConfig));
|
||||
internalVite.plugins.push(
|
||||
plugins.virtualEntrypoint('background', finalConfig),
|
||||
);
|
||||
internalVite.plugins.push(
|
||||
plugins.virtualEntrypoint('content-script', finalConfig),
|
||||
);
|
||||
internalVite.plugins.push(
|
||||
plugins.virtualEntrypoint('unlisted-script', finalConfig),
|
||||
);
|
||||
internalVite.plugins.push(plugins.devServerGlobals(finalConfig));
|
||||
internalVite.plugins.push(plugins.tsconfigPaths(finalConfig));
|
||||
internalVite.plugins.push(plugins.noopBackground());
|
||||
if (finalConfig.analysis.enabled) {
|
||||
internalVite.plugins.push(plugins.bundleAnalysis());
|
||||
}
|
||||
internalVite.plugins.push(plugins.globals(finalConfig));
|
||||
|
||||
internalVite.plugins.push(plugins.excludeBrowserPolyfill(finalConfig));
|
||||
|
||||
return internalVite;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { findEntrypoints } from './find-entrypoints';
|
||||
import { InternalConfig, BuildOutput } from '~/types';
|
||||
import pc from 'picocolors';
|
||||
import * as vite from 'vite';
|
||||
import fs from 'fs-extra';
|
||||
import { groupEntrypoints } from './group-entrypoints';
|
||||
import { formatDuration } from '~/core/utils/time';
|
||||
@@ -27,7 +26,7 @@ export async function internalBuild(
|
||||
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}`,
|
||||
`${config.builder.name} ${config.builder.version}`,
|
||||
)}`,
|
||||
);
|
||||
const startTime = Date.now();
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* Module ID used to build the background in dev mode if the extension doesn't include a background
|
||||
* script/service worker.
|
||||
*/
|
||||
export const VIRTUAL_NOOP_BACKGROUND_MODULE_ID = 'virtual:user-background';
|
||||
@@ -2,7 +2,6 @@ import { InternalConfig } from '~/types';
|
||||
import fs from 'fs-extra';
|
||||
import glob from 'fast-glob';
|
||||
import { unnormalizePath } from './paths';
|
||||
import path from 'node:path';
|
||||
|
||||
/**
|
||||
* Only write the contents to a file if it results in a change. This prevents unnecessary file
|
||||
@@ -36,23 +35,3 @@ export async function getPublicFiles(
|
||||
const files = await glob('**/*', { cwd: config.publicDir });
|
||||
return files.map(unnormalizePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively remove all directories that are empty/
|
||||
*/
|
||||
export async function removeEmptyDirs(dir: string): Promise<void> {
|
||||
const files = await fs.readdir(dir);
|
||||
for (const file of files) {
|
||||
const filePath = path.join(dir, file);
|
||||
const stats = await fs.stat(filePath);
|
||||
if (stats.isDirectory()) {
|
||||
await removeEmptyDirs(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.rmdir(dir);
|
||||
} catch {
|
||||
// noop on failure - this means the directory was not empty.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { InternalConfig } from '~/types';
|
||||
|
||||
export function getGlobals(
|
||||
config: InternalConfig,
|
||||
config: Omit<InternalConfig, 'builder'>,
|
||||
): Array<{ name: string; value: any; type: string }> {
|
||||
return [
|
||||
{
|
||||
@@ -47,10 +47,7 @@ export function getGlobals(
|
||||
];
|
||||
}
|
||||
|
||||
export function getEntrypointGlobals(
|
||||
config: InternalConfig,
|
||||
entrypointName: string,
|
||||
) {
|
||||
export function getEntrypointGlobals(entrypointName: string) {
|
||||
return [
|
||||
{
|
||||
name: surroundInUnderscore('ENTRYPOINT'),
|
||||
|
||||
@@ -23,7 +23,7 @@ import { getPackageJson } from './package';
|
||||
import { normalizePath } from './paths';
|
||||
import { writeFileIfDifferent } from './fs';
|
||||
import { produce } from 'immer';
|
||||
import * as vite from 'vite';
|
||||
import defu from 'defu';
|
||||
|
||||
/**
|
||||
* Writes the manifest to the output directory and the build output.
|
||||
@@ -44,9 +44,6 @@ export async function writeManifest(
|
||||
output.publicAssets.unshift({
|
||||
type: 'asset',
|
||||
fileName: 'manifest.json',
|
||||
name: 'manifest',
|
||||
needsCodeReference: false,
|
||||
source: str,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -78,9 +75,9 @@ export async function generateMainfest(
|
||||
};
|
||||
const userManifest = config.manifest;
|
||||
|
||||
const manifest = vite.mergeConfig(
|
||||
baseManifest,
|
||||
const manifest = defu(
|
||||
userManifest,
|
||||
baseManifest,
|
||||
) as Manifest.WebExtensionManifest;
|
||||
|
||||
addEntrypoints(manifest, entrypoints, buildOutput, config);
|
||||
|
||||
@@ -26,7 +26,7 @@ export async function isOnline(): Promise<boolean> {
|
||||
*/
|
||||
export async function fetchCached(
|
||||
url: string,
|
||||
config: InternalConfig,
|
||||
config: Omit<InternalConfig, 'builder'>,
|
||||
): Promise<string> {
|
||||
let content: string = '';
|
||||
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import nodePath from 'node:path';
|
||||
import * as vite from 'vite';
|
||||
import systemPath from 'node:path';
|
||||
import normalize from 'normalize-path';
|
||||
|
||||
/**
|
||||
* Converts system paths to normalized bundler path. On windows and unix, this returns paths with /
|
||||
* instead of \.
|
||||
*/
|
||||
export function normalizePath(path: string): string {
|
||||
return vite.normalizePath(path);
|
||||
return normalize(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a normalized path, convert it to the system path style. On Windows, switch to \, otherwise use /.
|
||||
*/
|
||||
export function unnormalizePath(path: string): string {
|
||||
return nodePath.normalize(path);
|
||||
return systemPath.normalize(path);
|
||||
}
|
||||
|
||||
export const CSS_EXTENSIONS = ['css', 'scss', 'sass', 'less', 'styl', 'stylus'];
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
import { resolve } from 'path';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import merge from 'lodash.merge';
|
||||
import { Rollup } from 'vite';
|
||||
import type { Manifest } from 'webextension-polyfill';
|
||||
import {
|
||||
FsCache,
|
||||
@@ -15,6 +14,9 @@ import {
|
||||
GenericEntrypoint,
|
||||
OptionsEntrypoint,
|
||||
PopupEntrypoint,
|
||||
OutputChunk,
|
||||
OutputFile,
|
||||
OutputAsset,
|
||||
} from '~/types';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
|
||||
@@ -140,49 +142,19 @@ export const fakeGenericEntrypoint = fakeObjectCreator<GenericEntrypoint>(
|
||||
}),
|
||||
);
|
||||
|
||||
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(),
|
||||
},
|
||||
preliminaryFileName: faker.string.alphanumeric(),
|
||||
sourcemapFileName: null,
|
||||
}),
|
||||
);
|
||||
export const fakeOutputChunk = fakeObjectCreator<OutputChunk>(() => ({
|
||||
type: 'chunk',
|
||||
fileName: faker.string.alphanumeric(),
|
||||
moduleIds: [],
|
||||
}));
|
||||
|
||||
export const fakeRollupOutputAsset = fakeObjectCreator<Rollup.OutputAsset>(
|
||||
() => ({
|
||||
type: 'asset',
|
||||
fileName: fakeFileName(),
|
||||
name: faker.string.alpha(),
|
||||
needsCodeReference: faker.datatype.boolean(),
|
||||
source: '',
|
||||
}),
|
||||
);
|
||||
export const fakeOutputAsset = fakeObjectCreator<OutputAsset>(() => ({
|
||||
type: 'asset',
|
||||
fileName: fakeFileName(),
|
||||
}));
|
||||
|
||||
export function fakeRollupOutput(): Rollup.OutputAsset | Rollup.OutputChunk {
|
||||
return faker.helpers.arrayElement([
|
||||
fakeRollupOutputAsset(),
|
||||
fakeRollupOutputChunk(),
|
||||
]);
|
||||
export function fakeOutputFile(): OutputFile {
|
||||
return faker.helpers.arrayElement([fakeOutputAsset(), fakeOutputChunk()]);
|
||||
}
|
||||
|
||||
export const fakeManifest = fakeObjectCreator<Manifest.WebExtensionManifest>(
|
||||
@@ -228,7 +200,6 @@ export const fakeInternalConfig = fakeObjectCreator<InternalConfig>(() => {
|
||||
debug: faker.datatype.boolean(),
|
||||
srcDir: fakeDir(),
|
||||
typesDir: fakeDir(),
|
||||
vite: () => ({}),
|
||||
wxtDir: fakeDir(),
|
||||
server: mock<WxtDevServer>(),
|
||||
analysis: {
|
||||
@@ -248,5 +219,6 @@ export const fakeInternalConfig = fakeObjectCreator<InternalConfig>(() => {
|
||||
experimental: {
|
||||
includeBrowserPolyfill: true,
|
||||
},
|
||||
builder: mock(),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Remove optional from key, but keep undefined if present
|
||||
*
|
||||
* @example
|
||||
* type Test = NullablyRequired<{a?: string, b: number}>
|
||||
* // type Test = {a: string | undefined, b: number}
|
||||
*/
|
||||
export type NullablyRequired<T> = { [K in keyof Required<T>]: T[K] };
|
||||
@@ -1,9 +1,9 @@
|
||||
import { UnimportOptions } from 'unimport';
|
||||
import { InternalConfig } from '~/types';
|
||||
import { mergeConfig } from 'vite';
|
||||
import { defu } from 'defu';
|
||||
|
||||
export function getUnimportOptions(
|
||||
config: InternalConfig,
|
||||
config: Omit<InternalConfig, 'builder'>,
|
||||
): Partial<UnimportOptions | false> {
|
||||
if (config.imports === false) return false;
|
||||
|
||||
@@ -23,8 +23,5 @@ export function getUnimportOptions(
|
||||
dirs: ['components', 'composables', 'hooks', 'utils'],
|
||||
};
|
||||
|
||||
return mergeConfig(
|
||||
defaultOptions,
|
||||
config.imports,
|
||||
) as Partial<UnimportOptions>;
|
||||
return defu(config.imports, defaultOptions);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
globals,
|
||||
webextensionPolyfillAlias,
|
||||
webextensionPolyfillInlineDeps,
|
||||
} from '../core/vite-plugins';
|
||||
} from '../core/builders/vite/plugins';
|
||||
import { getInternalConfig } from '~/core/utils/building';
|
||||
import { InlineConfig } from '../types';
|
||||
|
||||
|
||||
+138
-40
@@ -1,9 +1,10 @@
|
||||
import * as vite from 'vite';
|
||||
import type * as vite from 'vite';
|
||||
import type { Manifest, Scripting } from 'webextension-polyfill';
|
||||
import { UnimportOptions } from 'unimport';
|
||||
import { LogLevel } from 'consola';
|
||||
import { ContentScriptContext } from '../client/content-scripts/content-script-context';
|
||||
import type { PluginVisualizerOptions } from 'rollup-plugin-visualizer';
|
||||
import type { FSWatcher } from 'chokidar';
|
||||
|
||||
export interface InlineConfig {
|
||||
/**
|
||||
@@ -30,6 +31,12 @@ export interface InlineConfig {
|
||||
* @default "${config.srcDir}/entrypoints"
|
||||
*/
|
||||
entrypointsDir?: string;
|
||||
/**
|
||||
* Output directory that stored build folders and ZIPs.
|
||||
*
|
||||
* @default ".output"
|
||||
*/
|
||||
outDir?: string;
|
||||
/**
|
||||
* > Only available when using the JS API. Not available in `wxt.config.ts` files
|
||||
*
|
||||
@@ -83,17 +90,6 @@ export interface InlineConfig {
|
||||
* consola
|
||||
*/
|
||||
logger?: Logger;
|
||||
/**
|
||||
* Return custom Vite options from a function. See
|
||||
* <https://vitejs.dev/config/shared-options.html>.
|
||||
*
|
||||
* [`root`](#root), [`configFile`](#configfile), and [`mode`](#mode) should be set in WXT's config
|
||||
* instead of Vite's.
|
||||
*
|
||||
* This is a function because any vite plugins added need to be recreated for each individual
|
||||
* build step, incase they have internal state causing them to fail when reused.
|
||||
*/
|
||||
vite?: (env: ConfigEnv) => WxtViteConfig | Promise<WxtViteConfig>;
|
||||
/**
|
||||
* Customize the `manifest.json` output. Can be an object, promise, or function that returns an
|
||||
* object or promise.
|
||||
@@ -194,10 +190,6 @@ export interface InlineConfig {
|
||||
* Add additional paths to the `.wxt/tsconfig.json`. Use this instead of overwriting the `paths`
|
||||
* in the root `tsconfig.json` if you want to add new paths.
|
||||
*
|
||||
* Passed into Vite's
|
||||
* [`resolve.alias`](https://vitejs.dev/config/shared-options.html#resolve-alias) option and used
|
||||
* to generate the `.wxt/tsconfig.json`.
|
||||
*
|
||||
* The key is the import alias and the value is either a relative path to the root directory or an absolute path.
|
||||
*
|
||||
* @example
|
||||
@@ -233,43 +225,83 @@ export interface InlineConfig {
|
||||
};
|
||||
}
|
||||
|
||||
export interface WxtInlineViteConfig
|
||||
extends Omit<vite.InlineConfig, 'root' | 'configFile' | 'mode' | 'build'> {
|
||||
build?: Omit<vite.BuildOptions, 'outDir'>;
|
||||
// TODO: Extract to @wxt/vite-builder and use module augmentation to include the vite field
|
||||
export interface InlineConfig {
|
||||
/**
|
||||
* Return custom Vite options from a function. See
|
||||
* <https://vitejs.dev/config/shared-options.html>.
|
||||
*
|
||||
* [`root`](#root), [`configFile`](#configfile), and [`mode`](#mode) should be set in WXT's config
|
||||
* instead of Vite's.
|
||||
*
|
||||
* This is a function because any vite plugins added need to be recreated for each individual
|
||||
* build step, incase they have internal state causing them to fail when reused.
|
||||
*/
|
||||
vite?: (env: ConfigEnv) => WxtViteConfig | Promise<WxtViteConfig>;
|
||||
}
|
||||
|
||||
// TODO: Move into @wxt/vite-builder
|
||||
export type WxtViteConfig = Omit<
|
||||
vite.UserConfig,
|
||||
'root' | 'configFile' | 'mode'
|
||||
>;
|
||||
|
||||
export interface BuildOutput {
|
||||
manifest: Manifest.WebExtensionManifest;
|
||||
publicAssets: vite.Rollup.OutputAsset[];
|
||||
publicAssets: OutputAsset[];
|
||||
steps: BuildStepOutput[];
|
||||
}
|
||||
|
||||
export type OutputFile = OutputChunk | OutputAsset;
|
||||
|
||||
export interface OutputChunk {
|
||||
type: 'chunk';
|
||||
/**
|
||||
* Relative, normalized path relative to the output directory.
|
||||
*
|
||||
* Ex: "content-scripts/overlay.js"
|
||||
*/
|
||||
fileName: string;
|
||||
/**
|
||||
* Absolute, normalized paths to all dependencies this chunk relies on.
|
||||
*/
|
||||
moduleIds: string[];
|
||||
}
|
||||
|
||||
export interface OutputAsset {
|
||||
type: 'asset';
|
||||
/**
|
||||
* Relative, normalized path relative to the output directory.
|
||||
*
|
||||
* Ex: "icons/16.png"
|
||||
*/
|
||||
fileName: string;
|
||||
}
|
||||
|
||||
export interface BuildStepOutput {
|
||||
entrypoints: EntrypointGroup;
|
||||
chunks: (vite.Rollup.OutputChunk | vite.Rollup.OutputAsset)[];
|
||||
chunks: OutputFile[];
|
||||
}
|
||||
|
||||
export interface WxtDevServer extends vite.ViteDevServer {
|
||||
/**
|
||||
* Ex: `3000`
|
||||
*/
|
||||
port: number;
|
||||
/**
|
||||
* Ex: `"localhost"`
|
||||
*/
|
||||
hostname: string;
|
||||
/**
|
||||
* Ex: `"http://localhost:3000"`
|
||||
*/
|
||||
origin: string;
|
||||
export interface WxtDevServer
|
||||
extends Omit<WxtBuilderServer, 'listen'>,
|
||||
ServerInfo {
|
||||
/**
|
||||
* Stores the current build output of the server.
|
||||
*/
|
||||
currentOutput: BuildOutput;
|
||||
/**
|
||||
* Start the server on the first open port.
|
||||
* Start the server.
|
||||
*/
|
||||
start(): Promise<void>;
|
||||
/**
|
||||
* Transform the HTML for dev mode.
|
||||
*/
|
||||
transformHtml(
|
||||
url: string,
|
||||
html: string,
|
||||
originalUrl?: string | undefined,
|
||||
): Promise<string>;
|
||||
/**
|
||||
* Tell the extension to reload by running `browser.runtime.reload`.
|
||||
*/
|
||||
@@ -327,7 +359,7 @@ export interface BaseEntrypoint {
|
||||
* - `named.sandbox.html` → `named`
|
||||
* - `named.sandbox/index.html` → `named`
|
||||
* - `sandbox.html` → `sandbox`
|
||||
* - `sandbox.index.html` → `sandbox`
|
||||
* - `sandbox/index.html` → `sandbox`
|
||||
* - `overlay.content.ts` → `overlay`
|
||||
* - `overlay.content/index.ts` → `overlay`
|
||||
*
|
||||
@@ -590,7 +622,73 @@ export interface ExtensionRunnerConfig {
|
||||
startUrls?: string[];
|
||||
}
|
||||
|
||||
export type WxtViteConfig = Omit<
|
||||
vite.UserConfig,
|
||||
'root' | 'configFile' | 'mode'
|
||||
>;
|
||||
export interface WxtBuilder {
|
||||
/**
|
||||
* Name of tool used to build. Ex: "Vite" or "Webpack".
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Version of tool used to build. Ex: "5.0.2"
|
||||
*/
|
||||
version: string;
|
||||
/**
|
||||
* Build a single entrypoint group. This is effectively one of the multiple "steps" during the
|
||||
* build process.
|
||||
*/
|
||||
build(group: EntrypointGroup): Promise<BuildStepOutput>;
|
||||
/**
|
||||
* Start a dev server at the provided port.
|
||||
*/
|
||||
createServer(info: ServerInfo): Promise<WxtBuilderServer>;
|
||||
}
|
||||
|
||||
export interface WxtBuilderServer {
|
||||
/**
|
||||
* Start the server.
|
||||
*/
|
||||
listen(): Promise<void>;
|
||||
/**
|
||||
* Transform the HTML for dev mode.
|
||||
*/
|
||||
transformHtml(
|
||||
url: string,
|
||||
html: string,
|
||||
originalUrl?: string | undefined,
|
||||
): Promise<string>;
|
||||
/**
|
||||
* The web socket server used to communicate with the extension.
|
||||
*/
|
||||
ws: {
|
||||
/**
|
||||
* Send a message via the server's websocket, with an optional payload.
|
||||
*
|
||||
* @example
|
||||
* ws.send("wxt:reload-extension");
|
||||
* ws.send("wxt:reload-content-script", { ... });
|
||||
*/
|
||||
send(message: string, payload?: any): void;
|
||||
/**
|
||||
* Listen for messages over the server's websocket.
|
||||
*/
|
||||
on(message: string, cb: (payload: any) => void): void;
|
||||
};
|
||||
/**
|
||||
* Chokidar file watcher instance.
|
||||
*/
|
||||
watcher: FSWatcher;
|
||||
}
|
||||
|
||||
export interface ServerInfo {
|
||||
/**
|
||||
* Ex: `3000`
|
||||
*/
|
||||
port: number;
|
||||
/**
|
||||
* Ex: `"localhost"`
|
||||
*/
|
||||
hostname: string;
|
||||
/**
|
||||
* Ex: `"http://localhost:3000"`
|
||||
*/
|
||||
origin: string;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import * as vite from 'vite';
|
||||
import {
|
||||
WxtDevServer,
|
||||
Logger,
|
||||
@@ -8,6 +7,7 @@ import {
|
||||
ExtensionRunnerConfig,
|
||||
ConfigEnv,
|
||||
UserConfig,
|
||||
WxtBuilder,
|
||||
} from './external';
|
||||
import { UnimportOptions } from 'unimport';
|
||||
import { ResolvedConfig } from 'c12';
|
||||
@@ -31,7 +31,6 @@ export interface InternalConfig {
|
||||
env: ConfigEnv;
|
||||
logger: Logger;
|
||||
imports: false | Partial<UnimportOptions>;
|
||||
vite: (env: ConfigEnv) => Promise<vite.InlineConfig> | vite.InlineConfig;
|
||||
manifest: UserManifest;
|
||||
fsCache: FsCache;
|
||||
server?: WxtDevServer;
|
||||
@@ -56,6 +55,7 @@ export interface InternalConfig {
|
||||
experimental: {
|
||||
includeBrowserPolyfill: boolean;
|
||||
};
|
||||
builder: WxtBuilder;
|
||||
}
|
||||
|
||||
export interface FsCache {
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.14",
|
||||
"@types/react-dom": "^18.2.6",
|
||||
"@vitejs/plugin-react": "^4.0.3",
|
||||
"typescript": "^5.1.6",
|
||||
"wxt": "^0.10.0"
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"typescript": "^5.3.2",
|
||||
"wxt": "^0.11.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
"solid-js": "^1.7.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.1.6",
|
||||
"typescript": "^5.3.2",
|
||||
"vite": "^4.0.0",
|
||||
"vite-plugin-solid": "^2.7.0",
|
||||
"wxt": "^0.10.0"
|
||||
}
|
||||
|
||||
@@ -14,12 +14,12 @@
|
||||
"postinstall": "wxt prepare"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^2.4.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
||||
"@tsconfig/svelte": "^5.0.0",
|
||||
"svelte": "^4.0.5",
|
||||
"svelte-check": "^3.4.6",
|
||||
"tslib": "^2.6.0",
|
||||
"typescript": "^5.1.6",
|
||||
"wxt": "^0.10.0"
|
||||
"typescript": "^5.3.2",
|
||||
"wxt": "^0.11.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"postinstall": "wxt prepare"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.1.6",
|
||||
"wxt": "^0.10.0"
|
||||
"typescript": "^5.3.2",
|
||||
"wxt": "^0.11.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,9 +18,9 @@
|
||||
"vue": "^3.3.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.2.3",
|
||||
"typescript": "^5.1.6",
|
||||
"vue-tsc": "^1.8.4",
|
||||
"wxt": "^0.10.0"
|
||||
"@vitejs/plugin-vue": "^4.5.1",
|
||||
"typescript": "^5.3.2",
|
||||
"vue-tsc": "^1.8.25",
|
||||
"wxt": "^0.11.0"
|
||||
}
|
||||
}
|
||||
|
||||
+4
-3
@@ -1,7 +1,8 @@
|
||||
import { defineWorkspace } from 'vitest/config';
|
||||
import fs from 'fs-extra';
|
||||
import pc from 'picocolors';
|
||||
import { Plugin } from 'vite';
|
||||
import type { Plugin } from 'vite';
|
||||
import path from 'node:path';
|
||||
|
||||
const seed = Math.round(Math.random() * Number.MAX_SAFE_INTEGER);
|
||||
console.info('Test seed: ' + pc.cyan(seed));
|
||||
@@ -17,8 +18,8 @@ const testSeed = (): Plugin => ({
|
||||
|
||||
const resolve = {
|
||||
alias: {
|
||||
'~': 'src',
|
||||
'webextension-polyfill': './src/virtual/mock-browser',
|
||||
'~': path.resolve('src'),
|
||||
'webextension-polyfill': path.resolve('src/virtual/mock-browser'),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user