@crxjs/vite-plugin 2.0.0-beta.3 → 2.0.0-beta.30

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/dist/index.mjs CHANGED
@@ -1,20 +1,19 @@
1
1
  import { simple } from 'acorn-walk';
2
2
  import { createHash } from 'crypto';
3
- import debug$2 from 'debug';
4
- import v8 from 'v8';
3
+ import debug$3 from 'debug';
5
4
  import { posix } from 'path';
6
- import { Subject, filter, ReplaySubject, switchMap, of, startWith, map, mergeMap, firstValueFrom, takeUntil, first, toArray, retry, concatWith, Subscription, buffer } from 'rxjs';
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 getPort, { portNumbers } from 'get-port';
18
17
  import { load } from 'cheerio';
19
18
  import jsesc from 'jsesc';
20
19
  import colors from 'picocolors';
@@ -25,17 +24,22 @@ const pluginOptionsProvider = (options) => {
25
24
  name: pluginName$1,
26
25
  api: {
27
26
  crx: {
27
+ // during testing this can be null, we don't provide options through the test config
28
28
  options
29
29
  }
30
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
30
31
  }
31
32
  };
32
33
  };
33
- const getOptions = ({ plugins }) => {
34
+ const getOptions = async ({
35
+ plugins
36
+ }) => {
34
37
  if (typeof plugins === "undefined") {
35
38
  throw new Error("config.plugins is undefined");
36
39
  }
40
+ const awaitedPlugins = await Promise.all(plugins);
37
41
  let options;
38
- for (const p of plugins.flat()) {
42
+ for (const p of awaitedPlugins.flat()) {
39
43
  if (isCrxPlugin(p)) {
40
44
  if (p.name === pluginName$1) {
41
45
  const plugin = p;
@@ -54,12 +58,9 @@ function isCrxPlugin(p) {
54
58
  return !!p && typeof p === "object" && !(p instanceof Promise) && !Array.isArray(p) && p.name.startsWith("crx:");
55
59
  }
56
60
 
57
- 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(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 \"Cache-Control\": response.headers.get(\"Cache-Control\") ?? \"\"\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";
58
62
 
59
- const _debug = (id) => debug$2("crx").extend(id);
60
- const structuredClone = (obj) => {
61
- return v8.deserialize(v8.serialize(obj));
62
- };
63
+ const _debug = (id) => debug$3("crx").extend(id);
63
64
  const hash = (data, length = 5) => createHash("sha1").update(data).digest("base64").replace(/[^A-Za-z0-9]/g, "").slice(0, length);
64
65
  const isString = (x) => typeof x === "string";
65
66
  function isObject(value) {
@@ -202,7 +203,7 @@ function formatFileData(script) {
202
203
  return script;
203
204
  }
204
205
  function getFileName({ type, id }) {
205
- let fileName = id.replace(/t=\d+&/, "").replace(/^\//, "").replace(/\?/g, "__").replace(/&/g, "_").replace(/=/g, "--");
206
+ let fileName = id.replace(/t=\d+&/, "").replace(/\?t=\d+$/, "").replace(/^\//, "").replace(/\?/g, "__").replace(/&/g, "_").replace(/=/g, "--");
206
207
  if (fileName.includes("node_modules/")) {
207
208
  fileName = `vendor/${fileName.split("node_modules/").pop().replace(/\//g, "-")}`;
208
209
  } else if (fileName.startsWith("@")) {
@@ -270,6 +271,7 @@ const workerClientId = "/@crx/client-worker";
270
271
 
271
272
  const pluginBackground = () => {
272
273
  let config;
274
+ let browser;
273
275
  return [
274
276
  {
275
277
  name: "crx:background-client",
@@ -290,24 +292,39 @@ const pluginBackground = () => {
290
292
  },
291
293
  {
292
294
  name: "crx:background-loader-file",
295
+ // this should happen after other plugins; the loader file is an implementation detail
293
296
  enforce: "post",
297
+ async config(config2) {
298
+ const opts = await getOptions(config2);
299
+ browser = opts.browser || "chrome";
300
+ },
294
301
  configResolved(_config) {
295
302
  config = _config;
296
303
  },
297
304
  renderCrxManifest(manifest) {
298
- const worker = manifest.background?.service_worker;
305
+ const worker = browser === "firefox" ? manifest.background?.scripts[0] : manifest.background?.service_worker;
299
306
  let loader;
300
307
  if (config.command === "serve") {
301
308
  const port = config.server.port?.toString();
302
309
  if (typeof port === "undefined")
303
310
  throw new Error("server port is undefined in watch mode");
304
- loader = `import 'http:/localhost:${port}/@vite/env';
311
+ if (browser === "firefox") {
312
+ loader = `import('http://localhost:${port}/@vite/env');
313
+ `;
314
+ loader += `import('http://localhost:${port}${workerClientId}');
315
+ `;
316
+ if (worker)
317
+ loader += `import('http://localhost:${port}/${worker}');
318
+ `;
319
+ } else {
320
+ loader = `import 'http://localhost:${port}/@vite/env';
305
321
  `;
306
- loader += `import 'http://localhost:${port}${workerClientId}';
322
+ loader += `import 'http://localhost:${port}${workerClientId}';
307
323
  `;
308
- if (worker)
309
- loader += `import 'http://localhost:${port}/${worker}';
324
+ if (worker)
325
+ loader += `import 'http://localhost:${port}/${worker}';
310
326
  `;
327
+ }
311
328
  } else if (worker) {
312
329
  loader = `import './${worker}';
313
330
  `;
@@ -316,13 +333,21 @@ const pluginBackground = () => {
316
333
  }
317
334
  const refId = this.emitFile({
318
335
  type: "asset",
336
+ // fileName b/c service worker must be at root of crx
319
337
  fileName: getFileName({ type: "loader", id: "service-worker" }),
320
338
  source: loader
321
339
  });
322
- manifest.background = {
323
- service_worker: this.getFileName(refId),
324
- type: "module"
325
- };
340
+ if (browser !== "firefox") {
341
+ manifest.background = {
342
+ service_worker: this.getFileName(refId),
343
+ type: "module"
344
+ };
345
+ } else {
346
+ manifest.background = {
347
+ scripts: [this.getFileName(refId)],
348
+ type: "module"
349
+ };
350
+ }
326
351
  return manifest;
327
352
  }
328
353
  }
@@ -331,9 +356,9 @@ const pluginBackground = () => {
331
356
 
332
357
  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";
333
358
 
334
- 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";
359
+ 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";
335
360
 
336
- 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";
361
+ 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";
337
362
 
338
363
  const contentScripts = new RxMap();
339
364
  contentScripts.change$.pipe(filter(RxMap.isChangeType.set)).subscribe(({ map, value }) => {
@@ -391,6 +416,10 @@ const allFilesReady$ = buildEnd$.pipe(
391
416
  map(() => [...outputFiles.values()]),
392
417
  switchMap((files) => Promise.allSettled(files.map(({ file }) => file)))
393
418
  );
419
+ const timestamp$ = new BehaviorSubject(Date.now());
420
+ allFilesReady$.subscribe(() => {
421
+ timestamp$.next(Date.now());
422
+ });
394
423
  const isRejected = (x) => x?.status === "rejected";
395
424
  const fileWriterError$ = allFilesReady$.pipe(
396
425
  mergeMap((results) => results.filter(isRejected)),
@@ -424,16 +453,36 @@ function prepAsset(fileName, { id, source }) {
424
453
  }
425
454
  function prepScript(fileName, script) {
426
455
  return ($) => $.pipe(
456
+ // get script contents from dev server
427
457
  mergeMap(async ({ server }) => {
428
458
  const target = getOutputPath(server, fileName);
429
459
  const viteUrl = getViteUrl(script);
430
460
  const transformResult = await server.transformRequest(viteUrl);
431
461
  if (!transformResult)
432
462
  throw new TypeError(`Unable to load "${script.id}" from server.`);
433
- const { code, deps = [], dynamicDeps = [] } = transformResult;
434
- return { target, code, deps: [...deps, ...dynamicDeps].flat(), server };
463
+ const { deps = [], dynamicDeps = [], map: map2 } = transformResult;
464
+ let { code } = transformResult;
465
+ try {
466
+ if (map2 && server.config.build.sourcemap === "inline") {
467
+ code = code.replace(/\n*\/\/# sourceMappingURL=[^\n]+/g, "");
468
+ const sourceMap = convertSourceMap.fromObject(map2).toComment();
469
+ code += `
470
+ ${sourceMap}
471
+ `;
472
+ }
473
+ } catch (error) {
474
+ console.warn("Failed to inline source map", error);
475
+ }
476
+ return {
477
+ target,
478
+ code,
479
+ deps: [...deps, ...dynamicDeps].flat(),
480
+ server
481
+ };
435
482
  }),
483
+ // retry in case of dependency rebundle
436
484
  retry({ count: 10, delay: 100 }),
485
+ // patch content scripts
437
486
  mergeMap(async ({ target, server, ...rest }) => {
438
487
  const plugins = server.config.plugins;
439
488
  let { code, deps } = rest;
@@ -453,7 +502,8 @@ function prepScript(fileName, script) {
453
502
  if (i.n) {
454
503
  depSet.add(i.n);
455
504
  const fileName2 = getFileName({ type: "module", id: i.n });
456
- magic.overwrite(i.s, i.e, `/${fileName2}`);
505
+ const fullImport = code.substring(i.s, i.e);
506
+ magic.overwrite(i.s, i.e, fullImport.replace(i.n, `/${fileName2}`));
457
507
  }
458
508
  return { target, source: magic.toString(), deps: [...depSet] };
459
509
  })
@@ -524,8 +574,11 @@ function update(_id) {
524
574
  async function write(fileId) {
525
575
  const start2 = performance.now();
526
576
  const deps = await firstValueFrom(
577
+ // wait for start event
527
578
  start$.pipe(
579
+ // prepare either asset or script contents
528
580
  prepFileData(fileId),
581
+ // output file and add dependencies to file writer
529
582
  mergeMap(async ({ target, source, deps: deps2 }) => {
530
583
  const files = deps2.map((id) => {
531
584
  const r = [add({ id, type: "module" })];
@@ -541,6 +594,7 @@ async function write(fileId) {
541
594
  await outputFile(target, source, { encoding: "utf8" });
542
595
  return files;
543
596
  }),
597
+ // abort write operation on close event
544
598
  takeUntil(close$),
545
599
  concatWith(of([]))
546
600
  )
@@ -558,15 +612,15 @@ const pluginContentScripts = () => {
558
612
  {
559
613
  name: "crx:content-scripts",
560
614
  apply: "serve",
561
- config(config) {
562
- const { contentScripts: contentScripts2 = {} } = getOptions(config);
615
+ async config(config) {
616
+ const { contentScripts: contentScripts2 = {} } = await getOptions(config);
563
617
  hmrTimeout = contentScripts2.hmrTimeout ?? 5e3;
564
618
  preambleCode = preambleCode ?? contentScripts2.preambleCode;
565
619
  },
566
620
  async configureServer(_server) {
567
621
  server = _server;
568
622
  if (typeof preambleCode === "undefined" && server.config.plugins.some(
569
- ({ name = "none" }) => name.toLowerCase().includes("react")
623
+ ({ name = "none" }) => name.toLowerCase().includes("react") && !name.toLowerCase().includes("preact")
570
624
  )) {
571
625
  try {
572
626
  const react = await import('@vitejs/plugin-react');
@@ -631,6 +685,19 @@ const pluginContentScripts = () => {
631
685
  name: "crx:content-scripts",
632
686
  apply: "build",
633
687
  enforce: "pre",
688
+ config(config) {
689
+ return {
690
+ ...config,
691
+ build: {
692
+ ...config.build,
693
+ rollupOptions: {
694
+ ...config.build?.rollupOptions,
695
+ // keep exports for content script module api
696
+ preserveEntrySignatures: config.build?.rollupOptions?.preserveEntrySignatures ?? "exports-only"
697
+ }
698
+ }
699
+ };
700
+ },
634
701
  generateBundle() {
635
702
  for (const [key, script] of contentScripts)
636
703
  if (key === script.refId) {
@@ -661,8 +728,8 @@ const pluginContentScriptsCss = () => {
661
728
  return {
662
729
  name: "crx:content-scripts-css",
663
730
  enforce: "post",
664
- config(config) {
665
- const { contentScripts: contentScripts2 = {} } = getOptions(config);
731
+ async config(config) {
732
+ const { contentScripts: contentScripts2 = {} } = await getOptions(config);
666
733
  injectCss = contentScripts2.injectCss ?? true;
667
734
  },
668
735
  renderCrxManifest(manifest) {
@@ -691,7 +758,7 @@ const pluginDeclaredContentScripts = () => {
691
758
  return [];
692
759
  };
693
760
 
694
- const _dynamicScriptRegEx = /\b(import.meta).CRX_DYNAMIC_SCRIPT_(.+?);$/gm;
761
+ const _dynamicScriptRegEx = /\b(import.meta).CRX_DYNAMIC_SCRIPT_(.+?)[,;]/gm;
695
762
  const dynamicScriptRegEx = () => {
696
763
  _dynamicScriptRegEx.lastIndex = 0;
697
764
  return _dynamicScriptRegEx;
@@ -705,6 +772,34 @@ const pluginDynamicContentScripts = () => {
705
772
  configResolved(_config) {
706
773
  config = _config;
707
774
  },
775
+ configureServer(server) {
776
+ return () => {
777
+ server.middlewares.use(async (req, res, next) => {
778
+ try {
779
+ await allFilesReady();
780
+ next();
781
+ } catch (error) {
782
+ let err;
783
+ if (error instanceof Error) {
784
+ err = error;
785
+ } else if (typeof error === "string") {
786
+ err = new Error(error);
787
+ } else {
788
+ err = new Error(
789
+ `Unexpected error "${error}" in middleware for "${req.url}"`
790
+ );
791
+ }
792
+ server.ws.send({
793
+ type: "error",
794
+ err: {
795
+ message: err.message,
796
+ stack: err.stack ?? "no stack available"
797
+ }
798
+ });
799
+ }
800
+ });
801
+ };
802
+ },
708
803
  async resolveId(_source, importer) {
709
804
  if (importer && _source.includes("?script")) {
710
805
  const url = new URL(_source, "stub://stub");
@@ -773,7 +868,6 @@ const pluginDynamicContentScripts = () => {
773
868
  if (config.command === "build") {
774
869
  return `export default import.meta.CRX_DYNAMIC_SCRIPT_${script.refId};`;
775
870
  } else if (typeof script.fileName === "string") {
776
- await fileReady(script);
777
871
  return `export default ${JSON.stringify(script.fileName)};`;
778
872
  } else {
779
873
  throw new Error(
@@ -786,6 +880,12 @@ const pluginDynamicContentScripts = () => {
786
880
  {
787
881
  name: "crx:dynamic-content-scripts-build",
788
882
  apply: "build",
883
+ /**
884
+ * Replace dynamic script placeholders during build.
885
+ *
886
+ * Can't use `renderChunk` b/c pre plugin crx:content-scripts uses
887
+ * `generateBundle` to emit loaders. Must come after "enforce: pre".
888
+ */
789
889
  generateBundle(options, bundle) {
790
890
  for (const chunk of Object.values(bundle))
791
891
  if (chunk.type === "chunk") {
@@ -804,7 +904,7 @@ const pluginDynamicContentScripts = () => {
804
904
  );
805
905
  return `${JSON.stringify(
806
906
  `/${script.loaderName ?? script.fileName}`
807
- )};`;
907
+ )}${match.split(scriptKey)[1]}`;
808
908
  }
809
909
  );
810
910
  chunk.code = replaced;
@@ -815,29 +915,42 @@ const pluginDynamicContentScripts = () => {
815
915
  ];
816
916
  };
817
917
 
918
+ const { remove } = fsx;
818
919
  const logger = createLogger("error", { prefix: "crxjs" });
819
920
  const pluginFileWriter = () => {
820
921
  fileWriterError$.subscribe((error) => {
821
922
  logger.error(error.err.message, { error: error.err });
822
923
  });
823
- return {
824
- name: "crx:file-writer",
825
- apply: "serve",
826
- configureServer(server) {
827
- server.httpServer?.on("listening", async () => {
828
- try {
829
- await start({ server });
830
- } catch (error) {
831
- console.error(error);
832
- server.close();
924
+ return [
925
+ {
926
+ name: "crx:file-writer-empty-out-dir",
927
+ apply: "serve",
928
+ enforce: "pre",
929
+ async configResolved(config) {
930
+ if (config.build.emptyOutDir) {
931
+ await remove(config.build.outDir);
833
932
  }
834
- });
835
- server.httpServer?.on("close", () => close());
933
+ }
836
934
  },
837
- closeBundle() {
838
- outputFiles.clear();
935
+ {
936
+ name: "crx:file-writer",
937
+ apply: "serve",
938
+ configureServer(server) {
939
+ server.httpServer?.on("listening", async () => {
940
+ try {
941
+ await start({ server });
942
+ } catch (error) {
943
+ console.error(error);
944
+ server.close();
945
+ }
946
+ });
947
+ server.httpServer?.on("close", () => close());
948
+ },
949
+ closeBundle() {
950
+ outputFiles.clear();
951
+ }
839
952
  }
840
- };
953
+ ];
841
954
  };
842
955
 
843
956
  const _require = typeof require === "undefined" ? createRequire(import.meta.url) : require;
@@ -882,7 +995,9 @@ async function manifestFiles(manifest, options = {}) {
882
995
  ) ?? [];
883
996
  const contentScripts = manifest.content_scripts?.flatMap(({ js }) => js) ?? [];
884
997
  const contentStyles = manifest.content_scripts?.flatMap(({ css }) => css);
885
- const serviceWorker = manifest.background?.service_worker;
998
+ const serviceWorker = manifest.background && "service_worker" in manifest.background ? manifest.background.service_worker : void 0;
999
+ const backgroundScripts = manifest.background && "scripts" in manifest.background ? manifest.background.scripts : void 0;
1000
+ const background = serviceWorker ? [serviceWorker].filter(isString) : backgroundScripts ? backgroundScripts.filter(isString) : [];
886
1001
  const htmlPages = htmlFiles(manifest);
887
1002
  const icons = [
888
1003
  Object.values(
@@ -903,7 +1018,7 @@ async function manifestFiles(manifest, options = {}) {
903
1018
  return r;
904
1019
  })
905
1020
  );
906
- webAccessibleResources = resources.flat().filter(isString);
1021
+ webAccessibleResources = [...new Set(resources.flat())].filter(isString);
907
1022
  }
908
1023
  return {
909
1024
  contentScripts: [...new Set(contentScripts)].filter(isString),
@@ -912,7 +1027,7 @@ async function manifestFiles(manifest, options = {}) {
912
1027
  icons: [...new Set(icons)].filter(isString),
913
1028
  locales: [...new Set(locales)].filter(isString),
914
1029
  rulesets: [...new Set(rulesets)].filter(isString),
915
- background: [serviceWorker].filter(isString),
1030
+ background,
916
1031
  webAccessibleResources
917
1032
  };
918
1033
  }
@@ -927,7 +1042,8 @@ function htmlFiles(manifest) {
927
1042
  manifest.devtools_page,
928
1043
  manifest.options_page,
929
1044
  manifest.options_ui?.page,
930
- manifest.sandbox?.pages
1045
+ manifest.sandbox?.pages,
1046
+ manifest.side_panel?.default_path
931
1047
  ].flat().filter(isString).map((s) => s.split("#")[0]).sort();
932
1048
  return [...new Set(files)];
933
1049
  }
@@ -952,11 +1068,13 @@ const pluginFileWriterPublic = () => {
952
1068
  };
953
1069
  };
954
1070
 
955
- _debug("file-writer").extend("hmr");
956
- const isCrxHMRPayload = (p) => p.type === "custom" && p.event.startsWith("crx:");
1071
+ const debug$2 = _debug("file-writer").extend("hmr");
1072
+ const isCustomPayload = (p) => {
1073
+ return p.type === "custom";
1074
+ };
957
1075
  const hmrPayload$ = new Subject();
958
1076
  const crxHMRPayload$ = hmrPayload$.pipe(
959
- filter((p) => !isCrxHMRPayload(p)),
1077
+ filter((p) => !isCustomPayload(p)),
960
1078
  buffer(allFilesReady$),
961
1079
  mergeMap((pps) => {
962
1080
  let fullReload;
@@ -1015,6 +1133,7 @@ const crxHMRPayload$ = hmrPayload$.pipe(
1015
1133
  }
1016
1134
  }),
1017
1135
  map((data) => {
1136
+ debug$2(`hmr payload`, data);
1018
1137
  return {
1019
1138
  type: "custom",
1020
1139
  event: "crx:content-script-payload",
@@ -1054,6 +1173,7 @@ const pluginHMR = () => {
1054
1173
  name: "crx:hmr",
1055
1174
  apply: "serve",
1056
1175
  enforce: "pre",
1176
+ // server hmr host should be localhost
1057
1177
  async config({ server = {}, ...config2 }) {
1058
1178
  if (server.hmr === false)
1059
1179
  return;
@@ -1061,9 +1181,9 @@ const pluginHMR = () => {
1061
1181
  server.hmr = {};
1062
1182
  server.hmr = server.hmr ?? {};
1063
1183
  server.hmr.host = "localhost";
1064
- server.hmr.port = server.hmr.port ?? await getPort({ port: portNumbers(5200, 5300) });
1065
1184
  return { server, ...config2 };
1066
1185
  },
1186
+ // server should ignore outdir
1067
1187
  configResolved(_config) {
1068
1188
  config = _config;
1069
1189
  const { watch = {} } = config.server;
@@ -1101,19 +1221,24 @@ const pluginHMR = () => {
1101
1221
  closeBundle() {
1102
1222
  subs.unsubscribe();
1103
1223
  },
1224
+ // background changes require a full extension reload
1104
1225
  handleHotUpdate({ modules, server }) {
1105
1226
  const { root } = server.config;
1106
1227
  const relFiles = /* @__PURE__ */ new Set();
1107
- for (const m of modules)
1228
+ const fsFiles = /* @__PURE__ */ new Set();
1229
+ for (const m of modules) {
1108
1230
  if (m.id?.startsWith(root)) {
1109
1231
  relFiles.add(m.id.slice(server.config.root.length));
1232
+ } else if (m.url?.startsWith("/@fs")) {
1233
+ fsFiles.add(m.url);
1110
1234
  }
1235
+ }
1236
+ fsFiles.forEach((file) => update(file));
1111
1237
  if (inputManifestFiles.background.length) {
1112
1238
  const background = prefix$1("/", inputManifestFiles.background[0]);
1113
1239
  if (relFiles.has(background) || modules.some(isImporter(join(server.config.root, background)))) {
1114
1240
  debug$1("sending runtime reload");
1115
1241
  server.ws.send(crxRuntimeReload);
1116
- return [];
1117
1242
  }
1118
1243
  }
1119
1244
  for (const [key, script] of contentScripts)
@@ -1128,6 +1253,7 @@ const pluginHMR = () => {
1128
1253
  name: "crx:hmr",
1129
1254
  apply: "serve",
1130
1255
  enforce: "post",
1256
+ // get final output manifest for handleHotUpdate 👆
1131
1257
  async transformCrxManifest(manifest) {
1132
1258
  inputManifestFiles = await manifestFiles(manifest, { cwd: config.root });
1133
1259
  return null;
@@ -1218,6 +1344,7 @@ const pluginHtmlInlineScripts = () => {
1218
1344
  };
1219
1345
  const postPlugin = {
1220
1346
  name: "crx:html-auditor-post",
1347
+ // this hook isn't audited b/c we add it after we set up the auditors
1221
1348
  transformIndexHtml(html, ctx) {
1222
1349
  const key = toKey(ctx);
1223
1350
  const p = pages.get(key);
@@ -1283,9 +1410,9 @@ const pluginHtmlInlineScripts = () => {
1283
1410
  };
1284
1411
  };
1285
1412
 
1286
- var precontrollerJs = "const id = setInterval(() => location.reload(), 100);\nsetTimeout(() => clearInterval(id), 5e3);\n";
1413
+ 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";
1287
1414
 
1288
- 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";
1415
+ 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";
1289
1416
 
1290
1417
  const { readFile } = promises;
1291
1418
  const pluginManifest = () => {
@@ -1344,7 +1471,7 @@ const pluginManifest = () => {
1344
1471
  name: "crx:manifest-loader",
1345
1472
  enforce: "pre",
1346
1473
  buildStart(options) {
1347
- if (typeof options.input !== "undefined") {
1474
+ if (typeof options.input !== "undefined" && !("ssr" in this)) {
1348
1475
  refId = this.emitFile({
1349
1476
  type: "chunk",
1350
1477
  id: manifestId,
@@ -1464,7 +1591,7 @@ const pluginManifest = () => {
1464
1591
  })
1465
1592
  );
1466
1593
  }
1467
- if (manifest2.background?.service_worker) {
1594
+ if (manifest2.background && "service_worker" in manifest2.background) {
1468
1595
  const file = manifest2.background.service_worker;
1469
1596
  const id2 = join(config.root, file);
1470
1597
  const refId2 = this.emitFile({
@@ -1474,6 +1601,16 @@ const pluginManifest = () => {
1474
1601
  });
1475
1602
  manifest2.background.service_worker = refId2;
1476
1603
  }
1604
+ if (manifest2.background && "scripts" in manifest2.background) {
1605
+ const file = manifest2.background.scripts[0];
1606
+ const id2 = join(config.root, file);
1607
+ const refId2 = this.emitFile({
1608
+ type: "chunk",
1609
+ id: id2,
1610
+ name: basename(file)
1611
+ });
1612
+ manifest2.background.scripts = [refId2];
1613
+ }
1477
1614
  for (const file of htmlFiles(manifest2)) {
1478
1615
  const id2 = join(config.root, file);
1479
1616
  this.emitFile({
@@ -1484,7 +1621,7 @@ const pluginManifest = () => {
1484
1621
  }
1485
1622
  }
1486
1623
  const encoded = encodeManifest(manifest2);
1487
- return encoded;
1624
+ return { code: encoded, map: null };
1488
1625
  },
1489
1626
  async generateBundle(options, bundle) {
1490
1627
  const manifestName = this.getFileName(refId);
@@ -1498,11 +1635,16 @@ const pluginManifest = () => {
1498
1635
  );
1499
1636
  }
1500
1637
  } else {
1501
- if (manifest2.background?.service_worker) {
1638
+ if (manifest2.background && "service_worker" in manifest2.background) {
1502
1639
  const ref = manifest2.background.service_worker;
1503
1640
  const name = this.getFileName(ref);
1504
1641
  manifest2.background.service_worker = name;
1505
1642
  }
1643
+ if (manifest2.background && "scripts" in manifest2.background) {
1644
+ const ref = manifest2.background.scripts[0];
1645
+ const name = this.getFileName(ref);
1646
+ manifest2.background.scripts = [name];
1647
+ }
1506
1648
  manifest2.content_scripts = manifest2.content_scripts?.map(
1507
1649
  ({ js = [], ...rest }) => {
1508
1650
  return {
@@ -1562,6 +1704,7 @@ Public dir: "${config.publicDir}"`
1562
1704
  this.emitFile({
1563
1705
  type: "asset",
1564
1706
  fileName: f,
1707
+ // TODO: cache source buffer
1565
1708
  source: await readFile(filename)
1566
1709
  });
1567
1710
  }
@@ -1570,17 +1713,20 @@ Public dir: "${config.publicDir}"`
1570
1713
  if (config.command === "serve" && files.html.length) {
1571
1714
  const refId2 = this.emitFile({
1572
1715
  type: "asset",
1573
- name: "precontroller.js",
1574
- source: precontrollerJs
1716
+ name: "loading-page.js",
1717
+ source: loadingPageScript.replace(
1718
+ "%PORT%",
1719
+ `${config.server.port ?? 0}`
1720
+ )
1575
1721
  });
1576
- const precontrollerJsName = this.getFileName(refId2);
1722
+ const loadingPageScriptName = this.getFileName(refId2);
1577
1723
  files.html.map(
1578
1724
  (f) => this.emitFile({
1579
1725
  type: "asset",
1580
1726
  fileName: f,
1581
- source: precontrollerHtml.replace(
1727
+ source: loadingPageHtml.replace(
1582
1728
  "%SCRIPT%",
1583
- `/${precontrollerJsName}`
1729
+ `/${loadingPageScriptName}`
1584
1730
  )
1585
1731
  })
1586
1732
  );
@@ -1590,10 +1736,10 @@ Public dir: "${config.publicDir}"`
1590
1736
  this.emitFile({
1591
1737
  type: "asset",
1592
1738
  fileName: "manifest.json",
1593
- source: JSON.stringify(manifest2, null, 2)
1739
+ source: JSON.stringify(manifest2, null, 2) + "\n"
1594
1740
  });
1595
1741
  } else {
1596
- manifestJson.source = JSON.stringify(manifest2, null, 2);
1742
+ manifestJson.source = JSON.stringify(manifest2, null, 2) + "\n";
1597
1743
  }
1598
1744
  delete bundle[manifestName];
1599
1745
  }
@@ -1609,7 +1755,11 @@ function compileFileResources(fileName, {
1609
1755
  assets: /* @__PURE__ */ new Set(),
1610
1756
  css: /* @__PURE__ */ new Set(),
1611
1757
  imports: /* @__PURE__ */ new Set()
1612
- }) {
1758
+ }, processedFiles = /* @__PURE__ */ new Set()) {
1759
+ if (processedFiles.has(fileName)) {
1760
+ return resources;
1761
+ }
1762
+ processedFiles.add(fileName);
1613
1763
  const chunk = chunks.get(fileName);
1614
1764
  if (chunk) {
1615
1765
  const { modules, facadeModuleId, imports, dynamicImports } = chunk;
@@ -1618,7 +1768,7 @@ function compileFileResources(fileName, {
1618
1768
  for (const x of dynamicImports)
1619
1769
  resources.imports.add(x);
1620
1770
  for (const x of [...imports, ...dynamicImports])
1621
- compileFileResources(x, { chunks, files, config }, resources);
1771
+ compileFileResources(x, { chunks, files, config }, resources, processedFiles);
1622
1772
  for (const m of Object.keys(modules))
1623
1773
  if (m !== facadeModuleId) {
1624
1774
  const key = prefix$1("/", relative(config.root, m.split("?")[0]));
@@ -1631,7 +1781,8 @@ function compileFileResources(fileName, {
1631
1781
  compileFileResources(
1632
1782
  script.fileName,
1633
1783
  { chunks, files, config },
1634
- resources
1784
+ resources,
1785
+ processedFiles
1635
1786
  );
1636
1787
  }
1637
1788
  }
@@ -1650,7 +1801,7 @@ function compileFileResources(fileName, {
1650
1801
  const defineManifest = (manifest) => manifest;
1651
1802
  const defineDynamicResource = ({
1652
1803
  matches = ["http://*/*", "https://*/*"],
1653
- use_dynamic_url = true
1804
+ use_dynamic_url = false
1654
1805
  }) => ({
1655
1806
  matches,
1656
1807
  resources: [DYNAMIC_RESOURCE],
@@ -1662,22 +1813,34 @@ _debug("web-acc-res");
1662
1813
  const pluginWebAccessibleResources = () => {
1663
1814
  let config;
1664
1815
  let injectCss;
1816
+ let browser;
1665
1817
  return [
1666
1818
  {
1667
1819
  name: "crx:web-accessible-resources",
1668
1820
  apply: "serve",
1669
1821
  enforce: "post",
1822
+ async config(config2) {
1823
+ const opts = await getOptions(config2);
1824
+ browser = opts.browser || "chrome";
1825
+ },
1670
1826
  renderCrxManifest(manifest) {
1671
1827
  manifest.web_accessible_resources = manifest.web_accessible_resources ?? [];
1672
1828
  manifest.web_accessible_resources = manifest.web_accessible_resources.map(({ resources, ...rest }) => ({
1673
1829
  resources: resources.filter((r) => r !== DYNAMIC_RESOURCE),
1674
1830
  ...rest
1675
1831
  })).filter(({ resources }) => resources.length);
1676
- manifest.web_accessible_resources.push({
1677
- use_dynamic_url: true,
1832
+ const war = {
1833
+ // all web origins can access
1678
1834
  matches: ["<all_urls>"],
1679
- resources: ["**/*", "*"]
1680
- });
1835
+ // all resources are web accessible
1836
+ resources: ["**/*", "*"],
1837
+ // change the extension origin on every reload
1838
+ use_dynamic_url: false
1839
+ };
1840
+ if (browser === "firefox") {
1841
+ delete war.use_dynamic_url;
1842
+ }
1843
+ manifest.web_accessible_resources.push(war);
1681
1844
  return manifest;
1682
1845
  }
1683
1846
  },
@@ -1686,7 +1849,9 @@ const pluginWebAccessibleResources = () => {
1686
1849
  apply: "build",
1687
1850
  enforce: "post",
1688
1851
  async config({ build, ...config2 }, { command }) {
1689
- const { contentScripts: contentScripts2 = {} } = await getOptions(config2);
1852
+ const opts = await getOptions(config2);
1853
+ const contentScripts2 = opts.contentScripts || {};
1854
+ browser = opts.browser || "chrome";
1690
1855
  injectCss = contentScripts2.injectCss ?? true;
1691
1856
  return { ...config2, build: { ...build, manifest: command === "build" } };
1692
1857
  },
@@ -1715,9 +1880,11 @@ const pluginWebAccessibleResources = () => {
1715
1880
  dynamicScriptMatches.add("https://*/*");
1716
1881
  }
1717
1882
  if (contentScripts.size > 0) {
1883
+ const viteMajorVersion = parseInt(version.split(".")[0]);
1884
+ const manifestPath = viteMajorVersion > 4 ? ".vite/manifest.json" : "manifest.json";
1718
1885
  const viteManifest = parseJsonAsset(
1719
1886
  bundle,
1720
- "manifest.json"
1887
+ manifestPath
1721
1888
  );
1722
1889
  const viteFiles = /* @__PURE__ */ new Map();
1723
1890
  for (const [, file] of Object.entries(viteManifest))
@@ -1745,12 +1912,12 @@ const pluginWebAccessibleResources = () => {
1745
1912
  { chunks: bundleChunks, files: viteFiles, config }
1746
1913
  );
1747
1914
  contentScripts.get(key).css = [...css];
1748
- if (type === "loader")
1915
+ if (type === "loader" || isDynamicScript)
1749
1916
  imports.add(fileName);
1750
1917
  const resource = {
1751
1918
  matches: isDynamicScript ? [...dynamicScriptMatches] : matches,
1752
1919
  resources: [...assets, ...imports],
1753
- use_dynamic_url: isDynamicScript ? dynamicScriptDynamicUrl : true
1920
+ use_dynamic_url: isDynamicScript ? dynamicScriptDynamicUrl : false
1754
1921
  };
1755
1922
  if (isDynamicScript || !injectCss) {
1756
1923
  resource.resources.push(...css);
@@ -1794,6 +1961,11 @@ const pluginWebAccessibleResources = () => {
1794
1961
  use_dynamic_url
1795
1962
  });
1796
1963
  }
1964
+ if (browser === "firefox") {
1965
+ for (const war of combinedResources) {
1966
+ delete war.use_dynamic_url;
1967
+ }
1968
+ }
1797
1969
  if (combinedResources.length === 0)
1798
1970
  delete manifest.web_accessible_resources;
1799
1971
  else
@@ -1805,6 +1977,7 @@ const pluginWebAccessibleResources = () => {
1805
1977
  };
1806
1978
 
1807
1979
  const crx = (options) => {
1980
+ contentScripts.clear();
1808
1981
  return [
1809
1982
  pluginOptionsProvider(options),
1810
1983
  pluginBackground(),