diff --git a/package.json b/package.json index c1c2946f..1d19fe43 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5b7171b6..cbaa467a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: diff --git a/src/cli/commands/dev.ts b/src/cli/commands/dev.ts index bb42941c..8868d84b 100644 --- a/src/cli/commands/dev.ts +++ b/src/cli/commands/dev.ts @@ -23,7 +23,7 @@ export const dev = defineCommand< }; const server = await wxt.createServer(cliConfig); - await server.listen(server.port); + await server.start(); return true; }); diff --git a/src/client/utils/reloadContentScript.ts b/src/client/utils/reloadContentScript.ts new file mode 100644 index 00000000..73994819 --- /dev/null +++ b/src/client/utils/reloadContentScript.ts @@ -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'); +} diff --git a/src/client/virtual-modules/background-entrypoint.ts b/src/client/virtual-modules/background-entrypoint.ts index eed471ff..5b5d0bea 100644 --- a/src/client/virtual-modules/background-entrypoint.ts +++ b/src/client/virtual-modules/background-entrypoint.ts @@ -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); } diff --git a/src/client/virtual-modules/virtual-module-globals.d.ts b/src/client/virtual-modules/virtual-module-globals.d.ts index 13e49a5a..1e8e19a0 100644 --- a/src/client/virtual-modules/virtual-module-globals.d.ts +++ b/src/client/virtual-modules/virtual-module-globals.d.ts @@ -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; diff --git a/src/core/build.ts b/src/core/build.ts new file mode 100644 index 00000000..4c7f6055 --- /dev/null +++ b/src/core/build.ts @@ -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 { + 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 = { + 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 = { + 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, + }; +} diff --git a/src/core/server.ts b/src/core/server.ts new file mode 100644 index 00000000..e70b56bb --- /dev/null +++ b/src/core/server.ts @@ -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 { + 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 { + 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; +} diff --git a/src/core/types/external.ts b/src/core/types/external.ts index 9780d892..b2b663ba 100644 --- a/src/core/types/external.ts +++ b/src/core/types/external.ts @@ -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; /** * 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'; } diff --git a/src/core/utils/__tests__/arrays.test.ts b/src/core/utils/__tests__/arrays.test.ts new file mode 100644 index 00000000..444020e1 --- /dev/null +++ b/src/core/utils/__tests__/arrays.test.ts @@ -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); + }); + }); +}); diff --git a/src/core/utils/__tests__/detectDevChanges.test.ts b/src/core/utils/__tests__/detectDevChanges.test.ts index 30f80db3..6dd06967 100644 --- a/src/core/utils/__tests__/detectDevChanges.test.ts +++ b/src/core/utils/__tests__/detectDevChanges.test.ts @@ -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); + }); + }); }); diff --git a/src/core/utils/arrays.ts b/src/core/utils/arrays.ts new file mode 100644 index 00000000..40449bbc --- /dev/null +++ b/src/core/utils/arrays.ts @@ -0,0 +1,11 @@ +/** + * Checks if `predicate` returns truthy for all elements of the array. + */ +export function every( + 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; +} diff --git a/src/core/utils/detectDevChanges.ts b/src/core/utils/detectDevChanges.ts index 909a1416..80ac758e 100644 --- a/src/core/utils/detectDevChanges.ts +++ b/src/core/utils/detectDevChanges.ts @@ -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 diff --git a/src/core/utils/manifest.ts b/src/core/utils/manifest.ts index 9fca3460..e1e0a622 100644 --- a/src/core/utils/manifest.ts +++ b/src/core/utils/manifest.ts @@ -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(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(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()); @@ -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, ): 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); +} diff --git a/src/index.ts b/src/index.ts index 410e4ed6..5bf5cd08 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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 { export async function createServer( config?: InlineConfig, ): Promise { - 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 { - 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 = { - 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, - }; -} diff --git a/src/testing/fake-objects.ts b/src/testing/fake-objects.ts index 8125bceb..4afb5904 100644 --- a/src/testing/fake-objects.ts +++ b/src/testing/fake-objects.ts @@ -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( + () => ({ + 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(() => ({ + 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( + () => ({ + 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( () => ({ type: 'chunk',