@crxjs/vite-plugin 2.5.0 → 2.6.0

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
@@ -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);
@@ -465,8 +518,17 @@ fileWriterEvent$.pipe(
465
518
  const allFilesReady$ = buildEnd$.pipe(
466
519
  switchMap(() => outputFiles.change$.pipe(startWith({ type: "start" }))),
467
520
  map(() => [...outputFiles.values()]),
468
- switchMap((files) => Promise.allSettled(files.map(({ file }) => file)))
521
+ switchMap(
522
+ (files) => Promise.allSettled(files.map((file) => waitForOutputFile(file)))
523
+ )
469
524
  );
525
+ async function waitForOutputFile(file, seen = /* @__PURE__ */ new Set()) {
526
+ if (seen.has(file))
527
+ return;
528
+ seen.add(file);
529
+ const { deps } = await file.file;
530
+ await Promise.all(deps.map((dep) => waitForOutputFile(dep, seen)));
531
+ }
470
532
  const timestamp$ = new BehaviorSubject(Date.now());
471
533
  allFilesReady$.subscribe(() => {
472
534
  timestamp$.next(Date.now());
@@ -509,7 +571,14 @@ function prepScript(fileName, script) {
509
571
  // get script contents from dev server
510
572
  mergeMap(async ({ server }) => {
511
573
  const target = getOutputPath(server, fileName);
512
- const viteUrl = getViteUrl(script);
574
+ const originalViteUrl = getViteUrl(script);
575
+ const isVueSfcQuery = script.id.includes("?vue");
576
+ const viteUrl = getViteUrl(script, { timestamp: isVueSfcQuery });
577
+ if (isVueSfcQuery) {
578
+ const module = await server.moduleGraph.getModuleByUrl(originalViteUrl);
579
+ if (module)
580
+ server.moduleGraph.invalidateModule(module);
581
+ }
513
582
  const transformResult = await server.transformRequest(viteUrl);
514
583
  if (!transformResult)
515
584
  throw new TypeError(`Unable to load "${script.id}" from server.`);
@@ -667,6 +736,11 @@ async function allFilesReady() {
667
736
 
668
737
  const { outputFile } = fsx;
669
738
  const debug$4 = _debug("file-writer");
739
+ function queueWrite(script, previous) {
740
+ if (!previous)
741
+ return write(script);
742
+ return previous.file.catch(() => void 0).then(() => write(script));
743
+ }
670
744
  async function start({
671
745
  server
672
746
  }) {
@@ -708,18 +782,24 @@ function add(script) {
708
782
  file = formatFileData({
709
783
  ...script,
710
784
  fileName,
711
- file: write(script)
785
+ file: queueWrite(script)
712
786
  });
713
787
  outputFiles.set(file.fileName, file);
714
788
  debug$4("add: stored new file %s", file.fileName);
715
789
  } else {
716
790
  const isVirtualModule = script.id.startsWith("/@id/") || script.id.startsWith("/__");
717
- if (isVirtualModule) {
791
+ const isTimestampedModule = script.type === "module" && /[?&]t=\d+/.test(script.id);
792
+ if (isVirtualModule || isTimestampedModule) {
718
793
  debug$4(
719
- "add: virtual module already exists, triggering re-write for %s",
794
+ "add: module already exists, triggering re-write for %s",
720
795
  fileName
721
796
  );
722
- file.file = write(script);
797
+ file = formatFileData({
798
+ ...file,
799
+ ...script,
800
+ fileName,
801
+ file: queueWrite(script, file)
802
+ });
723
803
  outputFiles.set(fileName, file);
724
804
  }
725
805
  }
@@ -736,7 +816,7 @@ function update(_id) {
736
816
  const scriptFile = outputFiles.get(fileName);
737
817
  if (scriptFile) {
738
818
  debug$4("update: found file, calling write()");
739
- scriptFile.file = write({ id, type });
819
+ scriptFile.file = queueWrite({ id, type }, scriptFile);
740
820
  updatedFiles.push(scriptFile);
741
821
  outputFiles.set(fileName, scriptFile);
742
822
  }
@@ -794,7 +874,7 @@ const pluginContentScripts = () => {
794
874
  });
795
875
  if (worldMainIds.size) {
796
876
  const name = `[${pluginName}]`;
797
- const message = colors.yellow(
877
+ const message = pc.yellow(
798
878
  [
799
879
  `${name} Some content-scripts don't support HMR because the world is MAIN:`,
800
880
  ...[...worldMainIds].map((id) => ` ${id}`)
@@ -823,7 +903,7 @@ const pluginContentScripts = () => {
823
903
  try {
824
904
  const react = await import('@vitejs/plugin-react');
825
905
  preambleCode = react.default.preambleCode;
826
- } catch (error) {
906
+ } catch {
827
907
  preambleCode = false;
828
908
  }
829
909
  }
@@ -898,41 +978,50 @@ const pluginContentScripts = () => {
898
978
  };
899
979
  },
900
980
  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
- }
981
+ finalizeBuildContentScripts(this, bundle, worldMainIds);
932
982
  }
933
983
  }
934
984
  ];
935
985
  };
986
+ function finalizeBuildContentScripts(context, bundle, worldMainIds = /* @__PURE__ */ new Set()) {
987
+ const processed = /* @__PURE__ */ new Set();
988
+ for (const [key, script] of contentScripts) {
989
+ if (key !== script.refId || processed.has(script))
990
+ continue;
991
+ processed.add(script);
992
+ if (script.type === "module") {
993
+ script.fileName = script.fileName ?? context.getFileName(script.refId);
994
+ } else if (script.type === "loader") {
995
+ const fileName = script.fileName ?? context.getFileName(script.refId);
996
+ script.fileName = fileName;
997
+ const bundleFileInfo = bundle[fileName];
998
+ if (bundleFileInfo?.type !== "chunk")
999
+ continue;
1000
+ const shouldUseLoader = !(bundleFileInfo.imports.length === 0 && bundleFileInfo.dynamicImports.length === 0 && bundleFileInfo.exports.length === 0);
1001
+ if (shouldUseLoader) {
1002
+ if (typeof script.loaderName === "undefined") {
1003
+ const refId = context.emitFile({
1004
+ type: "asset",
1005
+ name: getFileName({
1006
+ type: "loader",
1007
+ id: basename(script.id)
1008
+ }),
1009
+ source: worldMainIds.has(script.id) ? createProMainLoader({
1010
+ fileName: `./${fileName.split("/").at(-1)}`
1011
+ }) : createProLoader({ fileName })
1012
+ });
1013
+ script.loaderName = context.getFileName(refId);
1014
+ }
1015
+ } else if (typeof script.loaderName === "undefined" && !bundleFileInfo.code.startsWith("(function(){")) {
1016
+ bundleFileInfo.code = `(function(){${bundleFileInfo.code}})()
1017
+ `;
1018
+ }
1019
+ } else if (script.type === "iife") {
1020
+ continue;
1021
+ }
1022
+ contentScripts.set(script.refId, formatFileData(script));
1023
+ }
1024
+ }
936
1025
 
937
1026
  const pluginContentScriptsCss = () => {
938
1027
  let injectCss;
@@ -1027,45 +1116,22 @@ const pluginContentScriptsIife = () => {
1027
1116
  name: pluginName,
1028
1117
  apply: "build",
1029
1118
  enforce: "post",
1030
- async generateBundle(options, bundle) {
1119
+ async generateBundle() {
1031
1120
  const opts = await getOptions({ plugins: config.plugins });
1032
1121
  const _manifest = opts.manifest;
1033
1122
  const manifest = typeof _manifest === "function" ? await _manifest({ command: "build", mode: config.mode }) : await Promise.resolve(_manifest);
1034
1123
  const standaloneFiles = (opts.contentScripts?.standaloneFiles || []).map(
1035
- (f) => f.replace(/^\//, "")
1124
+ normalizeContentScriptPath
1125
+ );
1126
+ const iifeEntries = collectIifeEntries(
1127
+ manifest,
1128
+ standaloneFiles,
1129
+ config.root
1036
1130
  );
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
1131
  if (iifeEntries.length === 0)
1066
1132
  return;
1067
1133
  console.log(
1068
- colors.cyan(`
1134
+ pc.cyan(`
1069
1135
  [${pluginName}] Building ${iifeEntries.length} content script(s) as IIFE...`)
1070
1136
  );
1071
1137
  for (const entry of iifeEntries) {
@@ -1073,52 +1139,106 @@ const pluginContentScriptsIife = () => {
1073
1139
  try {
1074
1140
  const iifeConfig = createIifeConfig(config, entry.id, outputFileName);
1075
1141
  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
- }
1142
+ const outputs = getBuildOutputs(result, entry.file);
1143
+ emitIifeOutputs(this, outputs, entry.file, outputFileName);
1144
+ registerIifeContentScript(entry, outputFileName);
1145
+ console.log(
1146
+ pc.green(` \u2713 ${basename(entry.file)} \u2192 ${outputFileName}`)
1147
+ );
1108
1148
  } catch (error) {
1109
1149
  console.error(
1110
- colors.red(` \u2717 Failed to build ${entry.file}:`),
1150
+ pc.red(` \u2717 Failed to build ${entry.file}:`),
1111
1151
  error
1112
1152
  );
1113
1153
  throw error;
1114
1154
  }
1115
1155
  }
1116
- console.log(colors.cyan(`[${pluginName}] IIFE build complete
1156
+ console.log(pc.cyan(`[${pluginName}] IIFE build complete
1117
1157
  `));
1118
1158
  }
1119
1159
  }
1120
1160
  ];
1121
1161
  };
1162
+ function normalizeContentScriptPath(file) {
1163
+ return file.replace(/^\//, "");
1164
+ }
1165
+ function isStandaloneFile(file, standaloneFiles) {
1166
+ return standaloneFiles.includes(normalizeContentScriptPath(file));
1167
+ }
1168
+ function collectIifeEntries(manifest, standaloneFiles, root) {
1169
+ const entries = [];
1170
+ const entryIds = /* @__PURE__ */ new Set();
1171
+ const addEntry = (entry) => {
1172
+ if (entryIds.has(entry.id))
1173
+ return;
1174
+ entryIds.add(entry.id);
1175
+ entries.push(entry);
1176
+ };
1177
+ for (const { js = [], matches = [] } of manifest.content_scripts ?? []) {
1178
+ for (const file of js) {
1179
+ if (isIifeContentScript(file) || isStandaloneFile(file, standaloneFiles)) {
1180
+ addEntry({ file, id: join(root, file), matches });
1181
+ }
1182
+ }
1183
+ }
1184
+ for (const [, script] of contentScripts.entries()) {
1185
+ if (script.type !== "iife" || !script.isDynamicScript)
1186
+ continue;
1187
+ addEntry({
1188
+ file: script.id,
1189
+ id: join(root, script.id),
1190
+ matches: script.matches ?? []
1191
+ });
1192
+ }
1193
+ return entries;
1194
+ }
1195
+ function getBuildOutputs(result, entryFile) {
1196
+ if ("on" in result) {
1197
+ throw new Error(`Unexpected watcher result for "${entryFile}"`);
1198
+ }
1199
+ return Array.isArray(result) ? result.flatMap((r) => r.output) : result.output;
1200
+ }
1201
+ function emitIifeOutputs(context, outputs, entryFile, outputFileName) {
1202
+ const entryChunk = outputs.find(
1203
+ (output) => output.type === "chunk" && output.isEntry
1204
+ );
1205
+ if (!entryChunk) {
1206
+ throw new Error(`Unable to generate IIFE bundle for "${entryFile}"`);
1207
+ }
1208
+ context.emitFile({
1209
+ type: "asset",
1210
+ fileName: outputFileName,
1211
+ source: entryChunk.code
1212
+ });
1213
+ for (const output of outputs) {
1214
+ if (output.type === "asset") {
1215
+ if (output.fileName !== outputFileName && output.fileName !== "manifest.json" && !output.fileName.startsWith(".vite/")) {
1216
+ context.emitFile({
1217
+ type: "asset",
1218
+ fileName: output.fileName,
1219
+ source: output.source
1220
+ });
1221
+ }
1222
+ } else if (!output.isEntry) {
1223
+ context.emitFile({
1224
+ type: "asset",
1225
+ fileName: output.fileName,
1226
+ source: output.code
1227
+ });
1228
+ }
1229
+ }
1230
+ }
1231
+ function registerIifeContentScript(entry, outputFileName) {
1232
+ const existingScript = contentScripts.get(entry.file);
1233
+ const script = existingScript ? { ...existingScript, fileName: outputFileName } : {
1234
+ type: "iife",
1235
+ id: entry.file,
1236
+ refId: entry.file,
1237
+ matches: entry.matches,
1238
+ fileName: outputFileName
1239
+ };
1240
+ contentScripts.set(entry.file, formatFileData(script));
1241
+ }
1122
1242
  function getIifeOutputPath(file) {
1123
1243
  const normalizedFile = file.replace(/^\//, "");
1124
1244
  const dir = dirname(normalizedFile);
@@ -1188,6 +1308,36 @@ const pluginDynamicContentScripts = () => {
1188
1308
  );
1189
1309
  }).catch(() => void 0);
1190
1310
  },
1311
+ buildStart() {
1312
+ if (config.command === "build") {
1313
+ const dynamicScripts = [];
1314
+ for (const [key, script] of contentScripts) {
1315
+ if (script.isDynamicScript && key === script.scriptId) {
1316
+ dynamicScripts.push(script);
1317
+ }
1318
+ }
1319
+ contentScripts.clear();
1320
+ for (const script of dynamicScripts) {
1321
+ const absoluteId = script.id.startsWith("/") ? `${config.root}${script.id}` : `${config.root}/${script.id}`;
1322
+ const refId = this.emitFile({
1323
+ type: "chunk",
1324
+ id: absoluteId,
1325
+ name: basename(script.id)
1326
+ });
1327
+ contentScripts.set(
1328
+ script.id,
1329
+ formatFileData({
1330
+ type: script.type,
1331
+ id: script.id,
1332
+ isDynamicScript: true,
1333
+ refId,
1334
+ scriptId: script.scriptId,
1335
+ matches: script.matches
1336
+ })
1337
+ );
1338
+ }
1339
+ }
1340
+ },
1191
1341
  configureServer(server) {
1192
1342
  return () => {
1193
1343
  server.middlewares.use(async (req, res, next) => {
@@ -1249,7 +1399,10 @@ const pluginDynamicContentScripts = () => {
1249
1399
  refId = this.emitFile({
1250
1400
  type: "chunk",
1251
1401
  id,
1252
- name: basename(id)
1402
+ name: basename(id),
1403
+ // Preserve content script entry exports so the build finalizer
1404
+ // can decide whether the script needs a loader wrapper.
1405
+ preserveSignature: "exports-only"
1253
1406
  });
1254
1407
  } else {
1255
1408
  refId = scriptId;
@@ -1283,9 +1436,29 @@ const pluginDynamicContentScripts = () => {
1283
1436
  const index = id.indexOf("?scriptId=");
1284
1437
  if (index > -1) {
1285
1438
  const scriptId = id.slice(index + "?scriptId=".length);
1286
- const script = contentScripts.get(scriptId);
1439
+ let script = contentScripts.get(scriptId);
1440
+ if (!script && config.command === "build") {
1441
+ const fileId = id.slice(0, index);
1442
+ const refId = this.emitFile({
1443
+ type: "chunk",
1444
+ id: fileId,
1445
+ name: basename(fileId)
1446
+ });
1447
+ script = formatFileData({
1448
+ type: "loader",
1449
+ id: relative(config.root, fileId),
1450
+ isDynamicScript: true,
1451
+ refId,
1452
+ scriptId,
1453
+ matches: []
1454
+ });
1455
+ contentScripts.set(script.id, script);
1456
+ }
1457
+ if (!script) {
1458
+ throw new Error(`Content script not found for scriptId: "${scriptId}"`);
1459
+ }
1287
1460
  if (config.command === "build") {
1288
- return `export default import.meta.CRX_DYNAMIC_SCRIPT_${script.refId};`;
1461
+ return `export default import.meta.CRX_DYNAMIC_SCRIPT_${script.scriptId};`;
1289
1462
  } else if (typeof script.fileName === "string") {
1290
1463
  return `export default ${JSON.stringify(script.fileName)};`;
1291
1464
  } else {
@@ -1493,6 +1666,89 @@ const isCustomPayload = (p) => {
1493
1666
  return p.type === "custom";
1494
1667
  };
1495
1668
  const hmrPayload$ = new Subject();
1669
+ function withTimestamp(id, timestamp) {
1670
+ if (id.includes("?t=") || id.includes("&t="))
1671
+ return id;
1672
+ const t = `t=${timestamp}` + (id.includes("?") ? "&" : "");
1673
+ const parts = id.split("?");
1674
+ parts[1] = typeof parts[1] === "undefined" ? t : t + parts[1];
1675
+ return parts.join("?");
1676
+ }
1677
+ function getUpdatePayloadFileIds(p, { timestamp = false } = {}) {
1678
+ const ids = /* @__PURE__ */ new Set();
1679
+ for (const u of p.updates) {
1680
+ for (const id of [u.path, u.acceptedPath]) {
1681
+ const isVirtualModule = id.startsWith("/@id/") || id.startsWith("/__");
1682
+ const isQueryModule = id.includes("?");
1683
+ if (isVirtualModule || isQueryModule)
1684
+ ids.add(timestamp ? withTimestamp(id, u.timestamp) : id);
1685
+ }
1686
+ }
1687
+ return [...ids];
1688
+ }
1689
+ function mapVitePayloadForCrx(p) {
1690
+ switch (p.type) {
1691
+ case "full-reload": {
1692
+ const fullReload = {
1693
+ type: "full-reload",
1694
+ path: p.path && getViteUrl({ id: p.path, type: "module" })
1695
+ };
1696
+ return fullReload;
1697
+ }
1698
+ case "prune": {
1699
+ const prune = {
1700
+ type: "prune",
1701
+ paths: p.paths.map((id) => getViteUrl({ id, type: "module" }))
1702
+ };
1703
+ return prune;
1704
+ }
1705
+ case "update": {
1706
+ debug$3("update payload with %d updates", p.updates.length);
1707
+ for (const u of p.updates) {
1708
+ debug$3(
1709
+ "update item: path=%s acceptedPath=%s type=%s",
1710
+ u.path,
1711
+ u.acceptedPath,
1712
+ u.type
1713
+ );
1714
+ }
1715
+ const update_ = {
1716
+ type: "update",
1717
+ updates: p.updates.map(({ acceptedPath: ap, path: p2, ...rest }) => ({
1718
+ ...rest,
1719
+ acceptedPath: prefix$1("/", getFileName({ id: ap, type: "module" })),
1720
+ path: prefix$1("/", getFileName({ id: p2, type: "module" }))
1721
+ }))
1722
+ };
1723
+ return update_;
1724
+ }
1725
+ default:
1726
+ return p;
1727
+ }
1728
+ }
1729
+ async function prepareVitePayloadForCrx(p) {
1730
+ if (p.type === "update") {
1731
+ const pendingFiles = getUpdatePayloadFileIds(p, {
1732
+ timestamp: true
1733
+ }).flatMap((id) => {
1734
+ debug$3("updating payload module: %s", id);
1735
+ return update(id).map((file) => file.file);
1736
+ });
1737
+ await Promise.all(pendingFiles);
1738
+ await firstValueFrom(allFilesReady$);
1739
+ }
1740
+ return mapVitePayloadForCrx(p);
1741
+ }
1742
+ function shouldForwardCrxPayload(p) {
1743
+ switch (p.type) {
1744
+ case "prune":
1745
+ return p.paths.length > 0;
1746
+ case "update":
1747
+ return p.updates.length > 0;
1748
+ default:
1749
+ return true;
1750
+ }
1751
+ }
1496
1752
  const crxHMRPayload$ = hmrPayload$.pipe(
1497
1753
  filter((p) => !isCustomPayload(p)),
1498
1754
  buffer(allFilesReady$),
@@ -1509,63 +1765,8 @@ const crxHMRPayload$ = hmrPayload$.pipe(
1509
1765
  payloads.push(fullReload);
1510
1766
  return payloads;
1511
1767
  }),
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
- }),
1768
+ mergeMap(prepareVitePayloadForCrx),
1769
+ filter(shouldForwardCrxPayload),
1569
1770
  map((data) => {
1570
1771
  debug$3(`hmr payload`, data);
1571
1772
  return {
@@ -1597,6 +1798,23 @@ const crxRuntimeReload = {
1597
1798
  type: "custom",
1598
1799
  event: "crx:runtime-reload"
1599
1800
  };
1801
+ function getChangedFilePath(root, file) {
1802
+ if (!file)
1803
+ return null;
1804
+ const normalizedRoot = normalize(root);
1805
+ const normalizedFile = normalize(file);
1806
+ const relativeFile = relative(normalizedRoot, normalizedFile);
1807
+ if (!relativeFile || relativeFile.startsWith("..") || isAbsolute(relativeFile)) {
1808
+ return null;
1809
+ }
1810
+ return prefix$1("/", relativeFile);
1811
+ }
1812
+ function stripTimestamp(id) {
1813
+ return id.replace(/([?&])t=\d+&/, "$1").replace(/[?&]t=\d+$/, "").replace(/\?$/, "");
1814
+ }
1815
+ function escapeRegExp(text) {
1816
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1817
+ }
1600
1818
  const pluginHMR = () => {
1601
1819
  let inputManifestFiles;
1602
1820
  let decoratedSend;
@@ -1663,13 +1881,14 @@ const pluginHMR = () => {
1663
1881
  // background changes require a full extension reload
1664
1882
  handleHotUpdate({ file, modules, server }) {
1665
1883
  const { root } = server.config;
1666
- const changedFilePath = file ? file.startsWith(root) ? prefix$1("/", file.slice(root.length)) : null : null;
1884
+ const changedFilePath = getChangedFilePath(root, file);
1667
1885
  const relFiles = /* @__PURE__ */ new Set();
1668
1886
  const fsFiles = /* @__PURE__ */ new Set();
1669
1887
  const virtualModules = /* @__PURE__ */ new Set();
1670
1888
  for (const m of modules) {
1671
- if (m.id?.startsWith(root)) {
1672
- relFiles.add(m.id.slice(server.config.root.length));
1889
+ const relFile = getChangedFilePath(root, m.id);
1890
+ if (relFile) {
1891
+ relFiles.add(relFile);
1673
1892
  } else if (m.url?.startsWith("/@fs")) {
1674
1893
  fsFiles.add(m.url);
1675
1894
  } else if (m.id?.startsWith("\0") || m.url?.startsWith("/@id/__x00__")) {
@@ -1741,13 +1960,14 @@ const pluginHMR = () => {
1741
1960
  },
1742
1961
  renderCrxDevScript(code, { id: _id, type }) {
1743
1962
  if (type === "module" && _id !== "/@vite/client" && code.includes("createHotContext")) {
1744
- const id = _id.replace(/t=\d+&/, "");
1745
- const escaped = id.replace(/([?&.])/g, "\\$1");
1963
+ const id = stripTimestamp(_id);
1964
+ const escaped = escapeRegExp(id);
1746
1965
  const regexp = new RegExp(
1747
- `(?<=createHotContext\\(")${escaped}(?="\\))`
1966
+ `(createHotContext\\(")${escaped}("\\))`,
1967
+ "g"
1748
1968
  );
1749
1969
  const fileUrl = prefix$1("/", getFileName({ id, type }));
1750
- const replaced = code.replace(regexp, fileUrl);
1970
+ const replaced = code.replace(regexp, `$1${fileUrl}$2`);
1751
1971
  return replaced;
1752
1972
  } else {
1753
1973
  return code;
@@ -1758,11 +1978,11 @@ const pluginHMR = () => {
1758
1978
  };
1759
1979
 
1760
1980
  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")}
1981
+ return ` ${pc.magentaBright("B R O W S E R")}
1982
+ ${pc.greenBright("E X T E N S I O N")}
1983
+ ${pc.blueBright("T O O L S")}
1764
1984
 
1765
- ${colors.green("\u279C")} ${colors.bold("CRXJS")}: ${colors.green(`Load ${colors.cyan(dir)} as unpacked extension`)}`;
1985
+ ${pc.green("\u279C")} ${pc.bold("CRXJS")}: ${pc.green(`Load ${pc.cyan(dir)} as unpacked extension`)}`;
1766
1986
  }
1767
1987
  const pluginPrint = () => {
1768
1988
  let outDir = "dist";
@@ -1923,14 +2143,50 @@ const pluginHtmlInlineScripts = () => {
1923
2143
  };
1924
2144
  };
1925
2145
 
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";
2146
+ 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
2147
 
1928
2148
  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
2149
 
1930
2150
  const { readFile } = promises;
2151
+ const loadingPageReadyPath = "/@crx/dev-ready";
2152
+ function normalizeHtmlPath(pathname) {
2153
+ let decoded;
2154
+ try {
2155
+ decoded = decodeURIComponent(pathname);
2156
+ } catch {
2157
+ return null;
2158
+ }
2159
+ const normalized = normalize(decoded.replace(/^\/+/, "") || "index.html");
2160
+ if (normalized.startsWith("..") || isAbsolute(normalized))
2161
+ return null;
2162
+ return normalized;
2163
+ }
2164
+ function getLoadingPageReadyHtmlPath(requestUrl) {
2165
+ if (!requestUrl)
2166
+ return null;
2167
+ let url;
2168
+ try {
2169
+ url = new URL(requestUrl, "http://crxjs.local");
2170
+ } catch {
2171
+ return null;
2172
+ }
2173
+ if (url.pathname !== loadingPageReadyPath)
2174
+ return void 0;
2175
+ const path = url.searchParams.get("path");
2176
+ if (!path)
2177
+ return null;
2178
+ let pageUrl;
2179
+ try {
2180
+ pageUrl = new URL(path, "http://crxjs.local");
2181
+ } catch {
2182
+ return null;
2183
+ }
2184
+ return normalizeHtmlPath(pageUrl.pathname);
2185
+ }
1931
2186
  const pluginManifest = () => {
1932
2187
  let manifest;
1933
2188
  let plugins = [];
2189
+ let devHtmlFiles = /* @__PURE__ */ new Set();
1934
2190
  let refId;
1935
2191
  let config;
1936
2192
  return [
@@ -1950,6 +2206,7 @@ const pluginManifest = () => {
1950
2206
  background: sw,
1951
2207
  html
1952
2208
  } = await manifestFiles(manifest, { cwd: config2.root });
2209
+ devHtmlFiles = new Set(html.map(normalizeHtmlPath).filter(isString));
1953
2210
  const { entries = [] } = config2.optimizeDeps ?? {};
1954
2211
  let { input = [] } = config2.build?.rollupOptions ?? {};
1955
2212
  if (typeof input === "string")
@@ -1983,6 +2240,25 @@ const pluginManifest = () => {
1983
2240
  plugins = resolvedConfig.plugins;
1984
2241
  }
1985
2242
  },
2243
+ configureServer(server) {
2244
+ server.middlewares.use((req, res, next) => {
2245
+ const htmlPath = getLoadingPageReadyHtmlPath(req.url);
2246
+ if (typeof htmlPath === "undefined") {
2247
+ next();
2248
+ return;
2249
+ }
2250
+ res.setHeader("Access-Control-Allow-Origin", "*");
2251
+ res.setHeader("Access-Control-Allow-Methods", "GET");
2252
+ if (!htmlPath) {
2253
+ res.statusCode = 400;
2254
+ res.end();
2255
+ return;
2256
+ }
2257
+ const exists = devHtmlFiles.has(htmlPath) && (existsSync(join(server.config.root, htmlPath)) || existsSync(join(server.config.publicDir, htmlPath)));
2258
+ res.statusCode = exists ? 204 : 404;
2259
+ res.end();
2260
+ });
2261
+ },
1986
2262
  buildStart(options) {
1987
2263
  if (options.plugins)
1988
2264
  plugins = options.plugins;
@@ -2136,7 +2412,10 @@ const pluginManifest = () => {
2136
2412
  const refId2 = this.emitFile({
2137
2413
  type: "chunk",
2138
2414
  id: id2,
2139
- name: basename(file)
2415
+ name: basename(file),
2416
+ // Preserve content script entry exports so the build finalizer
2417
+ // can decide whether the script needs a loader wrapper.
2418
+ preserveSignature: "exports-only"
2140
2419
  });
2141
2420
  contentScripts.set(
2142
2421
  file,
@@ -2206,6 +2485,7 @@ const pluginManifest = () => {
2206
2485
  }
2207
2486
  }
2208
2487
  } else {
2488
+ finalizeBuildContentScripts(this, bundle);
2209
2489
  if (manifest2.background && "service_worker" in manifest2.background) {
2210
2490
  const ref = manifest2.background.service_worker;
2211
2491
  const name = this.getFileName(ref);
@@ -2220,7 +2500,7 @@ const pluginManifest = () => {
2220
2500
  ({ js = [], ...rest }) => {
2221
2501
  return {
2222
2502
  js: js.map((id) => {
2223
- const script = contentScripts.get(id);
2503
+ const script = contentScripts.get(id) ?? contentScripts.get(prefix$1("/", id));
2224
2504
  const fileName = script?.loaderName ?? script?.fileName;
2225
2505
  if (typeof fileName === "undefined")
2226
2506
  throw new Error(
@@ -2242,11 +2522,11 @@ const pluginManifest = () => {
2242
2522
  const name = `[${plugin.name}]`;
2243
2523
  let message = error;
2244
2524
  if (error instanceof Error) {
2245
- message = colors.red(
2525
+ message = pc.red(
2246
2526
  `${name} ${error.stack ? error.stack : error.message}`
2247
2527
  );
2248
2528
  } else if (typeof error === "string") {
2249
- message = colors.red(`${name} ${error}`);
2529
+ message = pc.red(`${name} ${error}`);
2250
2530
  }
2251
2531
  console.log(message);
2252
2532
  throw new Error(`Error in ${plugin.name}.renderCrxManifest`);
@@ -2260,6 +2540,11 @@ const pluginManifest = () => {
2260
2540
  "webAccessibleResources"
2261
2541
  ];
2262
2542
  const files = await manifestFiles(manifest2, { cwd: config.root });
2543
+ if (config.command === "serve") {
2544
+ devHtmlFiles = new Set(
2545
+ files.html.map(normalizeHtmlPath).filter(isString)
2546
+ );
2547
+ }
2263
2548
  await Promise.all(
2264
2549
  assetTypes.map((k) => files[k]).flat().map(async (f) => {
2265
2550
  if (typeof bundle[f] === "undefined") {
@@ -2291,7 +2576,7 @@ Public dir: "${config.publicDir}"`
2291
2576
  const refId2 = this.emitFile({
2292
2577
  type: "asset",
2293
2578
  name: "loading-page.js",
2294
- source: loadingPageScript.replace("%PROTO%", config.server.https ? "https" : "http").replace("%PORT%", `${config.server.port ?? 0}`)
2579
+ source: loadingPageScript.replace("%PROTO%", config.server.https ? "https" : "http").replace("%PORT%", `${config.server.port ?? 0}`).replace("%READY_PATH%", loadingPageReadyPath)
2295
2580
  });
2296
2581
  const loadingPageScriptName = this.getFileName(refId2);
2297
2582
  files.html.map(
@@ -2585,6 +2870,7 @@ const crx = (options) => {
2585
2870
  contentScripts.clear();
2586
2871
  return [
2587
2872
  pluginOptionsProvider(options),
2873
+ pluginExtensionCors(),
2588
2874
  pluginContentScriptsIife(),
2589
2875
  // Must come early so isIifeModeEnabled is set before manifest plugin
2590
2876
  pluginBackground(),