@pyreon/vite-plugin 0.25.1 → 0.26.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5386,7 +5386,7 @@ var drawChart = (function (exports) {
5386
5386
  </script>
5387
5387
  <script>
5388
5388
  /*<!--*/
5389
- const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src/index.ts","uid":"3bd95fb3-1"}]},{"name":"rocketstyle-collapse-DGnwgDhC.js","children":[{"name":"src/rocketstyle-collapse.ts","uid":"3bd95fb3-3"}]}],"isRoot":true},"nodeParts":{"3bd95fb3-1":{"renderedLength":48036,"gzipLength":15642,"brotliLength":0,"metaUid":"3bd95fb3-0"},"3bd95fb3-3":{"renderedLength":4657,"gzipLength":2132,"brotliLength":0,"metaUid":"3bd95fb3-2"}},"nodeMetas":{"3bd95fb3-0":{"id":"/src/index.ts","moduleParts":{"index.js":"3bd95fb3-1"},"imported":[{"uid":"3bd95fb3-4"},{"uid":"3bd95fb3-5"},{"uid":"3bd95fb3-6"},{"uid":"3bd95fb3-2","dynamic":true},{"uid":"3bd95fb3-7","dynamic":true}],"importedBy":[],"isEntry":true},"3bd95fb3-2":{"id":"/src/rocketstyle-collapse.ts","moduleParts":{"rocketstyle-collapse-DGnwgDhC.js":"3bd95fb3-3"},"imported":[{"uid":"3bd95fb3-8"},{"uid":"3bd95fb3-9","dynamic":true}],"importedBy":[{"uid":"3bd95fb3-0"}]},"3bd95fb3-4":{"id":"node:fs","moduleParts":{},"imported":[],"importedBy":[{"uid":"3bd95fb3-0"}]},"3bd95fb3-5":{"id":"node:path","moduleParts":{},"imported":[],"importedBy":[{"uid":"3bd95fb3-0"}]},"3bd95fb3-6":{"id":"@pyreon/compiler","moduleParts":{},"imported":[],"importedBy":[{"uid":"3bd95fb3-0"}]},"3bd95fb3-7":{"id":"node:fs/promises","moduleParts":{},"imported":[],"importedBy":[{"uid":"3bd95fb3-0"}]},"3bd95fb3-8":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"3bd95fb3-2"}]},"3bd95fb3-9":{"id":"vite","moduleParts":{},"imported":[],"importedBy":[{"uid":"3bd95fb3-2"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5389
+ const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src/index.ts","uid":"f90e1d82-1"}]},{"name":"rocketstyle-collapse-XIiHU85Y.js","children":[{"name":"src/rocketstyle-collapse.ts","uid":"f90e1d82-3"}]}],"isRoot":true},"nodeParts":{"f90e1d82-1":{"renderedLength":56169,"gzipLength":18271,"brotliLength":0,"metaUid":"f90e1d82-0"},"f90e1d82-3":{"renderedLength":5493,"gzipLength":2490,"brotliLength":0,"metaUid":"f90e1d82-2"}},"nodeMetas":{"f90e1d82-0":{"id":"/src/index.ts","moduleParts":{"index.js":"f90e1d82-1"},"imported":[{"uid":"f90e1d82-4"},{"uid":"f90e1d82-5"},{"uid":"f90e1d82-6"},{"uid":"f90e1d82-2","dynamic":true},{"uid":"f90e1d82-7","dynamic":true}],"importedBy":[],"isEntry":true},"f90e1d82-2":{"id":"/src/rocketstyle-collapse.ts","moduleParts":{"rocketstyle-collapse-XIiHU85Y.js":"f90e1d82-3"},"imported":[{"uid":"f90e1d82-8"},{"uid":"f90e1d82-9","dynamic":true}],"importedBy":[{"uid":"f90e1d82-0"}]},"f90e1d82-4":{"id":"node:fs","moduleParts":{},"imported":[],"importedBy":[{"uid":"f90e1d82-0"}]},"f90e1d82-5":{"id":"node:path","moduleParts":{},"imported":[],"importedBy":[{"uid":"f90e1d82-0"}]},"f90e1d82-6":{"id":"@pyreon/compiler","moduleParts":{},"imported":[],"importedBy":[{"uid":"f90e1d82-0"}]},"f90e1d82-7":{"id":"node:fs/promises","moduleParts":{},"imported":[],"importedBy":[{"uid":"f90e1d82-0"}]},"f90e1d82-8":{"id":"@pyreon/reactivity","moduleParts":{},"imported":[],"importedBy":[{"uid":"f90e1d82-2"}]},"f90e1d82-9":{"id":"vite","moduleParts":{},"imported":[],"importedBy":[{"uid":"f90e1d82-2"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
5390
5390
 
5391
5391
  const run = () => {
5392
5392
  const width = window.innerWidth;
package/lib/index.js CHANGED
@@ -40,7 +40,7 @@ const __DEV__ = typeof process !== "undefined" && process.env.NODE_ENV !== "prod
40
40
  const _countSink = globalThis;
41
41
  let _createCollapseResolver = null;
42
42
  async function loadCreateCollapseResolver() {
43
- if (!_createCollapseResolver) _createCollapseResolver = (await import("./rocketstyle-collapse-DGnwgDhC.js")).createCollapseResolver;
43
+ if (!_createCollapseResolver) _createCollapseResolver = (await import("./rocketstyle-collapse-XIiHU85Y.js")).createCollapseResolver;
44
44
  return _createCollapseResolver;
45
45
  }
46
46
  const HMR_RUNTIME_ID = "\0pyreon/hmr-runtime";
@@ -108,7 +108,7 @@ const COMPAT_ALIASES = {
108
108
  * structural property of the monorepo (workspace layout), not the package
109
109
  * name — so it's robust against renames.
110
110
  */
111
- function isPyreonWorkspaceFile(id, cache) {
111
+ function _isPyreonWorkspaceFile(id, cache) {
112
112
  const queryIdx = id.indexOf("?");
113
113
  const filePath = queryIdx === -1 ? id : id.slice(0, queryIdx);
114
114
  if (!filePath || filePath[0] === "\0") return false;
@@ -137,7 +137,7 @@ function isPyreonWorkspaceFile(id, cache) {
137
137
  * Return the Pyreon compat target for an import specifier, or undefined if
138
138
  * the import should not be redirected.
139
139
  */
140
- function getCompatTarget(compat, id) {
140
+ function _getCompatTarget(compat, id) {
141
141
  if (!compat) return void 0;
142
142
  const aliased = COMPAT_ALIASES[compat][id];
143
143
  if (aliased) return aliased;
@@ -249,6 +249,27 @@ function pyreonPlugin(options) {
249
249
  source: "@pyreon/ui-core"
250
250
  };
251
251
  const collapseSources = new Set(collapseUserCfg.sources ?? ["@pyreon/ui-components"]);
252
+ const jsxAutoImportOpt = options?.jsxAutoImport;
253
+ const jsxAutoImportEnabled = jsxAutoImportOpt !== false;
254
+ const jsxAutoImportUserCfg = jsxAutoImportOpt && jsxAutoImportOpt !== true ? jsxAutoImportOpt : {};
255
+ const jsxAutoImportMappings = jsxAutoImportUserCfg.mappings ?? (jsxAutoImportUserCfg.source && jsxAutoImportUserCfg.names ? [{
256
+ source: jsxAutoImportUserCfg.source,
257
+ names: jsxAutoImportUserCfg.names
258
+ }] : [{
259
+ source: "@pyreon/primitives",
260
+ names: [
261
+ "Stack",
262
+ "Inline",
263
+ "Text",
264
+ "Button",
265
+ "Press",
266
+ "Field",
267
+ "Toggle"
268
+ ]
269
+ }, {
270
+ source: "@pyreon/core",
271
+ names: ["For", "Show"]
272
+ }]);
252
273
  const collapseComponentFilter = collapseUserCfg.components ? (n) => collapseUserCfg.components.includes(n) : null;
253
274
  let collapseResolver = null;
254
275
  let collapseResolverInit = null;
@@ -276,6 +297,7 @@ function pyreonPlugin(options) {
276
297
  const resolveCache = /* @__PURE__ */ new Map();
277
298
  const pyreonWorkspaceDirCache = /* @__PURE__ */ new Map();
278
299
  const islandRegistry = /* @__PURE__ */ new Map();
300
+ let _devServer;
279
301
  return {
280
302
  name: "pyreon",
281
303
  enforce: "pre",
@@ -334,8 +356,8 @@ function pyreonPlugin(options) {
334
356
  async resolveId(id, importer) {
335
357
  if (id === HMR_RUNTIME_IMPORT) return HMR_RUNTIME_ID;
336
358
  if (id === ISLANDS_REGISTRY_IMPORT) return ISLANDS_REGISTRY_ID;
337
- if (compat && (id === "@pyreon/core/jsx-runtime" || id === "@pyreon/core/jsx-dev-runtime") && importer && isPyreonWorkspaceFile(importer, pyreonWorkspaceDirCache)) return;
338
- const target = getCompatTarget(compat, id);
359
+ if (compat && (id === "@pyreon/core/jsx-runtime" || id === "@pyreon/core/jsx-dev-runtime") && importer && _isPyreonWorkspaceFile(importer, pyreonWorkspaceDirCache)) return;
360
+ const target = _getCompatTarget(compat, id);
339
361
  if (!target) return;
340
362
  return (await this.resolve(target, importer, { skipSelf: true }))?.id;
341
363
  },
@@ -357,9 +379,15 @@ function pyreonPlugin(options) {
357
379
  return;
358
380
  }
359
381
  scanSignalExports(code, normalizeModuleId(id), signalExportRegistry);
360
- if (islandsEnabled) scanIslandDeclarations(code, id, islandRegistry);
361
- const deferResult = transformDeferInline(code, id);
362
- const sourceForJsx = deferResult.changed ? deferResult.code : code;
382
+ if (islandsEnabled) {
383
+ if (scanIslandDeclarations(code, id, islandRegistry) && _devServer) {
384
+ const mod = _devServer.moduleGraph.getModuleById(ISLANDS_REGISTRY_ID);
385
+ if (mod) _devServer.moduleGraph.invalidateModule(mod);
386
+ }
387
+ }
388
+ const codeAfterAutoImport = jsxAutoImportEnabled ? autoImportCanonicalPrimitives(code, jsxAutoImportMappings) : code;
389
+ const deferResult = transformDeferInline(codeAfterAutoImport, id);
390
+ const sourceForJsx = deferResult.changed ? deferResult.code : codeAfterAutoImport;
363
391
  for (const w of deferResult.warnings) this.warn(`${w.message} (${id}:${w.line}:${w.column})`);
364
392
  const knownSignals = await resolveImportedSignals(sourceForJsx, id, signalExportRegistry, this, resolveCache);
365
393
  const isSsr = transformOptions?.ssr === true;
@@ -383,6 +411,7 @@ function pyreonPlugin(options) {
383
411
  },
384
412
  props: s.props,
385
413
  childrenText: s.childrenText,
414
+ ...s.childTree ? { childTree: s.childTree } : {},
386
415
  config: {
387
416
  provider: collapseProvider,
388
417
  theme: collapseTheme,
@@ -424,6 +453,7 @@ function pyreonPlugin(options) {
424
453
  };
425
454
  },
426
455
  configureServer(server) {
456
+ _devServer = server;
427
457
  generateProjectContext(projectRoot);
428
458
  let contextTimer = null;
429
459
  server.watcher.on("change", (file) => {
@@ -440,7 +470,7 @@ function pyreonPlugin(options) {
440
470
  const url = req.url ?? "/";
441
471
  if (isAssetRequest(url)) return next();
442
472
  try {
443
- await handleSsrRequest(server, ssrConfig.entry, url, req, res, next);
473
+ await _handleSsrRequest(server, ssrConfig.entry, url, req, res, next);
444
474
  } catch (err) {
445
475
  server.ssrFixStacktrace(err);
446
476
  next(err);
@@ -455,7 +485,7 @@ function pyreonPlugin(options) {
455
485
  }
456
486
  };
457
487
  }
458
- async function handleSsrRequest(server, entry, url, req, res, next) {
488
+ async function _handleSsrRequest(server, entry, url, req, res, next) {
459
489
  const mod = await server.ssrLoadModule(entry);
460
490
  const handler = mod.handler ?? mod.default;
461
491
  if (typeof handler !== "function") {
@@ -637,7 +667,7 @@ const SIGNAL_PREFIX_RE = /^((?:export\s+)?(?:const|let)\s+(\w+)\s*=\s*)signal(?:
637
667
  * (uppercase first letter — standard convention for JSX components).
638
668
  */
639
669
  const EXPORT_COMPONENT_RE = /export\s+(?:default\s+)?(?:function\s+([A-Z]\w*)|const\s+([A-Z]\w*)\s*[=:])/;
640
- function skipStringLiteral(code, start, quote) {
670
+ function _skipStringLiteral(code, start, quote) {
641
671
  let j = start + 1;
642
672
  while (j < code.length) {
643
673
  if (code[j] === "\\") {
@@ -649,7 +679,7 @@ function skipStringLiteral(code, start, quote) {
649
679
  }
650
680
  return j;
651
681
  }
652
- function extractBalancedArgs(code, start) {
682
+ function _extractBalancedArgs(code, start) {
653
683
  let depth = 1;
654
684
  for (let i = start; i < code.length; i++) {
655
685
  const ch = code[i];
@@ -657,7 +687,7 @@ function extractBalancedArgs(code, start) {
657
687
  else if (ch === ")") {
658
688
  depth--;
659
689
  if (depth === 0) return code.slice(start, i);
660
- } else if (ch === "\"" || ch === "'" || ch === "`") i = skipStringLiteral(code, i, ch);
690
+ } else if (ch === "\"" || ch === "'" || ch === "`") i = _skipStringLiteral(code, i, ch);
661
691
  }
662
692
  return null;
663
693
  }
@@ -671,7 +701,7 @@ function braceDepthAt(code, pos) {
671
701
  const ch = code[i];
672
702
  if (ch === "{") depth++;
673
703
  else if (ch === "}") depth--;
674
- else if (ch === "\"" || ch === "'" || ch === "`") i = skipStringLiteral(code, i, ch);
704
+ else if (ch === "\"" || ch === "'" || ch === "`") i = _skipStringLiteral(code, i, ch);
675
705
  }
676
706
  return depth;
677
707
  }
@@ -682,7 +712,7 @@ function rewriteSignals(code, moduleId) {
682
712
  let m = SIGNAL_PREFIX_RE.exec(code);
683
713
  while (m !== null) {
684
714
  const argsStart = m.index + m[0].length;
685
- const args = extractBalancedArgs(code, argsStart);
715
+ const args = _extractBalancedArgs(code, argsStart);
686
716
  if (args === null) {
687
717
  m = SIGNAL_PREFIX_RE.exec(code);
688
718
  continue;
@@ -760,7 +790,7 @@ function injectSignalNames(code, moduleId) {
760
790
  let m = reBound.exec(masked);
761
791
  while (m !== null) {
762
792
  const argsStart = m.index + m[0].length;
763
- const args = extractBalancedArgs(code, argsStart);
793
+ const args = _extractBalancedArgs(code, argsStart);
764
794
  if (args !== null && !hasMultipleArgs(args)) {
765
795
  matches.push({
766
796
  start: argsStart,
@@ -779,7 +809,7 @@ function injectSignalNames(code, moduleId) {
779
809
  while (m !== null) {
780
810
  if (!covered.has(m.index)) {
781
811
  const argsStart = m.index + m[0].length;
782
- const args = extractBalancedArgs(code, argsStart);
812
+ const args = _extractBalancedArgs(code, argsStart);
783
813
  if (args !== null && !hasMultipleArgs(args)) matches.push({
784
814
  start: argsStart,
785
815
  end: argsStart + args.length,
@@ -992,6 +1022,159 @@ function injectHmr(code, moduleId) {
992
1022
  function transformCompatAttributes(code) {
993
1023
  return code.replace(/(\s)className(\s*=)/g, "$1class$2").replace(/(\s)htmlFor(\s*=)/g, "$1for$2");
994
1024
  }
1025
+ /**
1026
+ * Auto-inject `import { ... } from '<source>'` for bare JSX references
1027
+ * to canonical primitives. Closes the Phase D2 "literally same .tsx
1028
+ * file across web + native" gap — the native PMTC compiler resolves
1029
+ * bare tags via its canonical-primitives table (no import needed); the
1030
+ * web build needs the imports for symbol resolution.
1031
+ *
1032
+ * Pass shape:
1033
+ * 1. Regex-scan `<Name` JSX opening tags + `<Name/>` self-closing
1034
+ * shapes against the configured names set.
1035
+ * 2. Parse existing imports from the source to find what's already
1036
+ * imported as a value (we don't auto-add a name that's already in
1037
+ * scope, regardless of source — a user-defined `<Button>` from a
1038
+ * local file takes precedence).
1039
+ * 3. Inject the auto-import ONLY for names that are used but not
1040
+ * already imported. Skips entirely if the diff is empty.
1041
+ *
1042
+ * Conservative by construction: regex matches only at JSX opening-tag
1043
+ * positions (`<Name` followed by `[\s/>]`). String/comment scans aren't
1044
+ * needed because the regex requires the `<` boundary. Names in regular
1045
+ * code positions (function calls, type references) don't trigger the
1046
+ * import.
1047
+ *
1048
+ * Same module's own export shape is detected — if the source exports
1049
+ * a `Stack` symbol via `export function Stack(...)` or `export const
1050
+ * Stack = ...`, the auto-import is suppressed for that name (it's
1051
+ * already a top-level identifier in scope, and importing from the
1052
+ * primitives package would shadow it).
1053
+ */
1054
+ function autoImportCanonicalPrimitives(code, mappings) {
1055
+ if (mappings.length === 0) return code;
1056
+ const nameToSource = /* @__PURE__ */ new Map();
1057
+ for (const { source, names } of mappings) for (const n of names) if (!nameToSource.has(n)) nameToSource.set(n, source);
1058
+ if (nameToSource.size === 0) return code;
1059
+ const masked = _maskCommentsAndStrings(code);
1060
+ const allNames = Array.from(nameToSource.keys());
1061
+ const nameAlt = allNames.join("|");
1062
+ const jsxTagRe = new RegExp(`<(${nameAlt})(?=[\\s/>])`, "g");
1063
+ const used = /* @__PURE__ */ new Set();
1064
+ let m;
1065
+ while ((m = jsxTagRe.exec(masked)) !== null) used.add(m[1]);
1066
+ if (used.size === 0) return code;
1067
+ const alreadyInScope = _collectImportedNames(masked);
1068
+ for (const name of allNames) if (new RegExp(`(?:^|\\n)\\s*(?:export\\s+)?(?:function\\s+${name}\\b|const\\s+${name}\\b|let\\s+${name}\\b|var\\s+${name}\\b|class\\s+${name}\\b)`).test(code)) alreadyInScope.add(name);
1069
+ const bySource = /* @__PURE__ */ new Map();
1070
+ for (const name of used) {
1071
+ if (alreadyInScope.has(name)) continue;
1072
+ const src = nameToSource.get(name);
1073
+ if (!bySource.has(src)) bySource.set(src, []);
1074
+ bySource.get(src).push(name);
1075
+ }
1076
+ if (bySource.size === 0) return code;
1077
+ let result = code;
1078
+ let workMask = masked;
1079
+ const orderedSources = mappings.map((m) => m.source).filter((s, i, a) => a.indexOf(s) === i);
1080
+ for (const source of orderedSources) {
1081
+ const toInject = bySource.get(source);
1082
+ if (!toInject || toInject.length === 0) continue;
1083
+ toInject.sort();
1084
+ const existing = new RegExp(`import\\s*\\{([^}]*)\\}\\s*from\\s*['"]${escapeRegex(source)}['"]`).exec(workMask);
1085
+ if (existing) {
1086
+ const realMatch = result.slice(existing.index, existing.index + existing[0].length);
1087
+ const existingSpecifiers = (/\{([^}]*)\}/.exec(realMatch)?.[1] ?? "").split(",").map((s) => s.trim()).filter(Boolean);
1088
+ const existingLocalNames = new Set(existingSpecifiers.map((s) => {
1089
+ const asIdx = s.indexOf(" as ");
1090
+ return asIdx >= 0 ? s.slice(asIdx + 4).trim() : s;
1091
+ }));
1092
+ const merged = [...existingSpecifiers];
1093
+ for (const n of toInject) if (!existingLocalNames.has(n)) merged.push(n);
1094
+ merged.sort();
1095
+ const newImport = `import { ${merged.join(", ")} } from '${source}'`;
1096
+ const before = result.slice(0, existing.index);
1097
+ const after = result.slice(existing.index + existing[0].length);
1098
+ result = before + newImport + after;
1099
+ const beforeM = workMask.slice(0, existing.index);
1100
+ const afterM = workMask.slice(existing.index + existing[0].length);
1101
+ workMask = beforeM + " ".repeat(newImport.length) + afterM;
1102
+ } else {
1103
+ const importLine = `import { ${toInject.join(", ")} } from '${source}'\n`;
1104
+ result = importLine + result;
1105
+ workMask = " ".repeat(importLine.length) + workMask;
1106
+ }
1107
+ }
1108
+ return result;
1109
+ }
1110
+ /**
1111
+ * Replace JS comments with spaces while preserving line/column
1112
+ * positions. Used by the auto-import scanner so `<Stack>` text inside
1113
+ * JSDoc doesn't false-positive as a JSX usage. Position preservation
1114
+ * lets the caller use the masked code for regex SEARCH and the
1115
+ * original code for SPLICE.
1116
+ *
1117
+ * String literals are deliberately LEFT VISIBLE — they often carry
1118
+ * the package name we need to match (`from '@pyreon/primitives'`).
1119
+ * The trade-off: a literal `'<Stack/>'` inside a string would
1120
+ * false-positive, but that's vanishingly rare compared to JSDoc
1121
+ * examples + the cost of either making string-aware AST parsing OR
1122
+ * masking only the LITERAL TEXT (not the quotes) is not worth it
1123
+ * for the marginal correctness gain.
1124
+ *
1125
+ * Handles:
1126
+ * - line comments `// ... newline`
1127
+ * - block comments `/* ... *​/`
1128
+ */
1129
+ function _maskCommentsAndStrings(code) {
1130
+ const out = new Array(code.length);
1131
+ let i = 0;
1132
+ const n = code.length;
1133
+ while (i < n) {
1134
+ const c = code[i] ?? "";
1135
+ const c2 = code[i + 1] ?? "";
1136
+ if (c === "/" && c2 === "*") {
1137
+ const end = code.indexOf("*/", i + 2);
1138
+ const stop = end < 0 ? n : end + 2;
1139
+ for (let j = i; j < stop; j++) out[j] = code[j] === "\n" ? "\n" : " ";
1140
+ i = stop;
1141
+ continue;
1142
+ }
1143
+ if (c === "/" && c2 === "/") {
1144
+ let j = i;
1145
+ while (j < n && code[j] !== "\n") {
1146
+ out[j] = " ";
1147
+ j++;
1148
+ }
1149
+ i = j;
1150
+ continue;
1151
+ }
1152
+ out[i] = c;
1153
+ i++;
1154
+ }
1155
+ return out.join("");
1156
+ }
1157
+ /** Collect every name imported via `import { ... }` / `import X` / `import * as X`. */
1158
+ function _collectImportedNames(code) {
1159
+ const out = /* @__PURE__ */ new Set();
1160
+ const namedRe = /import\s*(?:type\s+)?\{([^}]+)\}\s*from\s*['"][^'"]+['"]/g;
1161
+ let m;
1162
+ while ((m = namedRe.exec(code)) !== null) for (const spec of m[1].split(",")) {
1163
+ const trimmed = spec.trim();
1164
+ if (!trimmed) continue;
1165
+ const asIdx = trimmed.indexOf(" as ");
1166
+ out.add(asIdx >= 0 ? trimmed.slice(asIdx + 4).trim() : trimmed);
1167
+ }
1168
+ const defaultRe = /import\s+(\w+)(?:\s*,\s*\{[^}]*\})?\s+from\s*['"][^'"]+['"]/g;
1169
+ while ((m = defaultRe.exec(code)) !== null) out.add(m[1]);
1170
+ const nsRe = /import\s+\*\s+as\s+(\w+)\s+from\s*['"][^'"]+['"]/g;
1171
+ while ((m = nsRe.exec(code)) !== null) out.add(m[1]);
1172
+ return out;
1173
+ }
1174
+ /** Escape a string for safe use inside a RegExp. */
1175
+ function escapeRegex(s) {
1176
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1177
+ }
995
1178
  function getExt(id) {
996
1179
  const clean = id.split("?")[0] ?? id;
997
1180
  const dot = clean.lastIndexOf(".");
@@ -1068,15 +1251,30 @@ function scanIslandDeclarations(code, filePath, registry) {
1068
1251
  if (!nameMatch) continue;
1069
1252
  const hydrateMatch = /(?:^|[\s,{])hydrate\s*:\s*['"]([^'"]+)['"]/.exec(optsBlock);
1070
1253
  const hydrate = hydrateMatch ? hydrateMatch[1] : "load";
1071
- const loaderAbsPath = importPath.startsWith(".") ? resolveRelative(filePath, importPath) : importPath;
1254
+ const loaderAbsPath = normalizeModuleId(importPath.startsWith(".") ? resolveRelative(filePath, importPath) : importPath);
1072
1255
  decls.push({
1073
1256
  name: nameMatch[1],
1074
1257
  hydrate,
1075
1258
  loaderAbsPath
1076
1259
  });
1077
1260
  }
1078
- if (decls.length > 0) registry.set(normalizeModuleId(filePath), decls);
1079
- else registry.delete(normalizeModuleId(filePath));
1261
+ const key = normalizeModuleId(filePath);
1262
+ const changed = !islandDeclsEqual(registry.get(key), decls.length > 0 ? decls : void 0);
1263
+ if (decls.length > 0) registry.set(key, decls);
1264
+ else registry.delete(key);
1265
+ return changed;
1266
+ }
1267
+ /** PR-S12: structural equality check for IslandDecl arrays. */
1268
+ function islandDeclsEqual(a, b) {
1269
+ if (a === b) return true;
1270
+ if (!a || !b) return false;
1271
+ if (a.length !== b.length) return false;
1272
+ for (let i = 0; i < a.length; i++) {
1273
+ const ai = a[i];
1274
+ const bi = b[i];
1275
+ if (ai.name !== bi.name || ai.hydrate !== bi.hydrate || ai.loaderAbsPath !== bi.loaderAbsPath) return false;
1276
+ }
1277
+ return true;
1080
1278
  }
1081
1279
  /**
1082
1280
  * Resolve a dynamic-import specifier to an absolute path, mirroring how Node
@@ -1299,5 +1497,5 @@ export function __hmr_dispose(moduleId) {
1299
1497
  `;
1300
1498
 
1301
1499
  //#endregion
1302
- export { _computeLineStarts, _isTruthyEnv, _maskStringsAndComments, _offsetToLineCol, buildLpihClientScript, pyreonPlugin as default, registerLpihMiddleware, resolveLpihCachePath, writeLpihCacheFile };
1500
+ export { _collectImportedNames, _computeLineStarts, _extractBalancedArgs, _getCompatTarget, _handleSsrRequest, _isPyreonWorkspaceFile, _isTruthyEnv, _maskCommentsAndStrings, _maskStringsAndComments, _offsetToLineCol, _skipStringLiteral, buildLpihClientScript, pyreonPlugin as default, registerLpihMiddleware, resolveLpihCachePath, writeLpihCacheFile };
1303
1501
  //# sourceMappingURL=index.js.map