Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 06a8c5fb0e | |||
| eb86413f35 | |||
| b29d49cbf1 | |||
| c0a0edb30c | |||
| b15a933c75 | |||
| 1851be22a7 | |||
| 39f6c29e4f | |||
| 936e89fbfb | |||
| ad365ecde4 | |||
| b4283e37bd | |||
| b467ad760a | |||
| 9fc6408ef9 | |||
| 394cb1a520 | |||
| ffec88549b | |||
| 4758bea187 | |||
| 6671471f75 | |||
| ae78dc7fb3 | |||
| b62af3d007 | |||
| 43ae44ecd5 | |||
| 5bab9d2f59 | |||
| 7edf1c8103 | |||
| 53b4eac955 | |||
| f9a10c2c52 | |||
| 8206ca9164 | |||
| 1a823cc008 | |||
| efc011eb81 |
@@ -18,7 +18,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup
|
||||
- run: pnpm -r --sequential build
|
||||
- run: pnpm buildc all
|
||||
build-demo:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
|
||||
@@ -29,6 +29,7 @@ const chromeExtensionIds = [
|
||||
'nedcanggplmbbgmlpcjiafgjcpdimpea', // YTBlock - Block any content from YouTube™
|
||||
'oadbjpccljkplmhnjekgjamejnbadlne', // demo.fun - Interactive product demos that convert
|
||||
'iopdafdcollfgaoffingmahpffckmjni', // SmartEReply: Elevate Your LinkedIn™ Engagement with AI 🚀📈
|
||||
'khjdmjcmpolknpccmaaipmidphjokhdf', // WorkFlowy MultiFlow
|
||||
];
|
||||
|
||||
const { data, err, isLoading } = useListExtensionDetails(chromeExtensionIds);
|
||||
|
||||
+26
-26
@@ -4,30 +4,30 @@ Lets compare the features of WXT vs [Plasmo](https://docs.plasmo.com/framework)
|
||||
|
||||
## Overview
|
||||
|
||||
| Features | WXT | Plasmo | CRXJS |
|
||||
| ---------------------------------------------------- | :-------------------------: | :--------------------------------------: | :---------------------------------------------------------------------: |
|
||||
| Supports all browsers | ✅ | ✅ | ❌ See [#56](https://github.com/crxjs/chrome-extension-tools/issues/56) |
|
||||
| MV2 Support | ✅ | ✅ | 🟡 Either MV2 or MV3 |
|
||||
| MV3 Support | ✅ | ✅ | 🟡 Either MV2 or MV3 |
|
||||
| Create Extension ZIPs | ✅ | ✅ | ❌ |
|
||||
| Create Firefox Sources ZIP | ✅ | ❌ | ❌ |
|
||||
| First-class TypeScript support | ✅ | ✅ | ✅ |
|
||||
| Entrypoint discovery | File based | File based | ❌ |
|
||||
| Inline entrypoint config | ✅ | ✅ | Manifest based |
|
||||
| Auto-imports | ✅ | ❌ | ❌ |
|
||||
| Supports all frontend frameworks | ✅ | 🟡 Only React, Vue, and Svelte | ✅ |
|
||||
| Framework specific entrypoints (like `Popup.tsx`) | 🟡 `.html` `.ts` `.tsx` | ✅ `.html` `.ts` `.tsx` `.vue` `.svelte` | ❌ |
|
||||
| Automated publishing | ✅ | ✅ | ❌ |
|
||||
| Remote Code Bundling (Google Analytics) | ✅ | ✅ | ❌ |
|
||||
| Features | WXT | Plasmo | CRXJS |
|
||||
| ---------------------------------------------------- | :-------------------------: | :--------------------------------------: | :-------------------------------------: |
|
||||
| Supports all browsers | ✅ | ✅ | ✅ As of `v2.0.0-beta.23` |
|
||||
| MV2 Support | ✅ | ✅ | 🟡 Either MV2 or MV3 |
|
||||
| MV3 Support | ✅ | ✅ | 🟡 Either MV2 or MV3 |
|
||||
| Create Extension ZIPs | ✅ | ✅ | ❌ |
|
||||
| Create Firefox Sources ZIP | ✅ | ❌ | ❌ |
|
||||
| First-class TypeScript support | ✅ | ✅ | ✅ |
|
||||
| Entrypoint discovery | File based | File based | ❌ |
|
||||
| Inline entrypoint config | ✅ | ✅ | Manifest based |
|
||||
| Auto-imports | ✅ | ❌ | ❌ |
|
||||
| Supports all frontend frameworks | ✅ | 🟡 Only React, Vue, and Svelte | ✅ |
|
||||
| Framework specific entrypoints (like `Popup.tsx`) | 🟡 `.html` `.ts` `.tsx` | ✅ `.html` `.ts` `.tsx` `.vue` `.svelte` | ❌ |
|
||||
| Automated publishing | ✅ | ✅ | ❌ |
|
||||
| Remote Code Bundling (Google Analytics) | ✅ | ✅ | ❌ |
|
||||
| <strong style="opacity: 50%">Dev Mode</strong> | | |
|
||||
| `.env` Files | ✅ | ✅ | ✅ |
|
||||
| Opens browser and install extension | ✅ | ❌ | ❌ |
|
||||
| HMR for UIs | ✅ | 🟡 React only | ✅ |
|
||||
| Reload HTML Files on Change | ✅ | 🟡 Reloads entire extension | ✅ |
|
||||
| Reload Content Scripts on Change | ✅ | 🟡 Reloads entire extension | ✅ |
|
||||
| Reload Background on Change | 🟡 Reloads entire extension | 🟡 Reloads entire extension | 🟡 Reloads entire extension |
|
||||
| Respects Content Script `run_at` | ✅ | ✅ | ❌ ESM-style loaders run asynchronously |
|
||||
| <strong style="opacity: 50%">Built-in Utils</strong> | | | |
|
||||
| Storage | ✅ | ✅ | ❌ |
|
||||
| Messaging | ❌ | ✅ | ❌ |
|
||||
| Content Script UI | ✅ | ✅ | ❌ |
|
||||
| `.env` Files | ✅ | ✅ | ✅ |
|
||||
| Opens browser and install extension | ✅ | ❌ | ❌ |
|
||||
| HMR for UIs | ✅ | 🟡 React only | ✅ |
|
||||
| Reload HTML Files on Change | ✅ | 🟡 Reloads entire extension | ✅ |
|
||||
| Reload Content Scripts on Change | ✅ | 🟡 Reloads entire extension | ✅ |
|
||||
| Reload Background on Change | 🟡 Reloads entire extension | 🟡 Reloads entire extension | 🟡 Reloads entire extension |
|
||||
| Respects Content Script `run_at` | ✅ | ✅ | ❌ ESM-style loaders run asynchronously |
|
||||
| <strong style="opacity: 50%">Built-in Utils</strong> | | | |
|
||||
| Storage | ✅ | ✅ | ❌ |
|
||||
| Messaging | ❌ | ✅ | ❌ |
|
||||
| Content Script UI | ✅ | ✅ | ❌ |
|
||||
|
||||
@@ -15,7 +15,14 @@ npx wxt@latest init <project-name>
|
||||
```
|
||||
|
||||
```sh [bun]
|
||||
bunx wxt@latest init <project-name>
|
||||
# The "wxt init" command currently fails when ran with bunx.
|
||||
# Use NPX as a workaround, and select "bun" as your package
|
||||
# manager. To stay up to date with this issue, follow
|
||||
# https://github.com/wxt-dev/wxt/issues/707
|
||||
#
|
||||
# bunx wxt@latest init <project-name>
|
||||
|
||||
npx wxt@latest init <project-name>
|
||||
```
|
||||
|
||||
:::
|
||||
@@ -96,7 +103,6 @@ pnpm dev
|
||||
```
|
||||
|
||||
:::tip 🎉 Well done!
|
||||
|
||||
The dev command will build the extension for development, open the browser, and reload the different parts of the extension when you save changes.
|
||||
:::
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ Firefox does not support sandboxed pages.
|
||||
:patterns="[
|
||||
['sandbox.html', 'sandbox.html'],
|
||||
['sandbox/index.html', 'sandbox.html'],
|
||||
['<name>.sandbox.html', '<name>.html` '],
|
||||
['<name>.sandbox/index.html', '<name>.html` '],
|
||||
['<name>.sandbox.html', '<name>.html'],
|
||||
['<name>.sandbox/index.html', '<name>.html'],
|
||||
]"
|
||||
/>
|
||||
|
||||
|
||||
+2
-2
@@ -15,10 +15,10 @@
|
||||
"docs:gen": "typedoc --options docs/typedoc.json",
|
||||
"docs:dev": "pnpm -s docs:gen && vitepress dev docs",
|
||||
"docs:build": "pnpm -s docs:gen && vitepress build docs",
|
||||
"docs:preview": "pnpm -s docs:gen && vitepress preview docs",
|
||||
"build-deps": "pnpm tsx scripts/build-deps.ts"
|
||||
"docs:preview": "pnpm -s docs:gen && vitepress preview docs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aklinker1/buildc": "^1.0.7",
|
||||
"@aklinker1/check": "^1.3.1",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@vitest/coverage-v8": "^1.6.0",
|
||||
|
||||
@@ -21,12 +21,11 @@
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "pnpm -s build-deps && unbuild",
|
||||
"check": "pnpm -s build-deps && check",
|
||||
"build-deps": "pnpm -ws build-deps @wxt-dev/module-react"
|
||||
"build": "buildc -- unbuild",
|
||||
"check": "buildc --deps-only -- check"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"wxt": ">=0.18.5"
|
||||
"wxt": ">=0.18.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vitejs/plugin-react": "^4.3.1"
|
||||
|
||||
@@ -21,12 +21,11 @@
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "pnpm -s build-deps && unbuild",
|
||||
"check": "pnpm -s build-deps && check",
|
||||
"build-deps": "pnpm -ws build-deps @wxt-dev/module-solid"
|
||||
"build": "buildc -- unbuild",
|
||||
"check": "buildc --deps-only -- check"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"wxt": ">=0.18.5"
|
||||
"wxt": ">=0.18.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"vite-plugin-solid": "^2.10.2"
|
||||
|
||||
@@ -21,12 +21,11 @@
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "pnpm -s build-deps && unbuild",
|
||||
"check": "pnpm -s build-deps && check",
|
||||
"build-deps": "pnpm -ws build-deps @wxt-dev/module-svelte"
|
||||
"build": "buildc -- unbuild",
|
||||
"check": "buildc --deps-only -- check"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"wxt": ">=0.18.5"
|
||||
"wxt": ">=0.18.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^3.1.1"
|
||||
|
||||
@@ -21,12 +21,11 @@
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "pnpm -s build-deps && unbuild",
|
||||
"check": "pnpm -s build-deps && check",
|
||||
"build-deps": "pnpm -ws build-deps @wxt-dev/module-vue"
|
||||
"build": "buildc -- unbuild",
|
||||
"check": "buildc --deps-only -- check"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"wxt": ">=0.18.5"
|
||||
"wxt": ">=0.18.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vitejs/plugin-vue": "^5.0.5"
|
||||
|
||||
@@ -4,18 +4,17 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "pnpm -s build-deps && wxt",
|
||||
"build": "pnpm -s build-deps && wxt build",
|
||||
"build:all": "pnpm -s build-deps && run-s -s 'build:all:*'",
|
||||
"dev": "buildc --deps-only -- wxt",
|
||||
"build": "buildc -- wxt build",
|
||||
"build:all": "buildc --deps-only -- run-s -s 'build:all:*'",
|
||||
"build:all:chrome-mv3": "wxt build",
|
||||
"build:all:chrome-mv2": "wxt build --mv2",
|
||||
"build:all:firefox-mv3": "wxt build -b firefox --mv3",
|
||||
"build:all:firefox-mv2": "wxt build -b firefox",
|
||||
"test": "pnpm -s build-deps && vitest",
|
||||
"zip": "pnpm -s build-deps && wxt zip",
|
||||
"check": "pnpm -s build-deps && check",
|
||||
"postinstall": "pnpm -s build-deps && wxt prepare",
|
||||
"build-deps": "pnpm -sw build-deps wxt-demo"
|
||||
"test": "buildc --deps-only -- vitest",
|
||||
"zip": "buildc --deps-only -- wxt zip",
|
||||
"check": "buildc --deps-only -- check",
|
||||
"postinstall": "buildc --deps-only -- wxt prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.3.1",
|
||||
@@ -27,5 +26,8 @@
|
||||
"sass": "^1.77.5",
|
||||
"typescript": "^5.4.5",
|
||||
"wxt": "workspace:*"
|
||||
},
|
||||
"buildc": {
|
||||
"outDir": ".output/chrome-mv3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ export default defineContentScript({
|
||||
anchor: 'form[role=search]',
|
||||
onMount: (container) => {
|
||||
const app = document.createElement('div');
|
||||
app.textContent = 'Custom content script UI';
|
||||
app.textContent = browser.i18n.getMessage('prompt_for_name');
|
||||
container.append(app);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -19,7 +19,10 @@ export default defineConfig({
|
||||
open: true,
|
||||
},
|
||||
experimental: {
|
||||
viteRuntime: true,
|
||||
entrypointImporter: 'vite-node',
|
||||
},
|
||||
runner: {
|
||||
startUrls: ['https://duckduckgo.com'],
|
||||
},
|
||||
example: {
|
||||
a: 'a',
|
||||
|
||||
@@ -1,5 +1,65 @@
|
||||
# Changelog
|
||||
|
||||
## v0.18.10
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.18.9...wxt-v0.18.10)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Add `prepare:types` hook to extend `.wxt/` directory generation ([#767](https://github.com/wxt-dev/wxt/pull/767))
|
||||
- **modules:** Allow adding generated public files ([#769](https://github.com/wxt-dev/wxt/pull/769))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Await `prepare:types` hook ([b29d49c](https://github.com/wxt-dev/wxt/commit/b29d49c))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Refactor package manager test fixtures ([39f6c29](https://github.com/wxt-dev/wxt/commit/39f6c29))
|
||||
- Consolidate `unimport` code into a built-in module ([#771](https://github.com/wxt-dev/wxt/pull/771))
|
||||
|
||||
## v0.18.9
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.18.8...wxt-v0.18.9)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- **experimental:** Replace `viteRuntime` option with `entrypointImporter` option, and implement `vite-node` importer ([#761](https://github.com/wxt-dev/wxt/pull/761))
|
||||
|
||||
## v0.18.8
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.18.7...wxt-v0.18.8)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- **dev:** Reload extension when public files change ([#752](https://github.com/wxt-dev/wxt/pull/752))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Don't load plugins twice in HTML pages ([#746](https://github.com/wxt-dev/wxt/pull/746))
|
||||
- Ignore .wxt file changes in dev ([#755](https://github.com/wxt-dev/wxt/pull/755))
|
||||
- **modules:** `addViteConfig` ignored user vite config ([#760](https://github.com/wxt-dev/wxt/pull/760))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Refactor client web socket util ([#753](https://github.com/wxt-dev/wxt/pull/753))
|
||||
- Add E2E test for `addImportPreset` ([9fc6408](https://github.com/wxt-dev/wxt/commit/9fc6408))
|
||||
|
||||
## v0.18.7
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.18.6...wxt-v0.18.7)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- **dev:** Fix CSP error loading HTML plugins ([#723](https://github.com/wxt-dev/wxt/pull/723))
|
||||
- Generalize react-refresh preamble logic to virtualize all inline scripts ([#728](https://github.com/wxt-dev/wxt/pull/728))
|
||||
- **zip:** Revert dotfile changes from #674 ([#742](https://github.com/wxt-dev/wxt/pull/742), [#674](https://github.com/wxt-dev/wxt/issues/674))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- **deps:** Upgrade and sync all dependencies ([#725](https://github.com/wxt-dev/wxt/pull/725))
|
||||
- Extract build cache script to NPM package ([#737](https://github.com/wxt-dev/wxt/pull/737))
|
||||
|
||||
## v0.18.6
|
||||
|
||||
[compare changes](https://github.com/wxt-dev/wxt/compare/wxt-v0.18.5...wxt-v0.18.6)
|
||||
|
||||
@@ -47,10 +47,10 @@ describe('Auto Imports', () => {
|
||||
----------------------------------------
|
||||
// Generated by wxt
|
||||
/// <reference types="wxt/vite-builder-env" />
|
||||
/// <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="./types/imports.d.ts" />
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import { WxtHooks } from '~/types';
|
||||
|
||||
const hooks: WxtHooks = {
|
||||
ready: vi.fn(),
|
||||
'prepare:types': vi.fn(),
|
||||
'build:before': vi.fn(),
|
||||
'build:done': vi.fn(),
|
||||
'build:manifestGenerated': vi.fn(),
|
||||
@@ -41,6 +42,7 @@ describe('Hooks', () => {
|
||||
|
||||
expectHooksToBeCalled({
|
||||
ready: true,
|
||||
'prepare:types': true,
|
||||
'build:before': false,
|
||||
'build:done': false,
|
||||
'build:publicAssets': false,
|
||||
@@ -60,6 +62,7 @@ describe('Hooks', () => {
|
||||
|
||||
expectHooksToBeCalled({
|
||||
ready: true,
|
||||
'prepare:types': true,
|
||||
'build:before': true,
|
||||
'build:done': true,
|
||||
'build:publicAssets': true,
|
||||
@@ -79,6 +82,7 @@ describe('Hooks', () => {
|
||||
|
||||
expectHooksToBeCalled({
|
||||
ready: true,
|
||||
'prepare:types': true,
|
||||
'build:before': true,
|
||||
'build:done': true,
|
||||
'build:publicAssets': true,
|
||||
@@ -104,6 +108,7 @@ describe('Hooks', () => {
|
||||
|
||||
expectHooksToBeCalled({
|
||||
ready: true,
|
||||
'prepare:types': true,
|
||||
'build:before': true,
|
||||
'build:done': true,
|
||||
'build:publicAssets': true,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { TestProject } from '../utils';
|
||||
|
||||
describe.each([true, false])(
|
||||
describe.each(['jiti', 'vite-runtime', 'vite-node'] as const)(
|
||||
'Manifest Content (Vite runtime? %s)',
|
||||
(viteRuntime) => {
|
||||
(entrypointImporter) => {
|
||||
it.each([
|
||||
{ browser: undefined, outDir: 'chrome-mv3', expected: undefined },
|
||||
{ browser: 'chrome', outDir: 'chrome-mv3', expected: undefined },
|
||||
@@ -24,7 +24,7 @@ describe.each([true, false])(
|
||||
main: () => {},
|
||||
})`,
|
||||
);
|
||||
await project.build({ browser, experimental: { viteRuntime } });
|
||||
await project.build({ browser, experimental: { entrypointImporter } });
|
||||
|
||||
const safariManifest = await project.getOutputManifest(
|
||||
`.output/${outDir}/manifest.json`,
|
||||
|
||||
@@ -101,7 +101,13 @@ describe('Module Helpers', () => {
|
||||
|
||||
export default defineWxtModule((wxt) => {
|
||||
addPublicAssets(wxt, "${normalizePath(dir)}")
|
||||
})
|
||||
wxt.hooks.hook("build:publicAssets", (_, assets) => {
|
||||
assets.push({
|
||||
relativeDest: "example/generated.txt",
|
||||
contents: "",
|
||||
});
|
||||
});
|
||||
});
|
||||
`,
|
||||
);
|
||||
|
||||
@@ -114,6 +120,9 @@ describe('Module Helpers', () => {
|
||||
await expect(
|
||||
project.fileExists('.output/chrome-mv3/module.txt'),
|
||||
).resolves.toBe(true);
|
||||
await expect(
|
||||
project.fileExists('.output/chrome-mv3/example/generated.txt'),
|
||||
).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it("should not overwrite the user's public files", async () => {
|
||||
@@ -287,5 +296,29 @@ describe('Module Helpers', () => {
|
||||
|
||||
await expect(project.serializeOutput()).resolves.toContain(expectedText);
|
||||
});
|
||||
|
||||
it('should add preset', async () => {
|
||||
const project = new TestProject();
|
||||
project.addFile(
|
||||
'entrypoints/background.ts',
|
||||
`export default defineBackground(() => {
|
||||
customImport();
|
||||
});`,
|
||||
);
|
||||
project.addFile(
|
||||
'modules/test.ts',
|
||||
`import { defineWxtModule, addImportPreset } from 'wxt/modules';
|
||||
|
||||
export default defineWxtModule((wxt) => {
|
||||
addImportPreset(wxt, "vue");
|
||||
})`,
|
||||
);
|
||||
|
||||
await project.build();
|
||||
|
||||
await expect(
|
||||
project.serializeFile('.wxt/types/imports.d.ts'),
|
||||
).resolves.toContain("const ref: typeof import('vue')['ref']");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -233,10 +233,10 @@ describe('TypeScript Project', () => {
|
||||
----------------------------------------
|
||||
// Generated by wxt
|
||||
/// <reference types="wxt/vite-builder-env" />
|
||||
/// <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="./types/imports.d.ts" />
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -97,7 +97,8 @@ describe('Zipping', () => {
|
||||
expect(await project.fileExists(sourcesZip)).toBe(true);
|
||||
});
|
||||
|
||||
it('should not zip hidden files into sources by default', async () => {
|
||||
// TODO: Fix #738 and re-enable
|
||||
it.skip('should not zip hidden files into sources by default', async () => {
|
||||
const project = new TestProject({
|
||||
name: 'test',
|
||||
version: '1.0.0',
|
||||
@@ -117,7 +118,8 @@ describe('Zipping', () => {
|
||||
expect(await project.fileExists(unzipDir, '.env')).toBe(false);
|
||||
});
|
||||
|
||||
it('should allow zipping hidden files into sources when explicitly listed', async () => {
|
||||
// TODO: Fix #738 and re-enable
|
||||
it.skip('should allow zipping hidden files into sources when explicitly listed', async () => {
|
||||
const project = new TestProject({
|
||||
name: 'test',
|
||||
version: '1.0.0',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "wxt",
|
||||
"type": "module",
|
||||
"version": "0.18.6",
|
||||
"version": "0.18.10",
|
||||
"description": "Next gen framework for developing web extensions",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -92,12 +92,12 @@
|
||||
},
|
||||
"scripts": {
|
||||
"wxt": "tsx src/cli/index.ts",
|
||||
"build": "tsx scripts/build.ts",
|
||||
"check": "run-s -c check:*",
|
||||
"build": "buildc -- tsx scripts/build.ts",
|
||||
"check": "buildc --deps-only -- run-s -c check:*",
|
||||
"check:default": "check",
|
||||
"check:tsc-virtual": "tsc --noEmit -p src/virtual",
|
||||
"test": "vitest",
|
||||
"test:e2e": "vitest -r e2e",
|
||||
"test": "buildc --deps-only -- vitest",
|
||||
"test:e2e": "buildc --deps-only -- vitest -r e2e",
|
||||
"sync-releases": "pnpx changelogen@latest gh release"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -138,6 +138,7 @@
|
||||
"publish-browser-extension": "^2.1.3",
|
||||
"unimport": "^3.7.2",
|
||||
"vite": "^5.3.0",
|
||||
"vite-node": "^1.6.0",
|
||||
"web-ext-run": "^0.2.0",
|
||||
"webextension-polyfill": "^0.10.0"
|
||||
},
|
||||
|
||||
@@ -13,10 +13,10 @@ describe('Module Utilities', () => {
|
||||
const userConfig = {};
|
||||
const moduleConfig = { build: { sourcemap: true } };
|
||||
|
||||
wxt.config.vite = () => userConfig;
|
||||
wxt.config.vite = () => Promise.resolve(userConfig);
|
||||
addViteConfig(wxt, () => moduleConfig);
|
||||
await wxt.hooks.callHook('ready', wxt);
|
||||
const actual: any = wxt.config.vite(wxt.config.env);
|
||||
const actual = await wxt.config.vite(wxt.config.env);
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
@@ -32,7 +32,7 @@ describe('Module Utilities', () => {
|
||||
wxt.config.vite = () => userConfig;
|
||||
addViteConfig(wxt, () => moduleConfig);
|
||||
await wxt.hooks.callHook('ready', wxt);
|
||||
const actual: any = wxt.config.vite(wxt.config.env);
|
||||
const actual = await wxt.config.vite(wxt.config.env);
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import { WxtModule } from '~/types';
|
||||
import unimport from './unimport';
|
||||
|
||||
export const builtinModules: WxtModule<any>[] = [unimport];
|
||||
@@ -0,0 +1,122 @@
|
||||
import { addViteConfig, defineWxtModule } from '~/modules';
|
||||
import type {
|
||||
EslintGlobalsPropValue,
|
||||
WxtDirFileEntry,
|
||||
WxtModule,
|
||||
WxtResolvedUnimportOptions,
|
||||
} from '~/types';
|
||||
import { type Unimport, createUnimport } from 'unimport';
|
||||
import { Plugin } from 'vite';
|
||||
import { extname } from 'node:path';
|
||||
|
||||
export default defineWxtModule({
|
||||
name: 'wxt:built-in:unimport',
|
||||
setup(wxt) {
|
||||
const options = wxt.config.imports;
|
||||
if (options === false) return;
|
||||
|
||||
let unimport: Unimport;
|
||||
|
||||
// Add user module imports to config
|
||||
wxt.hooks.hook('ready', () => {
|
||||
const addModuleImports = (module: WxtModule<any>) => {
|
||||
if (!module.imports) return;
|
||||
|
||||
options.imports ??= [];
|
||||
options.imports.push(...module.imports);
|
||||
};
|
||||
|
||||
wxt.config.builtinModules.forEach(addModuleImports);
|
||||
wxt.config.userModules.forEach(addModuleImports);
|
||||
});
|
||||
|
||||
// Create unimport instance after READY so any modifications to the config
|
||||
// inside "ready" are applied.
|
||||
wxt.hooks.afterEach((event) => {
|
||||
if (event.name === 'ready') {
|
||||
unimport = createUnimport(options);
|
||||
}
|
||||
});
|
||||
|
||||
// Generate types
|
||||
wxt.hooks.hook('prepare:types', async (_, entries) => {
|
||||
// Update cache before each rebuild
|
||||
await unimport.init();
|
||||
|
||||
entries.push(await getImportsDeclarationEntry(unimport));
|
||||
|
||||
if (!options.eslintrc.enabled) return;
|
||||
entries.push(await getImportsEslintEntry(unimport, options));
|
||||
});
|
||||
|
||||
// Add vite plugin
|
||||
addViteConfig(wxt, () => ({
|
||||
plugins: [vitePlugin(unimport)],
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
export function vitePlugin(unimport: Unimport): Plugin {
|
||||
const ENABLED_EXTENSIONS = new Set([
|
||||
'.js',
|
||||
'.jsx',
|
||||
'.ts',
|
||||
'.tsx',
|
||||
'.vue',
|
||||
'.svelte',
|
||||
]);
|
||||
return {
|
||||
name: 'wxt:unimport',
|
||||
async transform(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;
|
||||
|
||||
const injected = await unimport.injectImports(code, id);
|
||||
return {
|
||||
code: injected.code,
|
||||
map: injected.s.generateMap({ hires: 'boundary', source: id }),
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function getImportsDeclarationEntry(
|
||||
unimport: Unimport,
|
||||
): Promise<WxtDirFileEntry> {
|
||||
// Load project imports into unimport memory so they are output via generateTypeDeclarations
|
||||
await unimport.init();
|
||||
|
||||
return {
|
||||
path: 'types/imports.d.ts',
|
||||
text: [
|
||||
'// Generated by wxt',
|
||||
await unimport.generateTypeDeclarations(),
|
||||
'',
|
||||
].join('\n'),
|
||||
tsReference: true,
|
||||
};
|
||||
}
|
||||
|
||||
async function getImportsEslintEntry(
|
||||
unimport: Unimport,
|
||||
options: WxtResolvedUnimportOptions,
|
||||
): Promise<WxtDirFileEntry> {
|
||||
const globals: Record<string, EslintGlobalsPropValue> = {};
|
||||
const eslintrc = { globals };
|
||||
|
||||
(await unimport.getImports())
|
||||
.map((i) => i.as ?? i.name)
|
||||
.filter(Boolean)
|
||||
.sort()
|
||||
.forEach((name) => {
|
||||
eslintrc.globals[name] = options.eslintrc.globalsPropValue;
|
||||
});
|
||||
|
||||
return {
|
||||
path: options.eslintrc.filePath,
|
||||
text: JSON.stringify(eslintrc, null, 2) + '\n',
|
||||
};
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { createViteBuilder } from '../index';
|
||||
import { fakeResolvedConfig } from '~/core/utils/testing/fake-objects';
|
||||
import { createHooks } from 'hookable';
|
||||
|
||||
describe('Vite Builder', () => {
|
||||
describe('importEntrypoint', () => {
|
||||
it('should import entrypoints, removing runtime values (like the main function)', async () => {
|
||||
const {
|
||||
default: { main: _, ...expected },
|
||||
} = await import('./fixtures/module');
|
||||
const builder = await createViteBuilder(
|
||||
fakeResolvedConfig({ root: __dirname }),
|
||||
createHooks(),
|
||||
);
|
||||
const actual = await builder.importEntrypoint<{ default: any }>(
|
||||
'./fixtures/module.ts',
|
||||
);
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -20,6 +20,10 @@ import {
|
||||
import { Hookable } from 'hookable';
|
||||
import { toArray } from '~/core/utils/arrays';
|
||||
import { safeVarName } from '~/core/utils/strings';
|
||||
import { importEntrypointFile } from '~/core/utils/building';
|
||||
import { ViteNodeServer } from 'vite-node/server';
|
||||
import { ViteNodeRunner } from 'vite-node/client';
|
||||
import { installSourcemapsSupport } from 'vite-node/source-map';
|
||||
|
||||
export async function createViteBuilder(
|
||||
wxtConfig: ResolvedConfig,
|
||||
@@ -57,7 +61,6 @@ export async function createViteBuilder(
|
||||
config.plugins.push(
|
||||
wxtPlugins.download(wxtConfig),
|
||||
wxtPlugins.devHtmlPrerender(wxtConfig, server),
|
||||
wxtPlugins.unimport(wxtConfig),
|
||||
wxtPlugins.resolveVirtualModules(wxtConfig),
|
||||
wxtPlugins.devServerGlobals(wxtConfig, server),
|
||||
wxtPlugins.tsconfigPaths(wxtConfig),
|
||||
@@ -204,21 +207,65 @@ export async function createViteBuilder(
|
||||
return {
|
||||
name: 'Vite',
|
||||
version: vite.version,
|
||||
async importEntrypoint(url) {
|
||||
const baseConfig = await getBaseConfig();
|
||||
const envConfig: vite.InlineConfig = {
|
||||
plugins: [
|
||||
wxtPlugins.webextensionPolyfillMock(wxtConfig),
|
||||
wxtPlugins.removeEntrypointMainFunction(wxtConfig, url),
|
||||
],
|
||||
};
|
||||
const config = vite.mergeConfig(baseConfig, envConfig);
|
||||
const server = await vite.createServer(config);
|
||||
await server.listen();
|
||||
const runtime = await vite.createViteRuntime(server, { hmr: false });
|
||||
const module = await runtime.executeUrl(url);
|
||||
await server.close();
|
||||
return module.default;
|
||||
async importEntrypoint(path) {
|
||||
switch (wxtConfig.experimental.entrypointImporter) {
|
||||
default:
|
||||
case 'jiti': {
|
||||
return await importEntrypointFile(path);
|
||||
}
|
||||
case 'vite-runtime': {
|
||||
const baseConfig = await getBaseConfig();
|
||||
const envConfig: vite.InlineConfig = {
|
||||
plugins: [
|
||||
wxtPlugins.webextensionPolyfillMock(wxtConfig),
|
||||
wxtPlugins.removeEntrypointMainFunction(wxtConfig, path),
|
||||
],
|
||||
};
|
||||
const config = vite.mergeConfig(baseConfig, envConfig);
|
||||
const server = await vite.createServer(config);
|
||||
await server.listen();
|
||||
const runtime = await vite.createViteRuntime(server, { hmr: false });
|
||||
const module = await runtime.executeUrl(path);
|
||||
await server.close();
|
||||
return module.default;
|
||||
}
|
||||
case 'vite-node': {
|
||||
const baseConfig = await getBaseConfig();
|
||||
// Disable dep optimization, as recommended by vite-node's README
|
||||
baseConfig.optimizeDeps ??= {};
|
||||
baseConfig.optimizeDeps.noDiscovery = true;
|
||||
baseConfig.optimizeDeps.include = [];
|
||||
const envConfig: vite.InlineConfig = {
|
||||
plugins: [
|
||||
wxtPlugins.webextensionPolyfillMock(wxtConfig),
|
||||
wxtPlugins.removeEntrypointMainFunction(wxtConfig, path),
|
||||
],
|
||||
};
|
||||
const config = vite.mergeConfig(baseConfig, envConfig);
|
||||
const server = await vite.createServer(config);
|
||||
await server.pluginContainer.buildStart({});
|
||||
const node = new ViteNodeServer(server);
|
||||
installSourcemapsSupport({
|
||||
getSourceMap: (source) => node.getSourceMap(source),
|
||||
});
|
||||
const runner = new ViteNodeRunner({
|
||||
root: server.config.root,
|
||||
base: server.config.base,
|
||||
// when having the server and runner in a different context,
|
||||
// you will need to handle the communication between them
|
||||
// and pass to this function
|
||||
fetchModule(id) {
|
||||
return node.fetchModule(id);
|
||||
},
|
||||
resolveId(id, importer) {
|
||||
return node.resolveId(id, importer);
|
||||
},
|
||||
});
|
||||
const res = await runner.executeFile(path);
|
||||
await server.close();
|
||||
return res.default;
|
||||
}
|
||||
}
|
||||
},
|
||||
async build(group) {
|
||||
let entryConfig;
|
||||
|
||||
@@ -2,7 +2,6 @@ export * from './devHtmlPrerender';
|
||||
export * from './devServerGlobals';
|
||||
export * from './download';
|
||||
export * from './multipageMove';
|
||||
export * from './unimport';
|
||||
export * from './resolveVirtualModules';
|
||||
export * from './tsconfigPaths';
|
||||
export * from './noopBackground';
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import { createUnimport } from 'unimport';
|
||||
import { ResolvedConfig } from '~/types';
|
||||
import type * as vite from 'vite';
|
||||
import { extname } from 'path';
|
||||
|
||||
const ENABLED_EXTENSIONS = new Set([
|
||||
'.js',
|
||||
'.jsx',
|
||||
'.ts',
|
||||
'.tsx',
|
||||
'.vue',
|
||||
'.svelte',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Inject any global imports defined by unimport
|
||||
*/
|
||||
export function unimport(config: ResolvedConfig): vite.PluginOption {
|
||||
const options = config.imports;
|
||||
if (options === false) return [];
|
||||
|
||||
const unimport = createUnimport(options);
|
||||
|
||||
return {
|
||||
name: 'wxt:unimport',
|
||||
async config() {
|
||||
await unimport.scanImportsFromDir(undefined, { cwd: config.srcDir });
|
||||
},
|
||||
async transform(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;
|
||||
|
||||
const injected = await unimport.injectImports(code, id);
|
||||
return {
|
||||
code: injected.code,
|
||||
map: injected.s.generateMap({ hires: 'boundary', source: id }),
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -46,13 +46,19 @@ try {
|
||||
// Use "pre" so the new script is added before vite bundles all the scripts
|
||||
order: 'pre',
|
||||
handler(html, _ctx) {
|
||||
const { document } = parseHTML(html);
|
||||
const script = document.createElement('script');
|
||||
script.type = 'module';
|
||||
script.src =
|
||||
const src =
|
||||
config.command === 'serve'
|
||||
? `http://${config.dev.server?.hostname}:${config.dev.server?.port}/@id/${virtualHtmlModuleId}`
|
||||
: virtualHtmlModuleId;
|
||||
|
||||
const { document } = parseHTML(html);
|
||||
const existing = document.querySelector(`script[src='${src}']`);
|
||||
if (existing) return;
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.type = 'module';
|
||||
script.src = src;
|
||||
|
||||
if (document.head == null) {
|
||||
const newHead = document.createElement('head');
|
||||
document.documentElement.prepend(newHead);
|
||||
|
||||
@@ -153,6 +153,7 @@ function createFileReloader(server: WxtDevServer) {
|
||||
|
||||
// Here, "path" is a non-normalized path (ie: C:\\users\\... instead of C:/users/...)
|
||||
if (path.startsWith(wxt.config.outBaseDir)) return;
|
||||
if (path.startsWith(wxt.config.wxtDir)) return;
|
||||
changeQueue.push([event, path]);
|
||||
|
||||
await fileChangedMutex.runExclusive(async () => {
|
||||
|
||||
@@ -2,12 +2,12 @@ import { describe, expect, it } from 'vitest';
|
||||
import path from 'node:path';
|
||||
import { bun } from '../bun';
|
||||
|
||||
const cwd = path.resolve(__dirname, 'fixtures/bun-project');
|
||||
|
||||
describe.skipIf(() => process.platform === 'win32')(
|
||||
'Bun Package Management Utils',
|
||||
() => {
|
||||
describe('listDependencies', () => {
|
||||
const cwd = path.resolve(__dirname, 'fixtures/simple-bun-project');
|
||||
|
||||
it('should list direct dependencies', async () => {
|
||||
const actual = await bun.listDependencies({ cwd });
|
||||
expect(actual).toEqual([
|
||||
|
||||
@@ -4,15 +4,14 @@ import { npm } from '../npm';
|
||||
import { execaCommand } from 'execa';
|
||||
import { exists } from 'fs-extra';
|
||||
|
||||
const cwd = path.resolve(__dirname, 'fixtures/npm-project');
|
||||
|
||||
describe('NPM Package Management Utils', () => {
|
||||
beforeAll(async () => {
|
||||
// NPM needs the modules installed for 'npm ls' to work
|
||||
await execaCommand('npm i', { cwd });
|
||||
}, 60e3);
|
||||
|
||||
describe('listDependencies', () => {
|
||||
const cwd = path.resolve(__dirname, 'fixtures/simple-npm-project');
|
||||
beforeAll(async () => {
|
||||
// NPM needs the modules installed for 'npm ls' to work
|
||||
await execaCommand('npm i', { cwd });
|
||||
}, 60e3);
|
||||
|
||||
it('should list direct dependencies', async () => {
|
||||
const actual = await npm.listDependencies({ cwd });
|
||||
expect(actual).toEqual([
|
||||
@@ -32,6 +31,8 @@ describe('NPM Package Management Utils', () => {
|
||||
});
|
||||
|
||||
describe('downloadDependency', () => {
|
||||
const cwd = path.resolve(__dirname, 'fixtures/simple-npm-project');
|
||||
|
||||
it('should download the dependency as a tarball', async () => {
|
||||
const downloadDir = path.resolve(cwd, 'dist');
|
||||
const id = 'mime-db@1.52.0';
|
||||
|
||||
@@ -3,17 +3,16 @@ import path from 'node:path';
|
||||
import { pnpm } from '../pnpm';
|
||||
import { execaCommand } from 'execa';
|
||||
|
||||
const cwd = path.resolve(__dirname, 'fixtures/pnpm-project');
|
||||
|
||||
process.env.WXT_PNPM_IGNORE_WORKSPACE = 'true';
|
||||
|
||||
describe('PNPM Package Management Utils', () => {
|
||||
beforeAll(async () => {
|
||||
// PNPM needs the modules installed, or 'pnpm ls' will return a blank list.
|
||||
await execaCommand('pnpm i --ignore-workspace', { cwd });
|
||||
});
|
||||
|
||||
describe('listDependencies', () => {
|
||||
const cwd = path.resolve(__dirname, 'fixtures/simple-pnpm-project');
|
||||
beforeAll(async () => {
|
||||
// PNPM needs the modules installed, or 'pnpm ls' will return a blank list.
|
||||
await execaCommand('pnpm i --ignore-workspace', { cwd });
|
||||
});
|
||||
|
||||
it('should list direct dependencies', async () => {
|
||||
const actual = await pnpm.listDependencies({ cwd });
|
||||
expect(actual).toEqual([
|
||||
|
||||
@@ -2,10 +2,10 @@ import { describe, expect, it } from 'vitest';
|
||||
import path from 'node:path';
|
||||
import { yarn } from '../yarn';
|
||||
|
||||
const cwd = path.resolve(__dirname, 'fixtures/yarn-project');
|
||||
|
||||
describe('Yarn Package Management Utils', () => {
|
||||
describe('listDependencies', () => {
|
||||
const cwd = path.resolve(__dirname, 'fixtures/simple-yarn-project');
|
||||
|
||||
it('should list direct dependencies', async () => {
|
||||
const actual = await yarn.listDependencies({ cwd });
|
||||
expect(actual).toEqual([
|
||||
|
||||
@@ -100,6 +100,11 @@ describe('Detect Dev Changes', () => {
|
||||
describe('Public Assets', () => {
|
||||
it("should return 'extension-reload' without any groups to rebuild when the changed file is a public asset", () => {
|
||||
const changes = ['/root/src/public/image.svg'];
|
||||
setFakeWxt({
|
||||
config: {
|
||||
publicDir: '/root/src/public',
|
||||
},
|
||||
});
|
||||
const asset1 = fakeOutputAsset({
|
||||
fileName: 'image.svg',
|
||||
});
|
||||
@@ -114,10 +119,7 @@ describe('Detect Dev Changes', () => {
|
||||
const expected: DevModeChange = {
|
||||
type: 'extension-reload',
|
||||
rebuildGroups: [],
|
||||
cachedOutput: {
|
||||
...currentOutput,
|
||||
publicAssets: [asset2],
|
||||
},
|
||||
cachedOutput: currentOutput,
|
||||
};
|
||||
|
||||
const actual = detectDevChanges(changes, currentOutput);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { describe, it, expect, vi, beforeEach, Mock } from 'vitest';
|
||||
import {
|
||||
BackgroundEntrypoint,
|
||||
BackgroundEntrypointOptions,
|
||||
@@ -12,16 +12,13 @@ import {
|
||||
import { resolve } from 'path';
|
||||
import { findEntrypoints } from '../find-entrypoints';
|
||||
import fs from 'fs-extra';
|
||||
import { importEntrypointFile } from '../import-entrypoint';
|
||||
import glob from 'fast-glob';
|
||||
import {
|
||||
fakeResolvedConfig,
|
||||
setFakeWxt,
|
||||
} from '~/core/utils/testing/fake-objects';
|
||||
import { unnormalizePath } from '~/core/utils/paths';
|
||||
|
||||
vi.mock('../import-entrypoint');
|
||||
const importEntrypointFileMock = vi.mocked(importEntrypointFile);
|
||||
import { wxt } from '~/core/wxt';
|
||||
|
||||
vi.mock('fast-glob');
|
||||
const globMock = vi.mocked(glob);
|
||||
@@ -39,9 +36,11 @@ describe('findEntrypoints', () => {
|
||||
outDir: resolve('.output'),
|
||||
command: 'build',
|
||||
});
|
||||
let importEntrypointMock: Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
setFakeWxt({ config });
|
||||
importEntrypointMock = vi.mocked(wxt.builder.importEntrypoint);
|
||||
});
|
||||
|
||||
it.each<[string, string, PopupEntrypoint]>([
|
||||
@@ -211,13 +210,13 @@ describe('findEntrypoints', () => {
|
||||
matches: ['<all_urls>'],
|
||||
};
|
||||
globMock.mockResolvedValueOnce([path]);
|
||||
importEntrypointFileMock.mockResolvedValue(options);
|
||||
importEntrypointMock.mockResolvedValue(options);
|
||||
|
||||
const entrypoints = await findEntrypoints();
|
||||
|
||||
expect(entrypoints).toHaveLength(1);
|
||||
expect(entrypoints[0]).toEqual({ ...expected, options });
|
||||
expect(importEntrypointFileMock).toBeCalledWith(expected.inputPath);
|
||||
expect(importEntrypointMock).toBeCalledWith(expected.inputPath);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -249,13 +248,13 @@ describe('findEntrypoints', () => {
|
||||
type: 'module',
|
||||
};
|
||||
globMock.mockResolvedValueOnce([path]);
|
||||
importEntrypointFileMock.mockResolvedValue(options);
|
||||
importEntrypointMock.mockResolvedValue(options);
|
||||
|
||||
const entrypoints = await findEntrypoints();
|
||||
|
||||
expect(entrypoints).toHaveLength(1);
|
||||
expect(entrypoints[0]).toEqual({ ...expected, options });
|
||||
expect(importEntrypointFileMock).toBeCalledWith(expected.inputPath);
|
||||
expect(importEntrypointMock).toBeCalledWith(expected.inputPath);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -338,12 +337,13 @@ describe('findEntrypoints', () => {
|
||||
config: {
|
||||
manifestVersion: 2,
|
||||
},
|
||||
builder: wxt.builder,
|
||||
});
|
||||
const options: BackgroundEntrypointOptions = {
|
||||
type: 'module',
|
||||
};
|
||||
globMock.mockResolvedValueOnce(['background.ts']);
|
||||
importEntrypointFileMock.mockResolvedValue(options);
|
||||
importEntrypointMock.mockResolvedValue(options);
|
||||
|
||||
const entrypoints = await findEntrypoints();
|
||||
|
||||
@@ -355,12 +355,13 @@ describe('findEntrypoints', () => {
|
||||
config: {
|
||||
manifestVersion: 3,
|
||||
},
|
||||
builder: wxt.builder,
|
||||
});
|
||||
const options: BackgroundEntrypointOptions = {
|
||||
type: 'module',
|
||||
};
|
||||
globMock.mockResolvedValueOnce(['background.ts']);
|
||||
importEntrypointFileMock.mockResolvedValue(options);
|
||||
importEntrypointMock.mockResolvedValue(options);
|
||||
|
||||
const entrypoints = await findEntrypoints();
|
||||
|
||||
@@ -373,6 +374,7 @@ describe('findEntrypoints', () => {
|
||||
...config,
|
||||
command: 'serve',
|
||||
},
|
||||
builder: wxt.builder,
|
||||
});
|
||||
globMock.mockResolvedValueOnce(['popup.html']);
|
||||
|
||||
@@ -410,13 +412,13 @@ describe('findEntrypoints', () => {
|
||||
};
|
||||
const options: BaseEntrypointOptions = {};
|
||||
globMock.mockResolvedValueOnce([path]);
|
||||
importEntrypointFileMock.mockResolvedValue(options);
|
||||
importEntrypointMock.mockResolvedValue(options);
|
||||
|
||||
const entrypoints = await findEntrypoints();
|
||||
|
||||
expect(entrypoints).toHaveLength(1);
|
||||
expect(entrypoints[0]).toEqual({ ...expected, options });
|
||||
expect(importEntrypointFileMock).toBeCalledWith(expected.inputPath);
|
||||
expect(importEntrypointMock).toBeCalledWith(expected.inputPath);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -701,7 +703,7 @@ describe('findEntrypoints', () => {
|
||||
describe('include option', () => {
|
||||
it("should filter out the background when include doesn't contain the target browser", async () => {
|
||||
globMock.mockResolvedValueOnce(['background.ts']);
|
||||
importEntrypointFileMock.mockResolvedValue({
|
||||
importEntrypointMock.mockResolvedValue({
|
||||
include: ['not' + config.browser],
|
||||
});
|
||||
|
||||
@@ -712,7 +714,7 @@ describe('findEntrypoints', () => {
|
||||
|
||||
it("should filter out content scripts when include doesn't contain the target browser", async () => {
|
||||
globMock.mockResolvedValueOnce(['example.content.ts']);
|
||||
importEntrypointFileMock.mockResolvedValue({
|
||||
importEntrypointMock.mockResolvedValue({
|
||||
include: ['not' + config.browser],
|
||||
});
|
||||
|
||||
@@ -776,7 +778,7 @@ describe('findEntrypoints', () => {
|
||||
describe('exclude option', () => {
|
||||
it('should filter out the background when exclude contains the target browser', async () => {
|
||||
globMock.mockResolvedValueOnce(['background.ts']);
|
||||
importEntrypointFileMock.mockResolvedValue({
|
||||
importEntrypointMock.mockResolvedValue({
|
||||
exclude: [config.browser],
|
||||
});
|
||||
|
||||
@@ -787,7 +789,7 @@ describe('findEntrypoints', () => {
|
||||
|
||||
it('should filter out content scripts when exclude contains the target browser', async () => {
|
||||
globMock.mockResolvedValueOnce(['example.content.ts']);
|
||||
importEntrypointFileMock.mockResolvedValue({
|
||||
importEntrypointMock.mockResolvedValue({
|
||||
exclude: [config.browser],
|
||||
});
|
||||
|
||||
@@ -850,7 +852,6 @@ describe('findEntrypoints', () => {
|
||||
'ui.content/index.ts',
|
||||
'injected.content/index.ts',
|
||||
]);
|
||||
importEntrypointFileMock.mockResolvedValue({});
|
||||
const filterEntrypoints = ['popup', 'ui'];
|
||||
setFakeWxt({
|
||||
config: {
|
||||
@@ -860,8 +861,11 @@ describe('findEntrypoints', () => {
|
||||
command: 'build',
|
||||
filterEntrypoints: new Set(filterEntrypoints),
|
||||
},
|
||||
builder: wxt.builder,
|
||||
});
|
||||
|
||||
importEntrypointMock.mockResolvedValue({});
|
||||
|
||||
const entrypoints = await findEntrypoints();
|
||||
const names = entrypoints.map((item) => item.name);
|
||||
expect(names).toHaveLength(2);
|
||||
|
||||
@@ -45,14 +45,18 @@ async function copyPublicDirectory(): Promise<BuildOutput['publicAssets']> {
|
||||
if (files.length === 0) return [];
|
||||
|
||||
const publicAssets: BuildOutput['publicAssets'] = [];
|
||||
for (const { absoluteSrc, relativeDest } of files) {
|
||||
const absoluteDest = resolve(wxt.config.outDir, relativeDest);
|
||||
for (const file of files) {
|
||||
const absoluteDest = resolve(wxt.config.outDir, file.relativeDest);
|
||||
|
||||
await fs.ensureDir(dirname(absoluteDest));
|
||||
await fs.copyFile(absoluteSrc, absoluteDest);
|
||||
if ('absoluteSrc' in file) {
|
||||
await fs.copyFile(file.absoluteSrc, absoluteDest);
|
||||
} else {
|
||||
await fs.writeFile(absoluteDest, file.contents, 'utf8');
|
||||
}
|
||||
publicAssets.push({
|
||||
type: 'asset',
|
||||
fileName: relativeDest,
|
||||
fileName: file.relativeDest,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import {
|
||||
BuildOutput,
|
||||
BuildStepOutput,
|
||||
EntrypointGroup,
|
||||
OutputAsset,
|
||||
OutputFile,
|
||||
} from '~/types';
|
||||
import { every, some } from '~/core/utils/arrays';
|
||||
@@ -50,12 +49,25 @@ export function detectDevChanges(
|
||||
findEffectedSteps(changedFile, currentOutput),
|
||||
),
|
||||
);
|
||||
if (changedSteps.size === 0) return { type: 'no-change' };
|
||||
if (changedSteps.size === 0) {
|
||||
const hasPublicChange = some(changedFiles, (file) =>
|
||||
file.startsWith(wxt.config.publicDir),
|
||||
);
|
||||
if (hasPublicChange) {
|
||||
return {
|
||||
type: 'extension-reload',
|
||||
rebuildGroups: [],
|
||||
cachedOutput: currentOutput,
|
||||
};
|
||||
} else {
|
||||
return { type: 'no-change' };
|
||||
}
|
||||
}
|
||||
|
||||
const unchangedOutput: BuildOutput = {
|
||||
manifest: currentOutput.manifest,
|
||||
steps: [],
|
||||
publicAssets: [],
|
||||
publicAssets: [...currentOutput.publicAssets],
|
||||
};
|
||||
const changedOutput: BuildOutput = {
|
||||
manifest: currentOutput.manifest,
|
||||
@@ -70,13 +82,6 @@ export function detectDevChanges(
|
||||
unchangedOutput.steps.push(step);
|
||||
}
|
||||
}
|
||||
for (const asset of currentOutput.publicAssets) {
|
||||
if (changedSteps.has(asset)) {
|
||||
changedOutput.publicAssets.push(asset);
|
||||
} else {
|
||||
unchangedOutput.publicAssets.push(asset);
|
||||
}
|
||||
}
|
||||
|
||||
const isOnlyHtmlChanges =
|
||||
changedFiles.length > 0 &&
|
||||
@@ -117,8 +122,8 @@ export function detectDevChanges(
|
||||
function findEffectedSteps(
|
||||
changedFile: string,
|
||||
currentOutput: BuildOutput,
|
||||
): DetectedChange[] {
|
||||
const changes: DetectedChange[] = [];
|
||||
): BuildStepOutput[] {
|
||||
const changes: BuildStepOutput[] = [];
|
||||
const changedPath = normalizePath(changedFile);
|
||||
|
||||
const isChunkEffected = (chunk: OutputFile): boolean =>
|
||||
@@ -135,11 +140,6 @@ function findEffectedSteps(
|
||||
if (effectedChunk) changes.push(step);
|
||||
}
|
||||
|
||||
const effectedAsset = currentOutput.publicAssets.find((chunk) =>
|
||||
isChunkEffected(chunk),
|
||||
);
|
||||
if (effectedAsset) changes.push(effectedAsset);
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
@@ -194,10 +194,3 @@ interface ContentScriptReload extends RebuildChange {
|
||||
type: 'content-script-reload';
|
||||
changedSteps: BuildStepOutput[];
|
||||
}
|
||||
|
||||
/**
|
||||
* When figuring out what needs reloaded, this stores the step that was changed, or the public
|
||||
* directory asset that was changed. It doesn't know what type of change is required yet. Just an
|
||||
* intermediate type.
|
||||
*/
|
||||
type DetectedChange = BuildStepOutput | OutputAsset;
|
||||
|
||||
@@ -28,7 +28,6 @@ import { VIRTUAL_NOOP_BACKGROUND_MODULE_ID } from '~/core/utils/constants';
|
||||
import { CSS_EXTENSIONS_PATTERN } from '~/core/utils/paths';
|
||||
import pc from 'picocolors';
|
||||
import { wxt } from '../../wxt';
|
||||
import { importEntrypointFile } from './import-entrypoint';
|
||||
|
||||
/**
|
||||
* Return entrypoints and their configuration by looking through the project's files.
|
||||
@@ -290,7 +289,7 @@ async function getUnlistedScriptEntrypoint({
|
||||
skipped,
|
||||
}: EntrypointInfo): Promise<GenericEntrypoint> {
|
||||
const defaultExport =
|
||||
await importEntrypoint<UnlistedScriptDefinition>(inputPath);
|
||||
await wxt.builder.importEntrypoint<UnlistedScriptDefinition>(inputPath);
|
||||
if (defaultExport == null) {
|
||||
throw Error(
|
||||
`${name}: Default export not found, did you forget to call "export default defineUnlistedScript(...)"?`,
|
||||
@@ -315,7 +314,7 @@ async function getBackgroundEntrypoint({
|
||||
let options: Omit<BackgroundDefinition, 'main'> = {};
|
||||
if (inputPath !== VIRTUAL_NOOP_BACKGROUND_MODULE_ID) {
|
||||
const defaultExport =
|
||||
await importEntrypoint<BackgroundDefinition>(inputPath);
|
||||
await wxt.builder.importEntrypoint<BackgroundDefinition>(inputPath);
|
||||
if (defaultExport == null) {
|
||||
throw Error(
|
||||
`${name}: Default export not found, did you forget to call "export default defineBackground(...)"?`,
|
||||
@@ -344,8 +343,15 @@ async function getContentScriptEntrypoint({
|
||||
name,
|
||||
skipped,
|
||||
}: EntrypointInfo): Promise<ContentScriptEntrypoint> {
|
||||
const { main: _, ...options } =
|
||||
await importEntrypoint<ContentScriptDefinition>(inputPath);
|
||||
const defaultExport =
|
||||
await wxt.builder.importEntrypoint<ContentScriptDefinition>(inputPath);
|
||||
if (defaultExport == null) {
|
||||
throw Error(
|
||||
`${name}: Default export not found, did you forget to call "export default defineContentScript(...)"?`,
|
||||
);
|
||||
}
|
||||
|
||||
const { main: _, ...options } = defaultExport;
|
||||
if (options == null) {
|
||||
throw Error(
|
||||
`${name}: Default export not found, did you forget to call "export default defineContentScript(...)"?`,
|
||||
@@ -488,9 +494,3 @@ const PATH_GLOB_TO_TYPE_MAP: Record<string, Entrypoint['type']> = {
|
||||
};
|
||||
|
||||
const CONTENT_SCRIPT_OUT_DIR = 'content-scripts';
|
||||
|
||||
function importEntrypoint<T>(path: string) {
|
||||
return wxt.config.experimental.viteRuntime
|
||||
? wxt.builder.importEntrypoint<T>(path)
|
||||
: importEntrypointFile<T>(path);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import { Unimport, createUnimport } from 'unimport';
|
||||
import {
|
||||
EslintGlobalsPropValue,
|
||||
Entrypoint,
|
||||
WxtResolvedUnimportOptions,
|
||||
} from '~/types';
|
||||
import { Entrypoint, WxtDirEntry, WxtDirFileEntry } from '~/types';
|
||||
import fs from 'fs-extra';
|
||||
import { relative, resolve } from 'path';
|
||||
import { dirname, relative, resolve } from 'node:path';
|
||||
import {
|
||||
getEntrypointBundlePath,
|
||||
isHtmlEntrypoint,
|
||||
@@ -25,61 +20,57 @@ export async function generateTypesDir(
|
||||
): Promise<void> {
|
||||
await fs.ensureDir(wxt.config.typesDir);
|
||||
|
||||
const references: string[] = [];
|
||||
const entries: WxtDirEntry[] = [
|
||||
// Hard-coded entries
|
||||
{ module: 'wxt/vite-builder-env' },
|
||||
];
|
||||
|
||||
if (wxt.config.imports !== false) {
|
||||
const unimport = createUnimport(wxt.config.imports);
|
||||
references.push(await writeImportsDeclarationFile(unimport));
|
||||
if (wxt.config.imports.eslintrc.enabled) {
|
||||
await writeImportsEslintFile(unimport, wxt.config.imports);
|
||||
}
|
||||
}
|
||||
// Add references to modules installed from NPM to the TS project so their
|
||||
// type augmentation can update InlineConfig correctly. Local modules defined
|
||||
// in <root>/modules are already apart of the project, so we don't need to
|
||||
// add them.
|
||||
wxt.config.userModules.forEach((module) => {
|
||||
if (module.type === 'node_module' && module.configKey != null)
|
||||
entries.push({ module: module.id });
|
||||
});
|
||||
|
||||
references.push(await writePathsDeclarationFile(entrypoints));
|
||||
references.push(await writeI18nDeclarationFile());
|
||||
references.push(await writeGlobalsDeclarationFile());
|
||||
// browser.runtime.getURL
|
||||
entries.push(await getPathsDeclarationEntry(entrypoints));
|
||||
|
||||
const mainReference = await writeMainDeclarationFile(references);
|
||||
await writeTsConfigFile(mainReference);
|
||||
}
|
||||
// browser.i18n.getMessage
|
||||
entries.push(await getI18nDeclarationEntry());
|
||||
|
||||
async function writeImportsDeclarationFile(unimport: Unimport) {
|
||||
const filePath = resolve(wxt.config.typesDir, 'imports.d.ts');
|
||||
// import.meta.env.*
|
||||
entries.push(await getGlobalsDeclarationEntry());
|
||||
|
||||
// Load project imports into unimport memory so they are output via generateTypeDeclarations
|
||||
await unimport.scanImportsFromDir(undefined, { cwd: wxt.config.srcDir });
|
||||
// tsconfig.json
|
||||
entries.push(await getTsConfigEntry());
|
||||
|
||||
await writeFileIfDifferent(
|
||||
filePath,
|
||||
['// Generated by wxt', await unimport.generateTypeDeclarations()].join(
|
||||
'\n',
|
||||
) + '\n',
|
||||
// Let modules add more entries
|
||||
await wxt.hooks.callHook('prepare:types', wxt, entries);
|
||||
|
||||
// Add main declaration file, not editable
|
||||
entries.push(getMainDeclarationEntry(entries));
|
||||
|
||||
// Write all the files
|
||||
const absoluteFileEntries = (
|
||||
entries.filter((entry) => 'path' in entry) as WxtDirFileEntry[]
|
||||
).map<WxtDirFileEntry>((entry) => ({
|
||||
...entry,
|
||||
path: resolve(wxt.config.wxtDir, entry.path),
|
||||
}));
|
||||
|
||||
await Promise.all(
|
||||
absoluteFileEntries.map(async (file) => {
|
||||
await fs.ensureDir(dirname(file.path));
|
||||
await writeFileIfDifferent(file.path, file.text);
|
||||
}),
|
||||
);
|
||||
|
||||
return filePath;
|
||||
}
|
||||
|
||||
async function writeImportsEslintFile(
|
||||
unimport: Unimport,
|
||||
options: WxtResolvedUnimportOptions,
|
||||
) {
|
||||
const globals: Record<string, EslintGlobalsPropValue> = {};
|
||||
const eslintrc = { globals };
|
||||
|
||||
(await unimport.getImports())
|
||||
.map((i) => i.as ?? i.name)
|
||||
.filter(Boolean)
|
||||
.sort()
|
||||
.forEach((name) => {
|
||||
eslintrc.globals[name] = options.eslintrc.globalsPropValue;
|
||||
});
|
||||
await fs.writeJson(options.eslintrc.filePath, eslintrc, { spaces: 2 });
|
||||
}
|
||||
|
||||
async function writePathsDeclarationFile(
|
||||
async function getPathsDeclarationEntry(
|
||||
entrypoints: Entrypoint[],
|
||||
): Promise<string> {
|
||||
const filePath = resolve(wxt.config.typesDir, 'paths.d.ts');
|
||||
): Promise<WxtDirFileEntry> {
|
||||
const unions = entrypoints
|
||||
.map((entry) =>
|
||||
getEntrypointBundlePath(
|
||||
@@ -108,16 +99,14 @@ declare module "wxt/browser" {
|
||||
}
|
||||
`;
|
||||
|
||||
await writeFileIfDifferent(
|
||||
filePath,
|
||||
template.replace('{{ union }}', unions || ' | never'),
|
||||
);
|
||||
|
||||
return filePath;
|
||||
return {
|
||||
path: 'types/paths.d.ts',
|
||||
text: template.replace('{{ union }}', unions || ' | never'),
|
||||
tsReference: true,
|
||||
};
|
||||
}
|
||||
|
||||
async function writeI18nDeclarationFile(): Promise<string> {
|
||||
const filePath = resolve(wxt.config.typesDir, 'i18n.d.ts');
|
||||
async function getI18nDeclarationEntry(): Promise<WxtDirFileEntry> {
|
||||
const defaultLocale = wxt.config.manifest.default_locale;
|
||||
const template = `// Generated by wxt
|
||||
import "wxt/browser";
|
||||
@@ -165,20 +154,19 @@ declare module "wxt/browser" {
|
||||
options?: GetMessageOptions,
|
||||
): string;`;
|
||||
});
|
||||
await writeFileIfDifferent(
|
||||
filePath,
|
||||
template.replace('{{ overrides }}', overrides.join('\n')),
|
||||
);
|
||||
|
||||
return filePath;
|
||||
return {
|
||||
path: 'types/i18n.d.ts',
|
||||
text: template.replace('{{ overrides }}', overrides.join('\n')),
|
||||
tsReference: true,
|
||||
};
|
||||
}
|
||||
|
||||
async function writeGlobalsDeclarationFile(): Promise<string> {
|
||||
const filePath = resolve(wxt.config.typesDir, 'globals.d.ts');
|
||||
async function getGlobalsDeclarationEntry(): Promise<WxtDirFileEntry> {
|
||||
const globals = [...getGlobals(wxt.config), ...getEntrypointGlobals('')];
|
||||
await writeFileIfDifferent(
|
||||
filePath,
|
||||
[
|
||||
return {
|
||||
path: 'types/globals.d.ts',
|
||||
text: [
|
||||
'// Generated by wxt',
|
||||
'export {}',
|
||||
'interface ImportMetaEnv {',
|
||||
@@ -187,39 +175,30 @@ async function writeGlobalsDeclarationFile(): Promise<string> {
|
||||
'interface ImportMeta {',
|
||||
' readonly env: ImportMetaEnv',
|
||||
'}',
|
||||
].join('\n') + '\n',
|
||||
);
|
||||
return filePath;
|
||||
'',
|
||||
].join('\n'),
|
||||
tsReference: true,
|
||||
};
|
||||
}
|
||||
|
||||
async function writeMainDeclarationFile(references: string[]): Promise<string> {
|
||||
const dir = wxt.config.wxtDir;
|
||||
const filePath = resolve(dir, 'wxt.d.ts');
|
||||
await writeFileIfDifferent(
|
||||
filePath,
|
||||
[
|
||||
'// Generated by wxt',
|
||||
`/// <reference types="wxt/vite-builder-env" />`,
|
||||
...references.map(
|
||||
(ref) =>
|
||||
`/// <reference types="./${normalizePath(relative(dir, ref))}" />`,
|
||||
),
|
||||
|
||||
// Add references to modules installed from NPM to the TS project so
|
||||
// their type augmentation can update InlineConfig correctly. Local
|
||||
// modules defined in <root>/modules are already apart of the project, so
|
||||
// we don't need to add them.
|
||||
...wxt.config.modules
|
||||
.filter(
|
||||
(module) => module.type === 'node_module' && module.configKey != null,
|
||||
)
|
||||
.map((module) => `/// <reference types="${module.id}" />`),
|
||||
].join('\n') + '\n',
|
||||
);
|
||||
return filePath;
|
||||
function getMainDeclarationEntry(references: WxtDirEntry[]): WxtDirFileEntry {
|
||||
const lines = ['// Generated by wxt'];
|
||||
references.forEach((ref) => {
|
||||
if ('module' in ref) {
|
||||
return lines.push(`/// <reference types="${ref.module}" />`);
|
||||
} else if (ref.tsReference) {
|
||||
const absolutePath = resolve(wxt.config.wxtDir, ref.path);
|
||||
const relativePath = relative(wxt.config.wxtDir, absolutePath);
|
||||
lines.push(`/// <reference types="./${normalizePath(relativePath)}" />`);
|
||||
}
|
||||
});
|
||||
return {
|
||||
path: 'wxt.d.ts',
|
||||
text: lines.join('\n') + '\n',
|
||||
};
|
||||
}
|
||||
|
||||
async function writeTsConfigFile(mainReference: string) {
|
||||
async function getTsConfigEntry(): Promise<WxtDirFileEntry> {
|
||||
const dir = wxt.config.wxtDir;
|
||||
const getTsconfigPath = (path: string) => normalizePath(relative(dir, path));
|
||||
const paths = Object.entries(wxt.config.alias)
|
||||
@@ -232,9 +211,7 @@ async function writeTsConfigFile(mainReference: string) {
|
||||
})
|
||||
.join(',\n');
|
||||
|
||||
await writeFileIfDifferent(
|
||||
resolve(dir, 'tsconfig.json'),
|
||||
`{
|
||||
const text = `{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
@@ -251,9 +228,13 @@ ${paths}
|
||||
},
|
||||
"include": [
|
||||
"${getTsconfigPath(wxt.config.root)}/**/*",
|
||||
"./${getTsconfigPath(mainReference)}"
|
||||
"./wxt.d.ts"
|
||||
],
|
||||
"exclude": ["${getTsconfigPath(wxt.config.outBaseDir)}"]
|
||||
}`,
|
||||
);
|
||||
}`;
|
||||
|
||||
return {
|
||||
path: 'tsconfig.json',
|
||||
text,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import { isModuleInstalled } from '../package';
|
||||
import fs from 'fs-extra';
|
||||
import { normalizePath } from '../paths';
|
||||
import glob from 'fast-glob';
|
||||
import { builtinModules } from '~/builtin-modules';
|
||||
|
||||
/**
|
||||
* Given an inline config, discover the config file if necessary, merge the results, resolve any
|
||||
@@ -127,15 +128,21 @@ export async function resolveConfig(
|
||||
};
|
||||
}
|
||||
|
||||
const modules = await resolveWxtModules(modulesDir, mergedConfig.modules);
|
||||
const moduleOptions = modules.reduce<Record<string, any>>((map, module) => {
|
||||
if (module.configKey) {
|
||||
map[module.configKey] =
|
||||
// @ts-expect-error
|
||||
mergedConfig[module.configKey];
|
||||
}
|
||||
return map;
|
||||
}, {});
|
||||
const userModules = await resolveWxtUserModules(
|
||||
modulesDir,
|
||||
mergedConfig.modules,
|
||||
);
|
||||
const moduleOptions = userModules.reduce<Record<string, any>>(
|
||||
(map, module) => {
|
||||
if (module.configKey) {
|
||||
map[module.configKey] =
|
||||
// @ts-expect-error
|
||||
mergedConfig[module.configKey];
|
||||
}
|
||||
return map;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
return {
|
||||
browser,
|
||||
@@ -146,7 +153,7 @@ export async function resolveConfig(
|
||||
filterEntrypoints,
|
||||
env,
|
||||
fsCache: createFsCache(wxtDir),
|
||||
imports: await getUnimportOptions(wxtDir, logger, mergedConfig),
|
||||
imports: await getUnimportOptions(wxtDir, srcDir, logger, mergedConfig),
|
||||
logger,
|
||||
manifest: await resolveManifestConfig(env, mergedConfig.manifest),
|
||||
manifestVersion,
|
||||
@@ -167,7 +174,7 @@ export async function resolveConfig(
|
||||
alias,
|
||||
experimental: defu(mergedConfig.experimental, {
|
||||
includeBrowserPolyfill: true,
|
||||
viteRuntime: false,
|
||||
entrypointImporter: 'jiti' as const,
|
||||
}),
|
||||
dev: {
|
||||
server: devServerConfig,
|
||||
@@ -175,7 +182,8 @@ export async function resolveConfig(
|
||||
},
|
||||
hooks: mergedConfig.hooks ?? {},
|
||||
vite: mergedConfig.vite ?? (() => ({})),
|
||||
modules,
|
||||
builtinModules,
|
||||
userModules,
|
||||
plugins: [],
|
||||
...moduleOptions,
|
||||
};
|
||||
@@ -285,20 +293,21 @@ function resolveAnalysisConfig(
|
||||
|
||||
async function getUnimportOptions(
|
||||
wxtDir: string,
|
||||
srcDir: string,
|
||||
logger: Logger,
|
||||
config: InlineConfig,
|
||||
): Promise<WxtResolvedUnimportOptions | false> {
|
||||
if (config.imports === false) return false;
|
||||
|
||||
const enabledConfig = config.imports?.eslintrc?.enabled;
|
||||
let enabled: boolean;
|
||||
switch (enabledConfig) {
|
||||
const rawEslintEnabled = config.imports?.eslintrc?.enabled;
|
||||
let eslintEnabled: boolean;
|
||||
switch (rawEslintEnabled) {
|
||||
case undefined:
|
||||
case 'auto':
|
||||
enabled = await isModuleInstalled('eslint');
|
||||
eslintEnabled = await isModuleInstalled('eslint');
|
||||
break;
|
||||
default:
|
||||
enabled = enabledConfig;
|
||||
eslintEnabled = rawEslintEnabled;
|
||||
}
|
||||
|
||||
const defaultOptions: WxtResolvedUnimportOptions = {
|
||||
@@ -315,8 +324,11 @@ async function getUnimportOptions(
|
||||
],
|
||||
warn: logger.warn,
|
||||
dirs: ['components', 'composables', 'hooks', 'utils'],
|
||||
dirsScanOptions: {
|
||||
cwd: srcDir,
|
||||
},
|
||||
eslintrc: {
|
||||
enabled,
|
||||
enabled: eslintEnabled,
|
||||
filePath: path.resolve(wxtDir, 'eslintrc-auto-import.json'),
|
||||
globalsPropValue: true,
|
||||
},
|
||||
@@ -378,7 +390,7 @@ export async function mergeBuilderConfig(
|
||||
throw Error('Builder not found. Make sure vite is installed.');
|
||||
}
|
||||
|
||||
export async function resolveWxtModules(
|
||||
export async function resolveWxtUserModules(
|
||||
modulesDir: string,
|
||||
modules: string[] = [],
|
||||
): Promise<WxtModuleWithMetadata<any>[]> {
|
||||
|
||||
@@ -239,7 +239,8 @@ export const fakeResolvedConfig = fakeObjectCreator<ResolvedConfig>(() => {
|
||||
command,
|
||||
entrypointsDir: fakeDir(),
|
||||
modulesDir: fakeDir(),
|
||||
modules: [],
|
||||
builtinModules: [],
|
||||
userModules: [],
|
||||
env: { browser, command, manifestVersion, mode },
|
||||
fsCache: mock<FsCache>(),
|
||||
imports: {
|
||||
@@ -297,7 +298,7 @@ export const fakeResolvedConfig = fakeObjectCreator<ResolvedConfig>(() => {
|
||||
alias: {},
|
||||
experimental: {
|
||||
includeBrowserPolyfill: true,
|
||||
viteRuntime: false,
|
||||
entrypointImporter: 'jiti',
|
||||
},
|
||||
dev: {
|
||||
reloadCommand: 'Alt+R',
|
||||
|
||||
@@ -5,11 +5,13 @@ import {
|
||||
WxtCommand,
|
||||
WxtDevServer,
|
||||
WxtHooks,
|
||||
WxtModule,
|
||||
} from '~/types';
|
||||
import { resolveConfig } from './utils/building';
|
||||
import { createHooks } from 'hookable';
|
||||
import { createWxtPackageManager } from './package-managers';
|
||||
import { createViteBuilder } from './builders/vite';
|
||||
import { builtinModules } from '~/builtin-modules';
|
||||
|
||||
/**
|
||||
* Global variable set once `createWxt` is called once. Since this variable is used everywhere, this
|
||||
@@ -46,18 +48,16 @@ export async function registerWxt(
|
||||
};
|
||||
|
||||
// Initialize modules
|
||||
for (const module of config.modules) {
|
||||
const initModule = async (module: WxtModule<any>) => {
|
||||
if (module.hooks) wxt.hooks.addHooks(module.hooks);
|
||||
if (wxt.config.imports !== false && module.imports) {
|
||||
wxt.config.imports.imports ??= [];
|
||||
wxt.config.imports.imports.push(...module.imports);
|
||||
}
|
||||
await module.setup?.(
|
||||
wxt,
|
||||
// @ts-expect-error: Untyped configKey field
|
||||
module.configKey ? config[module.configKey] : undefined,
|
||||
);
|
||||
}
|
||||
};
|
||||
for (const builtinModule of builtinModules) await initModule(builtinModule);
|
||||
for (const userModule of config.userModules) await initModule(userModule);
|
||||
|
||||
// Initialize hooks
|
||||
wxt.hooks.addHooks(config.hooks);
|
||||
|
||||
@@ -104,7 +104,8 @@ async function zipDir(
|
||||
// Ignore node_modules, otherwise this glob step takes forever
|
||||
ignore: ['**/node_modules'],
|
||||
onlyFiles: true,
|
||||
dot: true,
|
||||
// TODO: Fix #738
|
||||
// dot: true,
|
||||
})
|
||||
).filter((relativePath) => {
|
||||
return (
|
||||
|
||||
@@ -104,13 +104,11 @@ export function addViteConfig(
|
||||
): void {
|
||||
wxt.hooks.hook('ready', (wxt) => {
|
||||
const userVite = wxt.config.vite;
|
||||
wxt.config.vite = (env) =>
|
||||
vite.mergeConfig(
|
||||
// Use config added by module as base
|
||||
viteConfig(env) ?? {},
|
||||
// Overwrite module config with user config
|
||||
userVite(env),
|
||||
);
|
||||
wxt.config.vite = async (env) => {
|
||||
const fromUser = await userVite(env);
|
||||
const fromModule = viteConfig(env) ?? {};
|
||||
return vite.mergeConfig(fromModule, fromUser);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { logger } from './utils/logger';
|
||||
|
||||
interface WebSocketMessage {
|
||||
type: string;
|
||||
event: string;
|
||||
data?: any;
|
||||
}
|
||||
|
||||
export interface WxtWebSocket extends WebSocket {
|
||||
addWxtEventListener(
|
||||
type: 'wxt:reload-extension',
|
||||
callback: (event: CustomEvent<undefined>) => void,
|
||||
options?: AddEventListenerOptions | boolean,
|
||||
): void;
|
||||
addWxtEventListener(
|
||||
type: 'wxt:reload-content-script',
|
||||
callback?: (event: CustomEvent<ReloadContentScriptPayload>) => void,
|
||||
options?: AddEventListenerOptions | boolean,
|
||||
): void;
|
||||
addWxtEventListener(
|
||||
type: 'wxt:reload-page',
|
||||
callback?: (event: CustomEvent<string>) => void,
|
||||
options?: AddEventListenerOptions | boolean,
|
||||
): void;
|
||||
sendCustom(event: string, payload?: any): void;
|
||||
}
|
||||
|
||||
let ws: WxtWebSocket | undefined;
|
||||
|
||||
/**
|
||||
* Connect to the websocket and listen for messages.
|
||||
*
|
||||
* @param onMessage Optional callback that is called when a message is recieved and we've verified
|
||||
* it's structure is what we expect.
|
||||
*/
|
||||
export function getDevServerWebSocket(): WxtWebSocket {
|
||||
if (import.meta.env.COMMAND !== 'serve')
|
||||
throw Error(
|
||||
'Must be running WXT dev command to connect to call getDevServerWebSocket()',
|
||||
);
|
||||
|
||||
if (ws == null) {
|
||||
const serverUrl = `${__DEV_SERVER_PROTOCOL__}//${__DEV_SERVER_HOSTNAME__}:${__DEV_SERVER_PORT__}`;
|
||||
logger.debug('Connecting to dev server @', serverUrl);
|
||||
|
||||
ws = new WebSocket(serverUrl, 'vite-hmr') as WxtWebSocket;
|
||||
ws.addWxtEventListener = ws.addEventListener.bind(ws);
|
||||
ws.sendCustom = (event, payload) =>
|
||||
ws?.send(JSON.stringify({ type: 'custom', event, payload }));
|
||||
|
||||
ws.addEventListener('open', () => {
|
||||
logger.debug('Connected to dev server');
|
||||
});
|
||||
ws.addEventListener('close', () => {
|
||||
logger.debug('Disconnected from dev server');
|
||||
});
|
||||
ws.addEventListener('error', (event) => {
|
||||
logger.error('Failed to connect to dev server', event);
|
||||
});
|
||||
|
||||
ws.addEventListener('message', (e) => {
|
||||
try {
|
||||
const message = JSON.parse(e.data) as WebSocketMessage;
|
||||
if (message.type === 'custom') {
|
||||
ws?.dispatchEvent(
|
||||
new CustomEvent(message.event, { detail: message.data }),
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('Failed to handle message', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return ws;
|
||||
}
|
||||
|
||||
export interface ReloadContentScriptPayload {
|
||||
registration?: 'manifest' | 'runtime';
|
||||
contentScript: {
|
||||
matches: string[];
|
||||
js?: string[];
|
||||
css?: string[];
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import type * as vite from 'vite';
|
||||
import {
|
||||
unimport,
|
||||
download,
|
||||
tsconfigPaths,
|
||||
globals,
|
||||
@@ -8,6 +7,8 @@ import {
|
||||
} from '../core/builders/vite/plugins';
|
||||
import { resolveConfig } from '~/core/utils/building';
|
||||
import { InlineConfig } from '../types';
|
||||
import { vitePlugin as unimportPlugin } from '~/builtin-modules/unimport';
|
||||
import { createUnimport } from 'unimport';
|
||||
|
||||
/**
|
||||
* Vite plugin that configures Vitest with everything required to test a WXT extension, based on the `<root>/wxt.config.ts`
|
||||
@@ -25,11 +26,18 @@ import { InlineConfig } from '../types';
|
||||
* @param inlineConfig Customize WXT's config for testing. Any config specified here overrides the config from your `wxt.config.ts` file.
|
||||
*/
|
||||
export function WxtVitest(inlineConfig?: InlineConfig): vite.PluginOption {
|
||||
return resolveConfig(inlineConfig ?? {}, 'serve').then((config) => [
|
||||
webextensionPolyfillMock(config),
|
||||
unimport(config),
|
||||
globals(config),
|
||||
download(config),
|
||||
tsconfigPaths(config),
|
||||
]);
|
||||
return resolveConfig(inlineConfig ?? {}, 'serve').then(async (config) => {
|
||||
const plugins = [
|
||||
webextensionPolyfillMock(config),
|
||||
globals(config),
|
||||
download(config),
|
||||
tsconfigPaths(config),
|
||||
];
|
||||
if (config.imports !== false) {
|
||||
const unimport = createUnimport(config.imports);
|
||||
await unimport.init();
|
||||
plugins.push(unimportPlugin(unimport));
|
||||
}
|
||||
return plugins;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -317,14 +317,17 @@ export interface InlineConfig {
|
||||
*/
|
||||
includeBrowserPolyfill?: boolean;
|
||||
/**
|
||||
* When set to `true`, use the Vite Runtime API to load entrypoint options instead of the default, `jiti`.
|
||||
* Method used to import entrypoint files during the build process to extract their options.
|
||||
*
|
||||
* Lets you use imported variables and leverage your Vite config to add support for non-standard APIs/syntax.
|
||||
* - "jiti": Simplest and fastest, but doesn't allow using any imported variables outside the entrypoint's main function
|
||||
* - "vite-runtime" (unstable): Uses Vite 5.3's new runtime API to import the entrypoints. Automatically includes vite config based on your wxt.config.ts file
|
||||
* - "vite-node" (unstable): Uses `vite-node` to import the entrypoints. Automatically includes vite config based on your wxt.config.ts file
|
||||
*
|
||||
* @experimental Early access to try out the feature before it becomes the default.
|
||||
* @default false
|
||||
* @see {@link https://wxt.dev/guide/go-further/entrypoint-side-effects.html|Entrypoint Side-effect Docs}
|
||||
*
|
||||
* @default "jiti"
|
||||
*/
|
||||
viteRuntime?: boolean;
|
||||
entrypointImporter?: 'jiti' | 'vite-runtime' | 'vite-node';
|
||||
};
|
||||
/**
|
||||
* Config effecting dev mode only.
|
||||
@@ -1031,6 +1034,23 @@ export interface WxtHooks {
|
||||
* @returns Promise
|
||||
*/
|
||||
ready: (wxt: Wxt) => HookResult;
|
||||
/**
|
||||
* Called before WXT writes .wxt/tsconfig.json and .wxt/wxt.d.ts, allowing
|
||||
* addition of custom references and declarations in wxt.d.ts, or directly
|
||||
* modifying the options in `tsconfig.json`.
|
||||
*
|
||||
* @example
|
||||
* wxt.hooks.hook("prepare:types", (wxt, entries) => {
|
||||
* // Add a file, ".wxt/types/example.d.ts", that defines a global
|
||||
* // variable called "example" in the TS project.
|
||||
* entries.push({
|
||||
* path: "types/example.d.ts",
|
||||
* textContent: "declare const a: string;",
|
||||
* tsReference: true,
|
||||
* });
|
||||
* })
|
||||
*/
|
||||
'prepare:types': (wxt: Wxt, entries: WxtDirEntry[]) => HookResult;
|
||||
/**
|
||||
* Called before the build is started in both dev mode and build mode.
|
||||
*
|
||||
@@ -1174,7 +1194,7 @@ export interface ResolvedConfig {
|
||||
alias: Record<string, string>;
|
||||
experimental: {
|
||||
includeBrowserPolyfill: boolean;
|
||||
viteRuntime: boolean;
|
||||
entrypointImporter: 'jiti' | 'vite-runtime' | 'vite-node';
|
||||
};
|
||||
dev: {
|
||||
/** Only defined during dev command */
|
||||
@@ -1185,7 +1205,8 @@ export interface ResolvedConfig {
|
||||
reloadCommand: string | false;
|
||||
};
|
||||
hooks: NestedHooks<WxtHooks>;
|
||||
modules: WxtModuleWithMetadata<any>[];
|
||||
builtinModules: WxtModule<any>[];
|
||||
userModules: WxtModuleWithMetadata<any>[];
|
||||
/**
|
||||
* An array of string to import plugins from. These paths should be
|
||||
* resolvable by vite, and they should `export default defineWxtPlugin(...)`.
|
||||
@@ -1331,13 +1352,9 @@ export interface WxtModuleWithMetadata<TOptions extends WxtModuleOptions>
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface ResolvedPublicFile {
|
||||
/**
|
||||
* The absolute path to the file that will be copied to the output directory.
|
||||
* @example
|
||||
* "/path/to/any/file.css"
|
||||
*/
|
||||
absoluteSrc: string;
|
||||
export type ResolvedPublicFile = CopiedPublicFile | GeneratedPublicFile;
|
||||
|
||||
export interface ResolvedBasePublicFile {
|
||||
/**
|
||||
* The relative path in the output directory to copy the file to.
|
||||
* @example
|
||||
@@ -1346,4 +1363,47 @@ export interface ResolvedPublicFile {
|
||||
relativeDest: string;
|
||||
}
|
||||
|
||||
export interface CopiedPublicFile extends ResolvedBasePublicFile {
|
||||
/**
|
||||
* The absolute path to the file that will be copied to the output directory.
|
||||
* @example
|
||||
* "/path/to/any/file.css"
|
||||
*/
|
||||
absoluteSrc: string;
|
||||
}
|
||||
|
||||
export interface GeneratedPublicFile extends ResolvedBasePublicFile {
|
||||
/**
|
||||
* Text to write to the file.
|
||||
*/
|
||||
contents: string;
|
||||
}
|
||||
|
||||
export type WxtPlugin = () => void;
|
||||
|
||||
export type WxtDirEntry = WxtDirTypeReferenceEntry | WxtDirFileEntry;
|
||||
|
||||
/**
|
||||
* Represents type reference to a node module to be added to `.wxt/wxt.d.ts` file
|
||||
*/
|
||||
export interface WxtDirTypeReferenceEntry {
|
||||
module: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a file to be written to the project's `.wxt/` directory.
|
||||
*/
|
||||
export interface WxtDirFileEntry {
|
||||
/**
|
||||
* Path relative to the `.wxt/` directory. So "tsconfig.json" would resolve to ".wxt/tsconfig.json".
|
||||
*/
|
||||
path: string;
|
||||
/**
|
||||
* The text that will be written to the file.
|
||||
*/
|
||||
text: string;
|
||||
/**
|
||||
* Set to `true` to add a reference to this file in `.wxt/wxt.d.ts`.
|
||||
*/
|
||||
tsReference?: boolean;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import definition from 'virtual:user-background-entrypoint';
|
||||
import { initPlugins } from 'virtual:wxt-plugins';
|
||||
import { setupWebSocket } from './utils/setup-web-socket';
|
||||
import { getDevServerWebSocket } from '../sandbox/dev-server-websocket';
|
||||
import { logger } from '../sandbox/utils/logger';
|
||||
import { browser } from 'wxt/browser';
|
||||
import { keepServiceWorkerAlive } from './utils/keep-service-worker-alive';
|
||||
@@ -8,18 +8,19 @@ import { reloadContentScript } from './utils/reload-content-scripts';
|
||||
|
||||
if (import.meta.env.COMMAND === 'serve') {
|
||||
try {
|
||||
const ws = setupWebSocket((message) => {
|
||||
if (message.event === 'wxt:reload-extension') browser.runtime.reload();
|
||||
if (message.event === 'wxt:reload-content-script' && message.data != null)
|
||||
reloadContentScript(message.data);
|
||||
const ws = getDevServerWebSocket();
|
||||
ws.addWxtEventListener('wxt:reload-extension', () => {
|
||||
browser.runtime.reload();
|
||||
});
|
||||
ws.addWxtEventListener('wxt:reload-content-script', (event) => {
|
||||
reloadContentScript(event.detail);
|
||||
});
|
||||
|
||||
if (import.meta.env.MANIFEST_VERSION === 3) {
|
||||
// Tell the server the background script is loaded and ready to go
|
||||
ws.addEventListener('open', () => {
|
||||
const msg = { type: 'custom', event: 'wxt:background-initialized' };
|
||||
ws.send(JSON.stringify(msg));
|
||||
});
|
||||
ws.addEventListener('open', () =>
|
||||
ws.sendCustom('wxt:background-initialized'),
|
||||
);
|
||||
|
||||
// Web Socket will disconnect if the service worker is killed
|
||||
keepServiceWorkerAlive();
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
import { logger } from '../sandbox/utils/logger';
|
||||
import { setupWebSocket } from './utils/setup-web-socket';
|
||||
import { getDevServerWebSocket } from '../sandbox/dev-server-websocket';
|
||||
|
||||
if (import.meta.env.COMMAND === 'serve') {
|
||||
try {
|
||||
setupWebSocket((message) => {
|
||||
if (message.event === 'wxt:reload-page') {
|
||||
// We need to remove the initial slash from the path to compare correctly
|
||||
// "popup.html" === "/popup.html".substring(1)
|
||||
if (message.data === location.pathname.substring(1)) {
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
const ws = getDevServerWebSocket();
|
||||
ws.addWxtEventListener('wxt:reload-page', (event) => {
|
||||
// "popup.html" === "/popup.html".substring(1)
|
||||
if (event.detail === location.pathname.substring(1)) location.reload();
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error('Failed to setup web socket connection with dev server', err);
|
||||
|
||||
@@ -1,16 +1,7 @@
|
||||
import { browser } from 'wxt/browser';
|
||||
import { logger } from '../../sandbox/utils/logger';
|
||||
import { MatchPattern } from 'wxt/sandbox';
|
||||
|
||||
interface ContentScript {
|
||||
matches: string[];
|
||||
js?: string[];
|
||||
css?: string[];
|
||||
}
|
||||
interface ReloadContentScriptPayload {
|
||||
registration?: 'manifest' | 'runtime';
|
||||
contentScript: ContentScript;
|
||||
}
|
||||
import type { ReloadContentScriptPayload } from '../../sandbox/dev-server-websocket';
|
||||
|
||||
export function reloadContentScript(payload: ReloadContentScriptPayload) {
|
||||
const manifest = browser.runtime.getManifest();
|
||||
@@ -32,6 +23,8 @@ export async function reloadContentScriptMv3({
|
||||
}
|
||||
}
|
||||
|
||||
type ContentScript = ReloadContentScriptPayload['contentScript'];
|
||||
|
||||
export async function reloadManifestContentScriptMv3(
|
||||
contentScript: ContentScript,
|
||||
) {
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
import { logger } from '../../sandbox/utils/logger';
|
||||
|
||||
interface WebSocketMessage {
|
||||
type: string;
|
||||
event: string;
|
||||
data?: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to the websocket and listen for messages.
|
||||
*
|
||||
* @param onMessage Optional callback that is called when a message is recieved and we've verified
|
||||
* it's structure is what we expect.
|
||||
*/
|
||||
export function setupWebSocket(
|
||||
onMessage?: (message: WebSocketMessage) => void,
|
||||
) {
|
||||
const serverUrl = `${__DEV_SERVER_PROTOCOL__}//${__DEV_SERVER_HOSTNAME__}:${__DEV_SERVER_PORT__}`;
|
||||
logger.debug('Connecting to dev server @', serverUrl);
|
||||
const ws = new WebSocket(serverUrl, 'vite-hmr');
|
||||
|
||||
ws.addEventListener('open', () => {
|
||||
logger.debug('Connected to dev server');
|
||||
});
|
||||
ws.addEventListener('close', () => {
|
||||
logger.debug('Disconnected from dev server');
|
||||
});
|
||||
ws.addEventListener('error', (event) => {
|
||||
logger.error('Failed to connect to dev server', event);
|
||||
});
|
||||
|
||||
ws.addEventListener('message', (e) => {
|
||||
try {
|
||||
const message = JSON.parse(e.data) as WebSocketMessage;
|
||||
if (message.type === 'custom' && message.event?.startsWith?.('wxt:')) {
|
||||
onMessage?.(message);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('Failed to handle message', err);
|
||||
}
|
||||
});
|
||||
|
||||
return ws;
|
||||
}
|
||||
Generated
+26
-3
@@ -8,6 +8,9 @@ importers:
|
||||
|
||||
.:
|
||||
devDependencies:
|
||||
'@aklinker1/buildc':
|
||||
specifier: ^1.0.7
|
||||
version: 1.0.7(typescript@5.4.5)
|
||||
'@aklinker1/check':
|
||||
specifier: ^1.3.1
|
||||
version: 1.3.1(typescript@5.4.5)
|
||||
@@ -288,6 +291,9 @@ importers:
|
||||
vite:
|
||||
specifier: ^5.3.0
|
||||
version: 5.3.0(@types/node@20.14.2)
|
||||
vite-node:
|
||||
specifier: ^1.6.0
|
||||
version: 1.6.0(@types/node@20.14.2)
|
||||
web-ext-run:
|
||||
specifier: ^0.2.0
|
||||
version: 0.2.0
|
||||
@@ -374,6 +380,23 @@ importers:
|
||||
|
||||
packages:
|
||||
|
||||
/@aklinker1/buildc@1.0.7(typescript@5.4.5):
|
||||
resolution: {integrity: sha512-8njwYe/uxeeyqfj1ryOOwWolNaoJpVjFMoR8muNezo4D6IL6NBNN2XNhqaCShlDBjHM5NG7mdsd/5rueqsKReg==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
typescript: ^5.0.0
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
consola: 3.2.3
|
||||
dependency-graph: 1.0.0
|
||||
fast-glob: 3.3.2
|
||||
fs-extra: 11.2.0
|
||||
ohash: 1.1.3
|
||||
pathe: 1.1.2
|
||||
typescript: 5.4.5
|
||||
yaml: 2.4.5
|
||||
dev: true
|
||||
|
||||
/@aklinker1/check@1.3.1(typescript@5.4.5):
|
||||
resolution: {integrity: sha512-EALKJzoojytjC6cYLIjRjC+nNpVdqSWYjJm8FTUtWneemKDs+Q46PaMyEx2EYTRbj4fO0OXj9igABwbpp/vV4g==}
|
||||
hasBin: true
|
||||
@@ -721,6 +744,7 @@ packages:
|
||||
/@babel/highlight@7.24.7:
|
||||
resolution: {integrity: sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
requiresBuild: true
|
||||
dependencies:
|
||||
'@babel/helper-validator-identifier': 7.24.7
|
||||
chalk: 2.4.2
|
||||
@@ -7008,7 +7032,7 @@ packages:
|
||||
spdx-expression-parse: 3.0.1
|
||||
dev: true
|
||||
|
||||
/vite-node@1.6.0:
|
||||
/vite-node@1.6.0(@types/node@20.14.2):
|
||||
resolution: {integrity: sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
hasBin: true
|
||||
@@ -7027,7 +7051,6 @@ packages:
|
||||
- sugarss
|
||||
- supports-color
|
||||
- terser
|
||||
dev: true
|
||||
|
||||
/vite-plugin-solid@2.10.2(solid-js@1.8.17)(vite@5.3.0):
|
||||
resolution: {integrity: sha512-AOEtwMe2baBSXMXdo+BUwECC8IFHcKS6WQV/1NEd+Q7vHPap5fmIhLcAzr+DUJ04/KHx/1UBU0l1/GWP+rMAPQ==}
|
||||
@@ -7215,7 +7238,7 @@ packages:
|
||||
tinybench: 2.5.1
|
||||
tinypool: 0.8.4
|
||||
vite: 5.3.0(@types/node@20.14.2)
|
||||
vite-node: 1.6.0
|
||||
vite-node: 1.6.0(@types/node@20.14.2)
|
||||
why-is-node-running: 2.2.2
|
||||
transitivePeerDependencies:
|
||||
- less
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
//
|
||||
// Build all workspace packages, or only dependencies of a specific package.
|
||||
// Kind of a simple version of nx/turborepo, but we can keep using PNPM to run
|
||||
// all commands.
|
||||
//
|
||||
// Usage:
|
||||
// pnpm tsx scripts/build-deps.ts # Builds all packages in repo
|
||||
// pnpm tsx scripts/build-deps.ts <packageName> # Builds only dependencies of <packageName>
|
||||
//
|
||||
|
||||
import glob from 'fast-glob';
|
||||
import { DepGraph } from 'dependency-graph';
|
||||
import fs from 'fs-extra';
|
||||
import YAML from 'yaml';
|
||||
import { execa } from 'execa';
|
||||
import consola from 'consola';
|
||||
import { hashFile, hash } from 'hasha';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
// Quick hack to prevent recursive calls to this script. If it's being called
|
||||
// from another instance of this script, the parent instance is already
|
||||
// preforming this build, so it can be skipped.
|
||||
if (process.env.BUILD_DEPS) process.exit(0);
|
||||
process.env.BUILD_DEPS = 'true';
|
||||
|
||||
// Parse args
|
||||
const packageToBuild = process.argv[2];
|
||||
if (packageToBuild == null) {
|
||||
consola.info('Building all packages...');
|
||||
} else {
|
||||
consola.info(`Building dependencies of \`${packageToBuild}\``);
|
||||
}
|
||||
|
||||
// Build graph
|
||||
const workspace: { packages: string[] } = YAML.parse(
|
||||
await fs.readFile('pnpm-workspace.yaml', 'utf8'),
|
||||
);
|
||||
const packageDirs = await glob(workspace.packages, { onlyDirectories: true });
|
||||
|
||||
const graph = new DepGraph<Record<string, any>>();
|
||||
// Add all packages to chart
|
||||
for (const packageDir of packageDirs) {
|
||||
try {
|
||||
const packageJson = await fs.readJson(`${packageDir}/package.json`);
|
||||
graph.addNode(packageJson.name, { ...packageJson, dir: packageDir });
|
||||
} catch {
|
||||
// Package missing package.json, ignore it
|
||||
}
|
||||
}
|
||||
// Add dependencies between packages
|
||||
graph.entryNodes().forEach((packageName) => {
|
||||
const packageJson = graph.getNodeData(packageName);
|
||||
[
|
||||
...Object.entries<string>(packageJson.dependencies ?? {}),
|
||||
...Object.entries<string>(packageJson.devDependencies ?? {}),
|
||||
].forEach(([dep, version]) => {
|
||||
if (version !== 'workspace:*') return;
|
||||
graph.addDependency(packageName, dep);
|
||||
});
|
||||
});
|
||||
function printGraph(graph: DepGraph<any>) {
|
||||
consola.debug('Dependency Graph:');
|
||||
function printNode(node: string, level = 0) {
|
||||
consola.debug(`${''.padStart(level * 2, ' ')}- \`${node}\``);
|
||||
graph.dependenciesOf(node).forEach((dep) => printNode(dep, level + 1));
|
||||
}
|
||||
graph.entryNodes().forEach((entry) => printNode(entry));
|
||||
}
|
||||
printGraph(graph);
|
||||
if (packageToBuild) {
|
||||
// Remove nodes not associated with the package to build
|
||||
graph.entryNodes().forEach((entry) => {
|
||||
if (entry !== packageToBuild) graph.removeNode(entry);
|
||||
});
|
||||
}
|
||||
|
||||
// Get order
|
||||
const buildOrder = graph.overallOrder().filter(
|
||||
// Remove packageToBuild if provided
|
||||
(packageName) => packageName !== packageToBuild,
|
||||
);
|
||||
consola.info('Build order:', buildOrder);
|
||||
|
||||
// Build dependencies of a package
|
||||
async function hashDir(dir: string): Promise<string> {
|
||||
const files = await glob('**/*', {
|
||||
ignore: [
|
||||
'**/dist/**',
|
||||
'**/node_modules/**',
|
||||
'**/__tests__/**',
|
||||
'**/e2e/**',
|
||||
'CHANGELOG.md',
|
||||
'typedoc.json',
|
||||
'vitest.*.ts',
|
||||
],
|
||||
cwd: dir,
|
||||
});
|
||||
const hashes = await Promise.all(
|
||||
files.sort().map(async (file) => {
|
||||
const hash = await hashFile(resolve(dir, file), { algorithm: 'md5' });
|
||||
return `${hash}-${file}`;
|
||||
}),
|
||||
);
|
||||
consola.debug(hashes.join('\n'));
|
||||
return await hash(hashes.join('\n'), { algorithm: 'md5' });
|
||||
}
|
||||
async function buildPackage(packageName: string): Promise<void> {
|
||||
const packageJson = graph.getNodeData(packageName);
|
||||
const hash = await hashDir(packageJson.dir);
|
||||
consola.debug('Directory hash:', hash);
|
||||
const cacheDir = resolve('.cache', packageJson.dir, hash);
|
||||
const outputDir = resolve(packageJson.dir, 'dist');
|
||||
if (await fs.pathExists(cacheDir)) {
|
||||
await fs.ensureDir(outputDir);
|
||||
await fs.copy(cacheDir, outputDir);
|
||||
consola.success(`\`${packageName}\` cached`);
|
||||
} else {
|
||||
await execa('pnpm', ['--filter', packageName, 'build'], {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
await fs.ensureDir(cacheDir);
|
||||
await fs.copy(outputDir, cacheDir);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
for (const packageName of buildOrder) {
|
||||
await buildPackage(packageName);
|
||||
}
|
||||
} catch (err) {
|
||||
consola.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -30,6 +30,6 @@ await createGithubRelease(config, {
|
||||
name: `${pkgName} v${currentVersion}`,
|
||||
body: releases[0].body,
|
||||
// @ts-expect-error: Not typed in changelogen, but present: https://docs.github.com/en/rest/releases/releases?apiVersion=2022-11-28#create-a-release
|
||||
make_latest: pkg === 'wxt',
|
||||
make_latest: String(pkg === 'wxt'),
|
||||
});
|
||||
consola.success('Created release');
|
||||
|
||||
Reference in New Issue
Block a user