Compare commits

..

5 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
19 changed files with 738 additions and 296 deletions
+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 }}
+26
View File
@@ -1,5 +1,31 @@
# 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)
+3 -4
View File
@@ -17,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
+3 -2
View File
@@ -1,7 +1,7 @@
{
"name": "wxt",
"type": "module",
"version": "0.0.2",
"version": "0.1.0",
"description": "Next gen framework for developing web extensions",
"repository": {
"type": "git",
@@ -52,6 +52,7 @@
"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",
@@ -86,7 +87,7 @@
"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": {
+78 -114
View File
@@ -10,6 +10,7 @@ importers:
'@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
@@ -33,10 +34,11 @@ 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
@@ -60,7 +62,7 @@ importers:
'@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
@@ -70,7 +72,7 @@ importers:
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:
@@ -815,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:
{
@@ -947,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:
{
@@ -1072,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==,
@@ -1091,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:
{
@@ -1330,6 +1364,7 @@ packages:
integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==,
}
engines: { node: '>=8' }
dev: false
/ansi-regex/6.0.1:
resolution:
@@ -1576,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:
{
@@ -2072,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:
{
@@ -2230,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:
{
@@ -2903,6 +2904,7 @@ packages:
integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==,
}
engines: { node: '>=0.10.0' }
dev: false
/event-target-shim/5.0.1:
resolution:
@@ -2977,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:
{
@@ -4287,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:
{
@@ -4632,6 +4619,7 @@ packages:
{
integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==,
}
dev: false
/log-symbols/5.1.0:
resolution:
@@ -4669,6 +4657,7 @@ packages:
engines: { node: '>=10' }
dependencies:
yallist: 4.0.0
dev: false
/lru-cache/9.1.2:
resolution:
@@ -4721,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:
{
@@ -5684,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:
@@ -5849,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
@@ -6239,6 +6218,7 @@ packages:
hasBin: true
dependencies:
lru-cache: 6.0.0
dev: false
/setimmediate/1.0.5:
resolution:
@@ -6823,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:
{
@@ -7250,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
@@ -7309,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
@@ -7346,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
@@ -7366,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
@@ -7482,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:
{
@@ -7702,6 +7665,7 @@ packages:
{
integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==,
}
dev: false
/yaml/2.3.1:
resolution:
+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;
});
+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');
}
@@ -3,15 +3,26 @@ 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 {
setupWebSocket((message) => {
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);
});
// Web Socket will disconnect if the service worker is killed
keepServiceWorkerAlive();
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);
}
@@ -20,3 +20,4 @@ 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,
};
}
+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;
}
+14 -1
View File
@@ -54,6 +54,14 @@ export interface WxtDevServer extends vite.ViteDevServer {
* 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`.
*/
@@ -70,6 +78,12 @@ export interface WxtDevServer extends vite.ViteDevServer {
* 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';
@@ -125,7 +139,6 @@ export interface GenericEntrypoint extends BaseEntrypoint {
| 'sidepanel'
| 'devtools'
| 'unlisted-page'
| 'unlisted-page'
| 'unlisted-script';
}
+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);
});
});
});
@@ -4,7 +4,10 @@ import {
fakeBackgroundEntrypoint,
fakeContentScriptEntrypoint,
fakeFile,
fakeGenericEntrypoint,
fakeManifest,
fakeOptionsEntrypoint,
fakePopupEntrypoint,
fakeRollupOutputAsset,
fakeRollupOutputChunk,
} from '../../../testing/fake-objects';
@@ -129,4 +132,120 @@ describe('Detect Dev Changes', () => {
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;
}
+28 -8
View File
@@ -1,5 +1,6 @@
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:
@@ -60,9 +61,9 @@ export function detectDevChanges(
}
}
const isOnlyHtmlChanges = !changedFiles.find(
([_, file]) => !file.endsWith('.html'),
);
const isOnlyHtmlChanges =
changedFiles.length > 0 &&
every(changedFiles, ([_, file]) => file.endsWith('.html'));
if (isOnlyHtmlChanges) {
return {
type: 'html-reload',
@@ -71,6 +72,21 @@ export function detectDevChanges(
};
}
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,
@@ -114,9 +130,12 @@ function findEffectedSteps(
* Contains information about what files changed, what needs rebuilt, and the type of reload that is
* required.
*/
export type DevModeChange = NoChange | HtmlReload | ExtensionReload;
export type DevModeChange =
| NoChange
| HtmlReload
| ExtensionReload
| ContentScriptReload;
// | BrowserRestart
// | ContentScriptReload
interface NoChange {
type: 'no-change';
@@ -145,9 +164,10 @@ interface ExtensionReload extends RebuildChange {
// type: 'browser-restart';
// }
// interface ContentScriptReload extends RebuildChange {
// type: 'content-script-reload';
// }
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
+42 -17
View File
@@ -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;
}
@@ -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,11 +341,22 @@ 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: Omit<BuildOutput, 'manifest'>,
): string[] | undefined {
@@ -368,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);
}
+35 -145
View File
@@ -1,32 +1,19 @@
import {
BuildOutput,
WxtDevServer,
InlineConfig,
InternalConfig,
EntrypointGroup,
} 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 { 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 { getEntrypointOutputFile } from './core/utils/entrypoints';
import { buildInternal, rebuild } from './core/build';
import {
getEntrypointBundlePath,
getEntrypointOutputFile,
} from './core/utils/entrypoints';
getServerInfo,
reloadContentScripts,
reloadHtmlPages,
setupServer,
} from './core/server';
export { version } from '../package.json';
export * from './core/types/external';
@@ -44,33 +31,35 @@ 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');
};
let internalConfig = await getInternalConfig(
vite.mergeConfig(serverConfig, config ?? {}),
'serve',
);
const runner = createWebExtRunner();
let internalConfig = await getLatestInternalConfig();
const server = await setupServer(serverInfo, internalConfig);
internalConfig.server = server;
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;
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, currentOutput);
const changes = detectDevChanges(fileChanges, server.currentOutput);
if (changes.type === 'no-change') return;
@@ -90,10 +79,7 @@ export async function createServer(
.join(pc.dim(', '));
// Get latest config and Rebuild groups with changes
internalConfig = await getInternalConfig(
vite.mergeConfig(serverConfig, config ?? {}),
'serve',
);
internalConfig = await getLatestInternalConfig();
internalConfig.server = server;
const { output: newOutput } = await rebuild(
internalConfig,
@@ -101,119 +87,23 @@ export async function createServer(
changes.rebuildGroups,
changes.cachedOutput,
);
currentOutput = newOutput;
server.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}`);
reloadHtmlPages(changes.rebuildGroups, server, internalConfig);
break;
case 'content-script-reload':
reloadContentScripts(changes.changedSteps, internalConfig, server);
break;
}
consola.success(`Reloaded: ${rebuiltNames}`);
});
});
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;
},
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;
currentOutput = await buildInternal(internalConfig);
hasBuiltOnce = true;
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);
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;
}
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,
};
}
+59
View File
@@ -6,6 +6,8 @@ import {
BackgroundEntrypoint,
ContentScriptEntrypoint,
GenericEntrypoint,
OptionsEntrypoint,
PopupEntrypoint,
} from '..';
import { faker } from '@faker-js/faker';
import merge from 'lodash.merge';
@@ -75,6 +77,63 @@ export const fakeUnlistedScriptEntrypoint =
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',