@crxjs/vite-plugin 2.4.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
@@ -9,12 +9,12 @@ import { rollup } from 'rollup';
9
9
  import * as lexer from 'es-module-lexer';
10
10
  import { readFile as readFile$1 } from 'fs/promises';
11
11
  import MagicString from 'magic-string';
12
+ import { build, mergeConfig, createLogger, version } from 'vite';
12
13
  import convertSourceMap from 'convert-source-map';
13
14
  import pc from 'picocolors';
14
- import { createLogger, version } from 'vite';
15
15
  import { readFileSync, existsSync, promises } from 'fs';
16
16
  import { createRequire } from 'module';
17
- import fg from 'fast-glob';
17
+ import { glob, isDynamicPattern } from 'tinyglobby';
18
18
  import { parse } from 'node-html-parser';
19
19
  import jsesc from 'jsesc';
20
20
 
@@ -58,7 +58,7 @@ function isCrxPlugin(p) {
58
58
  return !!p && typeof p === "object" && !(p instanceof Promise) && !Array.isArray(p) && p.name.startsWith("crx:");
59
59
  }
60
60
 
61
- var workerHmrClient = "const ownOrigin = `chrome-extension://${chrome.runtime.id}`;\nself.addEventListener(\"fetch\", (fetchEvent) => {\n const url = new URL(fetchEvent.request.url);\n if (url.origin === ownOrigin) {\n fetchEvent.respondWith(sendToServer(fetchEvent.request));\n }\n});\nasync function sendToServer(req) {\n const url = new URL(req.url);\n const requestHeaders = new Headers(req.headers);\n url.protocol = __SERVER_PROTO__ + \":\";\n url.host = \"localhost\";\n url.port = __SERVER_PORT__;\n url.searchParams.set(\"t\", Date.now().toString());\n const response = await fetch(url.href.replace(/=$|=(?=&)/g, \"\"), {\n headers: requestHeaders\n });\n const responseHeaders = new Headers(response.headers);\n responseHeaders.set(\n \"Content-Type\",\n responseHeaders.get(\"Content-Type\") ?? \"text/javascript\"\n );\n responseHeaders.set(\n \"Cache-Control\",\n responseHeaders.get(\"Cache-Control\") ?? \"\"\n );\n return new Response(response.body, {\n headers: responseHeaders\n });\n}\nconst ports = /* @__PURE__ */ new Set();\nchrome.runtime.onConnect.addListener((port) => {\n if (port.name === \"@crx/client\") {\n ports.add(port);\n port.onDisconnect.addListener((port2) => {\n if (chrome.runtime.lastError) {\n console.error(chrome.runtime.lastError);\n }\n ports.delete(port2);\n });\n port.onMessage.addListener((message) => {\n });\n port.postMessage({ data: JSON.stringify({ type: \"connected\" }) });\n }\n});\nfunction notifyContentScripts(payload) {\n const data = JSON.stringify(payload);\n for (const port of ports)\n port.postMessage({ data });\n}\nconsole.log(\"[vite] connecting...\");\nconst socketProtocol = __HMR_PROTOCOL__ || (location.protocol === \"https:\" ? \"wss\" : \"ws\");\nconst socketToken = __HMR_TOKEN__;\nconst socketHost = `${__HMR_HOSTNAME__ || location.hostname}:${__HMR_PORT__}`;\nconst socket = new WebSocket(\n `${socketProtocol}://${socketHost}?token=${socketToken}`,\n \"vite-hmr\"\n);\nconst base = __BASE__ || \"/\";\nsocket.addEventListener(\"message\", async ({ data }) => {\n handleSocketMessage(JSON.parse(data));\n});\nfunction isCrxHmrPayload(x) {\n return x.type === \"custom\" && x.event.startsWith(\"crx:\");\n}\nfunction handleSocketMessage(payload) {\n if (isCrxHmrPayload(payload)) {\n handleCrxHmrPayload(payload);\n } else if (payload.type === \"connected\") {\n console.log(`[vite] connected.`);\n const interval = setInterval(() => socket.send(\"ping\"), __HMR_TIMEOUT__);\n socket.addEventListener(\"close\", () => clearInterval(interval));\n }\n}\nfunction handleCrxHmrPayload(payload) {\n notifyContentScripts(payload);\n switch (payload.event) {\n case \"crx:runtime-reload\":\n console.log(\"[crx] runtime reload\");\n chrome.runtime.reload();\n break;\n }\n}\nasync function waitForSuccessfulPing(ms = 1e3) {\n while (true) {\n try {\n await fetch(`${base}__vite_ping`);\n break;\n } catch (e) {\n await new Promise((resolve) => setTimeout(resolve, ms));\n }\n }\n}\nsocket.addEventListener(\"close\", async ({ wasClean }) => {\n if (wasClean)\n return;\n console.log(`[vite] server connection lost. polling for restart...`);\n await waitForSuccessfulPing();\n handleCrxHmrPayload({\n type: \"custom\",\n event: \"crx:runtime-reload\"\n });\n});\n";
61
+ var workerHmrClient = "const ownOrigin = `chrome-extension://${chrome.runtime.id}`;\nself.addEventListener(\"fetch\", (fetchEvent) => {\n const url = new URL(fetchEvent.request.url);\n if (url.origin === ownOrigin) {\n fetchEvent.respondWith(sendToServer(fetchEvent.request));\n }\n});\nasync function sendToServer(req) {\n const url = new URL(req.url);\n const requestHeaders = new Headers(req.headers);\n url.protocol = __SERVER_PROTO__ + \":\";\n url.host = \"localhost\";\n url.port = __SERVER_PORT__;\n url.searchParams.set(\"t\", Date.now().toString());\n const response = await fetch(url.href.replace(/=$|=(?=&)/g, \"\"), {\n headers: requestHeaders\n });\n const responseHeaders = new Headers(response.headers);\n responseHeaders.set(\n \"Content-Type\",\n responseHeaders.get(\"Content-Type\") ?? \"text/javascript\"\n );\n responseHeaders.set(\n \"Cache-Control\",\n responseHeaders.get(\"Cache-Control\") ?? \"\"\n );\n return new Response(response.body, {\n headers: responseHeaders\n });\n}\nconst ports = /* @__PURE__ */ new Set();\nchrome.runtime.onConnect.addListener((port) => {\n if (port.name === \"@crx/client\") {\n ports.add(port);\n port.onDisconnect.addListener((port2) => {\n if (chrome.runtime.lastError) {\n console.error(chrome.runtime.lastError);\n }\n ports.delete(port2);\n });\n port.onMessage.addListener((message) => {\n });\n port.postMessage({ data: JSON.stringify({ type: \"connected\" }) });\n }\n});\nfunction notifyContentScripts(payload) {\n const data = JSON.stringify(payload);\n for (const port of ports)\n port.postMessage({ data });\n}\nconsole.log(\"[vite] connecting...\");\nconst socketProtocol = __HMR_PROTOCOL__ || (location.protocol === \"https:\" ? \"wss\" : \"ws\");\nconst socketToken = __HMR_TOKEN__;\nconst socketHost = `${__HMR_HOSTNAME__ || location.hostname}:${__HMR_PORT__}`;\nconst socket = new WebSocket(\n `${socketProtocol}://${socketHost}?token=${socketToken}`,\n \"vite-hmr\"\n);\nconst base = __BASE__ || \"/\";\nsocket.addEventListener(\"message\", async ({ data }) => {\n handleSocketMessage(JSON.parse(data));\n});\nfunction isCrxHmrPayload(x) {\n return x.type === \"custom\" && x.event.startsWith(\"crx:\");\n}\nfunction handleSocketMessage(payload) {\n if (isCrxHmrPayload(payload)) {\n handleCrxHmrPayload(payload);\n } else if (payload.type === \"connected\") {\n console.log(`[vite] connected.`);\n const interval = setInterval(() => socket.send(\"ping\"), __HMR_TIMEOUT__);\n socket.addEventListener(\"close\", () => clearInterval(interval));\n }\n}\nfunction handleCrxHmrPayload(payload) {\n if (!__LIVE_RELOAD__) {\n if (payload.event === \"crx:runtime-reload\") {\n console.log(\"[crx] runtime reload suppressed (liveReload disabled)\");\n }\n return;\n }\n notifyContentScripts(payload);\n switch (payload.event) {\n case \"crx:runtime-reload\":\n console.log(\"[crx] runtime reload\");\n chrome.runtime.reload();\n break;\n }\n}\nasync function waitForSuccessfulPing(ms = 1e3) {\n while (true) {\n try {\n await fetch(`${base}__vite_ping`);\n break;\n } catch (e) {\n await new Promise((resolve) => setTimeout(resolve, ms));\n }\n }\n}\nsocket.addEventListener(\"close\", async ({ wasClean }) => {\n if (wasClean)\n return;\n console.log(`[vite] server connection lost. polling for restart...`);\n await waitForSuccessfulPing();\n if (__LIVE_RELOAD__) {\n handleCrxHmrPayload({\n type: \"custom\",\n event: \"crx:runtime-reload\"\n });\n } else {\n console.log(\n \"[crx] server reconnected, skipping reload (liveReload disabled)\"\n );\n }\n});\n";
62
62
 
63
63
  const _debug = (id) => debug$5("crx").extend(id);
64
64
  const hash = (data, length = 5) => createHash("sha1").update(data).digest("base64").replace(/[^A-Za-z0-9]/g, "").slice(0, length);
@@ -209,7 +209,7 @@ function formatFileData(script) {
209
209
  return script;
210
210
  }
211
211
  function getFileName({ type, id }) {
212
- let fileName = id.replace(/t=\d+&/, "").replace(/\?t=\d+$/, "").replace(/^\//, "").replace(/\?/g, "__").replace(/&/g, "_").replace(/=/g, "--");
212
+ let fileName = id.replace(/t=\d+&/, "").replace(/\?t=\d+$/, "").replace(/^\//, "").replace(/\?/g, "__").replace(/&/g, "_").replace(/=/g, "--").replace(/:/g, "-");
213
213
  if (fileName.includes("node_modules/")) {
214
214
  fileName = `vendor/${fileName.split("node_modules/").pop().replace(/\//g, "-")}`;
215
215
  } else if (fileName.startsWith("@")) {
@@ -244,11 +244,17 @@ 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") {
251
- throw new Error(`File type "${type}" not implemented.`);
257
+ throw new Error(`File type "iife" is handled via dedicated IIFE bundler, not Vite transform.`);
252
258
  } else if (type === "loader") {
253
259
  throw new Error("Vite does not transform loader files.");
254
260
  } else if (type === "module") {
@@ -293,6 +299,7 @@ function getContentCssIndex(id) {
293
299
  const pluginBackground = () => {
294
300
  let config;
295
301
  let browser;
302
+ let liveReload = true;
296
303
  return [
297
304
  {
298
305
  name: "crx:background-client",
@@ -305,7 +312,7 @@ const pluginBackground = () => {
305
312
  if (id === workerClientId) {
306
313
  const base = `${config.server.https ? "https" : "http"}://localhost:${config.server.port}/`;
307
314
  return defineClientValues(
308
- workerHmrClient.replace("__BASE__", JSON.stringify(base)),
315
+ workerHmrClient.replace("__BASE__", JSON.stringify(base)).replace("__LIVE_RELOAD__", JSON.stringify(liveReload)),
309
316
  config
310
317
  );
311
318
  }
@@ -318,6 +325,7 @@ const pluginBackground = () => {
318
325
  async config(config2) {
319
326
  const opts = await getOptions(config2);
320
327
  browser = opts.browser || "chrome";
328
+ liveReload = opts.liveReload !== false;
321
329
  },
322
330
  configResolved(_config) {
323
331
  config = _config;
@@ -376,7 +384,53 @@ const pluginBackground = () => {
376
384
  ];
377
385
  };
378
386
 
379
- var contentHmrPort = "function isCrxHMRPayload(x) {\n return x.type === \"custom\" && x.event.startsWith(\"crx:\");\n}\nclass HMRPort {\n port;\n callbacks = /* @__PURE__ */ new Map();\n constructor() {\n setInterval(() => {\n try {\n this.port?.postMessage({ data: \"ping\" });\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"Extension context invalidated.\")) {\n location.reload();\n } else\n throw error;\n }\n }, __CRX_HMR_TIMEOUT__);\n setInterval(this.initPort, 5 * 60 * 1e3);\n this.initPort();\n }\n initPort = () => {\n this.port?.disconnect();\n this.port = chrome.runtime.connect({ name: \"@crx/client\" });\n this.port.onDisconnect.addListener(this.handleDisconnect.bind(this));\n this.port.onMessage.addListener(this.handleMessage.bind(this));\n this.port.postMessage({ type: \"connected\" });\n };\n handleDisconnect = () => {\n if (this.callbacks.has(\"close\"))\n for (const cb of this.callbacks.get(\"close\")) {\n cb({ wasClean: true });\n }\n };\n handleMessage = (message) => {\n const forward = (data) => {\n if (this.callbacks.has(\"message\"))\n for (const cb of this.callbacks.get(\"message\")) {\n cb({ data });\n }\n };\n const payload = JSON.parse(message.data);\n if (isCrxHMRPayload(payload)) {\n if (payload.event === \"crx:runtime-reload\") {\n console.log(\"[crx] runtime reload\");\n setTimeout(() => location.reload(), 500);\n } else {\n forward(JSON.stringify(payload.data));\n }\n } else {\n forward(message.data);\n }\n };\n addEventListener = (event, callback) => {\n const cbs = this.callbacks.get(event) ?? /* @__PURE__ */ new Set();\n cbs.add(callback);\n this.callbacks.set(event, cbs);\n };\n send = (data) => {\n if (this.port)\n this.port.postMessage({ data });\n else\n throw new Error(\"HMRPort is not initialized\");\n };\n}\n\nexport { HMRPort };\n";
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
+
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";
380
434
 
381
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";
382
436
 
@@ -396,9 +450,10 @@ contentScripts.change$.pipe(filter(RxMap.isChangeType.set)).subscribe(({ map, va
396
450
  "resolvedId",
397
451
  "scriptId"
398
452
  ];
399
- for (const keyName of keyNames) {
400
- const key = value[keyName];
401
- 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) {
402
457
  continue;
403
458
  } else {
404
459
  map.set(key, value);
@@ -427,6 +482,21 @@ function createProMainLoader({ fileName }) {
427
482
  return contentProMainLoader.replace(/__SCRIPT__/g, JSON.stringify(fileName));
428
483
  }
429
484
 
485
+ const { outputFile: outputFile$1 } = fsx;
486
+ const getIifeGlobalName = (fileName) => {
487
+ const base = fileName.split("/").pop() ?? fileName;
488
+ const sanitized = base.replace(/\W+/g, "_").replace(/^_+/, "");
489
+ return `crx_${sanitized || "content_script"}`;
490
+ };
491
+ const resolveScriptInput = (server, id) => {
492
+ if (id.startsWith("/@fs/"))
493
+ return id.slice("/@fs/".length);
494
+ if (id.startsWith("/"))
495
+ return join(server.config.root, id.slice(1));
496
+ return id;
497
+ };
498
+ const isOutputChunk = (item) => item.type === "chunk";
499
+ const isOutputAsset = (item) => item.type === "asset";
430
500
  const serverEvent$ = new ReplaySubject(1);
431
501
  const close$ = serverEvent$.pipe(
432
502
  filter((e) => e.type === "close"),
@@ -448,8 +518,17 @@ fileWriterEvent$.pipe(
448
518
  const allFilesReady$ = buildEnd$.pipe(
449
519
  switchMap(() => outputFiles.change$.pipe(startWith({ type: "start" }))),
450
520
  map(() => [...outputFiles.values()]),
451
- switchMap((files) => Promise.allSettled(files.map(({ file }) => file)))
521
+ switchMap(
522
+ (files) => Promise.allSettled(files.map((file) => waitForOutputFile(file)))
523
+ )
452
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
+ }
453
532
  const timestamp$ = new BehaviorSubject(Date.now());
454
533
  allFilesReady$.subscribe(() => {
455
534
  timestamp$.next(Date.now());
@@ -486,11 +565,20 @@ function prepAsset(fileName, { id, source }) {
486
565
  );
487
566
  }
488
567
  function prepScript(fileName, script) {
568
+ if (script.type === "iife")
569
+ return prepIifeScript(fileName, script);
489
570
  return ($) => $.pipe(
490
571
  // get script contents from dev server
491
572
  mergeMap(async ({ server }) => {
492
573
  const target = getOutputPath(server, fileName);
493
- 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
+ }
494
582
  const transformResult = await server.transformRequest(viteUrl);
495
583
  if (!transformResult)
496
584
  throw new TypeError(`Unable to load "${script.id}" from server.`);
@@ -543,12 +631,116 @@ ${sourceMap}
543
631
  })
544
632
  );
545
633
  }
634
+ async function bundleIife(server, script, fileName) {
635
+ const input = resolveScriptInput(server, script.id);
636
+ const sourcemap = server.config.build.sourcemap === "inline" ? "inline" : false;
637
+ const result = await build({
638
+ root: server.config.root,
639
+ mode: server.config.mode,
640
+ configFile: false,
641
+ // Don't load user's config - use minimal IIFE-specific settings
642
+ logLevel: "silent",
643
+ resolve: {
644
+ // Copy resolve settings from the dev server for consistency
645
+ alias: server.config.resolve.alias,
646
+ extensions: server.config.resolve.extensions,
647
+ conditions: server.config.resolve.conditions
648
+ },
649
+ build: {
650
+ write: false,
651
+ // Don't write to disk, we'll handle that
652
+ manifest: false,
653
+ // Don't generate Vite manifest
654
+ rollupOptions: {
655
+ input,
656
+ output: {
657
+ format: "iife",
658
+ name: getIifeGlobalName(fileName),
659
+ entryFileNames: fileName,
660
+ inlineDynamicImports: true,
661
+ // Required for IIFE format
662
+ sourcemap
663
+ }
664
+ },
665
+ minify: false,
666
+ copyPublicDir: false
667
+ }
668
+ });
669
+ const outputs = Array.isArray(result) ? result : [result];
670
+ const firstOutput = outputs[0];
671
+ const output = "output" in firstOutput ? firstOutput.output : void 0;
672
+ if (!output) {
673
+ throw new Error(`Unable to generate IIFE bundle for "${script.id}"`);
674
+ }
675
+ const entryChunk = output.find(
676
+ (item) => isOutputChunk(item) && item.isEntry
677
+ );
678
+ if (!entryChunk) {
679
+ throw new Error(`Unable to generate IIFE bundle for "${script.id}"`);
680
+ }
681
+ const assets = output.filter(isOutputAsset).filter(
682
+ // Filter out manifest.json to avoid overwriting extension manifest
683
+ (asset) => asset.fileName !== "manifest.json" && !asset.fileName.startsWith(".vite/")
684
+ );
685
+ const extraChunks = output.filter(
686
+ (item) => isOutputChunk(item) && !item.isEntry
687
+ );
688
+ return {
689
+ code: entryChunk.code,
690
+ assets,
691
+ extraChunks
692
+ };
693
+ }
694
+ function prepIifeScript(fileName, script) {
695
+ return ($) => $.pipe(
696
+ mergeMap(async ({ server }) => {
697
+ const target = getOutputPath(server, fileName);
698
+ const { code, assets, extraChunks } = await bundleIife(
699
+ server,
700
+ script,
701
+ fileName
702
+ );
703
+ return { target, source: code, deps: [], server, assets, extraChunks };
704
+ }),
705
+ mergeMap(
706
+ async ({ target, source, deps, server, assets, extraChunks }) => {
707
+ const extras = [
708
+ ...assets.map((asset) => ({
709
+ fileName: asset.fileName,
710
+ source: asset.source
711
+ })),
712
+ ...extraChunks.map((chunk) => ({
713
+ fileName: chunk.fileName,
714
+ source: chunk.code
715
+ }))
716
+ ].filter((item) => item.fileName !== fileName);
717
+ await Promise.all(
718
+ extras.map(async (item) => {
719
+ const outputPath = getOutputPath(server, item.fileName);
720
+ if (typeof item.source === "undefined" || item.source === null)
721
+ return;
722
+ if (item.source instanceof Uint8Array)
723
+ await outputFile$1(outputPath, item.source);
724
+ else
725
+ await outputFile$1(outputPath, item.source, { encoding: "utf8" });
726
+ })
727
+ );
728
+ return { target, source, deps };
729
+ }
730
+ )
731
+ );
732
+ }
546
733
  async function allFilesReady() {
547
734
  await firstValueFrom(allFilesReady$);
548
735
  }
549
736
 
550
737
  const { outputFile } = fsx;
551
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
+ }
552
744
  async function start({
553
745
  server
554
746
  }) {
@@ -590,18 +782,24 @@ function add(script) {
590
782
  file = formatFileData({
591
783
  ...script,
592
784
  fileName,
593
- file: write(script)
785
+ file: queueWrite(script)
594
786
  });
595
787
  outputFiles.set(file.fileName, file);
596
788
  debug$4("add: stored new file %s", file.fileName);
597
789
  } else {
598
790
  const isVirtualModule = script.id.startsWith("/@id/") || script.id.startsWith("/__");
599
- if (isVirtualModule) {
791
+ const isTimestampedModule = script.type === "module" && /[?&]t=\d+/.test(script.id);
792
+ if (isVirtualModule || isTimestampedModule) {
600
793
  debug$4(
601
- "add: virtual module already exists, triggering re-write for %s",
794
+ "add: module already exists, triggering re-write for %s",
602
795
  fileName
603
796
  );
604
- file.file = write(script);
797
+ file = formatFileData({
798
+ ...file,
799
+ ...script,
800
+ fileName,
801
+ file: queueWrite(script, file)
802
+ });
605
803
  outputFiles.set(fileName, file);
606
804
  }
607
805
  }
@@ -618,7 +816,7 @@ function update(_id) {
618
816
  const scriptFile = outputFiles.get(fileName);
619
817
  if (scriptFile) {
620
818
  debug$4("update: found file, calling write()");
621
- scriptFile.file = write({ id, type });
819
+ scriptFile.file = queueWrite({ id, type }, scriptFile);
622
820
  updatedFiles.push(scriptFile);
623
821
  outputFiles.set(fileName, scriptFile);
624
822
  }
@@ -663,6 +861,7 @@ const pluginContentScripts = () => {
663
861
  let server;
664
862
  let preambleCode;
665
863
  let hmrTimeout;
864
+ let liveReload = true;
666
865
  let sub = new Subscription();
667
866
  const worldMainIds = /* @__PURE__ */ new Set();
668
867
  const findWorldMainIds = async (config, env) => {
@@ -690,9 +889,11 @@ const pluginContentScripts = () => {
690
889
  apply: "serve",
691
890
  async config(config, env) {
692
891
  await findWorldMainIds(config, env);
693
- const { contentScripts: contentScripts2 = {} } = await getOptions(config);
892
+ const opts = await getOptions(config);
893
+ const { contentScripts: contentScripts2 = {} } = opts;
694
894
  hmrTimeout = contentScripts2.hmrTimeout ?? 5e3;
695
895
  preambleCode = preambleCode ?? contentScripts2.preambleCode;
896
+ liveReload = opts.liveReload !== false;
696
897
  },
697
898
  async configureServer(_server) {
698
899
  server = _server;
@@ -702,7 +903,7 @@ const pluginContentScripts = () => {
702
903
  try {
703
904
  const react = await import('@vitejs/plugin-react');
704
905
  preambleCode = react.default.preambleCode;
705
- } catch (error) {
906
+ } catch {
706
907
  preambleCode = false;
707
908
  }
708
909
  }
@@ -728,7 +929,8 @@ const pluginContentScripts = () => {
728
929
  });
729
930
  script.fileName = loader.fileName;
730
931
  } else if (type === "iife") {
731
- throw new Error("IIFE content scripts are not implemented");
932
+ const file = add({ type: "iife", id });
933
+ script.fileName = file.fileName;
732
934
  } else {
733
935
  const file = add({ type: "module", id });
734
936
  script.fileName = file.fileName;
@@ -748,10 +950,7 @@ const pluginContentScripts = () => {
748
950
  return defined;
749
951
  }
750
952
  if (id === contentHmrPortId) {
751
- const defined = contentHmrPort.replace(
752
- "__CRX_HMR_TIMEOUT__",
753
- JSON.stringify(hmrTimeout)
754
- );
953
+ const defined = contentHmrPort.replace("__CRX_HMR_TIMEOUT__", JSON.stringify(hmrTimeout)).replace("__CRX_LIVE_RELOAD__", JSON.stringify(liveReload));
755
954
  return defined;
756
955
  }
757
956
  },
@@ -779,41 +978,50 @@ const pluginContentScripts = () => {
779
978
  };
780
979
  },
781
980
  generateBundle(_options, bundle) {
782
- for (const [key, script] of contentScripts)
783
- if (key === script.refId) {
784
- if (script.type === "module") {
785
- const fileName = this.getFileName(script.refId);
786
- script.fileName = fileName;
787
- } else if (script.type === "loader") {
788
- const fileName = this.getFileName(script.refId);
789
- script.fileName = fileName;
790
- const bundleFileInfo = bundle[fileName];
791
- const shouldUseLoader = !(bundleFileInfo.type === "chunk" && bundleFileInfo.imports.length === 0 && bundleFileInfo.dynamicImports.length === 0 && bundleFileInfo.exports.length === 0);
792
- if (shouldUseLoader) {
793
- const refId = this.emitFile({
794
- type: "asset",
795
- name: getFileName({
796
- type: "loader",
797
- id: basename(script.id)
798
- }),
799
- source: worldMainIds.has(script.id) ? createProMainLoader({
800
- fileName: `./${fileName.split("/").at(-1)}`
801
- }) : createProLoader({ fileName })
802
- });
803
- script.loaderName = this.getFileName(refId);
804
- } else {
805
- bundleFileInfo.code = `(function(){${bundleFileInfo.code}})()
806
- `;
807
- }
808
- } else if (script.type === "iife") {
809
- throw new Error("IIFE content scripts are not implemented");
810
- }
811
- contentScripts.set(script.refId, formatFileData(script));
812
- }
981
+ finalizeBuildContentScripts(this, bundle, worldMainIds);
813
982
  }
814
983
  }
815
984
  ];
816
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
+ }
817
1025
 
818
1026
  const pluginContentScriptsCss = () => {
819
1027
  let injectCss;
@@ -890,6 +1098,196 @@ const pluginDeclaredContentScripts = () => {
890
1098
  };
891
1099
  };
892
1100
 
1101
+ function isIifeContentScript(file) {
1102
+ return /\.iife\.(ts|tsx|js|jsx|mjs|cjs)$/.test(file);
1103
+ }
1104
+ const pluginContentScriptsIife = () => {
1105
+ const pluginName = "crx:content-scripts-iife";
1106
+ let config;
1107
+ return [
1108
+ {
1109
+ name: `${pluginName}-config`,
1110
+ enforce: "pre",
1111
+ configResolved(resolvedConfig) {
1112
+ config = resolvedConfig;
1113
+ }
1114
+ },
1115
+ {
1116
+ name: pluginName,
1117
+ apply: "build",
1118
+ enforce: "post",
1119
+ async generateBundle() {
1120
+ const opts = await getOptions({ plugins: config.plugins });
1121
+ const _manifest = opts.manifest;
1122
+ const manifest = typeof _manifest === "function" ? await _manifest({ command: "build", mode: config.mode }) : await Promise.resolve(_manifest);
1123
+ const standaloneFiles = (opts.contentScripts?.standaloneFiles || []).map(
1124
+ normalizeContentScriptPath
1125
+ );
1126
+ const iifeEntries = collectIifeEntries(
1127
+ manifest,
1128
+ standaloneFiles,
1129
+ config.root
1130
+ );
1131
+ if (iifeEntries.length === 0)
1132
+ return;
1133
+ console.log(
1134
+ pc.cyan(`
1135
+ [${pluginName}] Building ${iifeEntries.length} content script(s) as IIFE...`)
1136
+ );
1137
+ for (const entry of iifeEntries) {
1138
+ const outputFileName = getIifeOutputPath(entry.file);
1139
+ try {
1140
+ const iifeConfig = createIifeConfig(config, entry.id, outputFileName);
1141
+ const result = await build(iifeConfig);
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
+ );
1148
+ } catch (error) {
1149
+ console.error(
1150
+ pc.red(` \u2717 Failed to build ${entry.file}:`),
1151
+ error
1152
+ );
1153
+ throw error;
1154
+ }
1155
+ }
1156
+ console.log(pc.cyan(`[${pluginName}] IIFE build complete
1157
+ `));
1158
+ }
1159
+ }
1160
+ ];
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
+ }
1242
+ function getIifeOutputPath(file) {
1243
+ const normalizedFile = file.replace(/^\//, "");
1244
+ const dir = dirname(normalizedFile);
1245
+ const name = basename(normalizedFile).replace(/\.(ts|tsx|js|jsx|mjs|cjs)$/, "");
1246
+ return dir && dir !== "." ? `${dir}/${name}.js` : `${name}.js`;
1247
+ }
1248
+ function createIifeConfig(parentConfig, entryPath, outputFileName) {
1249
+ const entryName = basename(outputFileName, ".js");
1250
+ const baseConfig = {
1251
+ // Inherit relevant settings from parent config
1252
+ root: parentConfig.root,
1253
+ mode: parentConfig.mode,
1254
+ resolve: parentConfig.resolve,
1255
+ define: parentConfig.define,
1256
+ esbuild: parentConfig.esbuild
1257
+ };
1258
+ const iifeConfig = {
1259
+ configFile: false,
1260
+ logLevel: "warn",
1261
+ plugins: [],
1262
+ build: {
1263
+ write: false,
1264
+ // Don't write to disk, we'll add to bundle
1265
+ emptyOutDir: false,
1266
+ lib: {
1267
+ entry: entryPath,
1268
+ formats: ["iife"],
1269
+ name: safeVarName(entryName),
1270
+ fileName: () => outputFileName
1271
+ },
1272
+ rollupOptions: {
1273
+ output: {
1274
+ entryFileNames: outputFileName,
1275
+ // Ensure all dependencies are inlined
1276
+ inlineDynamicImports: true
1277
+ }
1278
+ },
1279
+ // Match parent config settings
1280
+ minify: parentConfig.build.minify,
1281
+ sourcemap: parentConfig.build.sourcemap,
1282
+ target: parentConfig.build.target
1283
+ }
1284
+ };
1285
+ return mergeConfig(baseConfig, iifeConfig);
1286
+ }
1287
+ function safeVarName(name) {
1288
+ return name.replace(/[^a-zA-Z0-9_$]/g, "_").replace(/^(\d)/, "_$1").replace(/^$/, "_");
1289
+ }
1290
+
893
1291
  const _dynamicScriptRegEx = /\b(import.meta).CRX_DYNAMIC_SCRIPT_(.+?)[,;]/gm;
894
1292
  const dynamicScriptRegEx = () => {
895
1293
  _dynamicScriptRegEx.lastIndex = 0;
@@ -897,12 +1295,48 @@ const dynamicScriptRegEx = () => {
897
1295
  };
898
1296
  const pluginDynamicContentScripts = () => {
899
1297
  let config;
1298
+ let standaloneFiles = [];
900
1299
  return [
901
1300
  {
902
1301
  name: "crx:dynamic-content-scripts-loader",
903
1302
  enforce: "pre",
904
1303
  configResolved(_config) {
905
1304
  config = _config;
1305
+ getOptions({ plugins: _config.plugins }).then((opts) => {
1306
+ standaloneFiles = (opts.contentScripts?.standaloneFiles || []).map(
1307
+ (f) => f.replace(/^\//, "")
1308
+ );
1309
+ }).catch(() => void 0);
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
+ }
906
1340
  },
907
1341
  configureServer(server) {
908
1342
  return () => {
@@ -933,9 +1367,9 @@ const pluginDynamicContentScripts = () => {
933
1367
  };
934
1368
  },
935
1369
  async resolveId(_source, importer) {
936
- if (importer && _source.includes("?script")) {
1370
+ if (importer && (_source.includes("?script") || _source.includes("?iife"))) {
937
1371
  const url = new URL(_source, "stub://stub");
938
- if (url.searchParams.has("script")) {
1372
+ if (url.searchParams.has("script") || url.searchParams.has("iife")) {
939
1373
  const [source] = _source.split("?");
940
1374
  const resolved = await this.resolve(source, importer, {
941
1375
  skipSelf: true
@@ -945,8 +1379,11 @@ const pluginDynamicContentScripts = () => {
945
1379
  `Could not resolve dynamic script: "${_source}" from "${importer}"`
946
1380
  );
947
1381
  const { id } = resolved;
1382
+ const relId = relative(config.root, id).replace(/^\//, "");
948
1383
  let type = "loader";
949
- if (url.searchParams.has("module")) {
1384
+ if (isIifeContentScript(relId) || standaloneFiles.includes(relId)) {
1385
+ type = "iife";
1386
+ } else if (url.searchParams.has("module")) {
950
1387
  type = "module";
951
1388
  } else if (url.searchParams.has("iife")) {
952
1389
  type = "iife";
@@ -962,17 +1399,20 @@ const pluginDynamicContentScripts = () => {
962
1399
  refId = this.emitFile({
963
1400
  type: "chunk",
964
1401
  id,
965
- 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"
966
1406
  });
967
1407
  } else {
968
1408
  refId = scriptId;
969
- const relId = relative(config.root, id);
1409
+ const relId2 = relative(config.root, id);
970
1410
  fileName = getFileName({
971
1411
  type: type === "iife" ? "iife" : "module",
972
- id: relId
1412
+ id: relId2
973
1413
  });
974
1414
  if (type === "loader")
975
- loaderName = getFileName({ type, id: relId });
1415
+ loaderName = getFileName({ type, id: relId2 });
976
1416
  }
977
1417
  script = formatFileData({
978
1418
  type,
@@ -996,9 +1436,29 @@ const pluginDynamicContentScripts = () => {
996
1436
  const index = id.indexOf("?scriptId=");
997
1437
  if (index > -1) {
998
1438
  const scriptId = id.slice(index + "?scriptId=".length);
999
- 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
+ }
1000
1460
  if (config.command === "build") {
1001
- return `export default import.meta.CRX_DYNAMIC_SCRIPT_${script.refId};`;
1461
+ return `export default import.meta.CRX_DYNAMIC_SCRIPT_${script.scriptId};`;
1002
1462
  } else if (typeof script.fileName === "string") {
1003
1463
  return `export default ${JSON.stringify(script.fileName)};`;
1004
1464
  } else {
@@ -1018,6 +1478,7 @@ const pluginDynamicContentScripts = () => {
1018
1478
  * Can't use `renderChunk` b/c pre plugin crx:content-scripts uses
1019
1479
  * `generateBundle` to emit loaders. Must come after "enforce: pre".
1020
1480
  */
1481
+ enforce: "post",
1021
1482
  generateBundle(options, bundle) {
1022
1483
  for (const chunk of Object.values(bundle))
1023
1484
  if (chunk.type === "chunk") {
@@ -1034,9 +1495,9 @@ const pluginDynamicContentScripts = () => {
1034
1495
  throw new Error(
1035
1496
  `Content script fileName is undefined: "${script.id}"`
1036
1497
  );
1037
- return `${JSON.stringify(
1038
- `/${script.loaderName ?? script.fileName}`
1039
- )}${match.split(scriptKey)[1]}`;
1498
+ const fileName = script.loaderName ?? script.fileName;
1499
+ const path = fileName;
1500
+ return `${JSON.stringify(path)}${match.split(scriptKey)[1]}`;
1040
1501
  }
1041
1502
  );
1042
1503
  chunk.code = replaced;
@@ -1121,7 +1582,7 @@ const pluginFileWriterPolyfill = () => {
1121
1582
  async function manifestFiles(manifest, options = {}) {
1122
1583
  let locales = [];
1123
1584
  if (manifest.default_locale)
1124
- locales = await fg("_locales/**/messages.json", options);
1585
+ locales = await glob("_locales/**/messages.json", options);
1125
1586
  const rulesets = manifest.declarative_net_request?.rule_resources.flatMap(
1126
1587
  ({ path }) => path
1127
1588
  ) ?? [];
@@ -1145,8 +1606,8 @@ async function manifestFiles(manifest, options = {}) {
1145
1606
  manifest.web_accessible_resources.flatMap(({ resources: resources2 }) => resources2).map(async (r) => {
1146
1607
  if (["*", "**/*"].includes(r))
1147
1608
  return void 0;
1148
- if (fg.isDynamicPattern(r))
1149
- return fg(r, options);
1609
+ if (isDynamicPattern(r))
1610
+ return glob(r, options);
1150
1611
  return r;
1151
1612
  })
1152
1613
  );
@@ -1164,7 +1625,7 @@ async function manifestFiles(manifest, options = {}) {
1164
1625
  };
1165
1626
  }
1166
1627
  async function dirFiles(dir) {
1167
- const files = await fg(join(dir, "**", "*"));
1628
+ const files = await glob(join(dir, "**", "*"));
1168
1629
  return files;
1169
1630
  }
1170
1631
  function htmlFiles(manifest) {
@@ -1205,6 +1666,89 @@ const isCustomPayload = (p) => {
1205
1666
  return p.type === "custom";
1206
1667
  };
1207
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
+ }
1208
1752
  const crxHMRPayload$ = hmrPayload$.pipe(
1209
1753
  filter((p) => !isCustomPayload(p)),
1210
1754
  buffer(allFilesReady$),
@@ -1221,63 +1765,8 @@ const crxHMRPayload$ = hmrPayload$.pipe(
1221
1765
  payloads.push(fullReload);
1222
1766
  return payloads;
1223
1767
  }),
1224
- map((p) => {
1225
- switch (p.type) {
1226
- case "full-reload": {
1227
- const fullReload = {
1228
- type: "full-reload",
1229
- path: p.path && getViteUrl({ id: p.path, type: "module" })
1230
- };
1231
- return fullReload;
1232
- }
1233
- case "prune": {
1234
- const prune = {
1235
- type: "prune",
1236
- paths: p.paths.map((id) => getViteUrl({ id, type: "module" }))
1237
- };
1238
- return prune;
1239
- }
1240
- case "update": {
1241
- debug$3("update payload with %d updates", p.updates.length);
1242
- for (const u of p.updates) {
1243
- debug$3(
1244
- "update item: path=%s acceptedPath=%s type=%s",
1245
- u.path,
1246
- u.acceptedPath,
1247
- u.type
1248
- );
1249
- const isVirtualModule = u.path.startsWith("/@id/") || u.path.startsWith("/__");
1250
- if (isVirtualModule) {
1251
- debug$3("updating virtual module: %s", u.path);
1252
- update(u.path);
1253
- }
1254
- }
1255
- const update_ = {
1256
- type: "update",
1257
- updates: p.updates.map(({ acceptedPath: ap, path: p2, ...rest }) => ({
1258
- ...rest,
1259
- acceptedPath: prefix$1("/", getFileName({ id: ap, type: "module" })),
1260
- path: prefix$1("/", getFileName({ id: p2, type: "module" }))
1261
- }))
1262
- };
1263
- return update_;
1264
- }
1265
- default:
1266
- return p;
1267
- }
1268
- }),
1269
- filter((p) => {
1270
- switch (p.type) {
1271
- case "full-reload":
1272
- return typeof p.path === "undefined";
1273
- case "prune":
1274
- return p.paths.length > 0;
1275
- case "update":
1276
- return p.updates.length > 0;
1277
- default:
1278
- return true;
1279
- }
1280
- }),
1768
+ mergeMap(prepareVitePayloadForCrx),
1769
+ filter(shouldForwardCrxPayload),
1281
1770
  map((data) => {
1282
1771
  debug$3(`hmr payload`, data);
1283
1772
  return {
@@ -1309,11 +1798,29 @@ const crxRuntimeReload = {
1309
1798
  type: "custom",
1310
1799
  event: "crx:runtime-reload"
1311
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
+ }
1312
1818
  const pluginHMR = () => {
1313
1819
  let inputManifestFiles;
1314
1820
  let decoratedSend;
1315
1821
  let config;
1316
1822
  let subs;
1823
+ let liveReload = true;
1317
1824
  return [
1318
1825
  {
1319
1826
  name: "crx:hmr",
@@ -1321,6 +1828,8 @@ const pluginHMR = () => {
1321
1828
  enforce: "pre",
1322
1829
  // server hmr host should be localhost
1323
1830
  async config({ server = {}, ...config2 }) {
1831
+ const opts = await getOptions({ ...config2, server });
1832
+ liveReload = opts.liveReload !== false;
1324
1833
  if (server.hmr === false)
1325
1834
  return;
1326
1835
  if (server.hmr === true)
@@ -1359,7 +1868,9 @@ const pluginHMR = () => {
1359
1868
  subs.add(fileWriterError$.subscribe(send));
1360
1869
  subs.add(
1361
1870
  crxHMRPayload$.subscribe((payload) => {
1362
- send(payload);
1871
+ if (liveReload) {
1872
+ send(payload);
1873
+ }
1363
1874
  })
1364
1875
  );
1365
1876
  }
@@ -1368,14 +1879,16 @@ const pluginHMR = () => {
1368
1879
  subs.unsubscribe();
1369
1880
  },
1370
1881
  // background changes require a full extension reload
1371
- handleHotUpdate({ modules, server }) {
1882
+ handleHotUpdate({ file, modules, server }) {
1372
1883
  const { root } = server.config;
1884
+ const changedFilePath = getChangedFilePath(root, file);
1373
1885
  const relFiles = /* @__PURE__ */ new Set();
1374
1886
  const fsFiles = /* @__PURE__ */ new Set();
1375
1887
  const virtualModules = /* @__PURE__ */ new Set();
1376
1888
  for (const m of modules) {
1377
- if (m.id?.startsWith(root)) {
1378
- relFiles.add(m.id.slice(server.config.root.length));
1889
+ const relFile = getChangedFilePath(root, m.id);
1890
+ if (relFile) {
1891
+ relFiles.add(relFile);
1379
1892
  } else if (m.url?.startsWith("/@fs")) {
1380
1893
  fsFiles.add(m.url);
1381
1894
  } else if (m.id?.startsWith("\0") || m.url?.startsWith("/@id/__x00__")) {
@@ -1386,13 +1899,17 @@ const pluginHMR = () => {
1386
1899
  }
1387
1900
  }
1388
1901
  }
1389
- fsFiles.forEach((file) => update(file));
1390
- virtualModules.forEach((file) => update(file));
1902
+ fsFiles.forEach((file2) => update(file2));
1903
+ virtualModules.forEach((file2) => update(file2));
1391
1904
  if (inputManifestFiles.background.length) {
1392
1905
  const background = prefix$1("/", inputManifestFiles.background[0]);
1393
1906
  if (relFiles.has(background) || modules.some(isImporter(join(server.config.root, background)))) {
1394
- debug$2("sending runtime reload");
1395
- server.ws.send(crxRuntimeReload);
1907
+ if (liveReload) {
1908
+ debug$2("sending runtime reload");
1909
+ server.ws.send(crxRuntimeReload);
1910
+ } else {
1911
+ debug$2("skipping runtime reload (liveReload disabled)");
1912
+ }
1396
1913
  }
1397
1914
  }
1398
1915
  for (const [key, script] of contentScripts)
@@ -1409,15 +1926,26 @@ const pluginHMR = () => {
1409
1926
  if (changedCssFiles.length > 0) {
1410
1927
  changedCssFiles.forEach((relFile) => update(relFile));
1411
1928
  update(script.id);
1412
- virtualModules.forEach((file) => update(file));
1929
+ virtualModules.forEach((file2) => update(file2));
1413
1930
  }
1414
1931
  }
1415
1932
  } else {
1416
1933
  if (relFiles.has(script.id) || modules.some(isImporter(join(server.config.root, script.id)))) {
1417
1934
  relFiles.forEach((relFile) => update(relFile));
1418
- virtualModules.forEach((file) => update(file));
1935
+ virtualModules.forEach((file2) => update(file2));
1419
1936
  }
1420
1937
  }
1938
+ const scriptPath = prefix$1("/", script.id);
1939
+ if (script.type === "iife" && changedFilePath === scriptPath) {
1940
+ debug$2("IIFE script changed, triggering rebuild: %s", script.id);
1941
+ const updatedFiles = update(scriptPath);
1942
+ Promise.all(updatedFiles.map((f) => f.file)).then(() => {
1943
+ if (liveReload) {
1944
+ debug$2("IIFE rebuild complete, sending runtime reload");
1945
+ server.ws.send(crxRuntimeReload);
1946
+ }
1947
+ });
1948
+ }
1421
1949
  }
1422
1950
  }
1423
1951
  },
@@ -1432,13 +1960,14 @@ const pluginHMR = () => {
1432
1960
  },
1433
1961
  renderCrxDevScript(code, { id: _id, type }) {
1434
1962
  if (type === "module" && _id !== "/@vite/client" && code.includes("createHotContext")) {
1435
- const id = _id.replace(/t=\d+&/, "");
1436
- const escaped = id.replace(/([?&.])/g, "\\$1");
1963
+ const id = stripTimestamp(_id);
1964
+ const escaped = escapeRegExp(id);
1437
1965
  const regexp = new RegExp(
1438
- `(?<=createHotContext\\(")${escaped}(?="\\))`
1966
+ `(createHotContext\\(")${escaped}("\\))`,
1967
+ "g"
1439
1968
  );
1440
1969
  const fileUrl = prefix$1("/", getFileName({ id, type }));
1441
- const replaced = code.replace(regexp, fileUrl);
1970
+ const replaced = code.replace(regexp, `$1${fileUrl}$2`);
1442
1971
  return replaced;
1443
1972
  } else {
1444
1973
  return code;
@@ -1614,14 +2143,50 @@ const pluginHtmlInlineScripts = () => {
1614
2143
  };
1615
2144
  };
1616
2145
 
1617
- 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";
1618
2147
 
1619
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";
1620
2149
 
1621
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
+ }
1622
2186
  const pluginManifest = () => {
1623
2187
  let manifest;
1624
2188
  let plugins = [];
2189
+ let devHtmlFiles = /* @__PURE__ */ new Set();
1625
2190
  let refId;
1626
2191
  let config;
1627
2192
  return [
@@ -1641,6 +2206,7 @@ const pluginManifest = () => {
1641
2206
  background: sw,
1642
2207
  html
1643
2208
  } = await manifestFiles(manifest, { cwd: config2.root });
2209
+ devHtmlFiles = new Set(html.map(normalizeHtmlPath).filter(isString));
1644
2210
  const { entries = [] } = config2.optimizeDeps ?? {};
1645
2211
  let { input = [] } = config2.build?.rollupOptions ?? {};
1646
2212
  if (typeof input === "string")
@@ -1674,6 +2240,25 @@ const pluginManifest = () => {
1674
2240
  plugins = resolvedConfig.plugins;
1675
2241
  }
1676
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
+ },
1677
2262
  buildStart(options) {
1678
2263
  if (options.plugins)
1679
2264
  plugins = options.plugins;
@@ -1810,14 +2395,27 @@ const pluginManifest = () => {
1810
2395
  }
1811
2396
  }
1812
2397
  } else {
2398
+ const opts = await getOptions({ plugins: config.plugins });
2399
+ const standaloneFiles = (opts.contentScripts?.standaloneFiles || []).map(
2400
+ (f) => f.replace(/^\//, "")
2401
+ );
2402
+ const isStandaloneFile = (file) => {
2403
+ const normalized = file.replace(/^\//, "");
2404
+ return standaloneFiles.includes(normalized);
2405
+ };
1813
2406
  if (manifest2.content_scripts)
1814
2407
  for (const { js = [], matches = [] } of manifest2.content_scripts)
1815
2408
  for (const file of js) {
2409
+ if (isIifeContentScript(file) || isStandaloneFile(file))
2410
+ continue;
1816
2411
  const id2 = join(config.root, file);
1817
2412
  const refId2 = this.emitFile({
1818
2413
  type: "chunk",
1819
2414
  id: id2,
1820
- 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"
1821
2419
  });
1822
2420
  contentScripts.set(
1823
2421
  file,
@@ -1887,6 +2485,7 @@ const pluginManifest = () => {
1887
2485
  }
1888
2486
  }
1889
2487
  } else {
2488
+ finalizeBuildContentScripts(this, bundle);
1890
2489
  if (manifest2.background && "service_worker" in manifest2.background) {
1891
2490
  const ref = manifest2.background.service_worker;
1892
2491
  const name = this.getFileName(ref);
@@ -1901,7 +2500,7 @@ const pluginManifest = () => {
1901
2500
  ({ js = [], ...rest }) => {
1902
2501
  return {
1903
2502
  js: js.map((id) => {
1904
- const script = contentScripts.get(id);
2503
+ const script = contentScripts.get(id) ?? contentScripts.get(prefix$1("/", id));
1905
2504
  const fileName = script?.loaderName ?? script?.fileName;
1906
2505
  if (typeof fileName === "undefined")
1907
2506
  throw new Error(
@@ -1941,6 +2540,11 @@ const pluginManifest = () => {
1941
2540
  "webAccessibleResources"
1942
2541
  ];
1943
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
+ }
1944
2548
  await Promise.all(
1945
2549
  assetTypes.map((k) => files[k]).flat().map(async (f) => {
1946
2550
  if (typeof bundle[f] === "undefined") {
@@ -1972,7 +2576,7 @@ Public dir: "${config.publicDir}"`
1972
2576
  const refId2 = this.emitFile({
1973
2577
  type: "asset",
1974
2578
  name: "loading-page.js",
1975
- 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)
1976
2580
  });
1977
2581
  const loadingPageScriptName = this.getFileName(refId2);
1978
2582
  files.html.map(
@@ -2266,6 +2870,9 @@ const crx = (options) => {
2266
2870
  contentScripts.clear();
2267
2871
  return [
2268
2872
  pluginOptionsProvider(options),
2873
+ pluginExtensionCors(),
2874
+ pluginContentScriptsIife(),
2875
+ // Must come early so isIifeModeEnabled is set before manifest plugin
2269
2876
  pluginBackground(),
2270
2877
  pluginContentScripts(),
2271
2878
  pluginDeclaredContentScripts(),