@crxjs/vite-plugin 2.0.0-beta.9 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,30 +1,54 @@
1
- # ![CRXJS](./banner-github.png)
1
+ ![CRXJS](./banner-github.png)
2
2
 
3
- [![npm (scoped)](https://img.shields.io/npm/v/@crxjs/vite-plugin.svg)](https://www.npmjs.com/package/@crxjs/vite-plugin)
4
- [![GitHub last commit](https://img.shields.io/github/last-commit/crxjs/chrome-extension-tools.svg?logo=github)](https://github.com/crxjs/rollup-plugin-chrome-extension)
5
- ![GitHub action badge](https://github.com/crxjs/chrome-extension-tools/actions/workflows/vite-plugin.yml/badge.svg)
3
+ <h1 align="center">CRXJS</h1>
6
4
 
7
- ## CRXJS Vite Plugin
5
+ <p align="center">
6
+ Modern Chrome extension development with built-in <code>HMR</code> and <code>zero-config</code> setup
7
+ </p>
8
8
 
9
- > Build a Chrome Extension with [Vite](https://vitejs.dev)⚡
9
+ <p align="center">
10
+ <a href="https://www.npmjs.com/package/@crxjs/vite-plugin">
11
+ <img src="https://img.shields.io/npm/v/@crxjs/vite-plugin?color=298cd6&amp;label=CRXJS&labelColor=f2bae4" alt="NPM version">
12
+ </a>
13
+ </p>
10
14
 
11
- CRXJS brings the Vite developer experience to Chrome Extensions:
15
+ <h2 align="center">
16
+ <a href="https://crxjs.dev/vite-plugin">📚 Documentation</a> |
17
+ <a href="https://discord.gg/5yHKEa9v">💬 Discord </a>
18
+ </h2>
12
19
 
13
- - True HMR for extension pages and content scripts
14
- - Simple config - everything is in the extension manifest
15
- - Static asset imports with automatic web-accessible resources
16
20
 
17
- ## Getting Started
21
+ ## 📦 Create CRXJS Project
22
+ ```shell
23
+ npm create crxjs@latest
24
+ ```
18
25
 
19
- See the CRXJS documentation to
20
- [get started in 90 seconds 🚀](https://crxjs.dev/vite-plugin)
26
+ > [!IMPORTANT]
27
+ > `@latest` MUST NOT be omitted, otherwise `npm` may resolve to a cached and outdated version of the package.
21
28
 
22
- ## Supporting
29
+ ## ✨ Features
23
30
 
24
- If these plugins have helped you ship your product faster, please consider
25
- [sponsoring me](https://github.com/sponsors/jacksteamdev) on GitHub.
31
+ - 🧩 **Full Vite Plugin Ecosystem** - Leverage any Vite-compatible plugins with zero extra setup
32
+ - ⚙️ **Zero Configuration** - Start developing immediately with intelligent defaults
33
+ - 3️⃣ **Manifest V3 Support** - Built for modern Chrome extensions with enhanced security
34
+ - 🔥 **True Hot Module Replacement** - Instant UI updates while preserving extension state 🎈**works with content scripts**
35
+ - 📁 **Static Asset Import** - Directly reference images/fonts in your code
36
+ - 🤖 **Auto Web-Accessible Resources** - Automatic generation of `web_accessible_resources` manifest entries
37
+
38
+ > [!NOTE]
39
+ > Looking for MV2 support? See [`rollup-plugin`](packages/rollup-plugin/README.md)
40
+
41
+ ## 💝 Contributors
42
+
43
+ This project exists thanks to all the people who contribute.
26
44
 
27
- ## Contributing
45
+ And thank you to all our backers! 🙏
28
46
 
29
- We encourage pull requests! This is a
30
- [pnpm monorepo](https://pnpm.io/workspaces), so use pnpm instead of Yarn or npm.
47
+ <a href="https://github.com/crxjs/chrome-extension-tools/graphs/contributors">
48
+ <img src="https://contrib.rocks/image?repo=crxjs/chrome-extension-tools" />
49
+ </a>
50
+
51
+ ## 🤝 Supporting
52
+
53
+ If these plugins have helped you ship your product faster, please consider
54
+ [sponsoring me](https://github.com/sponsors/jacksteamdev) on GitHub.
package/client.d.ts CHANGED
@@ -57,3 +57,18 @@ declare module '*?script&module' {
57
57
  const fileName: string
58
58
  export default fileName
59
59
  }
60
+
61
+ declare namespace ContentScriptAPI {
62
+ export interface ExecuteFnOptions {
63
+ perf: {
64
+ injectTime: number
65
+ loadTime: number
66
+ }
67
+ }
68
+ export interface ExecuteFn {
69
+ (options: ExecuteFnOptions): void
70
+ }
71
+ export interface ModuleExports {
72
+ onExecute?: ExecuteFn
73
+ }
74
+ }
package/dist/index.cjs CHANGED
@@ -5,7 +5,7 @@ Object.defineProperty(exports, '__esModule', { value: true });
5
5
  const defineManifest = (manifest) => manifest;
6
6
  const defineDynamicResource = ({
7
7
  matches = ["http://*/*", "https://*/*"],
8
- use_dynamic_url = true
8
+ use_dynamic_url = false
9
9
  }) => ({
10
10
  matches,
11
11
  resources: [DYNAMIC_RESOURCE],
package/dist/index.d.ts CHANGED
@@ -17,6 +17,14 @@ interface WebAccessibleResourceById {
17
17
  resources: string[];
18
18
  use_dynamic_url?: boolean;
19
19
  }
20
+ interface ChromeManifestBackground {
21
+ service_worker: string;
22
+ type?: 'module' | (string & {});
23
+ }
24
+ interface FirefoxManifestBackground {
25
+ scripts: string[];
26
+ persistent?: false;
27
+ }
20
28
  interface ManifestV3 {
21
29
  manifest_version: number;
22
30
  name: string;
@@ -25,11 +33,13 @@ interface ManifestV3 {
25
33
  description?: string | undefined;
26
34
  icons?: chrome.runtime.ManifestIcons | undefined;
27
35
  action?: chrome.runtime.ManifestAction | undefined;
28
- author?: string | undefined;
29
- background?: {
30
- service_worker: string;
31
- type?: 'module';
36
+ /**
37
+ * @see https://developer.chrome.com/docs/extensions/reference/manifest/author
38
+ */
39
+ author?: {
40
+ email: string;
32
41
  } | undefined;
42
+ background?: ChromeManifestBackground | FirefoxManifestBackground | undefined;
33
43
  chrome_settings_overrides?: {
34
44
  homepage?: string | undefined;
35
45
  search_provider?: chrome.runtime.SearchProvider | undefined;
@@ -118,12 +128,12 @@ interface ManifestV3 {
118
128
  } | undefined;
119
129
  incognito?: string | undefined;
120
130
  input_components?: {
121
- name?: string | undefined;
122
- type?: string | undefined;
131
+ name: string;
123
132
  id?: string | undefined;
124
- description?: string | undefined;
125
- language?: string | undefined;
126
- layouts?: string[] | undefined;
133
+ language?: string | string[] | undefined;
134
+ layouts?: string | string[] | undefined;
135
+ input_view?: string | undefined;
136
+ options_page?: string | undefined;
127
137
  }[] | undefined;
128
138
  key?: string | undefined;
129
139
  minimum_chrome_version?: string | undefined;
@@ -166,6 +176,9 @@ interface ManifestV3 {
166
176
  pages: string[];
167
177
  content_security_policy?: string | undefined;
168
178
  } | undefined;
179
+ side_panel?: {
180
+ default_path?: string | undefined;
181
+ } | undefined;
169
182
  short_name?: string | undefined;
170
183
  spellcheck?: {
171
184
  dictionary_language?: string | undefined;
@@ -189,9 +202,98 @@ interface ManifestV3 {
189
202
  web_accessible_resources?: (WebAccessibleResourceById | WebAccessibleResourceByMatch)[] | undefined;
190
203
  }
191
204
 
192
- declare type ManifestV3Export = ManifestV3 | Promise<ManifestV3> | ManifestV3Fn;
193
- declare type ManifestV3Fn = (env: ConfigEnv) => ManifestV3 | Promise<ManifestV3>;
194
- declare const defineManifest: (manifest: ManifestV3Export) => ManifestV3Export;
205
+ type ManifestV3Fn = (env: ConfigEnv) => ManifestV3 | Promise<ManifestV3>;
206
+ type ManifestV3Export = ManifestV3 | Promise<ManifestV3> | ManifestV3Fn;
207
+ type Code = '.' | '/' | '\\';
208
+ type ManifestFilePath<T extends string> = T extends `${Code}${string}` ? never : T extends `${string}.${infer Ext}` ? Ext extends '' ? never : T : never;
209
+ interface ManifestIcons<T extends string> {
210
+ [size: number]: ManifestFilePath<T>;
211
+ }
212
+ type FilePathFields<T extends string> = {
213
+ icons?: ManifestIcons<T>;
214
+ action?: {
215
+ /**
216
+ * - Relative to Vite project root (where vite.config.js is)
217
+ * - Format: "subdir/icon.png" (no leading ./ or /)
218
+ *
219
+ * @example "assets/icon.png"
220
+ */
221
+ default_icon?: ManifestIcons<T>;
222
+ default_title?: string;
223
+ /**
224
+ * - Relative to Vite project root (where vite.config.js is)
225
+ * - Format: "subdir/index.html" (no leading ./ or /)
226
+ *
227
+ * @example "src/popup.html"
228
+ */
229
+ default_popup?: ManifestFilePath<T>;
230
+ };
231
+ background?: {
232
+ /**
233
+ * - Relative to Vite project root (where vite.config.js is)
234
+ * - Format: "subdir/index.js" (no leading ./ or /)
235
+ *
236
+ * @example "src/background.js"
237
+ */
238
+ service_worker: ManifestFilePath<T>;
239
+ type?: 'module' | (string & {});
240
+ } | FirefoxManifestBackground;
241
+ content_scripts?: {
242
+ matches?: string[];
243
+ exclude_matches?: string[];
244
+ /**
245
+ * - Relative to Vite project root (where vite.config.js is)
246
+ * - Format: "subdir/content.css" (no leading ./ or /)
247
+ *
248
+ * @example "src/content.css"
249
+ */
250
+ css?: ManifestFilePath<T>[];
251
+ /**
252
+ * - Relative to Vite project root (where vite.config.js is)
253
+ * - Format: "subdir/content.js" (no leading ./ or /)
254
+ *
255
+ * @example "src/content.js"
256
+ */
257
+ js?: ManifestFilePath<T>[];
258
+ run_at?: string;
259
+ all_frames?: boolean;
260
+ match_about_blank?: boolean;
261
+ include_globs?: string[];
262
+ exclude_globs?: string[];
263
+ }[];
264
+ input_components?: {
265
+ name: string;
266
+ id?: string;
267
+ language?: string | string[];
268
+ layouts?: string | string[];
269
+ input_view?: string;
270
+ /**
271
+ * - Relative to Vite project root (where vite.config.js is)
272
+ * - Format: "subdir/options.html" (no leading ./ or /)
273
+ *
274
+ * @example "src/options.html"
275
+ */
276
+ options_page?: ManifestFilePath<T>;
277
+ }[];
278
+ /**
279
+ * - Relative to Vite project root (where vite.config.js is)
280
+ * - Format: "subdir/options.html" (no leading ./ or /)
281
+ *
282
+ * @example "src/options.html"
283
+ */
284
+ options_page?: ManifestFilePath<T>;
285
+ /**
286
+ * - Relative to Vite project root (where vite.config.js is)
287
+ * - Format: "subdir/devtools.html" (no leading ./ or /)
288
+ *
289
+ * @example "src/devtools.html"
290
+ */
291
+ devtools_page?: ManifestFilePath<T>;
292
+ };
293
+ type ManifestOptions<T extends string> = Omit<ManifestV3, keyof FilePathFields<string>> & FilePathFields<T>;
294
+ type ManifestV3Options<T extends string = string> = ManifestOptions<T> | Promise<ManifestOptions<T>> | ManifestV3Define<T>;
295
+ type ManifestV3Define<T extends string> = (env: ConfigEnv) => ManifestOptions<T> | Promise<ManifestOptions<T>>;
296
+ declare const defineManifest: <T extends string>(manifest: ManifestV3Options<T>) => ManifestV3Export;
195
297
  /**
196
298
  * Content script resources like CSS and image files must be declared in the
197
299
  * manifest under `web_accessible_resources`. Manifest V3 uses a match pattern
@@ -219,19 +321,25 @@ declare const defineManifest: (manifest: ManifestV3Export) => ManifestV3Export;
219
321
  */
220
322
  declare const defineDynamicResource: ({ matches, use_dynamic_url, }: Omit<WebAccessibleResourceByMatch, 'resources'>) => WebAccessibleResourceByMatch;
221
323
 
222
- declare type CrxDevAssetId = {
324
+ type CrxDevAssetId = {
223
325
  id: string;
224
326
  type: 'asset';
225
327
  source?: string | Uint8Array;
226
328
  };
227
- declare type CrxDevScriptId = {
329
+ type CrxDevScriptId = {
228
330
  id: string;
229
331
  type: 'module' | 'iife';
230
332
  };
231
333
  interface CrxPlugin extends Plugin {
232
- /** Runs during the transform hook for the manifest. Filenames use input filenames. */
334
+ /**
335
+ * Runs during the transform hook for the manifest. Filenames use input
336
+ * filenames.
337
+ */
233
338
  transformCrxManifest?: (this: PluginContext, manifest: ManifestV3) => Promise<ManifestV3 | null | undefined> | ManifestV3 | null | undefined;
234
- /** Runs during generateBundle, before manifest output. Filenames use output filenames. */
339
+ /**
340
+ * Runs during generateBundle, before manifest output. Filenames use output
341
+ * filenames.
342
+ */
235
343
  renderCrxManifest?: (this: PluginContext, manifest: ManifestV3, bundle: OutputBundle) => Promise<ManifestV3 | null | undefined> | ManifestV3 | null | undefined;
236
344
  /**
237
345
  * Runs in the file writer on content scripts during development. `script.id`
@@ -246,12 +354,18 @@ interface CrxOptions {
246
354
  injectCss?: boolean;
247
355
  };
248
356
  fastGlobOptions?: Options;
357
+ /**
358
+ * The browser that this extension is targeting, can be "firefox" or "chrome".
359
+ * Default is "chrome".
360
+ */
361
+ browser?: Browser;
249
362
  }
363
+ type Browser = 'firefox' | 'chrome';
250
364
 
251
365
  /** Resolves when all existing files in scriptFiles are written. */
252
366
  declare function allFilesReady(): Promise<void>;
253
367
 
254
- declare type FileWriterId = {
368
+ type FileWriterId = {
255
369
  type: CrxDevAssetId['type'] | CrxDevScriptId['type'] | 'loader';
256
370
  id: string;
257
371
  };
package/dist/index.mjs CHANGED
@@ -1,22 +1,22 @@
1
1
  import { simple } from 'acorn-walk';
2
2
  import { createHash } from 'crypto';
3
- import debug$2 from 'debug';
4
- import v8 from 'v8';
5
- import { posix } from 'path';
6
- import { Subject, filter, ReplaySubject, switchMap, of, startWith, map, mergeMap, firstValueFrom, takeUntil, first, toArray, retry, concatWith, Subscription, buffer } from 'rxjs';
3
+ import debug$3 from 'debug';
4
+ import { join, normalize, isAbsolute, basename, relative, resolve, dirname, parse } from 'pathe';
5
+ import { Subject, filter, ReplaySubject, switchMap, of, startWith, map, BehaviorSubject, mergeMap, firstValueFrom, takeUntil, first, toArray, retry, concatWith, Subscription, buffer } from 'rxjs';
7
6
  import fsx from 'fs-extra';
8
7
  import { performance } from 'perf_hooks';
9
8
  import { rollup } from 'rollup';
10
9
  import * as lexer from 'es-module-lexer';
11
10
  import { readFile as readFile$1 } from 'fs/promises';
12
11
  import MagicString from 'magic-string';
13
- import { createLogger } from 'vite';
12
+ import convertSourceMap from 'convert-source-map';
13
+ import { createLogger, version } from 'vite';
14
14
  import { readFileSync, existsSync, promises } from 'fs';
15
15
  import { createRequire } from 'module';
16
16
  import fg from 'fast-glob';
17
+ import pc from 'picocolors';
17
18
  import { load } from 'cheerio';
18
19
  import jsesc from 'jsesc';
19
- import colors from 'picocolors';
20
20
 
21
21
  const pluginName$1 = "crx:optionsProvider";
22
22
  const pluginOptionsProvider = (options) => {
@@ -24,8 +24,10 @@ const pluginOptionsProvider = (options) => {
24
24
  name: pluginName$1,
25
25
  api: {
26
26
  crx: {
27
+ // during testing this can be null, we don't provide options through the test config
27
28
  options
28
29
  }
30
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
29
31
  }
30
32
  };
31
33
  };
@@ -56,12 +58,9 @@ function isCrxPlugin(p) {
56
58
  return !!p && typeof p === "object" && !(p instanceof Promise) && !Array.isArray(p) && p.name.startsWith("crx:");
57
59
  }
58
60
 
59
- var workerHmrClient = "const ownOrigin = new URL(chrome.runtime.getURL(\"/\")).origin;\nself.addEventListener(\"fetch\", (fetchEvent) => {\n const url = new URL(fetchEvent.request.url);\n if (url.origin === ownOrigin) {\n fetchEvent.respondWith(sendToServer(url));\n }\n});\nasync function sendToServer(url) {\n url.protocol = \"http:\";\n url.host = \"localhost\";\n url.port = __SERVER_PORT__;\n url.searchParams.set(\"t\", Date.now().toString());\n const response = await fetch(url.href.replace(/=$|=(?=&)/g, \"\"));\n return new Response(response.body, {\n headers: {\n \"Content-Type\": response.headers.get(\"Content-Type\") ?? \"text/javascript\"\n }\n });\n}\nconst ports = /* @__PURE__ */ new Set();\nchrome.runtime.onConnect.addListener((port) => {\n if (port.name === \"@crx/client\") {\n ports.add(port);\n port.onDisconnect.addListener((port2) => ports.delete(port2));\n port.onMessage.addListener((message) => {\n });\n port.postMessage({ data: JSON.stringify({ type: \"connected\" }) });\n }\n});\nfunction notifyContentScripts(payload) {\n const data = JSON.stringify(payload);\n for (const port of ports)\n port.postMessage({ data });\n}\nconsole.log(\"[vite] connecting...\");\nconst socketProtocol = __HMR_PROTOCOL__ || (location.protocol === \"https:\" ? \"wss\" : \"ws\");\nconst socketHost = `${__HMR_HOSTNAME__ || location.hostname}:${__HMR_PORT__}`;\nconst socket = new WebSocket(`${socketProtocol}://${socketHost}`, \"vite-hmr\");\nconst base = __BASE__ || \"/\";\nsocket.addEventListener(\"message\", async ({ data }) => {\n handleSocketMessage(JSON.parse(data));\n});\nfunction isCrxHmrPayload(x) {\n return x.type === \"custom\" && x.event.startsWith(\"crx:\");\n}\nfunction handleSocketMessage(payload) {\n if (isCrxHmrPayload(payload)) {\n handleCrxHmrPayload(payload);\n } else if (payload.type === \"connected\") {\n console.log(`[vite] connected.`);\n const interval = setInterval(() => socket.send(\"ping\"), __HMR_TIMEOUT__);\n socket.addEventListener(\"close\", () => clearInterval(interval));\n }\n}\nfunction handleCrxHmrPayload(payload) {\n notifyContentScripts(payload);\n switch (payload.event) {\n case \"crx:runtime-reload\":\n console.log(\"[crx] runtime reload\");\n chrome.runtime.reload();\n break;\n }\n}\nasync function waitForSuccessfulPing(ms = 1e3) {\n while (true) {\n try {\n await fetch(`${base}__vite_ping`);\n break;\n } catch (e) {\n await new Promise((resolve) => setTimeout(resolve, ms));\n }\n }\n}\nsocket.addEventListener(\"close\", async ({ wasClean }) => {\n if (wasClean)\n return;\n console.log(`[vite] server connection lost. polling for restart...`);\n await waitForSuccessfulPing();\n handleCrxHmrPayload({\n type: \"custom\",\n event: \"crx:runtime-reload\"\n });\n});\n";
61
+ var workerHmrClient = "const ownOrigin = `chrome-extension://${chrome.runtime.id}`;\nself.addEventListener(\"fetch\", (fetchEvent) => {\n const url = new URL(fetchEvent.request.url);\n if (url.origin === ownOrigin) {\n fetchEvent.respondWith(sendToServer(fetchEvent.request));\n }\n});\nasync function sendToServer(req) {\n const url = new URL(req.url);\n const requestHeaders = new Headers(req.headers);\n url.protocol = \"http:\";\n url.host = \"localhost\";\n url.port = __SERVER_PORT__;\n url.searchParams.set(\"t\", Date.now().toString());\n const response = await fetch(url.href.replace(/=$|=(?=&)/g, \"\"), {\n headers: requestHeaders\n });\n const responseHeaders = new Headers(response.headers);\n responseHeaders.set(\"Content-Type\", responseHeaders.get(\"Content-Type\") ?? \"text/javascript\");\n responseHeaders.set(\"Cache-Control\", responseHeaders.get(\"Cache-Control\") ?? \"\");\n return new Response(response.body, {\n headers: responseHeaders\n });\n}\nconst ports = /* @__PURE__ */ new Set();\nchrome.runtime.onConnect.addListener((port) => {\n if (port.name === \"@crx/client\") {\n ports.add(port);\n port.onDisconnect.addListener((port2) => {\n if (chrome.runtime.lastError) {\n console.error(chrome.runtime.lastError);\n }\n ports.delete(port2);\n });\n port.onMessage.addListener((message) => {\n });\n port.postMessage({ data: JSON.stringify({ type: \"connected\" }) });\n }\n});\nfunction notifyContentScripts(payload) {\n const data = JSON.stringify(payload);\n for (const port of ports)\n port.postMessage({ data });\n}\nconsole.log(\"[vite] connecting...\");\nconst socketProtocol = __HMR_PROTOCOL__ || (location.protocol === \"https:\" ? \"wss\" : \"ws\");\nconst socketToken = __HMR_TOKEN__;\nconst socketHost = `${__HMR_HOSTNAME__ || location.hostname}:${__HMR_PORT__}`;\nconst socket = new WebSocket(`${socketProtocol}://${socketHost}?token=${socketToken}`, \"vite-hmr\");\nconst base = __BASE__ || \"/\";\nsocket.addEventListener(\"message\", async ({ data }) => {\n handleSocketMessage(JSON.parse(data));\n});\nfunction isCrxHmrPayload(x) {\n return x.type === \"custom\" && x.event.startsWith(\"crx:\");\n}\nfunction handleSocketMessage(payload) {\n if (isCrxHmrPayload(payload)) {\n handleCrxHmrPayload(payload);\n } else if (payload.type === \"connected\") {\n console.log(`[vite] connected.`);\n const interval = setInterval(() => socket.send(\"ping\"), __HMR_TIMEOUT__);\n socket.addEventListener(\"close\", () => clearInterval(interval));\n }\n}\nfunction handleCrxHmrPayload(payload) {\n notifyContentScripts(payload);\n switch (payload.event) {\n case \"crx:runtime-reload\":\n console.log(\"[crx] runtime reload\");\n chrome.runtime.reload();\n break;\n }\n}\nasync function waitForSuccessfulPing(ms = 1e3) {\n while (true) {\n try {\n await fetch(`${base}__vite_ping`);\n break;\n } catch (e) {\n await new Promise((resolve) => setTimeout(resolve, ms));\n }\n }\n}\nsocket.addEventListener(\"close\", async ({ wasClean }) => {\n if (wasClean)\n return;\n console.log(`[vite] server connection lost. polling for restart...`);\n await waitForSuccessfulPing();\n handleCrxHmrPayload({\n type: \"custom\",\n event: \"crx:runtime-reload\"\n });\n});\n";
60
62
 
61
- const _debug = (id) => debug$2("crx").extend(id);
62
- const structuredClone = (obj) => {
63
- return v8.deserialize(v8.serialize(obj));
64
- };
63
+ const _debug = (id) => debug$3("crx").extend(id);
65
64
  const hash = (data, length = 5) => createHash("sha1").update(data).digest("base64").replace(/[^A-Za-z0-9]/g, "").slice(0, length);
66
65
  const isString = (x) => typeof x === "string";
67
66
  function isObject(value) {
@@ -113,22 +112,6 @@ const getMatchPatternOrigin = (pattern) => {
113
112
  return pathname ? `${root}/*` : root;
114
113
  };
115
114
 
116
- const {
117
- basename,
118
- dirname,
119
- extname,
120
- delimiter,
121
- format,
122
- isAbsolute,
123
- join,
124
- normalize,
125
- parse,
126
- relative,
127
- resolve,
128
- toNamespacedPath,
129
- sep
130
- } = posix;
131
-
132
115
  function defineClientValues(code, config) {
133
116
  let options = config.server.hmr;
134
117
  options = options && typeof options !== "boolean" ? options : {};
@@ -152,7 +135,7 @@ function defineClientValues(code, config) {
152
135
  if (hmrBase !== "/") {
153
136
  hmrPort = normalize(`${hmrPort}${hmrBase}`);
154
137
  }
155
- return code.replace(`__MODE__`, JSON.stringify(config.mode)).replace(`__BASE__`, JSON.stringify(config.base)).replace(`__DEFINES__`, serializeDefine(config.define || {})).replace(`__HMR_PROTOCOL__`, JSON.stringify(protocol)).replace(`__HMR_HOSTNAME__`, JSON.stringify(host)).replace(`__HMR_PORT__`, JSON.stringify(hmrPort)).replace(`__HMR_TIMEOUT__`, JSON.stringify(timeout)).replace(`__HMR_ENABLE_OVERLAY__`, JSON.stringify(overlay)).replace(`__SERVER_PORT__`, JSON.stringify(config.server.port?.toString()));
138
+ return code.replace(`__MODE__`, JSON.stringify(config.mode)).replace(`__BASE__`, JSON.stringify(config.base)).replace(`__DEFINES__`, serializeDefine(config.define || {})).replace(`__HMR_TOKEN__`, JSON.stringify(config.webSocketToken || "")).replace(`__HMR_PROTOCOL__`, JSON.stringify(protocol)).replace(`__HMR_HOSTNAME__`, JSON.stringify(host)).replace(`__HMR_PORT__`, JSON.stringify(hmrPort)).replace(`__HMR_TIMEOUT__`, JSON.stringify(timeout)).replace(`__HMR_ENABLE_OVERLAY__`, JSON.stringify(overlay)).replace(`__SERVER_PORT__`, JSON.stringify(config.server.port?.toString()));
156
139
  function serializeDefine(define) {
157
140
  let res = `{`;
158
141
  for (const key in define) {
@@ -204,7 +187,7 @@ function formatFileData(script) {
204
187
  return script;
205
188
  }
206
189
  function getFileName({ type, id }) {
207
- let fileName = id.replace(/t=\d+&/, "").replace(/^\//, "").replace(/\?/g, "__").replace(/&/g, "_").replace(/=/g, "--");
190
+ let fileName = id.replace(/t=\d+&/, "").replace(/\?t=\d+$/, "").replace(/^\//, "").replace(/\?/g, "__").replace(/&/g, "_").replace(/=/g, "--");
208
191
  if (fileName.includes("node_modules/")) {
209
192
  fileName = `vendor/${fileName.split("node_modules/").pop().replace(/\//g, "-")}`;
210
193
  } else if (fileName.startsWith("@")) {
@@ -272,6 +255,7 @@ const workerClientId = "/@crx/client-worker";
272
255
 
273
256
  const pluginBackground = () => {
274
257
  let config;
258
+ let browser;
275
259
  return [
276
260
  {
277
261
  name: "crx:background-client",
@@ -292,24 +276,39 @@ const pluginBackground = () => {
292
276
  },
293
277
  {
294
278
  name: "crx:background-loader-file",
279
+ // this should happen after other plugins; the loader file is an implementation detail
295
280
  enforce: "post",
281
+ async config(config2) {
282
+ const opts = await getOptions(config2);
283
+ browser = opts.browser || "chrome";
284
+ },
296
285
  configResolved(_config) {
297
286
  config = _config;
298
287
  },
299
288
  renderCrxManifest(manifest) {
300
- const worker = manifest.background?.service_worker;
289
+ const worker = browser === "firefox" ? manifest.background?.scripts[0] : manifest.background?.service_worker;
301
290
  let loader;
302
291
  if (config.command === "serve") {
303
292
  const port = config.server.port?.toString();
304
293
  if (typeof port === "undefined")
305
294
  throw new Error("server port is undefined in watch mode");
306
- loader = `import 'http://localhost:${port}/@vite/env';
295
+ if (browser === "firefox") {
296
+ loader = `import('http://localhost:${port}/@vite/env');
297
+ `;
298
+ loader += `import('http://localhost:${port}${workerClientId}');
299
+ `;
300
+ if (worker)
301
+ loader += `import('http://localhost:${port}/${worker}');
302
+ `;
303
+ } else {
304
+ loader = `import 'http://localhost:${port}/@vite/env';
307
305
  `;
308
- loader += `import 'http://localhost:${port}${workerClientId}';
306
+ loader += `import 'http://localhost:${port}${workerClientId}';
309
307
  `;
310
- if (worker)
311
- loader += `import 'http://localhost:${port}/${worker}';
308
+ if (worker)
309
+ loader += `import 'http://localhost:${port}/${worker}';
312
310
  `;
311
+ }
313
312
  } else if (worker) {
314
313
  loader = `import './${worker}';
315
314
  `;
@@ -318,13 +317,21 @@ const pluginBackground = () => {
318
317
  }
319
318
  const refId = this.emitFile({
320
319
  type: "asset",
320
+ // fileName b/c service worker must be at root of crx
321
321
  fileName: getFileName({ type: "loader", id: "service-worker" }),
322
322
  source: loader
323
323
  });
324
- manifest.background = {
325
- service_worker: this.getFileName(refId),
326
- type: "module"
327
- };
324
+ if (browser !== "firefox") {
325
+ manifest.background = {
326
+ service_worker: this.getFileName(refId),
327
+ type: "module"
328
+ };
329
+ } else {
330
+ manifest.background = {
331
+ scripts: [this.getFileName(refId)],
332
+ type: "module"
333
+ };
334
+ }
328
335
  return manifest;
329
336
  }
330
337
  }
@@ -333,9 +340,9 @@ const pluginBackground = () => {
333
340
 
334
341
  var contentHmrPort = "function isCrxHMRPayload(x) {\n return x.type === \"custom\" && x.event.startsWith(\"crx:\");\n}\nclass HMRPort {\n port;\n callbacks = /* @__PURE__ */ new Map();\n constructor() {\n setInterval(() => {\n try {\n this.port?.postMessage({ data: \"ping\" });\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"Extension context invalidated.\")) {\n location.reload();\n } else\n throw error;\n }\n }, __CRX_HMR_TIMEOUT__);\n setInterval(this.initPort, 5 * 60 * 1e3);\n this.initPort();\n }\n initPort = () => {\n this.port?.disconnect();\n this.port = chrome.runtime.connect({ name: \"@crx/client\" });\n this.port.onDisconnect.addListener(this.handleDisconnect.bind(this));\n this.port.onMessage.addListener(this.handleMessage.bind(this));\n this.port.postMessage({ type: \"connected\" });\n };\n handleDisconnect = () => {\n if (this.callbacks.has(\"close\"))\n for (const cb of this.callbacks.get(\"close\")) {\n cb({ wasClean: true });\n }\n };\n handleMessage = (message) => {\n const forward = (data) => {\n if (this.callbacks.has(\"message\"))\n for (const cb of this.callbacks.get(\"message\")) {\n cb({ data });\n }\n };\n const payload = JSON.parse(message.data);\n if (isCrxHMRPayload(payload)) {\n if (payload.event === \"crx:runtime-reload\") {\n console.log(\"[crx] runtime reload\");\n setTimeout(() => location.reload(), 500);\n } else {\n forward(JSON.stringify(payload.data));\n }\n } else {\n forward(message.data);\n }\n };\n addEventListener = (event, callback) => {\n const cbs = this.callbacks.get(event) ?? /* @__PURE__ */ new Set();\n cbs.add(callback);\n this.callbacks.set(event, cbs);\n };\n send = (data) => {\n if (this.port)\n this.port.postMessage({ data });\n else\n throw new Error(\"HMRPort is not initialized\");\n };\n}\n\nexport { HMRPort };\n";
335
342
 
336
- var contentDevLoader = "(function () {\n 'use strict';\n\n (async () => {\n if (__PREAMBLE__)\n await import(\n /* @vite-ignore */\n chrome.runtime.getURL(__PREAMBLE__)\n );\n await import(\n /* @vite-ignore */\n chrome.runtime.getURL(__CLIENT__)\n );\n await import(\n /* @vite-ignore */\n chrome.runtime.getURL(__SCRIPT__)\n );\n })().catch(console.error);\n\n})();\n";
343
+ var contentDevLoader = "(function () {\n 'use strict';\n\n const injectTime = performance.now();\n (async () => {\n if (__PREAMBLE__)\n await import(\n /* @vite-ignore */\n chrome.runtime.getURL(__PREAMBLE__)\n );\n await import(\n /* @vite-ignore */\n chrome.runtime.getURL(__CLIENT__)\n );\n const { onExecute } = await import(\n /* @vite-ignore */\n chrome.runtime.getURL(__SCRIPT__)\n );\n onExecute?.({ perf: { injectTime, loadTime: performance.now() - injectTime } });\n })().catch(console.error);\n\n})();\n";
337
344
 
338
- var contentProLoader = "(function () {\n 'use strict';\n\n (async () => {\n await import(\n /* @vite-ignore */\n chrome.runtime.getURL(__SCRIPT__)\n );\n })().catch(console.error);\n\n})();\n";
345
+ var contentProLoader = "(function () {\n 'use strict';\n\n const injectTime = performance.now();\n (async () => {\n const { onExecute } = await import(\n /* @vite-ignore */\n chrome.runtime.getURL(__SCRIPT__)\n );\n onExecute?.({ perf: { injectTime, loadTime: performance.now() - injectTime } });\n })().catch(console.error);\n\n})();\n";
339
346
 
340
347
  const contentScripts = new RxMap();
341
348
  contentScripts.change$.pipe(filter(RxMap.isChangeType.set)).subscribe(({ map, value }) => {
@@ -393,6 +400,10 @@ const allFilesReady$ = buildEnd$.pipe(
393
400
  map(() => [...outputFiles.values()]),
394
401
  switchMap((files) => Promise.allSettled(files.map(({ file }) => file)))
395
402
  );
403
+ const timestamp$ = new BehaviorSubject(Date.now());
404
+ allFilesReady$.subscribe(() => {
405
+ timestamp$.next(Date.now());
406
+ });
396
407
  const isRejected = (x) => x?.status === "rejected";
397
408
  const fileWriterError$ = allFilesReady$.pipe(
398
409
  mergeMap((results) => results.filter(isRejected)),
@@ -426,16 +437,36 @@ function prepAsset(fileName, { id, source }) {
426
437
  }
427
438
  function prepScript(fileName, script) {
428
439
  return ($) => $.pipe(
440
+ // get script contents from dev server
429
441
  mergeMap(async ({ server }) => {
430
442
  const target = getOutputPath(server, fileName);
431
443
  const viteUrl = getViteUrl(script);
432
444
  const transformResult = await server.transformRequest(viteUrl);
433
445
  if (!transformResult)
434
446
  throw new TypeError(`Unable to load "${script.id}" from server.`);
435
- const { code, deps = [], dynamicDeps = [] } = transformResult;
436
- return { target, code, deps: [...deps, ...dynamicDeps].flat(), server };
447
+ const { deps = [], dynamicDeps = [], map: map2 } = transformResult;
448
+ let { code } = transformResult;
449
+ try {
450
+ if (map2 && server.config.build.sourcemap === "inline") {
451
+ code = code.replace(/\n*\/\/# sourceMappingURL=[^\n]+/g, "");
452
+ const sourceMap = convertSourceMap.fromObject(map2).toComment();
453
+ code += `
454
+ ${sourceMap}
455
+ `;
456
+ }
457
+ } catch (error) {
458
+ console.warn("Failed to inline source map", error);
459
+ }
460
+ return {
461
+ target,
462
+ code,
463
+ deps: [...deps, ...dynamicDeps].flat(),
464
+ server
465
+ };
437
466
  }),
467
+ // retry in case of dependency rebundle
438
468
  retry({ count: 10, delay: 100 }),
469
+ // patch content scripts
439
470
  mergeMap(async ({ target, server, ...rest }) => {
440
471
  const plugins = server.config.plugins;
441
472
  let { code, deps } = rest;
@@ -455,7 +486,8 @@ function prepScript(fileName, script) {
455
486
  if (i.n) {
456
487
  depSet.add(i.n);
457
488
  const fileName2 = getFileName({ type: "module", id: i.n });
458
- magic.overwrite(i.s, i.e, `/${fileName2}`);
489
+ const fullImport = code.substring(i.s, i.e);
490
+ magic.overwrite(i.s, i.e, fullImport.replace(i.n, `/${fileName2}`));
459
491
  }
460
492
  return { target, source: magic.toString(), deps: [...depSet] };
461
493
  })
@@ -526,8 +558,11 @@ function update(_id) {
526
558
  async function write(fileId) {
527
559
  const start2 = performance.now();
528
560
  const deps = await firstValueFrom(
561
+ // wait for start event
529
562
  start$.pipe(
563
+ // prepare either asset or script contents
530
564
  prepFileData(fileId),
565
+ // output file and add dependencies to file writer
531
566
  mergeMap(async ({ target, source, deps: deps2 }) => {
532
567
  const files = deps2.map((id) => {
533
568
  const r = [add({ id, type: "module" })];
@@ -543,6 +578,7 @@ async function write(fileId) {
543
578
  await outputFile(target, source, { encoding: "utf8" });
544
579
  return files;
545
580
  }),
581
+ // abort write operation on close event
546
582
  takeUntil(close$),
547
583
  concatWith(of([]))
548
584
  )
@@ -633,7 +669,20 @@ const pluginContentScripts = () => {
633
669
  name: "crx:content-scripts",
634
670
  apply: "build",
635
671
  enforce: "pre",
636
- generateBundle() {
672
+ config(config) {
673
+ return {
674
+ ...config,
675
+ build: {
676
+ ...config.build,
677
+ rollupOptions: {
678
+ ...config.build?.rollupOptions,
679
+ // keep exports for content script module api
680
+ preserveEntrySignatures: config.build?.rollupOptions?.preserveEntrySignatures ?? "exports-only"
681
+ }
682
+ }
683
+ };
684
+ },
685
+ generateBundle(_options, bundle) {
637
686
  for (const [key, script] of contentScripts)
638
687
  if (key === script.refId) {
639
688
  if (script.type === "module") {
@@ -642,12 +691,22 @@ const pluginContentScripts = () => {
642
691
  } else if (script.type === "loader") {
643
692
  const fileName = this.getFileName(script.refId);
644
693
  script.fileName = fileName;
645
- const refId = this.emitFile({
646
- type: "asset",
647
- name: getFileName({ type: "loader", id: basename(script.id) }),
648
- source: createProLoader({ fileName })
649
- });
650
- script.loaderName = this.getFileName(refId);
694
+ const bundleFileInfo = bundle[fileName];
695
+ const shouldUseLoader = !(bundleFileInfo.type === "chunk" && bundleFileInfo.imports.length === 0 && bundleFileInfo.dynamicImports.length === 0 && bundleFileInfo.exports.length === 0);
696
+ if (shouldUseLoader) {
697
+ const refId = this.emitFile({
698
+ type: "asset",
699
+ name: getFileName({
700
+ type: "loader",
701
+ id: basename(script.id)
702
+ }),
703
+ source: createProLoader({ fileName })
704
+ });
705
+ script.loaderName = this.getFileName(refId);
706
+ } else {
707
+ bundleFileInfo.code = `(function(){${bundleFileInfo.code}})()
708
+ `;
709
+ }
651
710
  } else if (script.type === "iife") {
652
711
  throw new Error("IIFE content scripts are not implemented");
653
712
  }
@@ -707,6 +766,34 @@ const pluginDynamicContentScripts = () => {
707
766
  configResolved(_config) {
708
767
  config = _config;
709
768
  },
769
+ configureServer(server) {
770
+ return () => {
771
+ server.middlewares.use(async (req, res, next) => {
772
+ try {
773
+ await allFilesReady();
774
+ next();
775
+ } catch (error) {
776
+ let err;
777
+ if (error instanceof Error) {
778
+ err = error;
779
+ } else if (typeof error === "string") {
780
+ err = new Error(error);
781
+ } else {
782
+ err = new Error(
783
+ `Unexpected error "${error}" in middleware for "${req.url}"`
784
+ );
785
+ }
786
+ server.ws.send({
787
+ type: "error",
788
+ err: {
789
+ message: err.message,
790
+ stack: err.stack ?? "no stack available"
791
+ }
792
+ });
793
+ }
794
+ });
795
+ };
796
+ },
710
797
  async resolveId(_source, importer) {
711
798
  if (importer && _source.includes("?script")) {
712
799
  const url = new URL(_source, "stub://stub");
@@ -775,7 +862,6 @@ const pluginDynamicContentScripts = () => {
775
862
  if (config.command === "build") {
776
863
  return `export default import.meta.CRX_DYNAMIC_SCRIPT_${script.refId};`;
777
864
  } else if (typeof script.fileName === "string") {
778
- await fileReady(script);
779
865
  return `export default ${JSON.stringify(script.fileName)};`;
780
866
  } else {
781
867
  throw new Error(
@@ -788,6 +874,12 @@ const pluginDynamicContentScripts = () => {
788
874
  {
789
875
  name: "crx:dynamic-content-scripts-build",
790
876
  apply: "build",
877
+ /**
878
+ * Replace dynamic script placeholders during build.
879
+ *
880
+ * Can't use `renderChunk` b/c pre plugin crx:content-scripts uses
881
+ * `generateBundle` to emit loaders. Must come after "enforce: pre".
882
+ */
791
883
  generateBundle(options, bundle) {
792
884
  for (const chunk of Object.values(bundle))
793
885
  if (chunk.type === "chunk") {
@@ -817,29 +909,42 @@ const pluginDynamicContentScripts = () => {
817
909
  ];
818
910
  };
819
911
 
912
+ const { remove } = fsx;
820
913
  const logger = createLogger("error", { prefix: "crxjs" });
821
914
  const pluginFileWriter = () => {
822
915
  fileWriterError$.subscribe((error) => {
823
916
  logger.error(error.err.message, { error: error.err });
824
917
  });
825
- return {
826
- name: "crx:file-writer",
827
- apply: "serve",
828
- configureServer(server) {
829
- server.httpServer?.on("listening", async () => {
830
- try {
831
- await start({ server });
832
- } catch (error) {
833
- console.error(error);
834
- server.close();
918
+ return [
919
+ {
920
+ name: "crx:file-writer-empty-out-dir",
921
+ apply: "serve",
922
+ enforce: "pre",
923
+ async configResolved(config) {
924
+ if (config.build.emptyOutDir) {
925
+ await remove(config.build.outDir);
835
926
  }
836
- });
837
- server.httpServer?.on("close", () => close());
927
+ }
838
928
  },
839
- closeBundle() {
840
- outputFiles.clear();
929
+ {
930
+ name: "crx:file-writer",
931
+ apply: "serve",
932
+ configureServer(server) {
933
+ server.httpServer?.on("listening", async () => {
934
+ try {
935
+ await start({ server });
936
+ } catch (error) {
937
+ console.error(error);
938
+ server.close();
939
+ }
940
+ });
941
+ server.httpServer?.on("close", () => close());
942
+ },
943
+ closeBundle() {
944
+ outputFiles.clear();
945
+ }
841
946
  }
842
- };
947
+ ];
843
948
  };
844
949
 
845
950
  const _require = typeof require === "undefined" ? createRequire(import.meta.url) : require;
@@ -884,7 +989,9 @@ async function manifestFiles(manifest, options = {}) {
884
989
  ) ?? [];
885
990
  const contentScripts = manifest.content_scripts?.flatMap(({ js }) => js) ?? [];
886
991
  const contentStyles = manifest.content_scripts?.flatMap(({ css }) => css);
887
- const serviceWorker = manifest.background?.service_worker;
992
+ const serviceWorker = manifest.background && "service_worker" in manifest.background ? manifest.background.service_worker : void 0;
993
+ const backgroundScripts = manifest.background && "scripts" in manifest.background ? manifest.background.scripts : void 0;
994
+ const background = serviceWorker ? [serviceWorker].filter(isString) : backgroundScripts ? backgroundScripts.filter(isString) : [];
888
995
  const htmlPages = htmlFiles(manifest);
889
996
  const icons = [
890
997
  Object.values(
@@ -905,7 +1012,7 @@ async function manifestFiles(manifest, options = {}) {
905
1012
  return r;
906
1013
  })
907
1014
  );
908
- webAccessibleResources = resources.flat().filter(isString);
1015
+ webAccessibleResources = [...new Set(resources.flat())].filter(isString);
909
1016
  }
910
1017
  return {
911
1018
  contentScripts: [...new Set(contentScripts)].filter(isString),
@@ -914,12 +1021,12 @@ async function manifestFiles(manifest, options = {}) {
914
1021
  icons: [...new Set(icons)].filter(isString),
915
1022
  locales: [...new Set(locales)].filter(isString),
916
1023
  rulesets: [...new Set(rulesets)].filter(isString),
917
- background: [serviceWorker].filter(isString),
1024
+ background,
918
1025
  webAccessibleResources
919
1026
  };
920
1027
  }
921
1028
  async function dirFiles(dir) {
922
- const files = await fg(`${dir}/**/*`);
1029
+ const files = await fg(join(dir, "**", "*"));
923
1030
  return files;
924
1031
  }
925
1032
  function htmlFiles(manifest) {
@@ -929,7 +1036,8 @@ function htmlFiles(manifest) {
929
1036
  manifest.devtools_page,
930
1037
  manifest.options_page,
931
1038
  manifest.options_ui?.page,
932
- manifest.sandbox?.pages
1039
+ manifest.sandbox?.pages,
1040
+ manifest.side_panel?.default_path
933
1041
  ].flat().filter(isString).map((s) => s.split("#")[0]).sort();
934
1042
  return [...new Set(files)];
935
1043
  }
@@ -954,11 +1062,13 @@ const pluginFileWriterPublic = () => {
954
1062
  };
955
1063
  };
956
1064
 
957
- _debug("file-writer").extend("hmr");
958
- const isCrxHMRPayload = (p) => p.type === "custom" && p.event.startsWith("crx:");
1065
+ const debug$2 = _debug("file-writer").extend("hmr");
1066
+ const isCustomPayload = (p) => {
1067
+ return p.type === "custom";
1068
+ };
959
1069
  const hmrPayload$ = new Subject();
960
1070
  const crxHMRPayload$ = hmrPayload$.pipe(
961
- filter((p) => !isCrxHMRPayload(p)),
1071
+ filter((p) => !isCustomPayload(p)),
962
1072
  buffer(allFilesReady$),
963
1073
  mergeMap((pps) => {
964
1074
  let fullReload;
@@ -1017,6 +1127,7 @@ const crxHMRPayload$ = hmrPayload$.pipe(
1017
1127
  }
1018
1128
  }),
1019
1129
  map((data) => {
1130
+ debug$2(`hmr payload`, data);
1020
1131
  return {
1021
1132
  type: "custom",
1022
1133
  event: "crx:content-script-payload",
@@ -1056,6 +1167,7 @@ const pluginHMR = () => {
1056
1167
  name: "crx:hmr",
1057
1168
  apply: "serve",
1058
1169
  enforce: "pre",
1170
+ // server hmr host should be localhost
1059
1171
  async config({ server = {}, ...config2 }) {
1060
1172
  if (server.hmr === false)
1061
1173
  return;
@@ -1065,6 +1177,7 @@ const pluginHMR = () => {
1065
1177
  server.hmr.host = "localhost";
1066
1178
  return { server, ...config2 };
1067
1179
  },
1180
+ // server should ignore outdir
1068
1181
  configResolved(_config) {
1069
1182
  config = _config;
1070
1183
  const { watch = {} } = config.server;
@@ -1102,19 +1215,24 @@ const pluginHMR = () => {
1102
1215
  closeBundle() {
1103
1216
  subs.unsubscribe();
1104
1217
  },
1218
+ // background changes require a full extension reload
1105
1219
  handleHotUpdate({ modules, server }) {
1106
1220
  const { root } = server.config;
1107
1221
  const relFiles = /* @__PURE__ */ new Set();
1108
- for (const m of modules)
1222
+ const fsFiles = /* @__PURE__ */ new Set();
1223
+ for (const m of modules) {
1109
1224
  if (m.id?.startsWith(root)) {
1110
1225
  relFiles.add(m.id.slice(server.config.root.length));
1226
+ } else if (m.url?.startsWith("/@fs")) {
1227
+ fsFiles.add(m.url);
1111
1228
  }
1229
+ }
1230
+ fsFiles.forEach((file) => update(file));
1112
1231
  if (inputManifestFiles.background.length) {
1113
1232
  const background = prefix$1("/", inputManifestFiles.background[0]);
1114
1233
  if (relFiles.has(background) || modules.some(isImporter(join(server.config.root, background)))) {
1115
1234
  debug$1("sending runtime reload");
1116
1235
  server.ws.send(crxRuntimeReload);
1117
- return [];
1118
1236
  }
1119
1237
  }
1120
1238
  for (const [key, script] of contentScripts)
@@ -1129,6 +1247,7 @@ const pluginHMR = () => {
1129
1247
  name: "crx:hmr",
1130
1248
  apply: "serve",
1131
1249
  enforce: "post",
1250
+ // get final output manifest for handleHotUpdate 👆
1132
1251
  async transformCrxManifest(manifest) {
1133
1252
  inputManifestFiles = await manifestFiles(manifest, { cwd: config.root });
1134
1253
  return null;
@@ -1151,6 +1270,31 @@ const pluginHMR = () => {
1151
1270
  ];
1152
1271
  };
1153
1272
 
1273
+ function printStr(dir) {
1274
+ return ` ${pc.magentaBright("B R O W S E R")}
1275
+ ${pc.greenBright("E X T E N S I O N")}
1276
+ ${pc.blueBright("T O O L S")}
1277
+
1278
+ ${pc.green("\u279C")} ${pc.bold("CRXJS")}: ${pc.green(`Load ${pc.cyan(dir)} as unpacked extension`)}`;
1279
+ }
1280
+ const pluginPrint = () => {
1281
+ let outDir = "dist";
1282
+ return [
1283
+ {
1284
+ name: "crx:print",
1285
+ enforce: "pre",
1286
+ configResolved(resolvedConfig) {
1287
+ outDir = resolvedConfig.build.outDir;
1288
+ },
1289
+ configureServer(server) {
1290
+ server.printUrls = () => {
1291
+ console.log(printStr(outDir));
1292
+ };
1293
+ }
1294
+ }
1295
+ ];
1296
+ };
1297
+
1154
1298
  var loader = "try {\n for (const p of JSON.parse(SCRIPTS)) {\n const url = new URL(p, \"https://stub\");\n url.searchParams.set(\"t\", Date.now().toString());\n const req = url.pathname + url.search;\n await import(\n /* @vite-ignore */\n req\n );\n }\n} catch (error) {\n console.error(error);\n}\n";
1155
1299
 
1156
1300
  const pluginName = "crx:html-inline-scripts";
@@ -1219,6 +1363,7 @@ const pluginHtmlInlineScripts = () => {
1219
1363
  };
1220
1364
  const postPlugin = {
1221
1365
  name: "crx:html-auditor-post",
1366
+ // this hook isn't audited b/c we add it after we set up the auditors
1222
1367
  transformIndexHtml(html, ctx) {
1223
1368
  const key = toKey(ctx);
1224
1369
  const p = pages.get(key);
@@ -1284,9 +1429,9 @@ const pluginHtmlInlineScripts = () => {
1284
1429
  };
1285
1430
  };
1286
1431
 
1287
- var precontrollerJs = "const id = setInterval(() => location.reload(), 100);\nsetTimeout(() => clearInterval(id), 5e3);\n";
1432
+ var loadingPageScript = "const VITE_URL = \"http://localhost:%PORT%\";\ndocument.body.innerHTML = `\n<div\n id=\"app\"\n style=\"\n border: 1px solid #ddd;\n padding: 20px;\n border-radius: 5px;\n box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);\n \"\n >\n <h1 style=\"color: #333\">Vite Dev Mode</h1>\n <p style=\"color: #666\">\n Cannot connect to the Vite Dev Server on <a href=\"${VITE_URL}\">${VITE_URL}</a>\n </p>\n <p style=\"color: #666\">\n Double-check that Vite is working and reload the extension.\n </p>\n <p style=\"color: #666\">\n This page will close when the extension reloads.\n </p>\n <button\n style=\"\n padding: 10px 20px;\n border: none;\n background-color: #007bff;\n color: #fff;\n border-radius: 5px;\n cursor: pointer;\n \"\n >\n Reload Extension\n </button>\n </div>`;\ndocument.body.querySelector(\"button\")?.addEventListener(\"click\", () => {\n chrome.runtime.reload();\n});\nlet tries = 0;\nlet ready = false;\ndo {\n try {\n await fetch(VITE_URL);\n ready = true;\n } catch {\n const timeout = Math.min(100 * Math.pow(2, ++tries), 5e3);\n console.log(`[CRXJS] Vite Dev Server is not available on ${VITE_URL}`);\n console.log(`[CRXJS] Retrying in ${timeout}ms...`);\n await new Promise((resolve) => setTimeout(resolve, timeout));\n }\n} while (!ready);\nlocation.reload();\n";
1288
1433
 
1289
- var precontrollerHtml = "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <title>Waiting for the extension service worker...</title>\n <script src=\"%SCRIPT%\"></script>\n </head>\n <body>\n <h1>Waiting for service worker</h1>\n\n <p>\n If you see this message, it means the service worker has not loaded fully.\n </p>\n\n <p>This page is never added in production.</p>\n </body>\n</html>\n";
1434
+ var loadingPageHtml = "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <title>Vite Dev Mode</title>\n <script src=\"%SCRIPT%\" type=\"module\"></script>\n </head>\n <body\n style=\"font-family: Arial, sans-serif; padding: 20px; text-align: center\"\n >\n <h1>Vite Dev Mode</h1>\n </body>\n</html>\n";
1290
1435
 
1291
1436
  const { readFile } = promises;
1292
1437
  const pluginManifest = () => {
@@ -1465,7 +1610,7 @@ const pluginManifest = () => {
1465
1610
  })
1466
1611
  );
1467
1612
  }
1468
- if (manifest2.background?.service_worker) {
1613
+ if (manifest2.background && "service_worker" in manifest2.background) {
1469
1614
  const file = manifest2.background.service_worker;
1470
1615
  const id2 = join(config.root, file);
1471
1616
  const refId2 = this.emitFile({
@@ -1475,6 +1620,16 @@ const pluginManifest = () => {
1475
1620
  });
1476
1621
  manifest2.background.service_worker = refId2;
1477
1622
  }
1623
+ if (manifest2.background && "scripts" in manifest2.background) {
1624
+ const file = manifest2.background.scripts[0];
1625
+ const id2 = join(config.root, file);
1626
+ const refId2 = this.emitFile({
1627
+ type: "chunk",
1628
+ id: id2,
1629
+ name: basename(file)
1630
+ });
1631
+ manifest2.background.scripts = [refId2];
1632
+ }
1478
1633
  for (const file of htmlFiles(manifest2)) {
1479
1634
  const id2 = join(config.root, file);
1480
1635
  this.emitFile({
@@ -1485,7 +1640,7 @@ const pluginManifest = () => {
1485
1640
  }
1486
1641
  }
1487
1642
  const encoded = encodeManifest(manifest2);
1488
- return encoded;
1643
+ return { code: encoded, map: null };
1489
1644
  },
1490
1645
  async generateBundle(options, bundle) {
1491
1646
  const manifestName = this.getFileName(refId);
@@ -1499,11 +1654,16 @@ const pluginManifest = () => {
1499
1654
  );
1500
1655
  }
1501
1656
  } else {
1502
- if (manifest2.background?.service_worker) {
1657
+ if (manifest2.background && "service_worker" in manifest2.background) {
1503
1658
  const ref = manifest2.background.service_worker;
1504
1659
  const name = this.getFileName(ref);
1505
1660
  manifest2.background.service_worker = name;
1506
1661
  }
1662
+ if (manifest2.background && "scripts" in manifest2.background) {
1663
+ const ref = manifest2.background.scripts[0];
1664
+ const name = this.getFileName(ref);
1665
+ manifest2.background.scripts = [name];
1666
+ }
1507
1667
  manifest2.content_scripts = manifest2.content_scripts?.map(
1508
1668
  ({ js = [], ...rest }) => {
1509
1669
  return {
@@ -1530,11 +1690,11 @@ const pluginManifest = () => {
1530
1690
  const name = `[${plugin.name}]`;
1531
1691
  let message = error;
1532
1692
  if (error instanceof Error) {
1533
- message = colors.red(
1693
+ message = pc.red(
1534
1694
  `${name} ${error.stack ? error.stack : error.message}`
1535
1695
  );
1536
1696
  } else if (typeof error === "string") {
1537
- message = colors.red(`${name} ${error}`);
1697
+ message = pc.red(`${name} ${error}`);
1538
1698
  }
1539
1699
  console.log(message);
1540
1700
  throw new Error(`Error in ${plugin.name}.renderCrxManifest`);
@@ -1563,6 +1723,7 @@ Public dir: "${config.publicDir}"`
1563
1723
  this.emitFile({
1564
1724
  type: "asset",
1565
1725
  fileName: f,
1726
+ // TODO: cache source buffer
1566
1727
  source: await readFile(filename)
1567
1728
  });
1568
1729
  }
@@ -1571,17 +1732,20 @@ Public dir: "${config.publicDir}"`
1571
1732
  if (config.command === "serve" && files.html.length) {
1572
1733
  const refId2 = this.emitFile({
1573
1734
  type: "asset",
1574
- name: "precontroller.js",
1575
- source: precontrollerJs
1735
+ name: "loading-page.js",
1736
+ source: loadingPageScript.replace(
1737
+ "%PORT%",
1738
+ `${config.server.port ?? 0}`
1739
+ )
1576
1740
  });
1577
- const precontrollerJsName = this.getFileName(refId2);
1741
+ const loadingPageScriptName = this.getFileName(refId2);
1578
1742
  files.html.map(
1579
1743
  (f) => this.emitFile({
1580
1744
  type: "asset",
1581
1745
  fileName: f,
1582
- source: precontrollerHtml.replace(
1746
+ source: loadingPageHtml.replace(
1583
1747
  "%SCRIPT%",
1584
- `/${precontrollerJsName}`
1748
+ `/${loadingPageScriptName}`
1585
1749
  )
1586
1750
  })
1587
1751
  );
@@ -1591,10 +1755,10 @@ Public dir: "${config.publicDir}"`
1591
1755
  this.emitFile({
1592
1756
  type: "asset",
1593
1757
  fileName: "manifest.json",
1594
- source: JSON.stringify(manifest2, null, 2)
1758
+ source: JSON.stringify(manifest2, null, 2) + "\n"
1595
1759
  });
1596
1760
  } else {
1597
- manifestJson.source = JSON.stringify(manifest2, null, 2);
1761
+ manifestJson.source = JSON.stringify(manifest2, null, 2) + "\n";
1598
1762
  }
1599
1763
  delete bundle[manifestName];
1600
1764
  }
@@ -1610,7 +1774,11 @@ function compileFileResources(fileName, {
1610
1774
  assets: /* @__PURE__ */ new Set(),
1611
1775
  css: /* @__PURE__ */ new Set(),
1612
1776
  imports: /* @__PURE__ */ new Set()
1613
- }) {
1777
+ }, processedFiles = /* @__PURE__ */ new Set()) {
1778
+ if (processedFiles.has(fileName)) {
1779
+ return resources;
1780
+ }
1781
+ processedFiles.add(fileName);
1614
1782
  const chunk = chunks.get(fileName);
1615
1783
  if (chunk) {
1616
1784
  const { modules, facadeModuleId, imports, dynamicImports } = chunk;
@@ -1619,7 +1787,7 @@ function compileFileResources(fileName, {
1619
1787
  for (const x of dynamicImports)
1620
1788
  resources.imports.add(x);
1621
1789
  for (const x of [...imports, ...dynamicImports])
1622
- compileFileResources(x, { chunks, files, config }, resources);
1790
+ compileFileResources(x, { chunks, files, config }, resources, processedFiles);
1623
1791
  for (const m of Object.keys(modules))
1624
1792
  if (m !== facadeModuleId) {
1625
1793
  const key = prefix$1("/", relative(config.root, m.split("?")[0]));
@@ -1632,7 +1800,8 @@ function compileFileResources(fileName, {
1632
1800
  compileFileResources(
1633
1801
  script.fileName,
1634
1802
  { chunks, files, config },
1635
- resources
1803
+ resources,
1804
+ processedFiles
1636
1805
  );
1637
1806
  }
1638
1807
  }
@@ -1651,7 +1820,7 @@ function compileFileResources(fileName, {
1651
1820
  const defineManifest = (manifest) => manifest;
1652
1821
  const defineDynamicResource = ({
1653
1822
  matches = ["http://*/*", "https://*/*"],
1654
- use_dynamic_url = true
1823
+ use_dynamic_url = false
1655
1824
  }) => ({
1656
1825
  matches,
1657
1826
  resources: [DYNAMIC_RESOURCE],
@@ -1663,22 +1832,34 @@ _debug("web-acc-res");
1663
1832
  const pluginWebAccessibleResources = () => {
1664
1833
  let config;
1665
1834
  let injectCss;
1835
+ let browser;
1666
1836
  return [
1667
1837
  {
1668
1838
  name: "crx:web-accessible-resources",
1669
1839
  apply: "serve",
1670
1840
  enforce: "post",
1841
+ async config(config2) {
1842
+ const opts = await getOptions(config2);
1843
+ browser = opts.browser || "chrome";
1844
+ },
1671
1845
  renderCrxManifest(manifest) {
1672
1846
  manifest.web_accessible_resources = manifest.web_accessible_resources ?? [];
1673
1847
  manifest.web_accessible_resources = manifest.web_accessible_resources.map(({ resources, ...rest }) => ({
1674
1848
  resources: resources.filter((r) => r !== DYNAMIC_RESOURCE),
1675
1849
  ...rest
1676
1850
  })).filter(({ resources }) => resources.length);
1677
- manifest.web_accessible_resources.push({
1678
- use_dynamic_url: true,
1851
+ const war = {
1852
+ // all web origins can access
1679
1853
  matches: ["<all_urls>"],
1680
- resources: ["**/*", "*"]
1681
- });
1854
+ // all resources are web accessible
1855
+ resources: ["**/*", "*"],
1856
+ // change the extension origin on every reload
1857
+ use_dynamic_url: false
1858
+ };
1859
+ if (browser === "firefox") {
1860
+ delete war.use_dynamic_url;
1861
+ }
1862
+ manifest.web_accessible_resources.push(war);
1682
1863
  return manifest;
1683
1864
  }
1684
1865
  },
@@ -1687,7 +1868,9 @@ const pluginWebAccessibleResources = () => {
1687
1868
  apply: "build",
1688
1869
  enforce: "post",
1689
1870
  async config({ build, ...config2 }, { command }) {
1690
- const { contentScripts: contentScripts2 = {} } = await getOptions(config2);
1871
+ const opts = await getOptions(config2);
1872
+ const contentScripts2 = opts.contentScripts || {};
1873
+ browser = opts.browser || "chrome";
1691
1874
  injectCss = contentScripts2.injectCss ?? true;
1692
1875
  return { ...config2, build: { ...build, manifest: command === "build" } };
1693
1876
  },
@@ -1716,9 +1899,11 @@ const pluginWebAccessibleResources = () => {
1716
1899
  dynamicScriptMatches.add("https://*/*");
1717
1900
  }
1718
1901
  if (contentScripts.size > 0) {
1902
+ const viteMajorVersion = parseInt(version.split(".")[0]);
1903
+ const manifestPath = viteMajorVersion > 4 ? ".vite/manifest.json" : "manifest.json";
1719
1904
  const viteManifest = parseJsonAsset(
1720
1905
  bundle,
1721
- "manifest.json"
1906
+ manifestPath
1722
1907
  );
1723
1908
  const viteFiles = /* @__PURE__ */ new Map();
1724
1909
  for (const [, file] of Object.entries(viteManifest))
@@ -1746,12 +1931,12 @@ const pluginWebAccessibleResources = () => {
1746
1931
  { chunks: bundleChunks, files: viteFiles, config }
1747
1932
  );
1748
1933
  contentScripts.get(key).css = [...css];
1749
- if (type === "loader")
1934
+ if (type === "loader" || isDynamicScript)
1750
1935
  imports.add(fileName);
1751
1936
  const resource = {
1752
1937
  matches: isDynamicScript ? [...dynamicScriptMatches] : matches,
1753
1938
  resources: [...assets, ...imports],
1754
- use_dynamic_url: isDynamicScript ? dynamicScriptDynamicUrl : true
1939
+ use_dynamic_url: isDynamicScript ? dynamicScriptDynamicUrl : false
1755
1940
  };
1756
1941
  if (isDynamicScript || !injectCss) {
1757
1942
  resource.resources.push(...css);
@@ -1795,6 +1980,11 @@ const pluginWebAccessibleResources = () => {
1795
1980
  use_dynamic_url
1796
1981
  });
1797
1982
  }
1983
+ if (browser === "firefox") {
1984
+ for (const war of combinedResources) {
1985
+ delete war.use_dynamic_url;
1986
+ }
1987
+ }
1798
1988
  if (combinedResources.length === 0)
1799
1989
  delete manifest.web_accessible_resources;
1800
1990
  else
@@ -1806,6 +1996,7 @@ const pluginWebAccessibleResources = () => {
1806
1996
  };
1807
1997
 
1808
1998
  const crx = (options) => {
1999
+ contentScripts.clear();
1809
2000
  return [
1810
2001
  pluginOptionsProvider(options),
1811
2002
  pluginBackground(),
@@ -1819,7 +2010,8 @@ const crx = (options) => {
1819
2010
  pluginWebAccessibleResources(),
1820
2011
  pluginContentScriptsCss(),
1821
2012
  pluginHMR(),
1822
- pluginManifest()
2013
+ pluginManifest(),
2014
+ pluginPrint()
1823
2015
  ].flat();
1824
2016
  };
1825
2017
  const chromeExtension = crx;
package/index.cjs ADDED
@@ -0,0 +1,8 @@
1
+ Object.assign(module.exports, require('./dist/index.cjs'))
2
+
3
+ // async functions, can be redirect from ESM build
4
+ const asyncFunctions = ['crx', 'chromeExtension', 'allFilesReady', 'filesReady']
5
+ asyncFunctions.forEach((name) => {
6
+ module.exports[name] = (...args) =>
7
+ import('./dist/index.mjs').then((i) => i[name](...args))
8
+ })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crxjs/vite-plugin",
3
- "version": "2.0.0-beta.9",
3
+ "version": "2.0.1",
4
4
  "description": "Build Chrome Extensions with this Vite plugin.",
5
5
  "keywords": [
6
6
  "rollup-plugin",
@@ -35,60 +35,45 @@
35
35
  "types": "./client.d.ts"
36
36
  }
37
37
  },
38
- "main": "dist/index.cjs",
38
+ "main": "index.cjs",
39
39
  "module": "dist/index.mjs",
40
40
  "types": "dist/index.d.ts",
41
41
  "files": [
42
42
  "dist",
43
43
  "types",
44
44
  "manifest.schema.json",
45
- "schema",
45
+ "index.cjs",
46
46
  "client.d.ts"
47
47
  ],
48
- "scripts": {
49
- "format": "prettier -w -c ../../.prettierrc.yaml",
50
- "build": "run-s build:clean build:js",
51
- "build:clean": "rimraf dist",
52
- "build:js": "rollup -c rollup.config.ts --configPlugin esbuild",
53
- "dev:js": "npm run build:js -- -w",
54
- "dev:lint": "tsc --noEmit --watch",
55
- "lint": "run-s lint:eslint lint:types",
56
- "lint:eslint": "eslint \"{src,test}/**/*.ts\"",
57
- "lint:types": "tsc --noEmit",
58
- "test": "vitest --mode unit",
59
- "test:e2e": "vitest --mode e2e",
60
- "test:run": "run-s test:run:*",
61
- "test:run:out": "vitest --run --mode out",
62
- "test:run:e2e": "vitest --run --mode e2e",
63
- "test:update": "run-s \"test:run:out --update\" \"test:run:e2e --update\""
64
- },
65
48
  "dependencies": {
66
49
  "@rollup/pluginutils": "^4.1.2",
67
50
  "@webcomponents/custom-elements": "^1.5.0",
68
51
  "acorn-walk": "^8.2.0",
69
52
  "cheerio": "^1.0.0-rc.10",
70
- "connect-injector": "^0.4.4",
53
+ "convert-source-map": "^1.7.0",
71
54
  "debug": "^4.3.3",
72
55
  "es-module-lexer": "^0.10.0",
73
56
  "fast-glob": "^3.2.11",
74
57
  "fs-extra": "^10.0.1",
75
58
  "jsesc": "^3.0.2",
76
- "magic-string": "^0.26.0",
59
+ "magic-string": "^0.30.12",
60
+ "pathe": "^2.0.1",
77
61
  "picocolors": "^1.0.0",
78
62
  "react-refresh": "^0.13.0",
79
- "rollup": "2.78.1",
63
+ "rollup": "2.79.2",
80
64
  "rxjs": "7.5.7"
81
65
  },
82
66
  "devDependencies": {
83
67
  "@extend-chrome/messages": "1.2.2",
84
68
  "@extend-chrome/storage": "1.5.0",
85
- "@rollup/plugin-alias": "4.0.2",
69
+ "@rollup/plugin-alias": "4.0.4",
86
70
  "@rollup/plugin-commonjs": "21.1.0",
87
71
  "@rollup/plugin-json": "^5.0.0",
88
72
  "@rollup/plugin-node-resolve": "13.2.0",
89
- "@sveltejs/vite-plugin-svelte": "1.1.0",
73
+ "@sveltejs/vite-plugin-svelte": "1.4.0",
90
74
  "@types/acorn": "4.0.6",
91
- "@types/chrome": "0.0.198",
75
+ "@types/chrome": "0.0.237",
76
+ "@types/convert-source-map": "^2.0.0",
92
77
  "@types/debug": "4.1.7",
93
78
  "@types/fs-extra": "9.0.13",
94
79
  "@types/jest-image-snapshot": "^5.1.0",
@@ -98,26 +83,43 @@
98
83
  "@types/react-dom": "17.0.18",
99
84
  "@typescript-eslint/eslint-plugin": "5.41.0",
100
85
  "@typescript-eslint/parser": "5.41.0",
101
- "@vitejs/plugin-react": "^2.1.0",
86
+ "@vitejs/plugin-react": "^2.2.0",
102
87
  "@vitejs/plugin-vue": "3.2.0",
103
88
  "chokidar": "^3.5.3",
104
- "esbuild": "0.15.16",
89
+ "esbuild": "0.17.14",
105
90
  "esbuild-runner": "2.2.2",
106
- "eslint": "8.26.0",
91
+ "eslint": "8.43.0",
107
92
  "eslint-plugin-react": "^7.29.4",
108
93
  "jest-image-snapshot": "^5.2.0",
109
94
  "npm-run-all": "^4.1.5",
110
- "playwright-chromium": "1.27.1",
95
+ "playwright-chromium": "1.52.0",
111
96
  "react": "17.0.2",
112
97
  "react-dom": "17.0.2",
113
98
  "rimraf": "3.0.2",
114
99
  "rollup-plugin-dts": "^4.2.0",
115
- "rollup-plugin-esbuild": "4.10.1",
100
+ "rollup-plugin-esbuild": "4.10.3",
116
101
  "svelte": "^3.48.0",
117
102
  "typescript": "^4.6.4",
118
- "vite": "^3.1.7",
119
- "vite-plugin-inspect": "0.7.5",
120
- "vitest": "0.24.3",
121
- "vue": "3.2.41"
103
+ "vite": "^3.2.11",
104
+ "vite-plugin-inspect": "0.7.25",
105
+ "vitest": "0.28.5",
106
+ "vue": "3.2.47"
107
+ },
108
+ "scripts": {
109
+ "format": "prettier -w -c ../../.prettierrc.yaml",
110
+ "build": "run-s build:clean build:js",
111
+ "build:clean": "rimraf dist",
112
+ "build:js": "rollup -c rollup.config.ts --configPlugin esbuild",
113
+ "dev:js": "npm run build:js -- -w",
114
+ "dev:lint": "tsc --noEmit --watch",
115
+ "lint": "run-s lint:eslint lint:types",
116
+ "lint:eslint": "eslint \"{src,test}/**/*.ts\"",
117
+ "lint:types": "tsc --noEmit",
118
+ "test": "vitest --mode unit",
119
+ "test:e2e": "vitest --mode e2e",
120
+ "test:run": "run-s test:run:*",
121
+ "test:run:out": "vitest --run --mode out",
122
+ "test:run:e2e": "vitest --run --mode e2e",
123
+ "test:update": "run-s \"test:run:out --update\" \"test:run:e2e --update\""
122
124
  }
123
- }
125
+ }