Compare commits

..

21 Commits

Author SHA1 Message Date
GitHub Actions 0110a2bd4e chore(release): v0.1.0 2023-07-10 23:24:09 +00:00
Aaron Klinker afe838a348 feat!: Initial release 2023-07-10 18:21:01 -05:00
Aaron Klinker 0255028ce2 docs: Update feature list 2023-07-10 18:03:27 -05:00
Aaron 249aa5eccc feat: Content scripts reloading (#25) 2023-07-10 17:47:52 -05:00
Aaron Klinker b7c078f4e4 ci: Create github release 2023-07-03 17:43:10 -05:00
GitHub Actions f6b6b8857d chore(release): v0.0.2 2023-07-03 02:54:39 +00:00
Aaron Klinker 54eb118f8c chore: Update cli screenshot 2023-07-02 21:50:42 -05:00
Aaron 75eb3de89e chore: Rename templates to virtual-modules (#24) 2023-07-02 21:49:01 -05:00
Aaron Klinker e14f81d97a docs: Fix milestone link in README 2023-07-02 21:38:40 -05:00
Aaron cd714ea196 feat: Reload HTML files individually (#23) 2023-07-02 21:37:28 -05:00
Aaron Klinker 684197dbe3 docs: Add milestone progress badge to README 2023-07-02 21:36:38 -05:00
Aaron Klinker c51f0e055f fix: Show Vite warnings and errors 2023-07-02 20:40:34 -05:00
Aaron Klinker 7f3a1e8d91 fix: Separate template builds to prevent sharing chunks 2023-07-02 20:40:13 -05:00
Aaron Klinker 37986bf230 fix: Overwrite files with the same name when renaming entrypoints in dev mode 2023-07-02 20:11:21 -05:00
Aaron 13a713cc28 feat: Setup background script web socket/reload (#22) 2023-07-02 20:00:18 -05:00
Aaron Klinker a78aada96e chore: Refactor build outputs to support transpiled templates 2023-07-02 17:05:23 -05:00
Aaron 3307fc42a0 feat: Reload extension when source code is changed (#17) 2023-07-02 16:04:13 -05:00
Aaron 8be53efd24 chore: Refactor build output type (#19) 2023-07-01 13:58:44 -05:00
Aaron f02abc5227 fix: Remove hash from content script css outputs (#20) 2023-07-01 13:54:18 -05:00
Aaron Klinker 2dd7a99937 fix: Output chunks to a chunks directory 2023-06-28 16:35:11 -05:00
Aaron Klinker a9af4d3b67 chore: Update changelog 2023-06-26 10:14:17 -05:00
45 changed files with 1829 additions and 341 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 280 KiB

After

Width:  |  Height:  |  Size: 150 KiB

+1 -1
View File
@@ -47,4 +47,4 @@ jobs:
pnpm publish
- name: GitHub Release
run: pnpx changelogen@latest gh release
run: pnpx changelogen@latest gh release --token ${{ secrets.GITHUB_TOKEN }}
+62 -1
View File
@@ -1,5 +1,67 @@
# Changelog
## v0.1.0
[compare changes](https://github.com/aklinker1/wxt/compare/v0.0.2...v0.1.0)
### 🚀 Enhancements
- Content scripts reloading ([#25](https://github.com/aklinker1/wxt/pull/25))
- ⚠️ Initial release ([afe838a](https://github.com/aklinker1/wxt/commit/afe838a))
### 📖 Documentation
- Update feature list ([0255028](https://github.com/aklinker1/wxt/commit/0255028))
### 🤖 CI
- Create github release ([b7c078f](https://github.com/aklinker1/wxt/commit/b7c078f))
#### ⚠️ Breaking Changes
- ⚠️ Initial release ([afe838a](https://github.com/aklinker1/wxt/commit/afe838a))
### ❤️ Contributors
- Aaron Klinker
- Aaron
## v0.0.2
[compare changes](https://github.com/aklinker1/wxt/compare/v0.0.1...v0.0.2)
### 🚀 Enhancements
- Reload extension when source code is changed ([#17](https://github.com/aklinker1/wxt/pull/17))
- Setup background script web socket/reload ([#22](https://github.com/aklinker1/wxt/pull/22))
- Reload HTML files individually ([#23](https://github.com/aklinker1/wxt/pull/23))
### 🩹 Fixes
- Output chunks to a chunks directory ([2dd7a99](https://github.com/aklinker1/wxt/commit/2dd7a99))
- Remove hash from content script css outputs ([#20](https://github.com/aklinker1/wxt/pull/20))
- Overwrite files with the same name when renaming entrypoints in dev mode ([37986bf](https://github.com/aklinker1/wxt/commit/37986bf))
- Separate template builds to prevent sharing chunks ([7f3a1e8](https://github.com/aklinker1/wxt/commit/7f3a1e8))
- Show Vite warnings and errors ([c51f0e0](https://github.com/aklinker1/wxt/commit/c51f0e0))
### 📖 Documentation
- Add milestone progress badge to README ([684197d](https://github.com/aklinker1/wxt/commit/684197d))
- Fix milestone link in README ([e14f81d](https://github.com/aklinker1/wxt/commit/e14f81d))
### 🏡 Chore
- Update changelog ([a9af4d3](https://github.com/aklinker1/wxt/commit/a9af4d3))
- Refactor build output type ([#19](https://github.com/aklinker1/wxt/pull/19))
- Refactor build outputs to support transpiled templates ([a78aada](https://github.com/aklinker1/wxt/commit/a78aada))
- Rename `templates` to `virtual-modules` ([#24](https://github.com/aklinker1/wxt/pull/24))
- Update cli screenshot ([54eb118](https://github.com/aklinker1/wxt/commit/54eb118))
### ❤️ Contributors
- Aaron Klinker
- Aaron
## v0.0.1
[compare changes](https://github.com/aklinker1/wxt/compare/v0.0.0...v0.0.1)
@@ -73,4 +135,3 @@
### ❤️ Contributors
- Aaron Klinker
- Aaron
+5 -4
View File
@@ -6,6 +6,8 @@
> **Warning**
>
> [![GitHub milestone](https://img.shields.io/github/milestones/progress-percent/aklinker1/wxt/1)](https://github.com/aklinker1/wxt/milestone/1)
>
> WXT is not ready for use yet. Production builds work, but dev mode is incomplete.
>
> See the [Initial Release Candidate](https://github.com/aklinker1/wxt/milestone/1) milestone for more details.
@@ -15,18 +17,17 @@
- 🌐 Supports all browsers
- ✅ Supports both MV2 and MV3
- 📂 Directory based entrypoints
- ⚡ Dev mode with HMR & auto-reload
- 🚔 TypeScript
- 🦾 Auto-imports
- ⚡ Dev mode with HMR
- ⬇️ Download and bundle remote URL imports
- 🎨 Frontend framework agnostic: works with Vue, React, Svelte, etc
### Todo
- 🤖 Automated publishing
- 🎨 Frontend framework agnostic: works with Vue, React, Svelte, etc
- 🖍️ Quickly bootstrap a new project
- 📏 Bundle analysis
- ⚡ Dev mode with HMR and **_auto-reload_**
- 🤖 Automated publishing
## Get Started
@@ -4,7 +4,7 @@ export default defineContentScript({
matches: ['*://*/*'],
async main() {
console.log(browser.runtime.id);
logId();
mountContentScriptUi();
},
});
+3 -6
View File
@@ -191,21 +191,18 @@ describe('Manifest Content', () => {
expect(manifest.content_scripts).toContainEqual({
matches: ['*://duckduckgo.com/*'],
run_at: 'document_end',
css: [expect.stringContaining('assets/four-')],
css: ['assets/four.css'],
js: ['content-scripts/four.js'],
});
expect(manifest.content_scripts).toContainEqual({
matches: ['*://google.com/*'],
run_at: 'document_end',
css: [
expect.stringContaining('assets/three-'),
expect.stringContaining('assets/two-'),
],
css: ['assets/three.css', 'assets/two.css'],
js: ['content-scripts/three.js', 'content-scripts/two.js'],
});
expect(manifest.content_scripts).toContainEqual({
matches: ['*://google.com/*'],
css: [expect.stringContaining('assets/one-')],
css: ['assets/one.css'],
js: ['content-scripts/one.js'],
});
});
+5 -9
View File
@@ -44,33 +44,29 @@ describe('Output Directory Structure', () => {
await project.build();
expect(await project.serializeOutput()).toMatchInlineSnapshot(`
".output/chrome-mv3/assets/one-dedc7a05.css
".output/chrome-mv3/assets/one.css
----------------------------------------
body{color:#00f}
================================================================================
.output/chrome-mv3/assets/two-74d94aed.css
.output/chrome-mv3/assets/two.css
----------------------------------------
body{color:red}
================================================================================
.output/chrome-mv3/content-scripts/one.js
----------------------------------------
(function(){\\"use strict\\";function c(n){return n}const e=\\"\\",t={matches:[\\"*://*/*\\"],main:()=>{}};(async()=>{try{await t.main()}catch(n){console.error(\`The content script crashed on startup!
\`,n)}})()})();
(function(){\\"use strict\\";function i(n){return n}const s=\\"\\",o={matches:[\\"*://*/*\\"],main:()=>{}};function t(n,...e){if(typeof e[0]==\\"string\\"){const c=e.shift();n(\`[wxt] \${c}\`,...e)}else n(\\"[wxt]\\",...e)}var r={debug:(...n)=>t(console.debug,...n),log:(...n)=>t(console.log,...n),warn:(...n)=>t(console.warn,...n),error:(...n)=>t(console.error,...n)};(async()=>{try{await o.main()}catch(n){r.error(\\"The content script crashed on startup!\\",n)}})()})();
================================================================================
.output/chrome-mv3/content-scripts/two.js
----------------------------------------
(function(){\\"use strict\\";function c(n){return n}const e=\\"\\",t={matches:[\\"*://*/*\\"],main:()=>{}};(async()=>{try{await t.main()}catch(n){console.error(\`The content script crashed on startup!
\`,n)}})()})();
(function(){\\"use strict\\";function i(n){return n}const s=\\"\\",o={matches:[\\"*://*/*\\"],main:()=>{}};function t(n,...e){if(typeof e[0]==\\"string\\"){const c=e.shift();n(\`[wxt] \${c}\`,...e)}else n(\\"[wxt]\\",...e)}var r={debug:(...n)=>t(console.debug,...n),log:(...n)=>t(console.log,...n),warn:(...n)=>t(console.warn,...n),error:(...n)=>t(console.error,...n)};(async()=>{try{await o.main()}catch(n){r.error(\\"The content script crashed on startup!\\",n)}})()})();
================================================================================
.output/chrome-mv3/manifest.json
----------------------------------------
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"content_scripts\\":[{\\"matches\\":[\\"*://*/*\\"],\\"css\\":[\\"assets/one-dedc7a05.css\\",\\"assets/two-74d94aed.css\\"],\\"js\\":[\\"content-scripts/one.js\\",\\"content-scripts/two.js\\"]}]}"
{\\"manifest_version\\":3,\\"name\\":\\"E2E Extension\\",\\"version\\":\\"0.0.0\\",\\"version_name\\":\\"0.0.0-test\\",\\"content_scripts\\":[{\\"matches\\":[\\"*://*/*\\"],\\"css\\":[\\"assets/one.css\\",\\"assets/two.css\\"],\\"js\\":[\\"content-scripts/one.js\\",\\"content-scripts/two.js\\"]}]}"
`);
});
});
File diff suppressed because one or more lines are too long
+14 -12
View File
@@ -1,7 +1,7 @@
{
"name": "wxt",
"type": "module",
"version": "0.0.1",
"version": "0.1.0",
"description": "Next gen framework for developing web extensions",
"repository": {
"type": "git",
@@ -22,10 +22,9 @@
},
"license": "MIT",
"files": [
"dist",
"templates"
"dist"
],
"bin": "dist/cli/index.cjs",
"bin": "dist/cli.cjs",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -36,17 +35,14 @@
"types": "./dist/index.d.ts"
},
"./client": {
"require": "./dist/client/index.cjs",
"import": "./dist/client/index.js",
"types": "./dist/client/index.d.ts"
"require": "./dist/client.cjs",
"import": "./dist/client.js",
"types": "./dist/client.d.ts"
}
},
"scripts": {
"wxt": "tsx src/cli/index.ts",
"build": "run-s -s build:*",
"build:js": "tsup src/index.ts --sourcemap --dts --format esm,cjs",
"build:cli": "tsup src/cli/index.ts -d dist/cli --sourcemap",
"build:client": "tsup src/client/index.ts -d dist/client --sourcemap --dts --format esm,cjs",
"build": "tsx scripts/build.ts",
"format": "prettier --write .",
"format:check": "prettier --write .",
"compile": "tsc --noEmit",
@@ -56,6 +52,8 @@
"prepublish": "pnpm -s build"
},
"dependencies": {
"@webext-core/match-patterns": "^1.0.1",
"async-mutex": "^0.4.0",
"c12": "^1.4.2",
"cac": "^6.7.14",
"consola": "^3.1.0",
@@ -73,19 +71,23 @@
"webextension-polyfill": "^0.10.0"
},
"devDependencies": {
"@faker-js/faker": "^8.0.2",
"@types/fs-extra": "^11.0.1",
"@types/lodash.merge": "^4.6.7",
"@types/node": "^20.3.1",
"@types/picomatch": "^2.3.0",
"@types/webextension-polyfill": "^0.10.0",
"@vitest/coverage-v8": "^0.32.2",
"lodash.merge": "^4.6.2",
"npm-run-all": "^4.1.5",
"ora": "^6.3.1",
"prettier": "^2.8.8",
"pretty-quick": "^3.1.3",
"simple-git-hooks": "^2.8.1",
"tsup": "^7.0.0",
"tsx": "^3.12.7",
"typescript": "^5.1.3",
"vitest": "^0.32.2",
"vitest": "^0.32.4",
"webextension-polyfill": "^0.10.0"
},
"peerDependencies": {
+235 -127
View File
@@ -3,11 +3,15 @@ lockfileVersion: 5.4
importers:
.:
specifiers:
'@faker-js/faker': ^8.0.2
'@types/fs-extra': ^11.0.1
'@types/lodash.merge': ^4.6.7
'@types/node': ^20.3.1
'@types/picomatch': ^2.3.0
'@types/webextension-polyfill': ^0.10.0
'@vitest/coverage-v8': ^0.32.2
'@webext-core/match-patterns': ^1.0.1
async-mutex: ^0.4.0
c12: ^1.4.2
cac: ^6.7.14
consola: ^3.1.0
@@ -17,7 +21,9 @@ importers:
jiti: ^1.18.2
json5: ^2.2.3
linkedom: ^0.14.26
lodash.merge: ^4.6.2
npm-run-all: ^4.1.5
ora: ^6.3.1
picocolors: ^1.0.0
picomatch: ^2.3.1
prettier: ^2.8.8
@@ -28,10 +34,12 @@ importers:
typescript: ^5.1.3
unimport: ^3.0.8
vite: ^4.3.9
vitest: ^0.32.2
vitest: ^0.32.4
web-ext: ^7.6.2
webextension-polyfill: ^0.10.0
dependencies:
'@webext-core/match-patterns': 1.0.1
async-mutex: 0.4.0
c12: 1.4.2
cac: 6.7.14
consola: 3.1.0
@@ -48,19 +56,23 @@ importers:
web-ext: 7.6.2
webextension-polyfill: 0.10.0
devDependencies:
'@faker-js/faker': 8.0.2
'@types/fs-extra': 11.0.1
'@types/lodash.merge': 4.6.7
'@types/node': 20.3.1
'@types/picomatch': 2.3.0
'@types/webextension-polyfill': 0.10.0
'@vitest/coverage-v8': 0.32.2_vitest@0.32.2
'@vitest/coverage-v8': 0.32.2_vitest@0.32.4
lodash.merge: 4.6.2
npm-run-all: 4.1.5
ora: 6.3.1
prettier: 2.8.8
pretty-quick: 3.1.3_prettier@2.8.8
simple-git-hooks: 2.8.1
tsup: 7.0.0_typescript@5.1.3
tsx: 3.12.7
typescript: 5.1.3
vitest: 0.32.2
vitest: 0.32.4
demo:
specifiers:
@@ -752,6 +764,14 @@ packages:
engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 }
dev: false
/@faker-js/faker/8.0.2:
resolution:
{
integrity: sha512-Uo3pGspElQW91PCvKSIAXoEgAUlRnH29sX2/p89kg7sP1m2PzCufHINd0FhTXQf6DYGiUlVncdSPa2F9wxed2A==,
}
engines: { node: ^14.17.0 || ^16.13.0 || >=18.0.0, npm: '>=6.14.13' }
dev: true
/@fluent/syntax/0.19.0:
resolution:
{
@@ -797,6 +817,16 @@ packages:
engines: { node: '>=8' }
dev: true
/@jest/schemas/29.6.0:
resolution:
{
integrity: sha512-rxLjXyJBTL4LQeJW3aKo0M/+GkCOXsO+8i9Iu7eDb6KwtP65ayoDsitrdPBtujxQ88k4wI2FNYfa6TOGwSn6cQ==,
}
engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 }
dependencies:
'@sinclair/typebox': 0.27.8
dev: true
/@jridgewell/gen-mapping/0.3.3:
resolution:
{
@@ -929,6 +959,13 @@ packages:
picomatch: 2.3.1
dev: false
/@sinclair/typebox/0.27.8:
resolution:
{
integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==,
}
dev: true
/@sindresorhus/is/5.4.1:
resolution:
{
@@ -1003,6 +1040,22 @@ packages:
'@types/node': 20.3.1
dev: true
/@types/lodash.merge/4.6.7:
resolution:
{
integrity: sha512-OwxUJ9E50gw3LnAefSHJPHaBLGEKmQBQ7CZe/xflHkyy/wH2zVyEIAKReHvVrrn7zKdF58p16We9kMfh7v0RRQ==,
}
dependencies:
'@types/lodash': 4.14.195
dev: true
/@types/lodash/4.14.195:
resolution:
{
integrity: sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg==,
}
dev: true
/@types/minimatch/3.0.5:
resolution:
{
@@ -1038,7 +1091,7 @@ packages:
'@types/node': 20.3.1
dev: false
/@vitest/coverage-v8/0.32.2_vitest@0.32.2:
/@vitest/coverage-v8/0.32.2_vitest@0.32.4:
resolution:
{
integrity: sha512-/+V3nB3fyeuuSeKxCfi6XmWjDIxpky7AWSkGVfaMjAk7di8igBwRsThLjultwIZdTDH1RAxpjmCXEfSqsMFZOA==,
@@ -1057,65 +1110,80 @@ packages:
std-env: 3.3.3
test-exclude: 6.0.0
v8-to-istanbul: 9.1.0
vitest: 0.32.2
vitest: 0.32.4
transitivePeerDependencies:
- supports-color
dev: true
/@vitest/expect/0.32.2:
/@vitest/expect/0.32.4:
resolution:
{
integrity: sha512-6q5yzweLnyEv5Zz1fqK5u5E83LU+gOMVBDuxBl2d2Jfx1BAp5M+rZgc5mlyqdnxquyoiOXpXmFNkcGcfFnFH3Q==,
integrity: sha512-m7EPUqmGIwIeoU763N+ivkFjTzbaBn0n9evsTOcde03ugy2avPs3kZbYmw3DkcH1j5mxhMhdamJkLQ6dM1bk/A==,
}
dependencies:
'@vitest/spy': 0.32.2
'@vitest/utils': 0.32.2
'@vitest/spy': 0.32.4
'@vitest/utils': 0.32.4
chai: 4.3.7
dev: true
/@vitest/runner/0.32.2:
/@vitest/runner/0.32.4:
resolution:
{
integrity: sha512-06vEL0C1pomOEktGoLjzZw+1Fb+7RBRhmw/06WkDrd1akkT9i12su0ku+R/0QM69dfkIL/rAIDTG+CSuQVDcKw==,
integrity: sha512-cHOVCkiRazobgdKLnczmz2oaKK9GJOw6ZyRcaPdssO1ej+wzHVIkWiCiNacb3TTYPdzMddYkCgMjZ4r8C0JFCw==,
}
dependencies:
'@vitest/utils': 0.32.2
concordance: 5.0.4
'@vitest/utils': 0.32.4
p-limit: 4.0.0
pathe: 1.1.1
dev: true
/@vitest/snapshot/0.32.2:
/@vitest/snapshot/0.32.4:
resolution:
{
integrity: sha512-JwhpeH/PPc7GJX38vEfCy9LtRzf9F4er7i4OsAJyV7sjPwjj+AIR8cUgpMTWK4S3TiamzopcTyLsZDMuldoi5A==,
integrity: sha512-IRpyqn9t14uqsFlVI2d7DFMImGMs1Q9218of40bdQQgMePwVdmix33yMNnebXcTzDU5eiV3eUsoxxH5v0x/IQA==,
}
dependencies:
magic-string: 0.30.0
pathe: 1.1.1
pretty-format: 27.5.1
pretty-format: 29.6.0
dev: true
/@vitest/spy/0.32.2:
/@vitest/spy/0.32.4:
resolution:
{
integrity: sha512-Q/ZNILJ4ca/VzQbRM8ur3Si5Sardsh1HofatG9wsJY1RfEaw0XKP8IVax2lI1qnrk9YPuG9LA2LkZ0EI/3d4ug==,
integrity: sha512-oA7rCOqVOOpE6rEoXuCOADX7Lla1LIa4hljI2MSccbpec54q+oifhziZIJXxlE/CvI2E+ElhBHzVu0VEvJGQKQ==,
}
dependencies:
tinyspy: 2.1.1
dev: true
/@vitest/utils/0.32.2:
/@vitest/utils/0.32.4:
resolution:
{
integrity: sha512-lnJ0T5i03j0IJaeW73hxe2AuVnZ/y1BhhCOuIcl9LIzXnbpXJT9Lrt6brwKHXLOiA7MZ6N5hSJjt0xE1dGNCzQ==,
integrity: sha512-Gwnl8dhd1uJ+HXrYyV0eRqfmk9ek1ASE/LWfTCuWMw+d07ogHqp4hEAV28NiecimK6UY9DpSEPh+pXBA5gtTBg==,
}
dependencies:
diff-sequences: 29.4.3
loupe: 2.3.6
pretty-format: 27.5.1
pretty-format: 29.6.0
dev: true
/@webcomponents/webcomponentsjs/2.8.0:
resolution:
{
integrity: sha512-loGD63sacRzOzSJgQnB9ZAhaQGkN7wl2Zuw7tsphI5Isa0irijrRo6EnJii/GgjGefIFO8AIO7UivzRhFaEk9w==,
}
dev: false
/@webext-core/match-patterns/1.0.1:
resolution:
{
integrity: sha512-huti7vdxWQ10XlkmEO/nC7gVmzAcxNRj+Kdp+HrnRzh2PY9pv5BLrjL8QXzN5EdJjxP154yr7/Q7+Z6xe0TZDA==,
}
dependencies:
'@webcomponents/webcomponentsjs': 2.8.0
dev: false
/abort-controller/3.0.0:
resolution:
{
@@ -1296,6 +1364,7 @@ packages:
integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==,
}
engines: { node: '>=8' }
dev: false
/ansi-regex/6.0.1:
resolution:
@@ -1303,7 +1372,6 @@ packages:
integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==,
}
engines: { node: '>=12' }
dev: false
/ansi-styles/3.2.1:
resolution:
@@ -1436,6 +1504,15 @@ packages:
}
dev: true
/async-mutex/0.4.0:
resolution:
{
integrity: sha512-eJFZ1YhRR8UN8eBLoNzcDPcy/jqjsg6I1AP+KvWQX80BqOSW1oJPJXDylPUEeMr2ZQvHgnQ//Lp6f3RQ1zI7HA==,
}
dependencies:
tslib: 2.6.0
dev: false
/async/3.2.4:
resolution:
{
@@ -1499,7 +1576,6 @@ packages:
{
integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==,
}
dev: false
/bcrypt-pbkdf/1.0.2:
resolution:
@@ -1517,6 +1593,17 @@ packages:
}
engines: { node: '>=8' }
/bl/5.1.0:
resolution:
{
integrity: sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==,
}
dependencies:
buffer: 6.0.3
inherits: 2.0.4
readable-stream: 3.6.2
dev: true
/bluebird/3.7.2:
resolution:
{
@@ -1524,13 +1611,6 @@ packages:
}
dev: false
/blueimp-md5/2.19.0:
resolution:
{
integrity: sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==,
}
dev: true
/boolbase/1.0.0:
resolution:
{
@@ -1610,7 +1690,6 @@ packages:
dependencies:
base64-js: 1.5.1
ieee754: 1.2.1
dev: false
/bundle-require/4.0.1_esbuild@0.18.5:
resolution:
@@ -1779,7 +1858,6 @@ packages:
integrity: sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA==,
}
engines: { node: ^12.17.0 || ^14.13 || >=16.0.0 }
dev: false
/check-error/1.0.2:
resolution:
@@ -1875,6 +1953,24 @@ packages:
engines: { node: '>=10' }
dev: false
/cli-cursor/4.0.0:
resolution:
{
integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==,
}
engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 }
dependencies:
restore-cursor: 4.0.0
dev: true
/cli-spinners/2.9.0:
resolution:
{
integrity: sha512-4/aL9X3Wh0yiMQlE+eeRhWP6vclO3QRtw1JHKIT0FFUs5FjpFmESqtMvYZ0+lbzBw900b95mS0hohy+qn2VK/g==,
}
engines: { node: '>=6' }
dev: true
/cliui/8.0.1:
resolution:
{
@@ -1893,7 +1989,6 @@ packages:
integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==,
}
engines: { node: '>=0.8' }
dev: false
/color-convert/1.9.3:
resolution:
@@ -2005,23 +2100,6 @@ packages:
typedarray: 0.0.6
dev: false
/concordance/5.0.4:
resolution:
{
integrity: sha512-OAcsnTEYu1ARJqWVGwf4zh4JDfHZEaSNlNccFmt8YjB2l/n19/PF2viLINHc57vO4FKIAFl2FWASIGZZWZ2Kxw==,
}
engines: { node: '>=10.18.0 <11 || >=12.14.0 <13 || >=14' }
dependencies:
date-time: 3.1.0
esutils: 2.0.3
fast-diff: 1.3.0
js-string-escape: 1.0.1
lodash: 4.17.21
md5-hex: 3.0.1
semver: 7.5.2
well-known-symbols: 2.0.0
dev: true
/config-chain/1.1.13:
resolution:
{
@@ -2163,16 +2241,6 @@ packages:
engines: { node: '>= 12' }
dev: false
/date-time/3.1.0:
resolution:
{
integrity: sha512-uqCUKXE5q1PNBXjPqvwhwJf9SwMoAHBgWJ6DcrnS5o+W2JOiIILl0JEdVD8SGujrNS02GGxgwAg2PN2zONgtjg==,
}
engines: { node: '>=6' }
dependencies:
time-zone: 1.0.0
dev: true
/debounce/1.2.1:
resolution:
{
@@ -2275,7 +2343,6 @@ packages:
}
dependencies:
clone: 1.0.4
dev: false
/defer-to-connect/2.0.1:
resolution:
@@ -2837,6 +2904,7 @@ packages:
integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==,
}
engines: { node: '>=0.10.0' }
dev: false
/event-target-shim/5.0.1:
resolution:
@@ -2911,13 +2979,6 @@ packages:
}
dev: false
/fast-diff/1.3.0:
resolution:
{
integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==,
}
dev: true
/fast-glob/3.2.12:
resolution:
{
@@ -3690,7 +3751,6 @@ packages:
{
integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==,
}
dev: false
/ignore/5.2.4:
resolution:
@@ -3931,6 +3991,14 @@ packages:
is-path-inside: 3.0.3
dev: false
/is-interactive/2.0.0:
resolution:
{
integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==,
}
engines: { node: '>=12' }
dev: true
/is-mergeable-object/1.1.1:
resolution:
{
@@ -4063,6 +4131,14 @@ packages:
}
dev: false
/is-unicode-supported/1.3.0:
resolution:
{
integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==,
}
engines: { node: '>=12' }
dev: true
/is-utf8/0.2.1:
resolution:
{
@@ -4206,14 +4282,6 @@ packages:
}
dev: false
/js-string-escape/1.0.1:
resolution:
{
integrity: sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==,
}
engines: { node: '>= 0.8' }
dev: true
/js-tokens/4.0.0:
resolution:
{
@@ -4538,7 +4606,6 @@ packages:
{
integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==,
}
dev: false
/lodash.sortby/4.7.0:
resolution:
@@ -4552,6 +4619,18 @@ packages:
{
integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==,
}
dev: false
/log-symbols/5.1.0:
resolution:
{
integrity: sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==,
}
engines: { node: '>=12' }
dependencies:
chalk: 5.2.0
is-unicode-supported: 1.3.0
dev: true
/loupe/2.3.6:
resolution:
@@ -4578,6 +4657,7 @@ packages:
engines: { node: '>=10' }
dependencies:
yallist: 4.0.0
dev: false
/lru-cache/9.1.2:
resolution:
@@ -4630,16 +4710,6 @@ packages:
}
dev: false
/md5-hex/3.0.1:
resolution:
{
integrity: sha512-BUiRtTtV39LIJwinWBjqVsU9xhdnz7/i889V859IBFpuqGAj6LuOvHv5XLbgZ2R7ptJoJaEcxkv88/h25T7Ciw==,
}
engines: { node: '>=8' }
dependencies:
blueimp-md5: 2.19.0
dev: true
/mem/5.1.1:
resolution:
{
@@ -5157,6 +5227,24 @@ packages:
word-wrap: 1.2.3
dev: false
/ora/6.3.1:
resolution:
{
integrity: sha512-ERAyNnZOfqM+Ao3RAvIXkYh5joP220yf59gVe2X/cI6SiCxIdi4c9HZKZD8R6q/RDXEje1THBju6iExiSsgJaQ==,
}
engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 }
dependencies:
chalk: 5.2.0
cli-cursor: 4.0.0
cli-spinners: 2.9.0
is-interactive: 2.0.0
is-unicode-supported: 1.3.0
log-symbols: 5.1.0
stdin-discarder: 0.1.0
strip-ansi: 7.1.0
wcwidth: 1.0.1
dev: true
/os-locale/5.0.0:
resolution:
{
@@ -5575,16 +5663,16 @@ packages:
hasBin: true
dev: true
/pretty-format/27.5.1:
/pretty-format/29.6.0:
resolution:
{
integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==,
integrity: sha512-XH+D4n7Ey0iSR6PdAnBs99cWMZdGsdKrR33iUHQNr79w1szKTCIZDVdXuccAsHVwDBp0XeWPfNEoaxP9EZgRmQ==,
}
engines: { node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0 }
engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 }
dependencies:
ansi-regex: 5.0.1
'@jest/schemas': 29.6.0
ansi-styles: 5.2.0
react-is: 17.0.2
react-is: 18.2.0
dev: true
/pretty-quick/3.1.3_prettier@2.8.8:
@@ -5740,10 +5828,10 @@ packages:
flat: 5.0.2
dev: false
/react-is/17.0.2:
/react-is/18.2.0:
resolution:
{
integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==,
integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==,
}
dev: true
@@ -5774,6 +5862,18 @@ packages:
util-deprecate: 1.0.2
dev: false
/readable-stream/3.6.2:
resolution:
{
integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==,
}
engines: { node: '>= 6' }
dependencies:
inherits: 2.0.4
string_decoder: 1.1.1
util-deprecate: 1.0.2
dev: true
/readable-stream/4.4.0:
resolution:
{
@@ -5953,6 +6053,17 @@ packages:
lowercase-keys: 3.0.0
dev: false
/restore-cursor/4.0.0:
resolution:
{
integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==,
}
engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 }
dependencies:
onetime: 5.1.2
signal-exit: 3.0.7
dev: true
/reusify/1.0.4:
resolution:
{
@@ -6004,7 +6115,6 @@ packages:
{
integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==,
}
dev: false
/safe-buffer/5.2.1:
resolution:
@@ -6108,6 +6218,7 @@ packages:
hasBin: true
dependencies:
lru-cache: 6.0.0
dev: false
/setimmediate/1.0.5:
resolution:
@@ -6378,6 +6489,16 @@ packages:
}
dev: true
/stdin-discarder/0.1.0:
resolution:
{
integrity: sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ==,
}
engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 }
dependencies:
bl: 5.1.0
dev: true
/stream-to-array/2.3.0:
resolution:
{
@@ -6476,7 +6597,6 @@ packages:
}
dependencies:
safe-buffer: 5.1.2
dev: false
/strip-ansi/6.0.1:
resolution:
@@ -6496,7 +6616,6 @@ packages:
engines: { node: '>=12' }
dependencies:
ansi-regex: 6.0.1
dev: false
/strip-bom-buf/2.0.0:
resolution:
@@ -6684,14 +6803,6 @@ packages:
}
dev: false
/time-zone/1.0.0:
resolution:
{
integrity: sha512-TIsDdtKo6+XrPtiTm1ssmMngN1sAhyKnTO2kunQWqNPWIVvCm15Wmw4SWInwTVgJ5u/Tr04+8Ei9TNcw4x4ONA==,
}
engines: { node: '>=4' }
dev: true
/tinybench/2.5.0:
resolution:
{
@@ -6777,6 +6888,13 @@ packages:
}
dev: true
/tslib/2.6.0:
resolution:
{
integrity: sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==,
}
dev: false
/tsup/7.0.0_typescript@5.1.3:
resolution:
{
@@ -7052,7 +7170,6 @@ packages:
{
integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==,
}
dev: false
/uuid/3.4.0:
resolution:
@@ -7105,10 +7222,10 @@ packages:
extsprintf: 1.3.0
dev: false
/vite-node/0.32.2_@types+node@20.3.1:
/vite-node/0.32.4_@types+node@20.3.1:
resolution:
{
integrity: sha512-dTQ1DCLwl2aEseov7cfQ+kDMNJpM1ebpyMMMwWzBvLbis8Nla/6c9WQcqpPssTwS6Rp/+U6KwlIj8Eapw4bLdA==,
integrity: sha512-L2gIw+dCxO0LK14QnUMoqSYpa9XRGnTTTDjW2h19Mr+GR0EFj4vx52W41gFXfMLqpA00eK4ZjOVYo1Xk//LFEw==,
}
engines: { node: '>=v14.18.0' }
hasBin: true
@@ -7164,10 +7281,10 @@ packages:
optionalDependencies:
fsevents: 2.3.2
/vitest/0.32.2:
/vitest/0.32.4:
resolution:
{
integrity: sha512-hU8GNNuQfwuQmqTLfiKcqEhZY72Zxb7nnN07koCUNmntNxbKQnVbeIS6sqUgR3eXSlbOpit8+/gr1KpqoMgWCQ==,
integrity: sha512-3czFm8RnrsWwIzVDu/Ca48Y/M+qh3vOnF16czJm98Q/AN1y3B6PBsyV8Re91Ty5s7txKNjEhpgtGPcfdbh2MZg==,
}
engines: { node: '>=v14.18.0' }
hasBin: true
@@ -7201,16 +7318,15 @@ packages:
'@types/chai': 4.3.5
'@types/chai-subset': 1.3.3
'@types/node': 20.3.1
'@vitest/expect': 0.32.2
'@vitest/runner': 0.32.2
'@vitest/snapshot': 0.32.2
'@vitest/spy': 0.32.2
'@vitest/utils': 0.32.2
'@vitest/expect': 0.32.4
'@vitest/runner': 0.32.4
'@vitest/snapshot': 0.32.4
'@vitest/spy': 0.32.4
'@vitest/utils': 0.32.4
acorn: 8.9.0
acorn-walk: 8.2.0
cac: 6.7.14
chai: 4.3.7
concordance: 5.0.4
debug: 4.3.4
local-pkg: 0.4.3
magic-string: 0.30.0
@@ -7221,7 +7337,7 @@ packages:
tinybench: 2.5.0
tinypool: 0.5.0
vite: 4.3.9_@types+node@20.3.1
vite-node: 0.32.2_@types+node@20.3.1
vite-node: 0.32.4_@types+node@20.3.1
why-is-node-running: 2.2.2
transitivePeerDependencies:
- less
@@ -7250,7 +7366,6 @@ packages:
}
dependencies:
defaults: 1.0.4
dev: false
/web-ext/7.6.2:
resolution:
@@ -7338,14 +7453,6 @@ packages:
}
dev: false
/well-known-symbols/2.0.0:
resolution:
{
integrity: sha512-ZMjC3ho+KXo0BfJb7JgtQ5IBuvnShdlACNkKkdsqBmYw3bPAaJfPeYUo6tLUaT5tG/Gkh7xkpBhKRQ9e7pyg9Q==,
}
engines: { node: '>=6' }
dev: true
/whatwg-url/7.1.0:
resolution:
{
@@ -7558,6 +7665,7 @@ packages:
{
integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==,
}
dev: false
/yaml/2.3.1:
resolution:
+83
View File
@@ -0,0 +1,83 @@
import tsup from 'tsup';
import glob from 'fast-glob';
import pc from 'picocolors';
import path from 'path';
import { formatDuration } from '../src/core/utils/formatDuration';
import ora from 'ora';
import fs from 'fs-extra';
import { consola } from 'consola';
const spinner = ora('Building WXT').start();
const startTime = Date.now();
const outDir = 'dist';
const virtualEntrypoints = ['background', 'content-script'];
await fs.rm(outDir, { recursive: true, force: true });
await Promise.all([
tsup.build({
entry: {
index: 'src/index.ts',
},
format: ['cjs', 'esm'],
sourcemap: true,
dts: true,
silent: true,
}),
tsup.build({
entry: { cli: 'src/cli/index.ts' },
format: ['cjs'],
sourcemap: 'inline',
silent: true,
}),
tsup.build({
entry: { client: 'src/client/index.ts' },
format: ['esm'],
sourcemap: 'inline',
dts: true,
silent: true,
}),
...virtualEntrypoints.map((entryName) =>
tsup.build({
entry: {
[`virtual-modules/${entryName}-entrypoint`]: `src/client/virtual-modules/${entryName}-entrypoint.ts`,
},
format: ['esm'],
sourcemap: true,
silent: true,
external: [`virtual:user-${entryName}`],
}),
),
tsup.build({
entry: {
'virtual-modules/reload-html': `src/client/virtual-modules/reload-html.ts`,
},
format: ['esm'],
sourcemap: true,
silent: true,
}),
]).catch((err) => {
spinner.fail();
console.error(err);
process.exit(1);
});
spinner.succeed();
const duration = Date.now() - startTime;
const outFiles = await glob(`${outDir}/**`);
outFiles.forEach((file, i) => {
const color = file.endsWith('.map')
? pc.dim
: file.endsWith('.d.ts')
? pc.blue
: pc.cyan;
const prefix = i === outFiles.length - 1 ? ' └─' : ' ├─';
console.log(
`${pc.gray(prefix)} ${pc.dim(outDir + path.sep)}${color(
path.relative(outDir, file),
)}`,
);
});
consola.success(`Finished in ${formatDuration(duration)}`);
+1
View File
@@ -0,0 +1 @@
declare const __TEST_SEED__: number;
+1 -1
View File
@@ -23,7 +23,7 @@ export const dev = defineCommand<
};
const server = await wxt.createServer(cliConfig);
await server.listen(server.port);
await server.start();
return true;
});
@@ -0,0 +1,11 @@
import browser from 'webextension-polyfill';
/**
* https://developer.chrome.com/blog/longer-esw-lifetimes/
*/
export function keepServiceWorkerAlive() {
setInterval(async () => {
// Calling an async browser API resets the service worker's timeout
await browser.runtime.getPlatformInfo();
}, 5e3);
}
+18
View File
@@ -0,0 +1,18 @@
function print(method: (...args: any[]) => void, ...args: any[]) {
if (typeof args[0] === 'string') {
const message = args.shift();
method(`[wxt] ${message}`, ...args);
} else {
method('[wxt]', ...args);
}
}
/**
* Wrapper around `console` with a "[wxt]" prefix
*/
export const logger = {
debug: (...args: any[]) => print(console.debug, ...args),
log: (...args: any[]) => print(console.log, ...args),
warn: (...args: any[]) => print(console.warn, ...args),
error: (...args: any[]) => print(console.error, ...args),
};
+48
View File
@@ -0,0 +1,48 @@
import browser, { Manifest } from 'webextension-polyfill';
import { logger } from './logger';
import { MatchPattern } from '@webext-core/match-patterns';
export function reloadContentScript(contentScript: Manifest.ContentScript) {
const manifest = browser.runtime.getManifest();
if (manifest.manifest_version == 2) {
void reloadContentScriptMv2(contentScript);
} else {
void reloadContentScriptMv3(contentScript);
}
}
export async function reloadContentScriptMv3(
contentScript: Manifest.ContentScript,
) {
const id = `wxt:${contentScript.js![0]}`;
logger.log('Reloading content script:', contentScript);
const registered = await browser.scripting.getRegisteredContentScripts();
logger.debug('Existing scripts:', registered);
const existing = registered.find((cs) => cs.id === id);
if (existing) {
logger.debug('Updating content script', existing);
await browser.scripting.updateContentScripts([{ ...contentScript, id }]);
} else {
logger.debug('Registering new content script...');
await browser.scripting.registerContentScripts([{ ...contentScript, id }]);
}
const allTabs = await browser.tabs.query({});
const matchPatterns = contentScript.matches.map(
(match) => new MatchPattern(match),
);
const matchingTabs = allTabs.filter((tab) => {
const url = tab.url;
if (!url) return false;
return !!matchPatterns.find((pattern) => pattern.includes(url));
});
await Promise.all(matchingTabs.map((tab) => browser.tabs.reload(tab.id)));
}
export async function reloadContentScriptMv2(
contentScript: Manifest.ContentScript,
) {
throw Error('TODO: reloadContentScriptMv2');
}
+44
View File
@@ -0,0 +1,44 @@
import { logger } from './logger';
interface WebSocketMessage {
type: string;
event: string;
data?: any;
}
/**
* Connect to the websocket and listen for messages.
*
* @param onMessage Optional callback that is called when a message is recieved and we've verified
* it's structure is what we expect.
*/
export function setupWebSocket(
onMessage?: (message: WebSocketMessage) => void,
) {
const serverUrl = `${__DEV_SERVER_PROTOCOL__}//${__DEV_SERVER_HOSTNAME__}:${__DEV_SERVER_PORT__}`;
logger.debug('Connecting to dev server @', serverUrl);
const ws = new WebSocket(serverUrl, 'vite-hmr');
ws.addEventListener('open', () => {
logger.debug('Connected to dev server');
});
ws.addEventListener('close', () => {
logger.debug('Disconnected from dev server');
});
ws.addEventListener('error', (event) => {
logger.error('Failed to connect to dev server', event);
});
ws.addEventListener('message', (e) => {
try {
const message = JSON.parse(e.data) as WebSocketMessage;
if (message.type === 'custom' && message.event?.startsWith?.('wxt:')) {
onMessage?.(message);
}
} catch (err) {
logger.error('Failed to handle message', err);
}
});
return ws;
}
@@ -0,0 +1,42 @@
import definition from 'virtual:user-background';
import { setupWebSocket } from '../utils/setupWebSocket';
import { logger } from '../utils/logger';
import browser from 'webextension-polyfill';
import { keepServiceWorkerAlive } from '../utils/keepServiceWorkerAlive';
import { reloadContentScript } from '../utils/reloadContentScript';
if (__COMMAND__ === 'serve') {
try {
const ws = setupWebSocket((message) => {
if (message.event === 'wxt:reload-extension') browser.runtime.reload();
if (message.event === 'wxt:reload-content-script' && message.data != null)
reloadContentScript(message.data);
});
if (__MANIFEST_VERSION__ === 3) {
// Tell the server the background script is loaded and ready to go
ws.addEventListener('open', () => {
const msg = { type: 'custom', event: 'wxt:background-initialized' };
ws.send(JSON.stringify(msg));
});
// Web Socket will disconnect if the service worker is killed
keepServiceWorkerAlive();
}
} catch (err) {
logger.error('Failed to setup web socket connection with dev server', err);
}
}
try {
const res = definition.main();
// @ts-expect-error: res shouldn't be a promise, but we're checking it anyways
if (res instanceof Promise) {
console.warn(
"The background's main() function return a promise, but it must be synchonous",
);
}
} catch (err) {
logger.error('The background script crashed on startup!');
throw err;
}
@@ -0,0 +1,10 @@
import definition from 'virtual:user-content-script';
import { logger } from '../utils/logger';
(async () => {
try {
await definition.main();
} catch (err) {
logger.error('The content script crashed on startup!', err);
}
})();
+20
View File
@@ -0,0 +1,20 @@
/// <reference types="vite/client" />
import { logger } from '../utils/logger';
import { setupWebSocket } from '../utils/setupWebSocket';
if (__COMMAND__ === 'serve') {
try {
setupWebSocket((message) => {
if (message.event === 'wxt:reload-page') {
// We need to remove the initial slash from the path to compare correctly
// "popup.html" === "/popup.html".substring(1)
if (message.data === location.pathname.substring(1)) {
location.reload();
}
}
});
} catch (err) {
logger.error('Failed to setup web socket connection with dev server', err);
}
}
+23
View File
@@ -0,0 +1,23 @@
// Types required to make the virtual modules happy.
declare module '*?raw' {
const content: any;
export default content;
}
declare module 'virtual:user-background' {
const definition: import('../../').BackgroundScriptDefintition;
export default definition;
}
declare module 'virtual:user-content-script' {
const definition: import('../../').ContentScriptDefinition;
export default definition;
}
// Globals defined by the vite-plugins/devServerGlobals.ts and utils/globals.ts
declare const __COMMAND__: 'build' | 'serve';
declare const __DEV_SERVER_PROTOCOL__: string;
declare const __DEV_SERVER_HOSTNAME__: string;
declare const __DEV_SERVER_PORT__: string;
declare const __MANIFEST_VERSION__: 2 | 3;
+106
View File
@@ -0,0 +1,106 @@
import { Manifest } from 'webextension-polyfill';
import { BuildOutput } from './types';
import { buildEntrypoints } from './build/buildEntrypoints';
import { findEntrypoints } from './build/findEntrypoints';
import { generateTypesDir } from './build/generateTypesDir';
import { InternalConfig, EntrypointGroup } from './types';
import { generateMainfest, writeManifest } from './utils/manifest';
import pc from 'picocolors';
import * as vite from 'vite';
import fs from 'fs-extra';
import { groupEntrypoints } from './utils/groupEntrypoints';
import { formatDuration } from './utils/formatDuration';
import { printBuildSummary } from './log/printBuildSummary';
/**
* Builds the extension based on an internal config.
*
* This function:
* 1. Cleans the output directory
* 2. Executes the rebuild function with a blank previous output so everything is built (see
* `rebuild` for more details)
* 3. Prints the summary
*/
export async function buildInternal(
config: InternalConfig,
): Promise<BuildOutput> {
const verb = config.command === 'serve' ? 'Pre-rendering' : 'Building';
const target = `${config.browser}-mv${config.manifestVersion}`;
config.logger.info(
`${verb} ${pc.cyan(target)} for ${pc.cyan(config.mode)} with ${pc.green(
`Vite ${vite.version}`,
)}`,
);
const startTime = Date.now();
// Cleanup
await fs.rm(config.outDir, { recursive: true, force: true });
await fs.ensureDir(config.outDir);
const entrypoints = await findEntrypoints(config);
const groups = groupEntrypoints(entrypoints);
const { output } = await rebuild(config, groups);
// Post-build
config.logger.success(
`Built extension in ${formatDuration(Date.now() - startTime)}`,
);
await printBuildSummary(output, config);
return output;
}
/**
* Given a configuration, list of entrypoints, and an existing, partial output, build the
* entrypoints and merge the new output with the existing output.
*
* This function will:
* 1. Generate the .wxt directory's types
* 2. Build the `entrypointGroups` (and copies public files)
* 3. Generate the latest manifest for all entrypoints
* 4. Write the new manifest to the file system
*/
export async function rebuild(
config: InternalConfig,
entrypointGroups: EntrypointGroup[],
existingOutput: Omit<BuildOutput, 'manifest'> = {
steps: [],
publicAssets: [],
},
): Promise<{ output: BuildOutput; manifest: Manifest.WebExtensionManifest }> {
// Update types directory with new files and types
const allEntrypoints = await findEntrypoints(config);
await generateTypesDir(allEntrypoints, config);
// Build and merge the outputs
const newOutput = await buildEntrypoints(entrypointGroups, config);
const mergedOutput: Omit<BuildOutput, 'manifest'> = {
steps: [...existingOutput.steps, ...newOutput.steps],
publicAssets: [...existingOutput.publicAssets, ...newOutput.publicAssets],
};
const newManifest = await generateMainfest(
allEntrypoints,
mergedOutput,
config,
);
const finalOutput: BuildOutput = {
manifest: newManifest,
...newOutput,
};
// Write manifest
await writeManifest(newManifest, finalOutput, config);
return {
output: {
manifest: newManifest,
steps: [...existingOutput.steps, ...finalOutput.steps],
publicAssets: [
...existingOutput.publicAssets,
...finalOutput.publicAssets,
],
},
manifest: newManifest,
};
}
+43 -24
View File
@@ -1,6 +1,11 @@
import * as vite from 'vite';
import { BuildOutput, Entrypoint, InternalConfig } from '../types';
import { groupEntrypoints } from '../utils/groupEntrypoints';
import {
BuildOutput,
BuildStepOutput,
Entrypoint,
EntrypointGroup,
InternalConfig,
} from '../types';
import * as plugins from '../vite-plugins';
import { removeEmptyDirs } from '../utils/removeEmptyDirs';
import { getEntrypointBundlePath } from '../utils/entrypoints';
@@ -9,25 +14,22 @@ import fs from 'fs-extra';
import { dirname, resolve } from 'path';
export async function buildEntrypoints(
entrypoints: Entrypoint[],
groups: EntrypointGroup[],
config: InternalConfig,
): Promise<BuildOutput> {
const groups = groupEntrypoints(entrypoints);
const outputs: BuildOutput[] = [];
): Promise<Omit<BuildOutput, 'manifest'>> {
const steps: BuildStepOutput[] = [];
for (const group of groups) {
const output = Array.isArray(group)
const step = Array.isArray(group)
? await buildMultipleEntrypoints(group, config)
: await buildSingleEntrypoint(group, config);
outputs.push(output);
steps.push(step);
}
const publicOutput = await copyPublicDirectory(config);
outputs.push(publicOutput);
const publicAssets = await copyPublicDirectory(config);
// Remove any empty directories from moving outputs around
await removeEmptyDirs(config.outDir);
return outputs.flat();
return { publicAssets, steps };
}
/**
@@ -36,7 +38,7 @@ export async function buildEntrypoints(
async function buildSingleEntrypoint(
entrypoint: Entrypoint,
config: InternalConfig,
): Promise<BuildOutput> {
): Promise<BuildStepOutput> {
// Should this entrypoint be wrapped by the vite-plugins/virtualEntrypoint plugin?
const isVirtual = ['background', 'content-script'].includes(entrypoint.type);
const entry = isVirtual
@@ -53,6 +55,8 @@ async function buildSingleEntrypoint(
},
rollupOptions: {
output: {
// There's only a single output for this build, so we use the desired bundle path for the
// entry output (like "content-scripts/overlay.js")
entryFileNames: getEntrypointBundlePath(
entrypoint,
config.outDir,
@@ -61,7 +65,7 @@ async function buildSingleEntrypoint(
// Output content script CSS to assets/ with a hash to prevent conflicts. Defaults to
// "[name].[ext]" in lib mode, which usually results in "style.css". That means multiple
// content scripts with styles would overwrite each other if it weren't changed below.
assetFileNames: `assets/${entrypoint.name}-[hash].[ext]`,
assetFileNames: `assets/${entrypoint.name}.[ext]`,
},
},
},
@@ -72,7 +76,10 @@ async function buildSingleEntrypoint(
) as vite.InlineConfig;
const result = await vite.build(entryConfig);
return getBuildOutput(result);
return {
entrypoints: entrypoint,
chunks: getBuildOutputChunks(result),
};
}
/**
@@ -81,7 +88,7 @@ async function buildSingleEntrypoint(
async function buildMultipleEntrypoints(
entrypoints: Entrypoint[],
config: InternalConfig,
): Promise<BuildOutput> {
): Promise<BuildStepOutput> {
const multiPage: vite.InlineConfig = {
plugins: [plugins.multipageMove(entrypoints, config)],
build: {
@@ -90,6 +97,14 @@ async function buildMultipleEntrypoints(
input[entry.name] = entry.inputPath;
return input;
}, {}),
output: {
// Include a hash to prevent conflicts
chunkFileNames: 'chunks/[name]-[hash].js',
// Include a hash to prevent conflicts
entryFileNames: 'chunks/[name]-[hash].js',
// We can't control the "name", so we need a hash to prevent conflicts
assetFileNames: 'assets/[name]-[hash].[ext]',
},
},
},
};
@@ -100,12 +115,15 @@ async function buildMultipleEntrypoints(
) as vite.InlineConfig;
const result = await vite.build(entryConfig);
return getBuildOutput(result);
return {
entrypoints,
chunks: getBuildOutputChunks(result),
};
}
function getBuildOutput(
function getBuildOutputChunks(
result: Awaited<ReturnType<typeof vite.build>>,
): BuildOutput {
): BuildStepOutput['chunks'] {
if ('on' in result) throw Error('wxt does not support vite watch mode.');
if (Array.isArray(result)) return result.flatMap(({ output }) => output);
return result.output;
@@ -113,19 +131,19 @@ function getBuildOutput(
async function copyPublicDirectory(
config: InternalConfig,
): Promise<BuildOutput> {
if (!(await fs.exists(config.publicDir))) return [];
): Promise<BuildOutput['publicAssets']> {
const publicAssets: BuildOutput['publicAssets'] = [];
if (!(await fs.exists(config.publicDir))) return publicAssets;
const files = await glob('**/*', { cwd: config.publicDir });
const outputs: BuildOutput = [];
for (const file of files) {
const srcPath = resolve(config.publicDir, file);
const outPath = resolve(config.outDir, file);
await fs.ensureDir(dirname(outPath));
await fs.copyFile(srcPath, outPath);
outputs.push({
publicAssets.push({
type: 'asset',
fileName: file,
name: file,
@@ -133,5 +151,6 @@ async function copyPublicDirectory(
source: await fs.readFile(srcPath),
});
}
return outputs;
return publicAssets;
}
+4 -1
View File
@@ -9,7 +9,10 @@ export async function printBuildSummary(
output: BuildOutput,
config: InternalConfig,
) {
const chunks = output.sort((l, r) => {
const chunks = [
...output.steps.flatMap((step) => step.chunks),
...output.publicAssets,
].sort((l, r) => {
const lWeight =
CHUNK_SORT_WEIGHTS[l.fileName] ??
CHUNK_SORT_WEIGHTS[extname(l.fileName)] ??
+131
View File
@@ -0,0 +1,131 @@
import {
BuildStepOutput,
EntrypointGroup,
InternalConfig,
WxtDevServer,
} from './types';
import * as vite from 'vite';
import { findOpenPort } from './utils/findOpenPort';
import { Manifest } from 'webextension-polyfill';
import { getEntrypointBundlePath } from './utils/entrypoints';
import { getContentScriptCssFiles } from './utils/manifest';
import { createWebExtRunner } from './runners/createWebExtRunner';
import { buildInternal } from './build';
export async function getServerInfo(): Promise<ServerInfo> {
const port = await findOpenPort(3000, 3010);
const hostname = 'localhost';
const origin = `http://${hostname}:${port}`;
const serverConfig: vite.InlineConfig = {
server: {
origin,
},
};
return {
port,
hostname,
origin,
viteServerConfig: serverConfig,
};
}
export async function setupServer(
serverInfo: ServerInfo,
config: InternalConfig,
): Promise<WxtDevServer> {
const runner = createWebExtRunner();
const viteServer = await vite.createServer(
vite.mergeConfig(serverInfo, config.vite),
);
const start = async () => {
await viteServer.listen(server.port);
config.logger.success(`Started dev server @ ${serverInfo.origin}`);
server.currentOutput = await buildInternal(config);
config.logger.info('Opening browser...');
await runner.openBrowser(config);
config.logger.success('Opened!');
};
const reloadExtension = () => {
viteServer.ws.send('wxt:reload-extension');
};
const reloadPage = (path: string) => {
// Can't use Vite's built-in "full-reload" event because it doesn't like our paths, it expects
// paths ending in "/index.html"
viteServer.ws.send('wxt:reload-page', path);
};
const reloadContentScript = (contentScript: Manifest.ContentScript) => {
viteServer.ws.send('wxt:reload-content-script', contentScript);
};
const server: WxtDevServer = {
...viteServer,
start,
currentOutput: {
manifest: {
manifest_version: 3,
name: '',
version: '',
},
publicAssets: [],
steps: [],
},
port: serverInfo.port,
hostname: serverInfo.hostname,
origin: serverInfo.origin,
reloadExtension,
reloadPage,
reloadContentScript,
};
return server;
}
/**
* From the server, tell the client to reload content scripts from the provided build step outputs.
*/
export function reloadContentScripts(
steps: BuildStepOutput[],
config: InternalConfig,
server: WxtDevServer,
) {
if (config.manifestVersion === 3) {
steps.forEach((step) => {
const entry = step.entrypoints;
if (Array.isArray(entry) || entry.type !== 'content-script') return;
const js = [getEntrypointBundlePath(entry, config.outDir, '.js')];
const css = getContentScriptCssFiles([entry], server.currentOutput);
server.reloadContentScript({
js,
css,
...entry.options,
});
});
} else {
server.reloadExtension();
}
}
export function reloadHtmlPages(
groups: EntrypointGroup[],
server: WxtDevServer,
config: InternalConfig,
) {
groups.flat().forEach((entry) => {
const path = getEntrypointBundlePath(entry, config.outDir, '.html');
server.reloadPage(path);
});
}
interface ServerInfo {
port: number;
hostname: string;
origin: string;
viteServerConfig: vite.InlineConfig;
}
+51 -4
View File
@@ -1,6 +1,7 @@
import * as vite from 'vite';
import { Manifest } from 'webextension-polyfill';
import { UnimportOptions } from 'unimport';
import { EntrypointGroup } from '.';
export interface InlineConfig {
root?: string;
@@ -29,13 +30,60 @@ export interface WxtInlineViteConfig
build?: Omit<vite.BuildOptions, 'outDir'>;
}
export type BuildOutput = (vite.Rollup.OutputChunk | vite.Rollup.OutputAsset)[];
export interface BuildOutput {
manifest: Manifest.WebExtensionManifest;
publicAssets: vite.Rollup.OutputAsset[];
steps: BuildStepOutput[];
}
export interface BuildStepOutput {
entrypoints: EntrypointGroup;
chunks: (vite.Rollup.OutputChunk | vite.Rollup.OutputAsset)[];
}
export interface WxtDevServer extends vite.ViteDevServer {
logger: Logger;
/**
* Ex: `3000`
*/
port: number;
/**
* Ex: `"localhost"`
*/
hostname: string;
/**
* Ex: `"http://localhost:3000"`
*/
origin: string;
/**
* Stores the current build output of the server.
*/
currentOutput: BuildOutput;
/**
* Start the server on the first open port.
*/
start(): Promise<void>;
/**
* Tell the extension to reload by running `browser.runtime.reload`.
*/
reloadExtension: () => void;
/**
* Tell an extension page to reload.
*
* The path is the bundle path, not the input paths, so if the input paths is
* "src/options/index.html", you would pass "options.html" because that's where it is written to
* in the dist directory, and where it's available at in the actual extension.
*
* @example
* server.reloadPage("popup.html")
* server.reloadPage("sandbox.html")
*/
reloadPage: (path: string) => void;
/**
* Tell the extension to restart a content script.
*
* @param contentScript The manifest definition for a content script
*/
reloadContentScript: (contentScript: Manifest.ContentScript) => void;
}
export type TargetBrowser = 'chrome' | 'firefox' | 'safari' | 'edge' | 'opera';
@@ -91,7 +139,6 @@ export interface GenericEntrypoint extends BaseEntrypoint {
| 'sidepanel'
| 'devtools'
| 'unlisted-page'
| 'unlisted-page'
| 'unlisted-script';
}
@@ -144,7 +191,7 @@ export interface ContentScriptDefinition {
matchAboutBlank?: boolean;
matchOriginAsFallback?: boolean;
world?: 'ISOLATED' | 'MAIN';
main(onStopped: OnContentScriptStopped): void | Promise<void>;
main(): void | Promise<void>;
}
export interface BackgroundScriptDefintition {
+18
View File
@@ -0,0 +1,18 @@
import { describe, it, expect } from 'vitest';
import { every } from '../arrays';
describe('Array Utils', () => {
describe('every', () => {
it('should return true when the array is empty', () => {
expect(every([], () => false)).toBe(true);
});
it("should return true when all item predicate's return true", () => {
expect(every([1, 1, 1], (item) => item === 1)).toBe(true);
});
it("should return false when a single item predicate's return false", () => {
expect(every([1, 2, 1], (item) => item === 1)).toBe(false);
});
});
});
@@ -0,0 +1,251 @@
import { describe, expect, it } from 'vitest';
import { DevModeChange, detectDevChanges } from '../detectDevChanges';
import {
fakeBackgroundEntrypoint,
fakeContentScriptEntrypoint,
fakeFile,
fakeGenericEntrypoint,
fakeManifest,
fakeOptionsEntrypoint,
fakePopupEntrypoint,
fakeRollupOutputAsset,
fakeRollupOutputChunk,
} from '../../../testing/fake-objects';
import { BuildOutput, BuildStepOutput } from '../../types';
describe('Detect Dev Changes', () => {
describe('No changes', () => {
it("should return 'no-change' when a build hasn't finished", () => {
const actual = detectDevChanges(
[['unknown', '/path/to/file.ts']],
undefined,
);
expect(actual).toEqual({ type: 'no-change' });
});
it("should return 'no-change' when the changed file isn't used by any of the entrypoints", () => {
const change: [string, string] = ['unknown', '/some/path.ts'];
const currentOutput: BuildOutput = {
manifest: fakeManifest(),
publicAssets: [],
steps: [
{
entrypoints: fakeContentScriptEntrypoint(),
chunks: [fakeRollupOutputChunk(), fakeRollupOutputChunk()],
},
{
entrypoints: fakeContentScriptEntrypoint(),
chunks: [
fakeRollupOutputChunk(),
fakeRollupOutputChunk(),
fakeRollupOutputChunk(),
],
},
],
};
const actual = detectDevChanges([change], currentOutput);
expect(actual).toEqual({ type: 'no-change' });
});
});
describe('Public Assets', () => {
it("should return 'extension-reload' without any groups to rebuild when the changed file is a public asset", () => {
const change: [string, string] = [
'unknown',
'/root/src/public/image.svg',
];
const asset1 = fakeRollupOutputAsset({
fileName: 'image.svg',
});
const asset2 = fakeRollupOutputAsset({
fileName: 'some-other-image.svg',
});
const currentOutput: BuildOutput = {
manifest: fakeManifest(),
publicAssets: [asset1, asset2],
steps: [],
};
const expected: DevModeChange = {
type: 'extension-reload',
rebuildGroups: [],
cachedOutput: {
...currentOutput,
publicAssets: [asset2],
},
};
const actual = detectDevChanges([change], currentOutput);
expect(actual).toEqual(expected);
});
});
describe('Background', () => {
it("should rebuild the background and reload the extension when the changed file in it's chunks' `moduleIds` field", () => {
const changedPath = '/root/utils/shared.ts';
const contentScript = fakeContentScriptEntrypoint({
inputPath: '/root/overlay.content.ts',
});
const background = fakeBackgroundEntrypoint({
inputPath: '/root/background.ts',
});
const step1: BuildStepOutput = {
entrypoints: contentScript,
chunks: [
fakeRollupOutputChunk({
moduleIds: [fakeFile(), fakeFile()],
}),
],
};
const step2: BuildStepOutput = {
entrypoints: background,
chunks: [
fakeRollupOutputChunk({
moduleIds: [fakeFile(), changedPath, fakeFile()],
}),
],
};
const currentOutput: BuildOutput = {
manifest: fakeManifest(),
publicAssets: [],
steps: [step1, step2],
};
const expected: DevModeChange = {
type: 'extension-reload',
cachedOutput: {
...currentOutput,
steps: [step1],
},
rebuildGroups: [background],
};
const actual = detectDevChanges(
[['unknown', changedPath]],
currentOutput,
);
expect(actual).toEqual(expected);
});
});
describe('HTML Pages', () => {
it('should rebuild then reload only the effected pages', async () => {
const changedPath = '/root/page1/index.html';
const htmlPage1 = fakePopupEntrypoint({
inputPath: changedPath,
});
const htmlPage2 = fakeOptionsEntrypoint({
inputPath: '/root/page2.html',
});
const htmlPage3 = fakeGenericEntrypoint({
type: 'sandbox',
inputPath: '/root/page3.html',
});
const step1: BuildStepOutput = {
entrypoints: [htmlPage1, htmlPage2],
chunks: [
fakeRollupOutputChunk({
moduleIds: [fakeFile(), changedPath],
}),
],
};
const step2: BuildStepOutput = {
entrypoints: [htmlPage3],
chunks: [
fakeRollupOutputChunk({
moduleIds: [fakeFile(), fakeFile(), fakeFile()],
}),
],
};
const currentOutput: BuildOutput = {
manifest: fakeManifest(),
publicAssets: [],
steps: [step1, step2],
};
const expected: DevModeChange = {
type: 'html-reload',
cachedOutput: {
...currentOutput,
steps: [step2],
},
rebuildGroups: [[htmlPage1, htmlPage2]],
};
const actual = detectDevChanges(
[['unknown', changedPath]],
currentOutput,
);
expect(actual).toEqual(expected);
});
});
describe('Content Scripts', () => {
it('should rebuild then reload only the effected content scripts', async () => {
const changedPath = '/root/utils/shared.ts';
const script1 = fakeContentScriptEntrypoint({
inputPath: '/root/overlay1.content/index.ts',
});
const script2 = fakeContentScriptEntrypoint({
inputPath: '/root/overlay2.ts',
});
const script3 = fakeContentScriptEntrypoint({
inputPath: '/root/overlay3.content/index.ts',
});
const step1: BuildStepOutput = {
entrypoints: script1,
chunks: [
fakeRollupOutputChunk({
moduleIds: [fakeFile(), changedPath],
}),
],
};
const step2: BuildStepOutput = {
entrypoints: script2,
chunks: [
fakeRollupOutputChunk({
moduleIds: [fakeFile(), fakeFile(), fakeFile()],
}),
],
};
const step3: BuildStepOutput = {
entrypoints: script3,
chunks: [
fakeRollupOutputChunk({
moduleIds: [changedPath, fakeFile(), fakeFile()],
}),
],
};
const currentOutput: BuildOutput = {
manifest: fakeManifest(),
publicAssets: [],
steps: [step1, step2, step3],
};
const expected: DevModeChange = {
type: 'content-script-reload',
cachedOutput: {
...currentOutput,
steps: [step2],
},
changedSteps: [step1, step3],
rebuildGroups: [script1, script3],
};
const actual = detectDevChanges(
[['unknown', changedPath]],
currentOutput,
);
expect(actual).toEqual(expected);
});
});
});
+11
View File
@@ -0,0 +1,11 @@
/**
* Checks if `predicate` returns truthy for all elements of the array.
*/
export function every<T>(
array: T[],
predicate: (item: T, index: number) => boolean,
): boolean {
for (let i = 0; i < array.length; i++)
if (!predicate(array[i], i)) return false;
return true;
}
+177
View File
@@ -0,0 +1,177 @@
import { BuildOutput, BuildStepOutput, EntrypointGroup } from '../types';
import * as vite from 'vite';
import { every } from './arrays';
/**
* Compare the changed files vs the build output and determine what kind of reload needs to happen:
*
* - Do nothing
* - CSS or JS file associated with an HTML page is changed - this is handled automatically by the
* dev server
* - Change isn't used by any of the entrypoints
* - Reload Content script
* - CSS or JS file associated with a content script
* - Background script will be told to reload the content script
* - Reload HTML file
* - HTML file itself is saved - HMR doesn't handle this because the HTML pages are pre-rendered
* - Chrome is OK reloading the page when the HTML file is changed without reloading the whole
* extension. Not sure about firefox, this might need to change to an extension reload
* - Reload extension
* - Background script is changed
* - Manifest is different
* - Restart browser
* - Config file changed (wxt.config.ts, .env, web-ext.config.ts, etc)
*/
export function detectDevChanges(
changedFiles: [event: string, path: string][],
currentOutput: BuildOutput | undefined,
): DevModeChange {
if (currentOutput == null) return { type: 'no-change' };
const changedSteps = new Set(
changedFiles.flatMap((changedFile) =>
findEffectedSteps(changedFile, currentOutput),
),
);
if (changedSteps.size === 0) return { type: 'no-change' };
const unchangedOutput: BuildOutput = {
manifest: currentOutput.manifest,
steps: [],
publicAssets: [],
};
const changedOutput: BuildOutput = {
manifest: currentOutput.manifest,
steps: [],
publicAssets: [],
};
for (const step of currentOutput.steps) {
if (changedSteps.has(step)) {
changedOutput.steps.push(step);
} else {
unchangedOutput.steps.push(step);
}
}
for (const asset of currentOutput.publicAssets) {
if (changedSteps.has(asset)) {
changedOutput.publicAssets.push(asset);
} else {
unchangedOutput.publicAssets.push(asset);
}
}
const isOnlyHtmlChanges =
changedFiles.length > 0 &&
every(changedFiles, ([_, file]) => file.endsWith('.html'));
if (isOnlyHtmlChanges) {
return {
type: 'html-reload',
cachedOutput: unchangedOutput,
rebuildGroups: changedOutput.steps.map((step) => step.entrypoints),
};
}
const isOnlyContentScripts =
changedOutput.steps.length > 0 &&
every(
changedOutput.steps.flatMap((step) => step.entrypoints),
(entry) => entry.type === 'content-script',
);
if (isOnlyContentScripts) {
return {
type: 'content-script-reload',
cachedOutput: unchangedOutput,
changedSteps: changedOutput.steps,
rebuildGroups: changedOutput.steps.map((step) => step.entrypoints),
};
}
return {
type: 'extension-reload',
cachedOutput: unchangedOutput,
rebuildGroups: changedOutput.steps.map((step) => step.entrypoints),
};
}
/**
* For a single change, return all the step of the build output that were effected by it.
*/
function findEffectedSteps(
changedFile: [event: string, path: string],
currentOutput: BuildOutput,
): DetectedChange[] {
const changes: DetectedChange[] = [];
const changedPath = changedFile[1];
const isChunkEffected = (
chunk: vite.Rollup.OutputChunk | vite.Rollup.OutputAsset,
): boolean =>
// If it's an HTML file with the same path, is is effected because HTML files need to be pre-rendered
// TODO: use bundle path to support `<name>/index.html`?
(chunk.type === 'asset' && changedPath.endsWith(chunk.fileName)) ||
// If it's a chunk that depends on the changed file, it is effected
(chunk.type === 'chunk' && chunk.moduleIds.includes(changedPath));
for (const step of currentOutput.steps) {
const effectedChunk = step.chunks.find((chunk) => isChunkEffected(chunk));
if (effectedChunk) changes.push(step);
}
const effectedAsset = currentOutput.publicAssets.find((chunk) =>
isChunkEffected(chunk),
);
if (effectedAsset) changes.push(effectedAsset);
return changes;
}
/**
* Contains information about what files changed, what needs rebuilt, and the type of reload that is
* required.
*/
export type DevModeChange =
| NoChange
| HtmlReload
| ExtensionReload
| ContentScriptReload;
// | BrowserRestart
interface NoChange {
type: 'no-change';
}
interface RebuildChange {
/**
* The list of entrypoints that need rebuilt.
*/
rebuildGroups: EntrypointGroup[];
/**
* The previous output stripped of any files are going to change.
*/
cachedOutput: BuildOutput;
}
interface HtmlReload extends RebuildChange {
type: 'html-reload';
}
interface ExtensionReload extends RebuildChange {
type: 'extension-reload';
}
// interface BrowserRestart extends RebuildChange {
// type: 'browser-restart';
// }
interface ContentScriptReload extends RebuildChange {
type: 'content-script-reload';
changedSteps: BuildStepOutput[];
}
/**
* When figuring out what needs reloaded, this stores the step that was changed, or the public
* directory asset that was changed. It doesn't know what type of change is required yet. Just an
* intermediate type.
*/
type DetectedChange = BuildStepOutput | vite.Rollup.OutputAsset;
+2 -1
View File
@@ -94,7 +94,7 @@ export async function getInternalConfig(
// Customize the default vite config
finalConfig.vite.root = root;
finalConfig.vite.configFile = false;
finalConfig.vite.logLevel = 'silent';
finalConfig.vite.logLevel = 'warn';
finalConfig.vite.build ??= {};
finalConfig.vite.build.outDir = outDir;
@@ -110,6 +110,7 @@ export async function getInternalConfig(
finalConfig.vite.plugins.push(
plugins.virtualEntrypoin('content-script', finalConfig),
);
finalConfig.vite.plugins.push(plugins.devServerGlobals(finalConfig));
finalConfig.vite.define ??= {};
getGlobals(finalConfig).forEach((global) => {
+5
View File
@@ -39,5 +39,10 @@ export function getGlobals(
value: config.browser === 'opera',
type: `boolean`,
},
{
name: '__COMMAND__',
value: config.command,
type: `"build" | "serve"`,
},
];
}
+1 -1
View File
@@ -6,7 +6,7 @@ import { scanExports } from 'unimport';
export async function importTsFile<T>(root: string, path: string): Promise<T> {
const clientImports = await scanExports(
resolve(root, 'node_modules/wxt/dist/client/index.js'),
resolve(root, 'node_modules/wxt/dist/client.js'),
);
const jiti = createJITI(__filename, {
cache: false,
+51 -24
View File
@@ -29,7 +29,7 @@ export async function writeManifest(
await fs.ensureDir(config.outDir);
await fs.writeFile(resolve(config.outDir, 'manifest.json'), str, 'utf-8');
output.unshift({
output.publicAssets.unshift({
type: 'asset',
fileName: 'manifest.json',
name: 'manifest',
@@ -43,7 +43,7 @@ export async function writeManifest(
*/
export async function generateMainfest(
entrypoints: Entrypoint[],
buildOutput: BuildOutput,
buildOutput: Omit<BuildOutput, 'manifest'>,
config: InternalConfig,
): Promise<Manifest.WebExtensionManifest> {
const pkg = await getPackageJson(config);
@@ -65,6 +65,7 @@ export async function generateMainfest(
addEntrypoints(manifest, entrypoints, buildOutput, config);
if (config.command === 'serve') addDevModeCsp(manifest, config);
if (config.command === 'serve') addDevModePermissions(manifest, config);
return manifest;
}
@@ -100,7 +101,7 @@ function simplifyVersion(versionName: string): string {
function addEntrypoints(
manifest: Manifest.WebExtensionManifest,
entrypoints: Entrypoint[],
buildOutput: BuildOutput,
buildOutput: Omit<BuildOutput, 'manifest'>,
config: InternalConfig,
): void {
const entriesByType = entrypoints.reduce<
@@ -270,24 +271,23 @@ function addEntrypoints(
}
if (contentScripts?.length) {
if (config.command === 'serve') {
const permissionsKey =
config.manifestVersion === 2 ? 'permissions' : 'host_permissions';
const hostPermissions = new Set<string>(manifest[permissionsKey] ?? []);
// Don't add content scripts to the manifest in dev mode for MV3 - they're managed and reloaded
// at runtime
if (config.command === 'serve' && config.manifestVersion === 3) {
const hostPermissions = new Set<string>(manifest.host_permissions ?? []);
contentScripts.forEach((script) => {
script.options.matches.forEach((matchPattern) => {
hostPermissions.add(matchPattern);
});
});
manifest[permissionsKey] = Array.from(hostPermissions).sort();
hostPermissions.forEach((permission) =>
addHostPermission(manifest, permission),
);
} else {
const hashToEntrypointsMap = contentScripts.reduce((map, script) => {
const hash = JSON.stringify(script.options);
if (!map.has(hash)) {
map.set(hash, [script]);
} else {
map.get(hash)?.push(script);
}
if (map.has(hash)) map.get(hash)?.push(script);
else map.set(hash, [script]);
return map;
}, new Map<string, ContentScriptEntrypoint[]>());
@@ -316,13 +316,9 @@ function addDevModeCsp(
const allowedCsp = config.server?.origin ?? 'http://localhost:*';
if (manifest.manifest_version === 3) {
manifest.host_permissions ??= [];
if (!manifest.host_permissions.includes(permission))
manifest.host_permissions.push(permission);
addHostPermission(manifest, permission);
} else {
manifest.permissions ??= [];
if (!manifest.permissions.includes(permission))
manifest.permissions.push(permission);
addPermission(manifest, permission);
}
const csp = new ContentSecurityPolicy(
@@ -345,20 +341,33 @@ function addDevModeCsp(
}
}
function addDevModePermissions(
manifest: Manifest.WebExtensionManifest,
config: InternalConfig,
) {
// For reloading the page
addPermission(manifest, 'tabs');
// For registering content scripts
if (config.manifestVersion === 3) addPermission(manifest, 'scripting');
}
/**
* Returns the bundle paths to CSS files associated with a list of content scripts, or undefined if
* there is no associated CSS.
*/
function getContentScriptCssFiles(
export function getContentScriptCssFiles(
contentScripts: ContentScriptEntrypoint[],
buildOutput: BuildOutput,
buildOutput: Omit<BuildOutput, 'manifest'>,
): string[] | undefined {
const css: string[] = [];
const allChunks = buildOutput.steps.flatMap((step) => step.chunks);
contentScripts.forEach((script) => {
const cssRegex = new RegExp(`^assets/${script.name}-[a-f0-9]{8}.css$`);
const relatedCss = buildOutput.find((chunk) =>
chunk.fileName.match(cssRegex),
// TODO: optimize and remove loop with a map
const relatedCss = allChunks.find(
(chunk) => chunk.fileName === `assets/${script.name}.css`,
);
if (relatedCss) css.push(relatedCss.fileName);
});
@@ -366,3 +375,21 @@ function getContentScriptCssFiles(
if (css.length > 0) return css;
return undefined;
}
function addPermission(
manifest: Manifest.WebExtensionManifest,
permission: string,
): void {
manifest.permissions ??= [];
if (manifest.permissions.includes(permission)) return;
manifest.permissions.push(permission);
}
function addHostPermission(
manifest: Manifest.WebExtensionManifest,
hostPermission: string,
): void {
manifest.host_permissions ??= [];
if (manifest.host_permissions.includes(hostPermission)) return;
manifest.host_permissions.push(hostPermission);
}
+21
View File
@@ -11,6 +11,21 @@ export function devHtmlPrerender(config: InternalConfig): vite.Plugin {
return {
apply: 'build',
name: 'wxt:dev-html-prerender',
config(userConfig) {
return vite.mergeConfig(
{
resolve: {
alias: {
'@wxt/reload-html': resolve(
config.root,
'node_modules/wxt/dist/virtual-modules/reload-html.js',
),
},
},
},
userConfig,
);
},
async transform(html, id) {
const server = config.server;
if (config.command !== 'serve' || server == null || !id.endsWith('.html'))
@@ -43,6 +58,12 @@ export function devHtmlPrerender(config: InternalConfig): vite.Plugin {
pointToDevServer('script[type=module]', 'src');
pointToDevServer('link[rel=stylesheet]', 'href');
// Add a script to add page reloading
const reloader = document.createElement('script');
reloader.src = '@wxt/reload-html';
reloader.type = 'module';
document.head.appendChild(reloader);
const newHtml = document.toString();
config.logger.debug('Transformed ' + id);
config.logger.debug('Old HTML:\n' + html);
+24
View File
@@ -0,0 +1,24 @@
import { Plugin } from 'vite';
import { InternalConfig } from '../types';
/**
* Defines global constants about the dev server. Helps scripts connect to the server's web socket.
*/
export function devServerGlobals(internalConfig: InternalConfig): Plugin {
return {
name: 'wxt:dev-server-globals',
config(config) {
if (internalConfig.server == null || internalConfig.command == 'build')
return;
config.define ??= {};
config.define.__DEV_SERVER_PROTOCOL__ = JSON.stringify('ws:');
config.define.__DEV_SERVER_HOSTNAME__ = JSON.stringify(
internalConfig.server.hostname,
);
config.define.__DEV_SERVER_PORT__ = JSON.stringify(
internalConfig.server.port,
);
},
};
}
+4 -3
View File
@@ -1,5 +1,6 @@
export * from './download';
export * from './unimport';
export * from './multipageMove';
export * from './devHtmlPrerender';
export * from './devServerGlobals';
export * from './download';
export * from './multipageMove';
export * from './unimport';
export * from './virtualEntrypoint';
+1 -1
View File
@@ -53,7 +53,7 @@ export function multipageMove(
const oldAbsPath = resolve(config.outDir, oldBundlePath);
const newAbsPath = resolve(config.outDir, newBundlePath);
await ensureDir(dirname(newAbsPath));
await fs.move(oldAbsPath, newAbsPath);
await fs.move(oldAbsPath, newAbsPath, { overwrite: true });
const renamedChunk = {
...bundle[oldBundlePath],
+5 -2
View File
@@ -29,10 +29,13 @@ export function virtualEntrypoin(
const inputPath = id.replace(resolvedVirtualId, '');
const template = await fs.readFile(
resolve(config.root, `node_modules/wxt/templates/virtual-${type}.ts`),
resolve(
config.root,
`node_modules/wxt/dist/virtual-modules/${type}-entrypoint.js`,
),
'utf-8',
);
return template.replaceAll('{{moduleId}}', inputPath);
return template.replace(`virtual:user-${type}`, inputPath);
},
};
}
+83 -83
View File
@@ -1,21 +1,19 @@
import {
BuildOutput,
WxtDevServer,
InlineConfig,
InternalConfig,
} from './core/types';
import { BuildOutput, WxtDevServer, InlineConfig } from './core/types';
import { getInternalConfig } from './core/utils/getInternalConfig';
import { findEntrypoints } from './core/build/findEntrypoints';
import { buildEntrypoints } from './core/build/buildEntrypoints';
import { generateMainfest, writeManifest } from './core/utils/manifest';
import { printBuildSummary } from './core/log/printBuildSummary';
import fs from 'fs-extra';
import { generateTypesDir } from './core/build/generateTypesDir';
import pc from 'picocolors';
import * as vite from 'vite';
import { findOpenPort } from './core/utils/findOpenPort';
import { formatDuration } from './core/utils/formatDuration';
import { createWebExtRunner } from './core/runners/createWebExtRunner';
import { detectDevChanges } from './core/utils/detectDevChanges';
import { Mutex } from 'async-mutex';
import { consola } from 'consola';
import { relative } from 'node:path';
import { getEntrypointOutputFile } from './core/utils/entrypoints';
import { buildInternal, rebuild } from './core/build';
import {
getServerInfo,
reloadContentScripts,
reloadHtmlPages,
setupServer,
} from './core/server';
export { version } from '../package.json';
export * from './core/types/external';
@@ -33,77 +31,79 @@ export async function build(config: InlineConfig): Promise<BuildOutput> {
export async function createServer(
config?: InlineConfig,
): Promise<WxtDevServer> {
const port = await findOpenPort(3000, 3010);
const hostname = 'localhost';
const origin = `http://${hostname}:${port}`;
const serverConfig: vite.InlineConfig = {
server: {
origin,
},
const serverInfo = await getServerInfo();
const getLatestInternalConfig = () => {
const viteConfig: vite.InlineConfig = vite.mergeConfig(
serverInfo.viteServerConfig,
config?.vite ?? {},
);
return getInternalConfig({ ...config, vite: viteConfig }, 'serve');
};
const internalConfig = await getInternalConfig(
vite.mergeConfig(serverConfig, config ?? {}),
'serve',
);
const runner = createWebExtRunner();
const viteServer = await vite.createServer(internalConfig.vite);
const server: WxtDevServer = {
...viteServer,
async listen(port, isRestart) {
const res = await viteServer.listen(port, isRestart);
if (!isRestart) {
internalConfig.logger.success(`Started dev server @ ${origin}`);
internalConfig.logger.info('Opening browser...');
await runner.openBrowser(internalConfig);
internalConfig.logger.success('Opened!');
}
return res;
},
logger: internalConfig.logger,
port,
hostname,
origin,
};
internalConfig.logger.info('Created dev server');
let internalConfig = await getLatestInternalConfig();
const server = await setupServer(serverInfo, internalConfig);
internalConfig.server = server;
await buildInternal(internalConfig);
const fileChangedMutex = new Mutex();
const changeQueue: Array<[string, string]> = [];
server.ws.on('wxt:background-initialized', () => {
// Register content scripts for the first time since they're not listed in the manifest
reloadContentScripts(server.currentOutput.steps, internalConfig, server);
});
server.watcher.on('all', async (event, path, _stats) => {
if (path.startsWith(internalConfig.outBaseDir)) return;
changeQueue.push([event, path]);
await fileChangedMutex.runExclusive(async () => {
const fileChanges = changeQueue.splice(0, changeQueue.length);
const changes = detectDevChanges(fileChanges, server.currentOutput);
if (changes.type === 'no-change') return;
// Log the entrypoints that were effected
consola.info(
`Changed: ${Array.from(new Set(fileChanges.map((change) => change[1])))
.map((file) => pc.dim(relative(internalConfig.root, file)))
.join(', ')}`,
);
const rebuiltNames = changes.rebuildGroups
.flat()
.map((entry) => {
return pc.cyan(
relative(internalConfig.outDir, getEntrypointOutputFile(entry, '')),
);
})
.join(pc.dim(', '));
// Get latest config and Rebuild groups with changes
internalConfig = await getLatestInternalConfig();
internalConfig.server = server;
const { output: newOutput } = await rebuild(
internalConfig,
// TODO: this excludes new entrypoints, so they're not built until the dev command is restarted
changes.rebuildGroups,
changes.cachedOutput,
);
server.currentOutput = newOutput;
// Perform reloads
switch (changes.type) {
case 'extension-reload':
server.reloadExtension();
break;
case 'html-reload':
reloadHtmlPages(changes.rebuildGroups, server, internalConfig);
break;
case 'content-script-reload':
reloadContentScripts(changes.changedSteps, internalConfig, server);
break;
}
consola.success(`Reloaded: ${rebuiltNames}`);
});
});
return server;
}
async function buildInternal(config: InternalConfig): Promise<BuildOutput> {
const verb = config.command === 'serve' ? 'Pre-rendering' : 'Building';
const target = `${config.browser}-mv${config.manifestVersion}`;
config.logger.info(
`${verb} ${pc.cyan(target)} for ${pc.cyan(config.mode)} with ${pc.green(
`Vite ${vite.version}`,
)}`,
);
const startTime = Date.now();
// Cleanup
await fs.rm(config.outDir, { recursive: true, force: true });
await fs.ensureDir(config.outDir);
// Build
const entrypoints = await findEntrypoints(config);
await generateTypesDir(entrypoints, config);
const output = await buildEntrypoints(entrypoints, config);
// Write manifest
const manifest = await generateMainfest(entrypoints, output, config);
await writeManifest(manifest, output, config);
// Post-build
config.logger.success(
`Built extension in ${formatDuration(Date.now() - startTime)}`,
);
await printBuildSummary(output, config);
return output;
}
+194
View File
@@ -0,0 +1,194 @@
/**
* SHOULD ONLY BE IMPORTED IN TESTS.
*/
import { resolve } from 'path';
import {
BackgroundEntrypoint,
ContentScriptEntrypoint,
GenericEntrypoint,
OptionsEntrypoint,
PopupEntrypoint,
} from '..';
import { faker } from '@faker-js/faker';
import merge from 'lodash.merge';
import { Rollup } from 'vite';
import { Manifest } from 'webextension-polyfill';
faker.seed(__TEST_SEED__);
type DeepPartial<T> = { [key in keyof T]+?: Partial<T[key]> };
function fakeObjectCreator<T>(base: () => T) {
return (overrides?: DeepPartial<T>): T => merge(base(), overrides);
}
export function fakeFileName(): string {
return faker.string.alphanumeric() + '.' + faker.string.alpha({ length: 3 });
}
export function fakeFile(root = process.cwd()): string {
return resolve(root, fakeFileName());
}
export function fakeDir(root = process.cwd()): string {
return resolve(root, faker.string.alphanumeric());
}
export const fakeContentScriptEntrypoint =
fakeObjectCreator<ContentScriptEntrypoint>(() => ({
type: 'content-script',
inputPath: fakeFile('src'),
name: faker.string.alpha(),
options: {
matches: [],
matchAboutBlank: faker.helpers.arrayElement([true, false, undefined]),
matchOriginAsFallback: faker.helpers.arrayElement([
true,
false,
undefined,
]),
runAt: faker.helpers.arrayElement([
'document_start',
'document_end',
'document_idle',
undefined,
]),
},
outputDir: fakeDir('.output'),
}));
export const fakeBackgroundEntrypoint = fakeObjectCreator<BackgroundEntrypoint>(
() => ({
type: 'background',
inputPath: fakeFile('src'),
name: faker.string.alpha(),
options: {
persistent: faker.helpers.arrayElement([true, false, undefined]),
type: faker.helpers.maybe(() => 'module'),
},
outputDir: fakeDir('.output'),
}),
);
export const fakeUnlistedScriptEntrypoint =
fakeObjectCreator<GenericEntrypoint>(() => ({
type: 'unlisted-script',
inputPath: fakeFile('src'),
name: faker.string.alpha(),
outputDir: fakeDir('.output'),
}));
export const fakeOptionsEntrypoint = fakeObjectCreator<OptionsEntrypoint>(
() => ({
type: 'options',
inputPath: fakeFile('src'),
name: faker.string.alpha(),
outputDir: fakeDir('.output'),
options: {
browserStyle: faker.helpers.arrayElement([true, false, undefined]),
chromeStyle: faker.helpers.arrayElement([true, false, undefined]),
openInTab: faker.helpers.arrayElement([true, false, undefined]),
},
}),
);
export const fakePopupEntrypoint = fakeObjectCreator<PopupEntrypoint>(() => ({
type: 'popup',
inputPath: fakeFile('src'),
name: faker.string.alpha(),
outputDir: fakeDir('.output'),
options: {
defaultTitle: faker.helpers.arrayElement([
faker.person.fullName(),
undefined,
]),
defaultIcon: faker.helpers.arrayElement([
{
'16': 'icon/16.png',
'24': 'icon/24.png',
'64': 'icon/64.png',
},
]),
mv2Key: faker.helpers.arrayElement([
'browser_action',
'page_action',
undefined,
]),
},
}));
export const fakeGenericEntrypoint = fakeObjectCreator<GenericEntrypoint>(
() => ({
type: faker.helpers.arrayElement([
'sandbox',
'bookmarks',
'history',
'newtab',
'sidepanel',
'devtools',
'unlisted-page',
'unlisted-script',
]),
inputPath: fakeFile('src'),
name: faker.string.alpha(),
outputDir: fakeDir('.output'),
}),
);
export const fakeRollupOutputChunk = fakeObjectCreator<Rollup.OutputChunk>(
() => ({
type: 'chunk',
code: '',
dynamicImports: [],
exports: [],
facadeModuleId: faker.helpers.arrayElement([null, fakeFile()]),
fileName: faker.string.alphanumeric(),
implicitlyLoadedBefore: [],
importedBindings: {},
imports: [],
isDynamicEntry: faker.datatype.boolean(),
isEntry: faker.datatype.boolean(),
isImplicitEntry: faker.datatype.boolean(),
map: null,
moduleIds: [],
modules: {},
name: faker.string.alpha(),
referencedFiles: [],
viteMetadata: {
importedAssets: new Set(),
importedCss: new Set(),
},
}),
);
export const fakeRollupOutputAsset = fakeObjectCreator<Rollup.OutputAsset>(
() => ({
type: 'asset',
fileName: fakeFileName(),
name: faker.string.alpha(),
needsCodeReference: faker.datatype.boolean(),
source: '',
}),
);
export function fakeRollupOutput(): Rollup.OutputAsset | Rollup.OutputChunk {
return faker.helpers.arrayElement([
fakeRollupOutputAsset(),
fakeRollupOutputChunk(),
]);
}
export const fakeManifest = fakeObjectCreator<Manifest.WebExtensionManifest>(
() => ({
manifest_version: faker.helpers.arrayElement([2, 3]),
name: faker.string.alphanumeric(),
version: `${faker.number.int()}.${faker.number.int()}.${faker.number.int()}`,
}),
);
export function fakeArray<T>(createItem: () => T, count = 3): T[] {
const array: T[] = [];
for (let i = 0; i < count; i++) {
array.push(createItem());
}
return array;
}
-11
View File
@@ -1,11 +0,0 @@
// Types required to make templates happy.
declare module '*?raw' {
const content: any;
export default content;
}
declare module '{{moduleId}}' {
const variable: any;
export default variable;
}
-13
View File
@@ -1,13 +0,0 @@
import definition from '{{moduleId}}';
try {
const res = definition.main();
if (res instanceof Promise) {
console.warn(
"The background's main() function return a promise, but it must be synchonous",
);
}
} catch (err) {
console.error('The background script crashed on startup!');
throw err;
}
-9
View File
@@ -1,9 +0,0 @@
import definition from '{{moduleId}}';
(async () => {
try {
await definition.main();
} catch (err) {
console.error('The content script crashed on startup!\n\n', err);
}
})();
+17
View File
@@ -1,6 +1,21 @@
import { defineWorkspace } from 'vitest/config';
import fs from 'fs-extra';
import pc from 'picocolors';
import { Plugin } from 'vite';
const seed = Math.round(Math.random() * Number.MAX_SAFE_INTEGER);
console.info('Test seed: ' + pc.cyan(seed));
// config.define doesn't work with workspaces, so we have to set it inisde a plugin
const testSeed = (): Plugin => ({
name: 'test-seed',
config(config) {
config.define ??= {};
config.define.__TEST_SEED__ = JSON.stringify(seed);
},
});
// Clear e2e test projects
await fs.rm('e2e/project', { recursive: true, force: true });
export default defineWorkspace([
@@ -11,6 +26,7 @@ export default defineWorkspace([
mockReset: true,
restoreMocks: true,
},
plugins: [testSeed()],
},
{
test: {
@@ -18,5 +34,6 @@ export default defineWorkspace([
dir: 'e2e',
singleThread: true,
},
plugins: [testSeed()],
},
]);