Compare commits

..

16 Commits

Author SHA1 Message Date
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
38 changed files with 1175 additions and 129 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 280 KiB

After

Width:  |  Height:  |  Size: 150 KiB

+36 -1
View File
@@ -1,5 +1,41 @@
# Changelog
## 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 +109,3 @@
### ❤️ Contributors
- Aaron Klinker
- Aaron
+2
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.
@@ -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
+12 -11
View File
@@ -1,7 +1,7 @@
{
"name": "wxt",
"type": "module",
"version": "0.0.1",
"version": "0.0.2",
"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,7 @@
"prepublish": "pnpm -s build"
},
"dependencies": {
"async-mutex": "^0.4.0",
"c12": "^1.4.2",
"cac": "^6.7.14",
"consola": "^3.1.0",
@@ -73,12 +70,16 @@
"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",
+157 -13
View File
@@ -3,11 +3,14 @@ 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
async-mutex: ^0.4.0
c12: ^1.4.2
cac: ^6.7.14
consola: ^3.1.0
@@ -17,7 +20,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
@@ -32,6 +37,7 @@ importers:
web-ext: ^7.6.2
webextension-polyfill: ^0.10.0
dependencies:
async-mutex: 0.4.0
c12: 1.4.2
cac: 6.7.14
consola: 3.1.0
@@ -48,12 +54,16 @@ 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
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
@@ -752,6 +762,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:
{
@@ -1003,6 +1021,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:
{
@@ -1303,7 +1337,6 @@ packages:
integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==,
}
engines: { node: '>=12' }
dev: false
/ansi-styles/3.2.1:
resolution:
@@ -1436,6 +1469,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 +1541,6 @@ packages:
{
integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==,
}
dev: false
/bcrypt-pbkdf/1.0.2:
resolution:
@@ -1517,6 +1558,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:
{
@@ -1610,7 +1662,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 +1830,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 +1925,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 +1961,6 @@ packages:
integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==,
}
engines: { node: '>=0.8' }
dev: false
/color-convert/1.9.3:
resolution:
@@ -2275,7 +2342,6 @@ packages:
}
dependencies:
clone: 1.0.4
dev: false
/defer-to-connect/2.0.1:
resolution:
@@ -3690,7 +3756,6 @@ packages:
{
integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==,
}
dev: false
/ignore/5.2.4:
resolution:
@@ -3931,6 +3996,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 +4136,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:
{
@@ -4538,7 +4619,6 @@ packages:
{
integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==,
}
dev: false
/lodash.sortby/4.7.0:
resolution:
@@ -4553,6 +4633,17 @@ packages:
integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==,
}
/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:
{
@@ -5157,6 +5248,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:
{
@@ -5774,6 +5883,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 +6074,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 +6136,6 @@ packages:
{
integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==,
}
dev: false
/safe-buffer/5.2.1:
resolution:
@@ -6378,6 +6509,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 +6617,6 @@ packages:
}
dependencies:
safe-buffer: 5.1.2
dev: false
/strip-ansi/6.0.1:
resolution:
@@ -6496,7 +6636,6 @@ packages:
engines: { node: '>=12' }
dependencies:
ansi-regex: 6.0.1
dev: false
/strip-bom-buf/2.0.0:
resolution:
@@ -6777,6 +6916,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 +7198,6 @@ packages:
{
integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==,
}
dev: false
/uuid/3.4.0:
resolution:
@@ -7250,7 +7395,6 @@ packages:
}
dependencies:
defaults: 1.0.4
dev: false
/web-ext/7.6.2:
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;
@@ -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),
};
+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,31 @@
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';
if (__COMMAND__ === 'serve') {
try {
setupWebSocket((message) => {
if (message.event === 'wxt:reload-extension') browser.runtime.reload();
});
// 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);
}
}
+22
View File
@@ -0,0 +1,22 @@
// 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;
+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)] ??
+37 -3
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,46 @@ 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;
/**
* 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;
}
export type TargetBrowser = 'chrome' | 'firefox' | 'safari' | 'edge' | 'opera';
@@ -144,7 +178,7 @@ export interface ContentScriptDefinition {
matchAboutBlank?: boolean;
matchOriginAsFallback?: boolean;
world?: 'ISOLATED' | 'MAIN';
main(onStopped: OnContentScriptStopped): void | Promise<void>;
main(): void | Promise<void>;
}
export interface BackgroundScriptDefintition {
@@ -0,0 +1,132 @@
import { describe, expect, it } from 'vitest';
import { DevModeChange, detectDevChanges } from '../detectDevChanges';
import {
fakeBackgroundEntrypoint,
fakeContentScriptEntrypoint,
fakeFile,
fakeManifest,
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);
});
});
});
+157
View File
@@ -0,0 +1,157 @@
import { BuildOutput, BuildStepOutput, EntrypointGroup } from '../types';
import * as vite from 'vite';
/**
* 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.find(
([_, file]) => !file.endsWith('.html'),
);
if (isOnlyHtmlChanges) {
return {
type: 'html-reload',
cachedOutput: unchangedOutput,
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;
// | BrowserRestart
// | ContentScriptReload
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';
// }
/**
* 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,
+9 -7
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);
@@ -100,7 +100,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<
@@ -351,14 +351,16 @@ function addDevModeCsp(
*/
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);
});
+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);
},
};
}
+120 -10
View File
@@ -3,6 +3,7 @@ import {
WxtDevServer,
InlineConfig,
InternalConfig,
EntrypointGroup,
} from './core/types';
import { getInternalConfig } from './core/utils/getInternalConfig';
import { findEntrypoints } from './core/build/findEntrypoints';
@@ -16,6 +17,16 @@ import * as vite from 'vite';
import { findOpenPort } from './core/utils/findOpenPort';
import { formatDuration } from './core/utils/formatDuration';
import { createWebExtRunner } from './core/runners/createWebExtRunner';
import { groupEntrypoints } from './core/utils/groupEntrypoints';
import { Manifest } from 'webextension-polyfill';
import { detectDevChanges } from './core/utils/detectDevChanges';
import { Mutex } from 'async-mutex';
import { consola } from 'consola';
import { relative } from 'node:path';
import {
getEntrypointBundlePath,
getEntrypointOutputFile,
} from './core/utils/entrypoints';
export { version } from '../package.json';
export * from './core/types/external';
@@ -41,13 +52,77 @@ export async function createServer(
origin,
},
};
const internalConfig = await getInternalConfig(
let internalConfig = await getInternalConfig(
vite.mergeConfig(serverConfig, config ?? {}),
'serve',
);
const runner = createWebExtRunner();
let hasBuiltOnce = false;
let currentOutput: BuildOutput | undefined;
const fileChangedMutex = new Mutex();
const changeQueue: Array<[string, string]> = [];
const viteServer = await vite.createServer(internalConfig.vite);
viteServer.watcher.on('all', async (event, path, stats) => {
if (!hasBuiltOnce || path.startsWith(internalConfig.outBaseDir)) return;
changeQueue.push([event, path]);
await fileChangedMutex.runExclusive(async () => {
const fileChanges = changeQueue.splice(0, changeQueue.length);
const changes = detectDevChanges(fileChanges, 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 getInternalConfig(
vite.mergeConfig(serverConfig, config ?? {}),
'serve',
);
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,
);
currentOutput = newOutput;
// Perform reloads
switch (changes.type) {
case 'extension-reload':
server.reloadExtension();
consola.success(`Reloaded extension: ${rebuiltNames}`);
break;
case 'html-reload':
changes.rebuildGroups.flat().forEach((entry) => {
const path = getEntrypointBundlePath(
entry,
internalConfig.outDir,
'.html',
);
server.reloadPage(path);
});
consola.success(`Reloaded pages: ${rebuiltNames}`);
break;
}
});
});
const server: WxtDevServer = {
...viteServer,
async listen(port, isRestart) {
@@ -63,15 +138,23 @@ export async function createServer(
return res;
},
logger: internalConfig.logger,
port,
hostname,
origin,
reloadExtension: () => {
server.ws.send('wxt:reload-extension');
},
reloadPage: (path) => {
// Can't use Vite's built-in "full-reload" event because it doesn't like our paths, it expects
// paths ending in "/index.html"
server.ws.send('wxt:reload-page', path);
},
};
internalConfig.logger.info('Created dev server');
internalConfig.server = server;
await buildInternal(internalConfig);
currentOutput = await buildInternal(internalConfig);
hasBuiltOnce = true;
return server;
}
@@ -90,14 +173,9 @@ async function buildInternal(config: InternalConfig): Promise<BuildOutput> {
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);
const groups = groupEntrypoints(entrypoints);
const { output } = await rebuild(config, groups);
// Post-build
config.logger.success(
@@ -107,3 +185,35 @@ async function buildInternal(config: InternalConfig): Promise<BuildOutput> {
return output;
}
export async function rebuild(
config: InternalConfig,
entrypointGroups: EntrypointGroup[],
existingOutput: Omit<BuildOutput, 'manifest'> = {
steps: [],
publicAssets: [],
},
): Promise<{ output: BuildOutput; manifest: Manifest.WebExtensionManifest }> {
// Build
const allEntrypoints = await findEntrypoints(config);
await generateTypesDir(allEntrypoints, config);
const buildOutput = await buildEntrypoints(entrypointGroups, config);
const manifest = await generateMainfest(allEntrypoints, buildOutput, config);
const output: BuildOutput = {
manifest,
...buildOutput,
};
// Write manifest
await writeManifest(manifest, output, config);
return {
output: {
manifest,
steps: [...existingOutput.steps, ...output.steps],
publicAssets: [...existingOutput.publicAssets, ...output.publicAssets],
},
manifest,
};
}
+135
View File
@@ -0,0 +1,135 @@
/**
* SHOULD ONLY BE IMPORTED IN TESTS.
*/
import { resolve } from 'path';
import {
BackgroundEntrypoint,
ContentScriptEntrypoint,
GenericEntrypoint,
} 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 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()],
},
]);