@crxjs/vite-plugin 2.5.0 → 2.6.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/dist/index.mjs CHANGED
@@ -2,7 +2,7 @@ import { simple } from 'acorn-walk';
2
2
  import { createHash } from 'crypto';
3
3
  import debug$5 from 'debug';
4
4
  import { join, normalize, dirname, basename, isAbsolute, relative, resolve, parse as parse$1 } from 'pathe';
5
- import { Subject, filter, ReplaySubject, switchMap, of, startWith, map, BehaviorSubject, mergeMap, firstValueFrom, takeUntil, first, toArray, retry, concatWith, Subscription, buffer } from 'rxjs';
5
+ import { Subject, filter, ReplaySubject, switchMap, of, startWith, tap, debounceTime, map, share, BehaviorSubject, mergeMap, firstValueFrom, takeUntil, first, toArray, retry, concatWith, Subscription, buffer } from 'rxjs';
6
6
  import fsx from 'fs-extra';
7
7
  import { performance } from 'perf_hooks';
8
8
  import { rollup } from 'rollup';
@@ -11,7 +11,7 @@ import { readFile as readFile$1 } from 'fs/promises';
11
11
  import MagicString from 'magic-string';
12
12
  import { build, mergeConfig, createLogger, version } from 'vite';
13
13
  import convertSourceMap from 'convert-source-map';
14
- import colors from 'picocolors';
14
+ import pc from 'picocolors';
15
15
  import { readFileSync, existsSync, promises } from 'fs';
16
16
  import { createRequire } from 'module';
17
17
  import { glob, isDynamicPattern } from 'tinyglobby';
@@ -244,7 +244,13 @@ function getOutputPath(server, fileName) {
244
244
  const target = isAbsolute(outDir) ? join(outDir, fileName) : join(root, outDir, fileName);
245
245
  return target;
246
246
  }
247
- function getViteUrl({ type, id }) {
247
+ function getViteUrl({ type, id }, { timestamp = false } = {}) {
248
+ if (timestamp && !id.startsWith("/@") && !id.includes("?v=")) {
249
+ const t = `t=${Date.now()}` + (id.includes("?") ? "&" : "");
250
+ const parts = id.split("?");
251
+ parts[1] = typeof parts[1] === "undefined" ? t : t + parts[1];
252
+ id = parts.join("?");
253
+ }
248
254
  if (type === "asset") {
249
255
  throw new Error(`File type "${type}" not implemented.`);
250
256
  } else if (type === "iife") {
@@ -378,6 +384,52 @@ const pluginBackground = () => {
378
384
  ];
379
385
  };
380
386
 
387
+ const extensionOrigins = [/^chrome-extension:\/\//, /^moz-extension:\/\//];
388
+ function isExtensionOrigin(origin) {
389
+ return origin ? extensionOrigins.some((pattern) => pattern.test(origin)) : false;
390
+ }
391
+ function addExtensionOrigins(origin) {
392
+ if (origin === true)
393
+ return true;
394
+ if (typeof origin === "function") {
395
+ return (requestOrigin, cb) => {
396
+ if (isExtensionOrigin(requestOrigin)) {
397
+ cb(null, true);
398
+ return;
399
+ }
400
+ origin(requestOrigin, cb);
401
+ };
402
+ }
403
+ if (Array.isArray(origin))
404
+ return [...origin, ...extensionOrigins];
405
+ if (origin)
406
+ return [origin, ...extensionOrigins];
407
+ return extensionOrigins;
408
+ }
409
+ function addExtensionCors(cors) {
410
+ if (cors === true)
411
+ return true;
412
+ if (cors && typeof cors === "object") {
413
+ return {
414
+ ...cors,
415
+ origin: addExtensionOrigins(cors.origin)
416
+ };
417
+ }
418
+ return { origin: extensionOrigins };
419
+ }
420
+ function pluginExtensionCors() {
421
+ return {
422
+ name: "crx:extension-cors",
423
+ apply: "serve",
424
+ config(config) {
425
+ config.server = {
426
+ ...config.server,
427
+ cors: addExtensionCors(config.server?.cors)
428
+ };
429
+ }
430
+ };
431
+ }
432
+
381
433
  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 if (__CRX_LIVE_RELOAD__) {\n console.log(\"[crx] runtime reload\");\n setTimeout(() => location.reload(), 500);\n } else {\n console.log(\"[crx] runtime reload suppressed (liveReload disabled)\");\n }\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";
382
434
 
383
435
  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";
@@ -398,9 +450,10 @@ contentScripts.change$.pipe(filter(RxMap.isChangeType.set)).subscribe(({ map, va
398
450
  "resolvedId",
399
451
  "scriptId"
400
452
  ];
401
- for (const keyName of keyNames) {
402
- const key = value[keyName];
403
- if (typeof key === "undefined" || map.has(key)) {
453
+ const keys = keyNames.map((keyName) => value[keyName]);
454
+ keys.push(value.id.replace(/^\//, ""));
455
+ for (const key of keys) {
456
+ if (typeof key === "undefined" || map.get(key) === value) {
404
457
  continue;
405
458
  } else {
406
459
  map.set(key, value);
@@ -462,11 +515,59 @@ fileWriterEvent$.pipe(
462
515
  filter((e) => e.type === "build_start"),
463
516
  switchMap((e) => of(e))
464
517
  );
465
- const allFilesReady$ = buildEnd$.pipe(
466
- switchMap(() => outputFiles.change$.pipe(startWith({ type: "start" }))),
467
- map(() => [...outputFiles.values()]),
468
- switchMap((files) => Promise.allSettled(files.map(({ file }) => file)))
518
+ const allFilesReadyDebounceMs = 100;
519
+ let currentAllFilesReadyGeneration = 0;
520
+ let completedAllFilesReadyGeneration = 0;
521
+ let lastAllFilesReadyResults;
522
+ const allFilesReadyState$ = buildEnd$.pipe(
523
+ switchMap(
524
+ () => outputFiles.change$.pipe(
525
+ startWith({ type: "start" }),
526
+ tap(() => {
527
+ currentAllFilesReadyGeneration += 1;
528
+ }),
529
+ debounceTime(allFilesReadyDebounceMs)
530
+ )
531
+ ),
532
+ map(() => ({
533
+ generation: currentAllFilesReadyGeneration,
534
+ files: [...outputFiles.values()]
535
+ })),
536
+ switchMap(async ({ generation, files }) => {
537
+ const seen = /* @__PURE__ */ new Set();
538
+ const results = await Promise.allSettled(
539
+ files.map((file) => waitForOutputFile(file, seen))
540
+ );
541
+ return { generation, results };
542
+ }),
543
+ tap(({ generation, results }) => {
544
+ completedAllFilesReadyGeneration = generation;
545
+ lastAllFilesReadyResults = results;
546
+ }),
547
+ share()
548
+ );
549
+ const allFilesReady$ = allFilesReadyState$.pipe(
550
+ map(({ results }) => results)
469
551
  );
552
+ async function waitForOutputFile(file, seen = /* @__PURE__ */ new Set()) {
553
+ if (seen.has(file))
554
+ return;
555
+ seen.add(file);
556
+ const { deps } = await file.file;
557
+ await Promise.all(deps.map((dep) => waitForOutputFile(dep, seen)));
558
+ }
559
+ async function waitForAllFilesReadyResults() {
560
+ const targetGeneration = currentAllFilesReadyGeneration;
561
+ if (lastAllFilesReadyResults && completedAllFilesReadyGeneration >= targetGeneration) {
562
+ return lastAllFilesReadyResults;
563
+ }
564
+ const { results } = await firstValueFrom(
565
+ allFilesReadyState$.pipe(
566
+ filter(({ generation }) => generation >= targetGeneration)
567
+ )
568
+ );
569
+ return results;
570
+ }
470
571
  const timestamp$ = new BehaviorSubject(Date.now());
471
572
  allFilesReady$.subscribe(() => {
472
573
  timestamp$.next(Date.now());
@@ -509,7 +610,14 @@ function prepScript(fileName, script) {
509
610
  // get script contents from dev server
510
611
  mergeMap(async ({ server }) => {
511
612
  const target = getOutputPath(server, fileName);
512
- const viteUrl = getViteUrl(script);
613
+ const originalViteUrl = getViteUrl(script);
614
+ const isVueSfcQuery = script.id.includes("?vue");
615
+ const viteUrl = getViteUrl(script, { timestamp: isVueSfcQuery });
616
+ if (isVueSfcQuery) {
617
+ const module = await server.moduleGraph.getModuleByUrl(originalViteUrl);
618
+ if (module)
619
+ server.moduleGraph.invalidateModule(module);
620
+ }
513
621
  const transformResult = await server.transformRequest(viteUrl);
514
622
  if (!transformResult)
515
623
  throw new TypeError(`Unable to load "${script.id}" from server.`);
@@ -662,11 +770,16 @@ function prepIifeScript(fileName, script) {
662
770
  );
663
771
  }
664
772
  async function allFilesReady() {
665
- await firstValueFrom(allFilesReady$);
773
+ await waitForAllFilesReadyResults();
666
774
  }
667
775
 
668
776
  const { outputFile } = fsx;
669
777
  const debug$4 = _debug("file-writer");
778
+ function queueWrite(script, previous) {
779
+ if (!previous)
780
+ return write(script);
781
+ return previous.file.catch(() => void 0).then(() => write(script));
782
+ }
670
783
  async function start({
671
784
  server
672
785
  }) {
@@ -708,18 +821,24 @@ function add(script) {
708
821
  file = formatFileData({
709
822
  ...script,
710
823
  fileName,
711
- file: write(script)
824
+ file: queueWrite(script)
712
825
  });
713
826
  outputFiles.set(file.fileName, file);
714
827
  debug$4("add: stored new file %s", file.fileName);
715
828
  } else {
716
829
  const isVirtualModule = script.id.startsWith("/@id/") || script.id.startsWith("/__");
717
- if (isVirtualModule) {
830
+ const isTimestampedModule = script.type === "module" && /[?&]t=\d+/.test(script.id);
831
+ if (isVirtualModule || isTimestampedModule) {
718
832
  debug$4(
719
- "add: virtual module already exists, triggering re-write for %s",
833
+ "add: module already exists, triggering re-write for %s",
720
834
  fileName
721
835
  );
722
- file.file = write(script);
836
+ file = formatFileData({
837
+ ...file,
838
+ ...script,
839
+ fileName,
840
+ file: queueWrite(script, file)
841
+ });
723
842
  outputFiles.set(fileName, file);
724
843
  }
725
844
  }
@@ -736,7 +855,7 @@ function update(_id) {
736
855
  const scriptFile = outputFiles.get(fileName);
737
856
  if (scriptFile) {
738
857
  debug$4("update: found file, calling write()");
739
- scriptFile.file = write({ id, type });
858
+ scriptFile.file = queueWrite({ id, type }, scriptFile);
740
859
  updatedFiles.push(scriptFile);
741
860
  outputFiles.set(fileName, scriptFile);
742
861
  }
@@ -794,7 +913,7 @@ const pluginContentScripts = () => {
794
913
  });
795
914
  if (worldMainIds.size) {
796
915
  const name = `[${pluginName}]`;
797
- const message = colors.yellow(
916
+ const message = pc.yellow(
798
917
  [
799
918
  `${name} Some content-scripts don't support HMR because the world is MAIN:`,
800
919
  ...[...worldMainIds].map((id) => ` ${id}`)
@@ -823,7 +942,7 @@ const pluginContentScripts = () => {
823
942
  try {
824
943
  const react = await import('@vitejs/plugin-react');
825
944
  preambleCode = react.default.preambleCode;
826
- } catch (error) {
945
+ } catch {
827
946
  preambleCode = false;
828
947
  }
829
948
  }
@@ -898,41 +1017,50 @@ const pluginContentScripts = () => {
898
1017
  };
899
1018
  },
900
1019
  generateBundle(_options, bundle) {
901
- for (const [key, script] of contentScripts)
902
- if (key === script.refId) {
903
- if (script.type === "module") {
904
- const fileName = this.getFileName(script.refId);
905
- script.fileName = fileName;
906
- } else if (script.type === "loader") {
907
- const fileName = this.getFileName(script.refId);
908
- script.fileName = fileName;
909
- const bundleFileInfo = bundle[fileName];
910
- const shouldUseLoader = !(bundleFileInfo.type === "chunk" && bundleFileInfo.imports.length === 0 && bundleFileInfo.dynamicImports.length === 0 && bundleFileInfo.exports.length === 0);
911
- if (shouldUseLoader) {
912
- const refId = this.emitFile({
913
- type: "asset",
914
- name: getFileName({
915
- type: "loader",
916
- id: basename(script.id)
917
- }),
918
- source: worldMainIds.has(script.id) ? createProMainLoader({
919
- fileName: `./${fileName.split("/").at(-1)}`
920
- }) : createProLoader({ fileName })
921
- });
922
- script.loaderName = this.getFileName(refId);
923
- } else {
924
- bundleFileInfo.code = `(function(){${bundleFileInfo.code}})()
925
- `;
926
- }
927
- } else if (script.type === "iife") {
928
- continue;
929
- }
930
- contentScripts.set(script.refId, formatFileData(script));
931
- }
1020
+ finalizeBuildContentScripts(this, bundle, worldMainIds);
932
1021
  }
933
1022
  }
934
1023
  ];
935
1024
  };
1025
+ function finalizeBuildContentScripts(context, bundle, worldMainIds = /* @__PURE__ */ new Set()) {
1026
+ const processed = /* @__PURE__ */ new Set();
1027
+ for (const [key, script] of contentScripts) {
1028
+ if (key !== script.refId || processed.has(script))
1029
+ continue;
1030
+ processed.add(script);
1031
+ if (script.type === "module") {
1032
+ script.fileName = script.fileName ?? context.getFileName(script.refId);
1033
+ } else if (script.type === "loader") {
1034
+ const fileName = script.fileName ?? context.getFileName(script.refId);
1035
+ script.fileName = fileName;
1036
+ const bundleFileInfo = bundle[fileName];
1037
+ if (bundleFileInfo?.type !== "chunk")
1038
+ continue;
1039
+ const shouldUseLoader = !(bundleFileInfo.imports.length === 0 && bundleFileInfo.dynamicImports.length === 0 && bundleFileInfo.exports.length === 0);
1040
+ if (shouldUseLoader) {
1041
+ if (typeof script.loaderName === "undefined") {
1042
+ const refId = context.emitFile({
1043
+ type: "asset",
1044
+ name: getFileName({
1045
+ type: "loader",
1046
+ id: basename(script.id)
1047
+ }),
1048
+ source: worldMainIds.has(script.id) ? createProMainLoader({
1049
+ fileName: `./${fileName.split("/").at(-1)}`
1050
+ }) : createProLoader({ fileName })
1051
+ });
1052
+ script.loaderName = context.getFileName(refId);
1053
+ }
1054
+ } else if (typeof script.loaderName === "undefined" && !bundleFileInfo.code.startsWith("(function(){")) {
1055
+ bundleFileInfo.code = `(function(){${bundleFileInfo.code}})()
1056
+ `;
1057
+ }
1058
+ } else if (script.type === "iife") {
1059
+ continue;
1060
+ }
1061
+ contentScripts.set(script.refId, formatFileData(script));
1062
+ }
1063
+ }
936
1064
 
937
1065
  const pluginContentScriptsCss = () => {
938
1066
  let injectCss;
@@ -1027,45 +1155,22 @@ const pluginContentScriptsIife = () => {
1027
1155
  name: pluginName,
1028
1156
  apply: "build",
1029
1157
  enforce: "post",
1030
- async generateBundle(options, bundle) {
1158
+ async generateBundle() {
1031
1159
  const opts = await getOptions({ plugins: config.plugins });
1032
1160
  const _manifest = opts.manifest;
1033
1161
  const manifest = typeof _manifest === "function" ? await _manifest({ command: "build", mode: config.mode }) : await Promise.resolve(_manifest);
1034
1162
  const standaloneFiles = (opts.contentScripts?.standaloneFiles || []).map(
1035
- (f) => f.replace(/^\//, "")
1163
+ normalizeContentScriptPath
1164
+ );
1165
+ const iifeEntries = collectIifeEntries(
1166
+ manifest,
1167
+ standaloneFiles,
1168
+ config.root
1036
1169
  );
1037
- const isStandaloneFile = (file) => {
1038
- const normalized = file.replace(/^\//, "");
1039
- return standaloneFiles.includes(normalized);
1040
- };
1041
- const iifeEntries = [];
1042
- if (manifest.content_scripts) {
1043
- for (const { js = [], matches = [] } of manifest.content_scripts) {
1044
- for (const file of js) {
1045
- if (isIifeContentScript(file) || isStandaloneFile(file)) {
1046
- const id = join(config.root, file);
1047
- iifeEntries.push({ file, id, matches });
1048
- }
1049
- }
1050
- }
1051
- }
1052
- for (const [, script] of contentScripts.entries()) {
1053
- if (script.type === "iife" && script.isDynamicScript) {
1054
- const id = join(config.root, script.id);
1055
- if (!iifeEntries.some((e) => e.id === id)) {
1056
- iifeEntries.push({
1057
- file: script.id,
1058
- id,
1059
- matches: script.matches ?? [],
1060
- isDynamic: true
1061
- });
1062
- }
1063
- }
1064
- }
1065
1170
  if (iifeEntries.length === 0)
1066
1171
  return;
1067
1172
  console.log(
1068
- colors.cyan(`
1173
+ pc.cyan(`
1069
1174
  [${pluginName}] Building ${iifeEntries.length} content script(s) as IIFE...`)
1070
1175
  );
1071
1176
  for (const entry of iifeEntries) {
@@ -1073,52 +1178,106 @@ const pluginContentScriptsIife = () => {
1073
1178
  try {
1074
1179
  const iifeConfig = createIifeConfig(config, entry.id, outputFileName);
1075
1180
  const result = await build(iifeConfig);
1076
- if ("on" in result) {
1077
- console.error(colors.red(` Unexpected watcher result for ${entry.file}`));
1078
- continue;
1079
- }
1080
- const outputs = Array.isArray(result) ? result.flatMap((r) => "output" in r ? r.output : []) : result.output;
1081
- for (const chunk of outputs) {
1082
- if (chunk.type === "chunk" && chunk.isEntry) {
1083
- bundle[outputFileName] = {
1084
- ...chunk,
1085
- fileName: outputFileName
1086
- };
1087
- const existingScript = contentScripts.get(entry.file);
1088
- if (existingScript) {
1089
- existingScript.fileName = outputFileName;
1090
- contentScripts.set(entry.file, formatFileData(existingScript));
1091
- } else {
1092
- contentScripts.set(
1093
- entry.file,
1094
- formatFileData({
1095
- type: "iife",
1096
- id: entry.file,
1097
- refId: entry.file,
1098
- matches: entry.matches,
1099
- fileName: outputFileName
1100
- })
1101
- );
1102
- }
1103
- console.log(
1104
- colors.green(` \u2713 ${basename(entry.file)} \u2192 ${outputFileName}`)
1105
- );
1106
- }
1107
- }
1181
+ const outputs = getBuildOutputs(result, entry.file);
1182
+ emitIifeOutputs(this, outputs, entry.file, outputFileName);
1183
+ registerIifeContentScript(entry, outputFileName);
1184
+ console.log(
1185
+ pc.green(` \u2713 ${basename(entry.file)} \u2192 ${outputFileName}`)
1186
+ );
1108
1187
  } catch (error) {
1109
1188
  console.error(
1110
- colors.red(` \u2717 Failed to build ${entry.file}:`),
1189
+ pc.red(` \u2717 Failed to build ${entry.file}:`),
1111
1190
  error
1112
1191
  );
1113
1192
  throw error;
1114
1193
  }
1115
1194
  }
1116
- console.log(colors.cyan(`[${pluginName}] IIFE build complete
1195
+ console.log(pc.cyan(`[${pluginName}] IIFE build complete
1117
1196
  `));
1118
1197
  }
1119
1198
  }
1120
1199
  ];
1121
1200
  };
1201
+ function normalizeContentScriptPath(file) {
1202
+ return file.replace(/^\//, "");
1203
+ }
1204
+ function isStandaloneFile(file, standaloneFiles) {
1205
+ return standaloneFiles.includes(normalizeContentScriptPath(file));
1206
+ }
1207
+ function collectIifeEntries(manifest, standaloneFiles, root) {
1208
+ const entries = [];
1209
+ const entryIds = /* @__PURE__ */ new Set();
1210
+ const addEntry = (entry) => {
1211
+ if (entryIds.has(entry.id))
1212
+ return;
1213
+ entryIds.add(entry.id);
1214
+ entries.push(entry);
1215
+ };
1216
+ for (const { js = [], matches = [] } of manifest.content_scripts ?? []) {
1217
+ for (const file of js) {
1218
+ if (isIifeContentScript(file) || isStandaloneFile(file, standaloneFiles)) {
1219
+ addEntry({ file, id: join(root, file), matches });
1220
+ }
1221
+ }
1222
+ }
1223
+ for (const [, script] of contentScripts.entries()) {
1224
+ if (script.type !== "iife" || !script.isDynamicScript)
1225
+ continue;
1226
+ addEntry({
1227
+ file: script.id,
1228
+ id: join(root, script.id),
1229
+ matches: script.matches ?? []
1230
+ });
1231
+ }
1232
+ return entries;
1233
+ }
1234
+ function getBuildOutputs(result, entryFile) {
1235
+ if ("on" in result) {
1236
+ throw new Error(`Unexpected watcher result for "${entryFile}"`);
1237
+ }
1238
+ return Array.isArray(result) ? result.flatMap((r) => r.output) : result.output;
1239
+ }
1240
+ function emitIifeOutputs(context, outputs, entryFile, outputFileName) {
1241
+ const entryChunk = outputs.find(
1242
+ (output) => output.type === "chunk" && output.isEntry
1243
+ );
1244
+ if (!entryChunk) {
1245
+ throw new Error(`Unable to generate IIFE bundle for "${entryFile}"`);
1246
+ }
1247
+ context.emitFile({
1248
+ type: "asset",
1249
+ fileName: outputFileName,
1250
+ source: entryChunk.code
1251
+ });
1252
+ for (const output of outputs) {
1253
+ if (output.type === "asset") {
1254
+ if (output.fileName !== outputFileName && output.fileName !== "manifest.json" && !output.fileName.startsWith(".vite/")) {
1255
+ context.emitFile({
1256
+ type: "asset",
1257
+ fileName: output.fileName,
1258
+ source: output.source
1259
+ });
1260
+ }
1261
+ } else if (!output.isEntry) {
1262
+ context.emitFile({
1263
+ type: "asset",
1264
+ fileName: output.fileName,
1265
+ source: output.code
1266
+ });
1267
+ }
1268
+ }
1269
+ }
1270
+ function registerIifeContentScript(entry, outputFileName) {
1271
+ const existingScript = contentScripts.get(entry.file);
1272
+ const script = existingScript ? { ...existingScript, fileName: outputFileName } : {
1273
+ type: "iife",
1274
+ id: entry.file,
1275
+ refId: entry.file,
1276
+ matches: entry.matches,
1277
+ fileName: outputFileName
1278
+ };
1279
+ contentScripts.set(entry.file, formatFileData(script));
1280
+ }
1122
1281
  function getIifeOutputPath(file) {
1123
1282
  const normalizedFile = file.replace(/^\//, "");
1124
1283
  const dir = dirname(normalizedFile);
@@ -1188,6 +1347,36 @@ const pluginDynamicContentScripts = () => {
1188
1347
  );
1189
1348
  }).catch(() => void 0);
1190
1349
  },
1350
+ buildStart() {
1351
+ if (config.command === "build") {
1352
+ const dynamicScripts = [];
1353
+ for (const [key, script] of contentScripts) {
1354
+ if (script.isDynamicScript && key === script.scriptId) {
1355
+ dynamicScripts.push(script);
1356
+ }
1357
+ }
1358
+ contentScripts.clear();
1359
+ for (const script of dynamicScripts) {
1360
+ const absoluteId = script.id.startsWith("/") ? `${config.root}${script.id}` : `${config.root}/${script.id}`;
1361
+ const refId = this.emitFile({
1362
+ type: "chunk",
1363
+ id: absoluteId,
1364
+ name: basename(script.id)
1365
+ });
1366
+ contentScripts.set(
1367
+ script.id,
1368
+ formatFileData({
1369
+ type: script.type,
1370
+ id: script.id,
1371
+ isDynamicScript: true,
1372
+ refId,
1373
+ scriptId: script.scriptId,
1374
+ matches: script.matches
1375
+ })
1376
+ );
1377
+ }
1378
+ }
1379
+ },
1191
1380
  configureServer(server) {
1192
1381
  return () => {
1193
1382
  server.middlewares.use(async (req, res, next) => {
@@ -1249,7 +1438,10 @@ const pluginDynamicContentScripts = () => {
1249
1438
  refId = this.emitFile({
1250
1439
  type: "chunk",
1251
1440
  id,
1252
- name: basename(id)
1441
+ name: basename(id),
1442
+ // Preserve content script entry exports so the build finalizer
1443
+ // can decide whether the script needs a loader wrapper.
1444
+ preserveSignature: "exports-only"
1253
1445
  });
1254
1446
  } else {
1255
1447
  refId = scriptId;
@@ -1283,9 +1475,29 @@ const pluginDynamicContentScripts = () => {
1283
1475
  const index = id.indexOf("?scriptId=");
1284
1476
  if (index > -1) {
1285
1477
  const scriptId = id.slice(index + "?scriptId=".length);
1286
- const script = contentScripts.get(scriptId);
1478
+ let script = contentScripts.get(scriptId);
1479
+ if (!script && config.command === "build") {
1480
+ const fileId = id.slice(0, index);
1481
+ const refId = this.emitFile({
1482
+ type: "chunk",
1483
+ id: fileId,
1484
+ name: basename(fileId)
1485
+ });
1486
+ script = formatFileData({
1487
+ type: "loader",
1488
+ id: relative(config.root, fileId),
1489
+ isDynamicScript: true,
1490
+ refId,
1491
+ scriptId,
1492
+ matches: []
1493
+ });
1494
+ contentScripts.set(script.id, script);
1495
+ }
1496
+ if (!script) {
1497
+ throw new Error(`Content script not found for scriptId: "${scriptId}"`);
1498
+ }
1287
1499
  if (config.command === "build") {
1288
- return `export default import.meta.CRX_DYNAMIC_SCRIPT_${script.refId};`;
1500
+ return `export default import.meta.CRX_DYNAMIC_SCRIPT_${script.scriptId};`;
1289
1501
  } else if (typeof script.fileName === "string") {
1290
1502
  return `export default ${JSON.stringify(script.fileName)};`;
1291
1503
  } else {
@@ -1493,6 +1705,89 @@ const isCustomPayload = (p) => {
1493
1705
  return p.type === "custom";
1494
1706
  };
1495
1707
  const hmrPayload$ = new Subject();
1708
+ function withTimestamp(id, timestamp) {
1709
+ if (id.includes("?t=") || id.includes("&t="))
1710
+ return id;
1711
+ const t = `t=${timestamp}` + (id.includes("?") ? "&" : "");
1712
+ const parts = id.split("?");
1713
+ parts[1] = typeof parts[1] === "undefined" ? t : t + parts[1];
1714
+ return parts.join("?");
1715
+ }
1716
+ function getUpdatePayloadFileIds(p, { timestamp = false } = {}) {
1717
+ const ids = /* @__PURE__ */ new Set();
1718
+ for (const u of p.updates) {
1719
+ for (const id of [u.path, u.acceptedPath]) {
1720
+ const isVirtualModule = id.startsWith("/@id/") || id.startsWith("/__");
1721
+ const isQueryModule = id.includes("?");
1722
+ if (isVirtualModule || isQueryModule)
1723
+ ids.add(timestamp ? withTimestamp(id, u.timestamp) : id);
1724
+ }
1725
+ }
1726
+ return [...ids];
1727
+ }
1728
+ function mapVitePayloadForCrx(p) {
1729
+ switch (p.type) {
1730
+ case "full-reload": {
1731
+ const fullReload = {
1732
+ type: "full-reload",
1733
+ path: p.path && getViteUrl({ id: p.path, type: "module" })
1734
+ };
1735
+ return fullReload;
1736
+ }
1737
+ case "prune": {
1738
+ const prune = {
1739
+ type: "prune",
1740
+ paths: p.paths.map((id) => getViteUrl({ id, type: "module" }))
1741
+ };
1742
+ return prune;
1743
+ }
1744
+ case "update": {
1745
+ debug$3("update payload with %d updates", p.updates.length);
1746
+ for (const u of p.updates) {
1747
+ debug$3(
1748
+ "update item: path=%s acceptedPath=%s type=%s",
1749
+ u.path,
1750
+ u.acceptedPath,
1751
+ u.type
1752
+ );
1753
+ }
1754
+ const update_ = {
1755
+ type: "update",
1756
+ updates: p.updates.map(({ acceptedPath: ap, path: p2, ...rest }) => ({
1757
+ ...rest,
1758
+ acceptedPath: prefix$1("/", getFileName({ id: ap, type: "module" })),
1759
+ path: prefix$1("/", getFileName({ id: p2, type: "module" }))
1760
+ }))
1761
+ };
1762
+ return update_;
1763
+ }
1764
+ default:
1765
+ return p;
1766
+ }
1767
+ }
1768
+ async function prepareVitePayloadForCrx(p) {
1769
+ if (p.type === "update") {
1770
+ const pendingFiles = getUpdatePayloadFileIds(p, {
1771
+ timestamp: true
1772
+ }).flatMap((id) => {
1773
+ debug$3("updating payload module: %s", id);
1774
+ return update(id).map((file) => file.file);
1775
+ });
1776
+ await Promise.all(pendingFiles);
1777
+ await allFilesReady();
1778
+ }
1779
+ return mapVitePayloadForCrx(p);
1780
+ }
1781
+ function shouldForwardCrxPayload(p) {
1782
+ switch (p.type) {
1783
+ case "prune":
1784
+ return p.paths.length > 0;
1785
+ case "update":
1786
+ return p.updates.length > 0;
1787
+ default:
1788
+ return true;
1789
+ }
1790
+ }
1496
1791
  const crxHMRPayload$ = hmrPayload$.pipe(
1497
1792
  filter((p) => !isCustomPayload(p)),
1498
1793
  buffer(allFilesReady$),
@@ -1509,63 +1804,8 @@ const crxHMRPayload$ = hmrPayload$.pipe(
1509
1804
  payloads.push(fullReload);
1510
1805
  return payloads;
1511
1806
  }),
1512
- map((p) => {
1513
- switch (p.type) {
1514
- case "full-reload": {
1515
- const fullReload = {
1516
- type: "full-reload",
1517
- path: p.path && getViteUrl({ id: p.path, type: "module" })
1518
- };
1519
- return fullReload;
1520
- }
1521
- case "prune": {
1522
- const prune = {
1523
- type: "prune",
1524
- paths: p.paths.map((id) => getViteUrl({ id, type: "module" }))
1525
- };
1526
- return prune;
1527
- }
1528
- case "update": {
1529
- debug$3("update payload with %d updates", p.updates.length);
1530
- for (const u of p.updates) {
1531
- debug$3(
1532
- "update item: path=%s acceptedPath=%s type=%s",
1533
- u.path,
1534
- u.acceptedPath,
1535
- u.type
1536
- );
1537
- const isVirtualModule = u.path.startsWith("/@id/") || u.path.startsWith("/__");
1538
- if (isVirtualModule) {
1539
- debug$3("updating virtual module: %s", u.path);
1540
- update(u.path);
1541
- }
1542
- }
1543
- const update_ = {
1544
- type: "update",
1545
- updates: p.updates.map(({ acceptedPath: ap, path: p2, ...rest }) => ({
1546
- ...rest,
1547
- acceptedPath: prefix$1("/", getFileName({ id: ap, type: "module" })),
1548
- path: prefix$1("/", getFileName({ id: p2, type: "module" }))
1549
- }))
1550
- };
1551
- return update_;
1552
- }
1553
- default:
1554
- return p;
1555
- }
1556
- }),
1557
- filter((p) => {
1558
- switch (p.type) {
1559
- case "full-reload":
1560
- return typeof p.path === "undefined";
1561
- case "prune":
1562
- return p.paths.length > 0;
1563
- case "update":
1564
- return p.updates.length > 0;
1565
- default:
1566
- return true;
1567
- }
1568
- }),
1807
+ mergeMap(prepareVitePayloadForCrx),
1808
+ filter(shouldForwardCrxPayload),
1569
1809
  map((data) => {
1570
1810
  debug$3(`hmr payload`, data);
1571
1811
  return {
@@ -1597,6 +1837,23 @@ const crxRuntimeReload = {
1597
1837
  type: "custom",
1598
1838
  event: "crx:runtime-reload"
1599
1839
  };
1840
+ function getChangedFilePath(root, file) {
1841
+ if (!file)
1842
+ return null;
1843
+ const normalizedRoot = normalize(root);
1844
+ const normalizedFile = normalize(file);
1845
+ const relativeFile = relative(normalizedRoot, normalizedFile);
1846
+ if (!relativeFile || relativeFile.startsWith("..") || isAbsolute(relativeFile)) {
1847
+ return null;
1848
+ }
1849
+ return prefix$1("/", relativeFile);
1850
+ }
1851
+ function stripTimestamp(id) {
1852
+ return id.replace(/([?&])t=\d+&/, "$1").replace(/[?&]t=\d+$/, "").replace(/\?$/, "");
1853
+ }
1854
+ function escapeRegExp(text) {
1855
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1856
+ }
1600
1857
  const pluginHMR = () => {
1601
1858
  let inputManifestFiles;
1602
1859
  let decoratedSend;
@@ -1663,13 +1920,14 @@ const pluginHMR = () => {
1663
1920
  // background changes require a full extension reload
1664
1921
  handleHotUpdate({ file, modules, server }) {
1665
1922
  const { root } = server.config;
1666
- const changedFilePath = file ? file.startsWith(root) ? prefix$1("/", file.slice(root.length)) : null : null;
1923
+ const changedFilePath = getChangedFilePath(root, file);
1667
1924
  const relFiles = /* @__PURE__ */ new Set();
1668
1925
  const fsFiles = /* @__PURE__ */ new Set();
1669
1926
  const virtualModules = /* @__PURE__ */ new Set();
1670
1927
  for (const m of modules) {
1671
- if (m.id?.startsWith(root)) {
1672
- relFiles.add(m.id.slice(server.config.root.length));
1928
+ const relFile = getChangedFilePath(root, m.id);
1929
+ if (relFile) {
1930
+ relFiles.add(relFile);
1673
1931
  } else if (m.url?.startsWith("/@fs")) {
1674
1932
  fsFiles.add(m.url);
1675
1933
  } else if (m.id?.startsWith("\0") || m.url?.startsWith("/@id/__x00__")) {
@@ -1741,13 +1999,14 @@ const pluginHMR = () => {
1741
1999
  },
1742
2000
  renderCrxDevScript(code, { id: _id, type }) {
1743
2001
  if (type === "module" && _id !== "/@vite/client" && code.includes("createHotContext")) {
1744
- const id = _id.replace(/t=\d+&/, "");
1745
- const escaped = id.replace(/([?&.])/g, "\\$1");
2002
+ const id = stripTimestamp(_id);
2003
+ const escaped = escapeRegExp(id);
1746
2004
  const regexp = new RegExp(
1747
- `(?<=createHotContext\\(")${escaped}(?="\\))`
2005
+ `(createHotContext\\(")${escaped}("\\))`,
2006
+ "g"
1748
2007
  );
1749
2008
  const fileUrl = prefix$1("/", getFileName({ id, type }));
1750
- const replaced = code.replace(regexp, fileUrl);
2009
+ const replaced = code.replace(regexp, `$1${fileUrl}$2`);
1751
2010
  return replaced;
1752
2011
  } else {
1753
2012
  return code;
@@ -1758,11 +2017,11 @@ const pluginHMR = () => {
1758
2017
  };
1759
2018
 
1760
2019
  function printStr(dir) {
1761
- return ` ${colors.magentaBright("B R O W S E R")}
1762
- ${colors.greenBright("E X T E N S I O N")}
1763
- ${colors.blueBright("T O O L S")}
2020
+ return ` ${pc.magentaBright("B R O W S E R")}
2021
+ ${pc.greenBright("E X T E N S I O N")}
2022
+ ${pc.blueBright("T O O L S")}
1764
2023
 
1765
- ${colors.green("\u279C")} ${colors.bold("CRXJS")}: ${colors.green(`Load ${colors.cyan(dir)} as unpacked extension`)}`;
2024
+ ${pc.green("\u279C")} ${pc.bold("CRXJS")}: ${pc.green(`Load ${pc.cyan(dir)} as unpacked extension`)}`;
1766
2025
  }
1767
2026
  const pluginPrint = () => {
1768
2027
  let outDir = "dist";
@@ -1923,14 +2182,50 @@ const pluginHtmlInlineScripts = () => {
1923
2182
  };
1924
2183
  };
1925
2184
 
1926
- var loadingPageScript = "const VITE_URL = \"%PROTO%://localhost:%PORT%\";\ndocument.body.innerHTML = /* html */\n`\n<style>\n :root {\n color-scheme: light;\n --ink: #111827;\n --muted: #5f6b7a;\n --muted-subtle: rgba(95, 107, 122, 0.7);\n --muted-hint: rgba(95, 107, 122, 0.6);\n --card: #ffffff;\n --badge-bg: rgba(17, 24, 39, 0.04);\n --accent: #ff6b2c;\n --accent-2: #2563eb;\n --link-underline: rgba(37, 99, 235, 0.45);\n --glow-1: rgba(37, 99, 235, 0.12);\n --glow-2: rgba(255, 107, 44, 0.1);\n --button-grad-1: #ff7a43;\n --button-grad-2: #ff9a73;\n --button-shadow: rgba(255, 107, 44, 0.18);\n --button-shadow-hover: rgba(255, 107, 44, 0.22);\n --pulse: rgba(255, 107, 44, 0.6);\n --pulse-ring: rgba(255, 107, 44, 0.5);\n }\n\n @media (prefers-color-scheme: dark) {\n :root {\n color-scheme: dark;\n --ink: #e5e7eb;\n --muted: #a3aab5;\n --muted-subtle: rgba(163, 170, 181, 0.78);\n --muted-hint: rgba(163, 170, 181, 0.6);\n --card: #0f172a;\n --badge-bg: rgba(148, 163, 184, 0.16);\n --accent: #ff8a5a;\n --accent-2: #7aa2ff;\n --link-underline: rgba(122, 162, 255, 0.45);\n --glow-1: rgba(96, 165, 250, 0.14);\n --glow-2: rgba(251, 146, 60, 0.16);\n --button-grad-1: #f07b4d;\n --button-grad-2: #f39a76;\n --button-shadow: rgba(240, 123, 77, 0.16);\n --button-shadow-hover: rgba(240, 123, 77, 0.2);\n --pulse: rgba(255, 138, 90, 0.65);\n --pulse-ring: rgba(255, 138, 90, 0.5);\n }\n }\n\n * {\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n }\n\n body {\n font-family: \"IBM Plex Sans\", \"Inter\", -apple-system, system-ui, sans-serif;\n color: var(--ink);\n background: transparent;\n width: 420px;\n height: 250px;\n margin: 0;\n padding: 0 !important;\n }\n\n #app {\n background: var(--card);\n position: relative;\n overflow: hidden;\n display: flex;\n flex-direction: column;\n min-height: 100%;\n padding: 24px 24px 20px;\n gap: 14px;\n justify-content: center;\n }\n\n #app::before {\n content: \"\";\n position: absolute;\n inset: 0;\n background:\n radial-gradient(240px 140px at 100% 0%, var(--glow-1), transparent 70%),\n radial-gradient(220px 140px at 0% 100%, var(--glow-2), transparent 70%);\n pointer-events: none;\n }\n\n .header {\n position: relative;\n display: flex;\n flex-direction: column;\n gap: 6px;\n align-items: flex-start;\n padding-right: 96px;\n }\n\n .header-text {\n display: flex;\n flex-direction: column;\n gap: 4px;\n }\n\n .badge {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n font-size: 10px;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: var(--muted);\n background: var(--badge-bg);\n border-radius: 999px;\n padding: 6px 10px;\n white-space: nowrap;\n position: absolute;\n top: 0;\n right: 0;\n }\n\n .pulse {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n background: var(--accent);\n box-shadow: 0 0 0 0 var(--pulse);\n animation: pulse 1.6s ease-in-out infinite;\n }\n\n h1 {\n font-size: clamp(18px, 4vw, 22px);\n letter-spacing: -0.02em;\n line-height: 1.25;\n }\n\n p {\n margin: 0;\n color: var(--muted);\n font-size: 13px;\n line-height: 1.6;\n }\n\n .subtle {\n color: var(--muted-subtle);\n font-size: 12px;\n }\n\n a {\n color: var(--accent-2);\n text-decoration: none;\n border-bottom: 1px dashed var(--link-underline);\n }\n\n .content {\n display: flex;\n flex-direction: column;\n gap: 10px;\n }\n\n .footer {\n display: flex;\n flex-direction: column;\n gap: 10px;\n align-items: center;\n }\n\n .actions {\n display: flex;\n flex-wrap: wrap;\n gap: 12px;\n justify-content: center;\n }\n\n button {\n appearance: none;\n border: none;\n border-radius: 999px;\n background: linear-gradient(135deg, var(--button-grad-1), var(--button-grad-2));\n color: white;\n padding: 8px 14px;\n font-size: 12px;\n font-weight: 600;\n cursor: pointer;\n box-shadow: 0 8px 16px var(--button-shadow);\n transition: transform 160ms ease, box-shadow 160ms ease;\n }\n\n button:hover {\n transform: translateY(-1px);\n box-shadow: 0 10px 20px var(--button-shadow-hover);\n }\n\n button:focus-visible {\n outline: 2px solid var(--accent-2);\n outline-offset: 2px;\n }\n\n .hint {\n font-size: 11px;\n color: var(--muted-hint);\n text-align: center;\n }\n\n @keyframes pulse {\n 0% { box-shadow: 0 0 0 0 var(--pulse-ring); }\n 70% { box-shadow: 0 0 0 10px rgba(255, 107, 44, 0); }\n 100% { box-shadow: 0 0 0 0 rgba(255, 107, 44, 0); }\n }\n</style>\n\n<div id=\"app\">\n <div class=\"header\">\n <span class=\"badge\"><span class=\"pulse\"></span>dev server</span>\n <div class=\"header-text\">\n <h1>CRXJS DEV MODE</h1>\n <p class=\"subtle\">Connecting to the Vite dev server\\u2026</p>\n </div>\n </div>\n\n <div class=\"content\">\n <p>\n Cannot connect to <a href=\"${VITE_URL}\">${VITE_URL}</a>.\n Make sure Vite is running, then reload the extension.\n </p>\n <p>This page will close automatically after the extension reloads.</p>\n </div>\n\n <div class=\"footer\">\n <div class=\"actions\">\n <button>Reload Extension</button>\n </div>\n <div class=\"hint\">Tip: if the URL is wrong, restart Vite so it picks the right port.</div>\n </div>\n</div>\n`;\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";
2185
+ var loadingPageScript = "const VITE_URL = \"%PROTO%://localhost:%PORT%\";\nconst READY_PATH = \"%READY_PATH%\";\nconst VITE_PAGE_URL = new URL(location.pathname + location.search, VITE_URL).href;\nconst RELOAD_DELAY = 100;\nfunction getReadyUrl() {\n const url = new URL(READY_PATH, VITE_URL);\n url.searchParams.set(\"path\", location.pathname + location.search);\n url.searchParams.set(\"t\", Date.now().toString());\n return url.href;\n}\ndocument.body.innerHTML = /* html */\n`\n<style>\n :root {\n color-scheme: light;\n --ink: #111827;\n --muted: #5f6b7a;\n --muted-subtle: rgba(95, 107, 122, 0.7);\n --muted-hint: rgba(95, 107, 122, 0.6);\n --card: #ffffff;\n --badge-bg: rgba(17, 24, 39, 0.04);\n --accent: #ff6b2c;\n --accent-2: #2563eb;\n --link-underline: rgba(37, 99, 235, 0.45);\n --glow-1: rgba(37, 99, 235, 0.12);\n --glow-2: rgba(255, 107, 44, 0.1);\n --button-grad-1: #ff7a43;\n --button-grad-2: #ff9a73;\n --button-shadow: rgba(255, 107, 44, 0.18);\n --button-shadow-hover: rgba(255, 107, 44, 0.22);\n --pulse: rgba(255, 107, 44, 0.6);\n --pulse-ring: rgba(255, 107, 44, 0.5);\n }\n\n @media (prefers-color-scheme: dark) {\n :root {\n color-scheme: dark;\n --ink: #e5e7eb;\n --muted: #a3aab5;\n --muted-subtle: rgba(163, 170, 181, 0.78);\n --muted-hint: rgba(163, 170, 181, 0.6);\n --card: #0f172a;\n --badge-bg: rgba(148, 163, 184, 0.16);\n --accent: #ff8a5a;\n --accent-2: #7aa2ff;\n --link-underline: rgba(122, 162, 255, 0.45);\n --glow-1: rgba(96, 165, 250, 0.14);\n --glow-2: rgba(251, 146, 60, 0.16);\n --button-grad-1: #f07b4d;\n --button-grad-2: #f39a76;\n --button-shadow: rgba(240, 123, 77, 0.16);\n --button-shadow-hover: rgba(240, 123, 77, 0.2);\n --pulse: rgba(255, 138, 90, 0.65);\n --pulse-ring: rgba(255, 138, 90, 0.5);\n }\n }\n\n * {\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n }\n\n body {\n font-family: \"IBM Plex Sans\", \"Inter\", -apple-system, system-ui, sans-serif;\n color: var(--ink);\n background: transparent;\n width: 420px;\n height: 250px;\n margin: 0;\n padding: 0 !important;\n }\n\n #app {\n background: var(--card);\n position: relative;\n overflow: hidden;\n display: flex;\n flex-direction: column;\n min-height: 100%;\n padding: 24px 24px 20px;\n gap: 14px;\n justify-content: center;\n }\n\n #app::before {\n content: \"\";\n position: absolute;\n inset: 0;\n background:\n radial-gradient(240px 140px at 100% 0%, var(--glow-1), transparent 70%),\n radial-gradient(220px 140px at 0% 100%, var(--glow-2), transparent 70%);\n pointer-events: none;\n }\n\n .header {\n position: relative;\n display: flex;\n flex-direction: column;\n gap: 6px;\n align-items: flex-start;\n padding-right: 96px;\n }\n\n .header-text {\n display: flex;\n flex-direction: column;\n gap: 4px;\n }\n\n .badge {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n font-size: 10px;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: var(--muted);\n background: var(--badge-bg);\n border-radius: 999px;\n padding: 6px 10px;\n white-space: nowrap;\n position: absolute;\n top: 0;\n right: 0;\n }\n\n .pulse {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n background: var(--accent);\n box-shadow: 0 0 0 0 var(--pulse);\n animation: pulse 1.6s ease-in-out infinite;\n }\n\n h1 {\n font-size: clamp(18px, 4vw, 22px);\n letter-spacing: -0.02em;\n line-height: 1.25;\n }\n\n p {\n margin: 0;\n color: var(--muted);\n font-size: 13px;\n line-height: 1.6;\n }\n\n .subtle {\n color: var(--muted-subtle);\n font-size: 12px;\n }\n\n a {\n color: var(--accent-2);\n text-decoration: none;\n border-bottom: 1px dashed var(--link-underline);\n }\n\n .content {\n display: flex;\n flex-direction: column;\n gap: 10px;\n }\n\n .footer {\n display: flex;\n flex-direction: column;\n gap: 10px;\n align-items: center;\n }\n\n .actions {\n display: flex;\n flex-wrap: wrap;\n gap: 12px;\n justify-content: center;\n }\n\n button {\n appearance: none;\n border: none;\n border-radius: 999px;\n background: linear-gradient(135deg, var(--button-grad-1), var(--button-grad-2));\n color: white;\n padding: 8px 14px;\n font-size: 12px;\n font-weight: 600;\n cursor: pointer;\n box-shadow: 0 8px 16px var(--button-shadow);\n transition: transform 160ms ease, box-shadow 160ms ease;\n }\n\n button:hover {\n transform: translateY(-1px);\n box-shadow: 0 10px 20px var(--button-shadow-hover);\n }\n\n button:focus-visible {\n outline: 2px solid var(--accent-2);\n outline-offset: 2px;\n }\n\n .hint {\n font-size: 11px;\n color: var(--muted-hint);\n text-align: center;\n }\n\n @keyframes pulse {\n 0% { box-shadow: 0 0 0 0 var(--pulse-ring); }\n 70% { box-shadow: 0 0 0 10px rgba(255, 107, 44, 0); }\n 100% { box-shadow: 0 0 0 0 rgba(255, 107, 44, 0); }\n }\n</style>\n\n<div id=\"app\">\n <div class=\"header\">\n <span class=\"badge\"><span class=\"pulse\"></span>dev server</span>\n <div class=\"header-text\">\n <h1>CRXJS DEV MODE</h1>\n <p class=\"subtle\">Connecting to the Vite dev server\\u2026</p>\n </div>\n </div>\n\n <div class=\"content\">\n <p>\n Cannot connect to <a href=\"${VITE_PAGE_URL}\">${VITE_PAGE_URL}</a>.\n Make sure Vite is running, then reload the extension.\n </p>\n <p>This page will close automatically after the extension reloads.</p>\n </div>\n\n <div class=\"footer\">\n <div class=\"actions\">\n <button>Reload Extension</button>\n </div>\n <div class=\"hint\">Tip: if the URL is wrong, restart Vite so it picks the right port.</div>\n </div>\n</div>\n`;\ndocument.body.querySelector(\"button\")?.addEventListener(\"click\", () => {\n chrome.runtime.reload();\n});\nlet tries = 0;\nlet ready = false;\ndo {\n try {\n const response = await fetch(getReadyUrl());\n if (!response.ok)\n throw new Error(`HTTP ${response.status}`);\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_PAGE_URL}`);\n console.log(`[CRXJS] Retrying in ${timeout}ms...`);\n await new Promise((resolve) => setTimeout(resolve, timeout));\n }\n} while (!ready);\nconsole.log(`[CRXJS] Vite Dev Server is ready on ${VITE_PAGE_URL}`);\nconsole.log(`[CRXJS] Reloading in ${RELOAD_DELAY}ms...`);\nawait new Promise((resolve) => setTimeout(resolve, RELOAD_DELAY));\nlocation.reload();\n";
1927
2186
 
1928
2187
  var loadingPageHtml = "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <title>CRXJS DEV MODE</title>\n <script src=\"%SCRIPT%\" type=\"module\"></script>\n </head>\n <body>\n <p>An unknown error occurred. Failed to load the script.</p>\n </body>\n</html>\n";
1929
2188
 
1930
2189
  const { readFile } = promises;
2190
+ const loadingPageReadyPath = "/@crx/dev-ready";
2191
+ function normalizeHtmlPath(pathname) {
2192
+ let decoded;
2193
+ try {
2194
+ decoded = decodeURIComponent(pathname);
2195
+ } catch {
2196
+ return null;
2197
+ }
2198
+ const normalized = normalize(decoded.replace(/^\/+/, "") || "index.html");
2199
+ if (normalized.startsWith("..") || isAbsolute(normalized))
2200
+ return null;
2201
+ return normalized;
2202
+ }
2203
+ function getLoadingPageReadyHtmlPath(requestUrl) {
2204
+ if (!requestUrl)
2205
+ return null;
2206
+ let url;
2207
+ try {
2208
+ url = new URL(requestUrl, "http://crxjs.local");
2209
+ } catch {
2210
+ return null;
2211
+ }
2212
+ if (url.pathname !== loadingPageReadyPath)
2213
+ return void 0;
2214
+ const path = url.searchParams.get("path");
2215
+ if (!path)
2216
+ return null;
2217
+ let pageUrl;
2218
+ try {
2219
+ pageUrl = new URL(path, "http://crxjs.local");
2220
+ } catch {
2221
+ return null;
2222
+ }
2223
+ return normalizeHtmlPath(pageUrl.pathname);
2224
+ }
1931
2225
  const pluginManifest = () => {
1932
2226
  let manifest;
1933
2227
  let plugins = [];
2228
+ let devHtmlFiles = /* @__PURE__ */ new Set();
1934
2229
  let refId;
1935
2230
  let config;
1936
2231
  return [
@@ -1950,6 +2245,7 @@ const pluginManifest = () => {
1950
2245
  background: sw,
1951
2246
  html
1952
2247
  } = await manifestFiles(manifest, { cwd: config2.root });
2248
+ devHtmlFiles = new Set(html.map(normalizeHtmlPath).filter(isString));
1953
2249
  const { entries = [] } = config2.optimizeDeps ?? {};
1954
2250
  let { input = [] } = config2.build?.rollupOptions ?? {};
1955
2251
  if (typeof input === "string")
@@ -1983,6 +2279,25 @@ const pluginManifest = () => {
1983
2279
  plugins = resolvedConfig.plugins;
1984
2280
  }
1985
2281
  },
2282
+ configureServer(server) {
2283
+ server.middlewares.use((req, res, next) => {
2284
+ const htmlPath = getLoadingPageReadyHtmlPath(req.url);
2285
+ if (typeof htmlPath === "undefined") {
2286
+ next();
2287
+ return;
2288
+ }
2289
+ res.setHeader("Access-Control-Allow-Origin", "*");
2290
+ res.setHeader("Access-Control-Allow-Methods", "GET");
2291
+ if (!htmlPath) {
2292
+ res.statusCode = 400;
2293
+ res.end();
2294
+ return;
2295
+ }
2296
+ const exists = devHtmlFiles.has(htmlPath) && (existsSync(join(server.config.root, htmlPath)) || existsSync(join(server.config.publicDir, htmlPath)));
2297
+ res.statusCode = exists ? 204 : 404;
2298
+ res.end();
2299
+ });
2300
+ },
1986
2301
  buildStart(options) {
1987
2302
  if (options.plugins)
1988
2303
  plugins = options.plugins;
@@ -2136,7 +2451,10 @@ const pluginManifest = () => {
2136
2451
  const refId2 = this.emitFile({
2137
2452
  type: "chunk",
2138
2453
  id: id2,
2139
- name: basename(file)
2454
+ name: basename(file),
2455
+ // Preserve content script entry exports so the build finalizer
2456
+ // can decide whether the script needs a loader wrapper.
2457
+ preserveSignature: "exports-only"
2140
2458
  });
2141
2459
  contentScripts.set(
2142
2460
  file,
@@ -2206,6 +2524,7 @@ const pluginManifest = () => {
2206
2524
  }
2207
2525
  }
2208
2526
  } else {
2527
+ finalizeBuildContentScripts(this, bundle);
2209
2528
  if (manifest2.background && "service_worker" in manifest2.background) {
2210
2529
  const ref = manifest2.background.service_worker;
2211
2530
  const name = this.getFileName(ref);
@@ -2220,7 +2539,7 @@ const pluginManifest = () => {
2220
2539
  ({ js = [], ...rest }) => {
2221
2540
  return {
2222
2541
  js: js.map((id) => {
2223
- const script = contentScripts.get(id);
2542
+ const script = contentScripts.get(id) ?? contentScripts.get(prefix$1("/", id));
2224
2543
  const fileName = script?.loaderName ?? script?.fileName;
2225
2544
  if (typeof fileName === "undefined")
2226
2545
  throw new Error(
@@ -2242,11 +2561,11 @@ const pluginManifest = () => {
2242
2561
  const name = `[${plugin.name}]`;
2243
2562
  let message = error;
2244
2563
  if (error instanceof Error) {
2245
- message = colors.red(
2564
+ message = pc.red(
2246
2565
  `${name} ${error.stack ? error.stack : error.message}`
2247
2566
  );
2248
2567
  } else if (typeof error === "string") {
2249
- message = colors.red(`${name} ${error}`);
2568
+ message = pc.red(`${name} ${error}`);
2250
2569
  }
2251
2570
  console.log(message);
2252
2571
  throw new Error(`Error in ${plugin.name}.renderCrxManifest`);
@@ -2260,6 +2579,11 @@ const pluginManifest = () => {
2260
2579
  "webAccessibleResources"
2261
2580
  ];
2262
2581
  const files = await manifestFiles(manifest2, { cwd: config.root });
2582
+ if (config.command === "serve") {
2583
+ devHtmlFiles = new Set(
2584
+ files.html.map(normalizeHtmlPath).filter(isString)
2585
+ );
2586
+ }
2263
2587
  await Promise.all(
2264
2588
  assetTypes.map((k) => files[k]).flat().map(async (f) => {
2265
2589
  if (typeof bundle[f] === "undefined") {
@@ -2291,7 +2615,7 @@ Public dir: "${config.publicDir}"`
2291
2615
  const refId2 = this.emitFile({
2292
2616
  type: "asset",
2293
2617
  name: "loading-page.js",
2294
- source: loadingPageScript.replace("%PROTO%", config.server.https ? "https" : "http").replace("%PORT%", `${config.server.port ?? 0}`)
2618
+ source: loadingPageScript.replace("%PROTO%", config.server.https ? "https" : "http").replace("%PORT%", `${config.server.port ?? 0}`).replace("%READY_PATH%", loadingPageReadyPath)
2295
2619
  });
2296
2620
  const loadingPageScriptName = this.getFileName(refId2);
2297
2621
  files.html.map(
@@ -2585,6 +2909,7 @@ const crx = (options) => {
2585
2909
  contentScripts.clear();
2586
2910
  return [
2587
2911
  pluginOptionsProvider(options),
2912
+ pluginExtensionCors(),
2588
2913
  pluginContentScriptsIife(),
2589
2914
  // Must come early so isIifeModeEnabled is set before manifest plugin
2590
2915
  pluginBackground(),