@crxjs/vite-plugin 2.0.0-beta.2 → 2.0.0-beta.22

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,20 @@
1
1
  import { simple } from 'acorn-walk';
2
2
  import { createHash } from 'crypto';
3
- import debug$2 from 'debug';
3
+ import debug$3 from 'debug';
4
4
  import v8 from 'v8';
5
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';
6
+ import { Subject, filter, ReplaySubject, switchMap, of, startWith, map, BehaviorSubject, mergeMap, firstValueFrom, takeUntil, first, toArray, retry, concatWith, Subscription, buffer } from 'rxjs';
7
7
  import fsx from 'fs-extra';
8
8
  import { performance } from 'perf_hooks';
9
9
  import { rollup } from 'rollup';
10
10
  import * as lexer from 'es-module-lexer';
11
11
  import { readFile as readFile$1 } from 'fs/promises';
12
12
  import MagicString from 'magic-string';
13
- import { createLogger } from 'vite';
13
+ import convertSourceMap from 'convert-source-map';
14
+ import { createLogger, version } from 'vite';
14
15
  import { readFileSync, existsSync, promises } from 'fs';
15
16
  import { createRequire } from 'module';
16
17
  import fg from 'fast-glob';
17
- import getPort, { portNumbers } from 'get-port';
18
18
  import { load } from 'cheerio';
19
19
  import jsesc from 'jsesc';
20
20
  import colors from 'picocolors';
@@ -25,17 +25,22 @@ const pluginOptionsProvider = (options) => {
25
25
  name: pluginName$1,
26
26
  api: {
27
27
  crx: {
28
+ // during testing this can be null, we don't provide options through the test config
28
29
  options
29
30
  }
31
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
30
32
  }
31
33
  };
32
34
  };
33
- const getOptions = ({ plugins }) => {
35
+ const getOptions = async ({
36
+ plugins
37
+ }) => {
34
38
  if (typeof plugins === "undefined") {
35
39
  throw new Error("config.plugins is undefined");
36
40
  }
41
+ const awaitedPlugins = await Promise.all(plugins);
37
42
  let options;
38
- for (const p of plugins.flat()) {
43
+ for (const p of awaitedPlugins.flat()) {
39
44
  if (isCrxPlugin(p)) {
40
45
  if (p.name === pluginName$1) {
41
46
  const plugin = p;
@@ -56,7 +61,7 @@ function isCrxPlugin(p) {
56
61
 
57
62
  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";
58
63
 
59
- const _debug = (id) => debug$2("crx").extend(id);
64
+ const _debug = (id) => debug$3("crx").extend(id);
60
65
  const structuredClone = (obj) => {
61
66
  return v8.deserialize(v8.serialize(obj));
62
67
  };
@@ -202,7 +207,7 @@ function formatFileData(script) {
202
207
  return script;
203
208
  }
204
209
  function getFileName({ type, id }) {
205
- let fileName = id.replace(/t=\d+&/, "").replace(/^\//, "").replace(/\?/g, "__").replace(/&/g, "_").replace(/=/g, "--");
210
+ let fileName = id.replace(/t=\d+&/, "").replace(/\?t=\d+$/, "").replace(/^\//, "").replace(/\?/g, "__").replace(/&/g, "_").replace(/=/g, "--");
206
211
  if (fileName.includes("node_modules/")) {
207
212
  fileName = `vendor/${fileName.split("node_modules/").pop().replace(/\//g, "-")}`;
208
213
  } else if (fileName.startsWith("@")) {
@@ -270,6 +275,7 @@ const workerClientId = "/@crx/client-worker";
270
275
 
271
276
  const pluginBackground = () => {
272
277
  let config;
278
+ let browser;
273
279
  return [
274
280
  {
275
281
  name: "crx:background-client",
@@ -290,24 +296,39 @@ const pluginBackground = () => {
290
296
  },
291
297
  {
292
298
  name: "crx:background-loader-file",
299
+ // this should happen after other plugins; the loader file is an implementation detail
293
300
  enforce: "post",
301
+ async config(config2) {
302
+ const opts = await getOptions(config2);
303
+ browser = opts.browser || "chrome";
304
+ },
294
305
  configResolved(_config) {
295
306
  config = _config;
296
307
  },
297
308
  renderCrxManifest(manifest) {
298
- const worker = manifest.background?.service_worker;
309
+ const worker = browser === "firefox" ? manifest.background?.scripts[0] : manifest.background?.service_worker;
299
310
  let loader;
300
311
  if (config.command === "serve") {
301
312
  const port = config.server.port?.toString();
302
313
  if (typeof port === "undefined")
303
314
  throw new Error("server port is undefined in watch mode");
304
- loader = `import 'http:/localhost:${port}/@vite/env';
315
+ if (browser === "firefox") {
316
+ loader = `import('http://localhost:${port}/@vite/env');
317
+ `;
318
+ loader += `import('http://localhost:${port}${workerClientId}');
319
+ `;
320
+ if (worker)
321
+ loader += `import('http://localhost:${port}/${worker}');
322
+ `;
323
+ } else {
324
+ loader = `import 'http://localhost:${port}/@vite/env';
305
325
  `;
306
- loader += `import 'http://localhost:${port}${workerClientId}';
326
+ loader += `import 'http://localhost:${port}${workerClientId}';
307
327
  `;
308
- if (worker)
309
- loader += `import 'http://localhost:${port}/${worker}';
328
+ if (worker)
329
+ loader += `import 'http://localhost:${port}/${worker}';
310
330
  `;
331
+ }
311
332
  } else if (worker) {
312
333
  loader = `import './${worker}';
313
334
  `;
@@ -316,13 +337,21 @@ const pluginBackground = () => {
316
337
  }
317
338
  const refId = this.emitFile({
318
339
  type: "asset",
340
+ // fileName b/c service worker must be at root of crx
319
341
  fileName: getFileName({ type: "loader", id: "service-worker" }),
320
342
  source: loader
321
343
  });
322
- manifest.background = {
323
- service_worker: this.getFileName(refId),
324
- type: "module"
325
- };
344
+ if (browser !== "firefox") {
345
+ manifest.background = {
346
+ service_worker: this.getFileName(refId),
347
+ type: "module"
348
+ };
349
+ } else {
350
+ manifest.background = {
351
+ scripts: [this.getFileName(refId)],
352
+ type: "module"
353
+ };
354
+ }
326
355
  return manifest;
327
356
  }
328
357
  }
@@ -331,9 +360,9 @@ const pluginBackground = () => {
331
360
 
332
361
  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
362
 
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";
363
+ 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
364
 
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";
365
+ 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
366
 
338
367
  const contentScripts = new RxMap();
339
368
  contentScripts.change$.pipe(filter(RxMap.isChangeType.set)).subscribe(({ map, value }) => {
@@ -391,6 +420,10 @@ const allFilesReady$ = buildEnd$.pipe(
391
420
  map(() => [...outputFiles.values()]),
392
421
  switchMap((files) => Promise.allSettled(files.map(({ file }) => file)))
393
422
  );
423
+ const timestamp$ = new BehaviorSubject(Date.now());
424
+ allFilesReady$.subscribe(() => {
425
+ timestamp$.next(Date.now());
426
+ });
394
427
  const isRejected = (x) => x?.status === "rejected";
395
428
  const fileWriterError$ = allFilesReady$.pipe(
396
429
  mergeMap((results) => results.filter(isRejected)),
@@ -424,16 +457,36 @@ function prepAsset(fileName, { id, source }) {
424
457
  }
425
458
  function prepScript(fileName, script) {
426
459
  return ($) => $.pipe(
460
+ // get script contents from dev server
427
461
  mergeMap(async ({ server }) => {
428
462
  const target = getOutputPath(server, fileName);
429
463
  const viteUrl = getViteUrl(script);
430
464
  const transformResult = await server.transformRequest(viteUrl);
431
465
  if (!transformResult)
432
466
  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 };
467
+ const { deps = [], dynamicDeps = [], map: map2 } = transformResult;
468
+ let { code } = transformResult;
469
+ try {
470
+ if (map2 && server.config.build.sourcemap === "inline") {
471
+ code = code.replace(/\n*\/\/# sourceMappingURL=[^\n]+/g, "");
472
+ const sourceMap = convertSourceMap.fromObject(map2).toComment();
473
+ code += `
474
+ ${sourceMap}
475
+ `;
476
+ }
477
+ } catch (error) {
478
+ console.warn("Failed to inline source map", error);
479
+ }
480
+ return {
481
+ target,
482
+ code,
483
+ deps: [...deps, ...dynamicDeps].flat(),
484
+ server
485
+ };
435
486
  }),
487
+ // retry in case of dependency rebundle
436
488
  retry({ count: 10, delay: 100 }),
489
+ // patch content scripts
437
490
  mergeMap(async ({ target, server, ...rest }) => {
438
491
  const plugins = server.config.plugins;
439
492
  let { code, deps } = rest;
@@ -453,7 +506,8 @@ function prepScript(fileName, script) {
453
506
  if (i.n) {
454
507
  depSet.add(i.n);
455
508
  const fileName2 = getFileName({ type: "module", id: i.n });
456
- magic.overwrite(i.s, i.e, `/${fileName2}`);
509
+ const fullImport = code.substring(i.s, i.e);
510
+ magic.overwrite(i.s, i.e, fullImport.replace(i.n, `/${fileName2}`));
457
511
  }
458
512
  return { target, source: magic.toString(), deps: [...depSet] };
459
513
  })
@@ -524,8 +578,11 @@ function update(_id) {
524
578
  async function write(fileId) {
525
579
  const start2 = performance.now();
526
580
  const deps = await firstValueFrom(
581
+ // wait for start event
527
582
  start$.pipe(
583
+ // prepare either asset or script contents
528
584
  prepFileData(fileId),
585
+ // output file and add dependencies to file writer
529
586
  mergeMap(async ({ target, source, deps: deps2 }) => {
530
587
  const files = deps2.map((id) => {
531
588
  const r = [add({ id, type: "module" })];
@@ -541,6 +598,7 @@ async function write(fileId) {
541
598
  await outputFile(target, source, { encoding: "utf8" });
542
599
  return files;
543
600
  }),
601
+ // abort write operation on close event
544
602
  takeUntil(close$),
545
603
  concatWith(of([]))
546
604
  )
@@ -558,15 +616,15 @@ const pluginContentScripts = () => {
558
616
  {
559
617
  name: "crx:content-scripts",
560
618
  apply: "serve",
561
- config(config) {
562
- const { contentScripts: contentScripts2 = {} } = getOptions(config);
619
+ async config(config) {
620
+ const { contentScripts: contentScripts2 = {} } = await getOptions(config);
563
621
  hmrTimeout = contentScripts2.hmrTimeout ?? 5e3;
564
622
  preambleCode = preambleCode ?? contentScripts2.preambleCode;
565
623
  },
566
624
  async configureServer(_server) {
567
625
  server = _server;
568
626
  if (typeof preambleCode === "undefined" && server.config.plugins.some(
569
- ({ name = "none" }) => name.toLowerCase().includes("react")
627
+ ({ name = "none" }) => name.toLowerCase().includes("react") && !name.toLowerCase().includes("preact")
570
628
  )) {
571
629
  try {
572
630
  const react = await import('@vitejs/plugin-react');
@@ -631,6 +689,19 @@ const pluginContentScripts = () => {
631
689
  name: "crx:content-scripts",
632
690
  apply: "build",
633
691
  enforce: "pre",
692
+ config(config) {
693
+ return {
694
+ ...config,
695
+ build: {
696
+ ...config.build,
697
+ rollupOptions: {
698
+ ...config.build?.rollupOptions,
699
+ // keep exports for content script module api
700
+ preserveEntrySignatures: config.build?.rollupOptions?.preserveEntrySignatures ?? "exports-only"
701
+ }
702
+ }
703
+ };
704
+ },
634
705
  generateBundle() {
635
706
  for (const [key, script] of contentScripts)
636
707
  if (key === script.refId) {
@@ -661,8 +732,8 @@ const pluginContentScriptsCss = () => {
661
732
  return {
662
733
  name: "crx:content-scripts-css",
663
734
  enforce: "post",
664
- config(config) {
665
- const { contentScripts: contentScripts2 = {} } = getOptions(config);
735
+ async config(config) {
736
+ const { contentScripts: contentScripts2 = {} } = await getOptions(config);
666
737
  injectCss = contentScripts2.injectCss ?? true;
667
738
  },
668
739
  renderCrxManifest(manifest) {
@@ -691,7 +762,7 @@ const pluginDeclaredContentScripts = () => {
691
762
  return [];
692
763
  };
693
764
 
694
- const _dynamicScriptRegEx = /\b(import.meta).CRX_DYNAMIC_SCRIPT_(.+?);$/gm;
765
+ const _dynamicScriptRegEx = /\b(import.meta).CRX_DYNAMIC_SCRIPT_(.+?)[,;]/gm;
695
766
  const dynamicScriptRegEx = () => {
696
767
  _dynamicScriptRegEx.lastIndex = 0;
697
768
  return _dynamicScriptRegEx;
@@ -705,6 +776,34 @@ const pluginDynamicContentScripts = () => {
705
776
  configResolved(_config) {
706
777
  config = _config;
707
778
  },
779
+ configureServer(server) {
780
+ return () => {
781
+ server.middlewares.use(async (req, res, next) => {
782
+ try {
783
+ await allFilesReady();
784
+ next();
785
+ } catch (error) {
786
+ let err;
787
+ if (error instanceof Error) {
788
+ err = error;
789
+ } else if (typeof error === "string") {
790
+ err = new Error(error);
791
+ } else {
792
+ err = new Error(
793
+ `Unexpected error "${error}" in middleware for "${req.url}"`
794
+ );
795
+ }
796
+ server.ws.send({
797
+ type: "error",
798
+ err: {
799
+ message: err.message,
800
+ stack: err.stack ?? "no stack available"
801
+ }
802
+ });
803
+ }
804
+ });
805
+ };
806
+ },
708
807
  async resolveId(_source, importer) {
709
808
  if (importer && _source.includes("?script")) {
710
809
  const url = new URL(_source, "stub://stub");
@@ -773,7 +872,6 @@ const pluginDynamicContentScripts = () => {
773
872
  if (config.command === "build") {
774
873
  return `export default import.meta.CRX_DYNAMIC_SCRIPT_${script.refId};`;
775
874
  } else if (typeof script.fileName === "string") {
776
- await fileReady(script);
777
875
  return `export default ${JSON.stringify(script.fileName)};`;
778
876
  } else {
779
877
  throw new Error(
@@ -786,6 +884,12 @@ const pluginDynamicContentScripts = () => {
786
884
  {
787
885
  name: "crx:dynamic-content-scripts-build",
788
886
  apply: "build",
887
+ /**
888
+ * Replace dynamic script placeholders during build.
889
+ *
890
+ * Can't use `renderChunk` b/c pre plugin crx:content-scripts uses
891
+ * `generateBundle` to emit loaders. Must come after "enforce: pre".
892
+ */
789
893
  generateBundle(options, bundle) {
790
894
  for (const chunk of Object.values(bundle))
791
895
  if (chunk.type === "chunk") {
@@ -804,7 +908,7 @@ const pluginDynamicContentScripts = () => {
804
908
  );
805
909
  return `${JSON.stringify(
806
910
  `/${script.loaderName ?? script.fileName}`
807
- )};`;
911
+ )}${match.split(scriptKey)[1]}`;
808
912
  }
809
913
  );
810
914
  chunk.code = replaced;
@@ -815,29 +919,42 @@ const pluginDynamicContentScripts = () => {
815
919
  ];
816
920
  };
817
921
 
922
+ const { remove } = fsx;
818
923
  const logger = createLogger("error", { prefix: "crxjs" });
819
924
  const pluginFileWriter = () => {
820
925
  fileWriterError$.subscribe((error) => {
821
926
  logger.error(error.err.message, { error: error.err });
822
927
  });
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();
928
+ return [
929
+ {
930
+ name: "crx:file-writer-empty-out-dir",
931
+ apply: "serve",
932
+ enforce: "pre",
933
+ async configResolved(config) {
934
+ if (config.build.emptyOutDir) {
935
+ await remove(config.build.outDir);
833
936
  }
834
- });
835
- server.httpServer?.on("close", () => close());
937
+ }
836
938
  },
837
- closeBundle() {
838
- outputFiles.clear();
939
+ {
940
+ name: "crx:file-writer",
941
+ apply: "serve",
942
+ configureServer(server) {
943
+ server.httpServer?.on("listening", async () => {
944
+ try {
945
+ await start({ server });
946
+ } catch (error) {
947
+ console.error(error);
948
+ server.close();
949
+ }
950
+ });
951
+ server.httpServer?.on("close", () => close());
952
+ },
953
+ closeBundle() {
954
+ outputFiles.clear();
955
+ }
839
956
  }
840
- };
957
+ ];
841
958
  };
842
959
 
843
960
  const _require = typeof require === "undefined" ? createRequire(import.meta.url) : require;
@@ -882,7 +999,9 @@ async function manifestFiles(manifest, options = {}) {
882
999
  ) ?? [];
883
1000
  const contentScripts = manifest.content_scripts?.flatMap(({ js }) => js) ?? [];
884
1001
  const contentStyles = manifest.content_scripts?.flatMap(({ css }) => css);
885
- const serviceWorker = manifest.background?.service_worker;
1002
+ const serviceWorker = manifest.background && "service_worker" in manifest.background ? manifest.background.service_worker : void 0;
1003
+ const backgroundScripts = manifest.background && "scripts" in manifest.background ? manifest.background.scripts : void 0;
1004
+ const background = serviceWorker ? [serviceWorker].filter(isString) : backgroundScripts ? backgroundScripts.filter(isString) : [];
886
1005
  const htmlPages = htmlFiles(manifest);
887
1006
  const icons = [
888
1007
  Object.values(
@@ -903,7 +1022,7 @@ async function manifestFiles(manifest, options = {}) {
903
1022
  return r;
904
1023
  })
905
1024
  );
906
- webAccessibleResources = resources.flat().filter(isString);
1025
+ webAccessibleResources = [...new Set(resources.flat())].filter(isString);
907
1026
  }
908
1027
  return {
909
1028
  contentScripts: [...new Set(contentScripts)].filter(isString),
@@ -912,7 +1031,7 @@ async function manifestFiles(manifest, options = {}) {
912
1031
  icons: [...new Set(icons)].filter(isString),
913
1032
  locales: [...new Set(locales)].filter(isString),
914
1033
  rulesets: [...new Set(rulesets)].filter(isString),
915
- background: [serviceWorker].filter(isString),
1034
+ background,
916
1035
  webAccessibleResources
917
1036
  };
918
1037
  }
@@ -927,7 +1046,8 @@ function htmlFiles(manifest) {
927
1046
  manifest.devtools_page,
928
1047
  manifest.options_page,
929
1048
  manifest.options_ui?.page,
930
- manifest.sandbox?.pages
1049
+ manifest.sandbox?.pages,
1050
+ manifest.side_panel?.default_path
931
1051
  ].flat().filter(isString).map((s) => s.split("#")[0]).sort();
932
1052
  return [...new Set(files)];
933
1053
  }
@@ -952,11 +1072,13 @@ const pluginFileWriterPublic = () => {
952
1072
  };
953
1073
  };
954
1074
 
955
- _debug("file-writer").extend("hmr");
956
- const isCrxHMRPayload = (p) => p.type === "custom" && p.event.startsWith("crx:");
1075
+ const debug$2 = _debug("file-writer").extend("hmr");
1076
+ const isCustomPayload = (p) => {
1077
+ return p.type === "custom";
1078
+ };
957
1079
  const hmrPayload$ = new Subject();
958
1080
  const crxHMRPayload$ = hmrPayload$.pipe(
959
- filter((p) => !isCrxHMRPayload(p)),
1081
+ filter((p) => !isCustomPayload(p)),
960
1082
  buffer(allFilesReady$),
961
1083
  mergeMap((pps) => {
962
1084
  let fullReload;
@@ -1015,6 +1137,7 @@ const crxHMRPayload$ = hmrPayload$.pipe(
1015
1137
  }
1016
1138
  }),
1017
1139
  map((data) => {
1140
+ debug$2(`hmr payload`, data);
1018
1141
  return {
1019
1142
  type: "custom",
1020
1143
  event: "crx:content-script-payload",
@@ -1054,6 +1177,7 @@ const pluginHMR = () => {
1054
1177
  name: "crx:hmr",
1055
1178
  apply: "serve",
1056
1179
  enforce: "pre",
1180
+ // server hmr host should be localhost
1057
1181
  async config({ server = {}, ...config2 }) {
1058
1182
  if (server.hmr === false)
1059
1183
  return;
@@ -1061,9 +1185,9 @@ const pluginHMR = () => {
1061
1185
  server.hmr = {};
1062
1186
  server.hmr = server.hmr ?? {};
1063
1187
  server.hmr.host = "localhost";
1064
- server.hmr.port = server.hmr.port ?? await getPort({ port: portNumbers(5200, 5300) });
1065
1188
  return { server, ...config2 };
1066
1189
  },
1190
+ // server should ignore outdir
1067
1191
  configResolved(_config) {
1068
1192
  config = _config;
1069
1193
  const { watch = {} } = config.server;
@@ -1101,6 +1225,7 @@ const pluginHMR = () => {
1101
1225
  closeBundle() {
1102
1226
  subs.unsubscribe();
1103
1227
  },
1228
+ // background changes require a full extension reload
1104
1229
  handleHotUpdate({ modules, server }) {
1105
1230
  const { root } = server.config;
1106
1231
  const relFiles = /* @__PURE__ */ new Set();
@@ -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
  }
@@ -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: true
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,7 +1912,7 @@ 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,
@@ -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(),