nukejs 0.0.6 → 0.0.8

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.
Files changed (49) hide show
  1. package/README.md +89 -6
  2. package/dist/{as-is/Link.js → Link.js} +3 -1
  3. package/dist/Link.js.map +7 -0
  4. package/dist/app.d.ts +3 -2
  5. package/dist/app.js +3 -13
  6. package/dist/app.js.map +2 -2
  7. package/dist/build-common.d.ts +6 -0
  8. package/dist/build-common.js +20 -6
  9. package/dist/build-common.js.map +2 -2
  10. package/dist/build-node.d.ts +1 -1
  11. package/dist/build-node.js +6 -17
  12. package/dist/build-node.js.map +2 -2
  13. package/dist/build-vercel.js +1 -1
  14. package/dist/build-vercel.js.map +2 -2
  15. package/dist/builder.d.ts +4 -10
  16. package/dist/builder.js +7 -38
  17. package/dist/builder.js.map +2 -2
  18. package/dist/bundle.js +60 -4
  19. package/dist/bundle.js.map +2 -2
  20. package/dist/component-analyzer.d.ts +6 -0
  21. package/dist/component-analyzer.js +12 -1
  22. package/dist/component-analyzer.js.map +2 -2
  23. package/dist/hmr-bundle.js +17 -4
  24. package/dist/hmr-bundle.js.map +2 -2
  25. package/dist/html-store.d.ts +7 -0
  26. package/dist/html-store.js.map +2 -2
  27. package/dist/http-server.d.ts +2 -9
  28. package/dist/http-server.js +16 -2
  29. package/dist/http-server.js.map +2 -2
  30. package/dist/index.d.ts +2 -2
  31. package/dist/index.js +2 -2
  32. package/dist/index.js.map +1 -1
  33. package/dist/renderer.js +2 -7
  34. package/dist/renderer.js.map +2 -2
  35. package/dist/router.d.ts +20 -19
  36. package/dist/router.js +14 -6
  37. package/dist/router.js.map +2 -2
  38. package/dist/ssr.js +21 -4
  39. package/dist/ssr.js.map +2 -2
  40. package/dist/use-html.js +5 -1
  41. package/dist/use-html.js.map +2 -2
  42. package/dist/{as-is/useRouter.js → use-router.js} +1 -1
  43. package/dist/{as-is/useRouter.js.map → use-router.js.map} +2 -2
  44. package/package.json +1 -1
  45. package/dist/as-is/Link.js.map +0 -7
  46. package/dist/as-is/Link.tsx +0 -20
  47. package/dist/as-is/useRouter.ts +0 -33
  48. /package/dist/{as-is/Link.d.ts → Link.d.ts} +0 -0
  49. /package/dist/{as-is/useRouter.d.ts → use-router.d.ts} +0 -0
package/dist/builder.js CHANGED
@@ -6,7 +6,6 @@ import { fileURLToPath } from "url";
6
6
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
7
  const srcDir = path.resolve(__dirname, "");
8
8
  const outDir = path.resolve(__dirname, "../dist");
9
- const AS_IS = "as-is";
10
9
  function cleanDist(dir) {
11
10
  if (!fs.existsSync(dir)) return;
12
11
  fs.rmSync(dir, { recursive: true, force: true });
@@ -24,44 +23,28 @@ function collectFiles(dir, exclude = []) {
24
23
  }
25
24
  return files;
26
25
  }
27
- function copyDir(src, dest) {
28
- if (!fs.existsSync(src)) return;
29
- fs.mkdirSync(dest, { recursive: true });
30
- for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
31
- const s = path.join(src, entry.name);
32
- const d = path.join(dest, entry.name);
33
- entry.isDirectory() ? copyDir(s, d) : fs.copyFileSync(s, d);
34
- }
35
- }
36
26
  function processDist(dir) {
37
- const excludeFolder = "as-is";
38
27
  (function walk(currentDir) {
39
28
  fs.readdirSync(currentDir, { withFileTypes: true }).forEach((d) => {
40
29
  const fullPath = path.join(currentDir, d.name);
41
30
  if (d.isDirectory()) {
42
- if (d.name !== excludeFolder) walk(fullPath);
31
+ walk(fullPath);
43
32
  } else if (fullPath.endsWith(".js")) {
44
33
  let content = fs.readFileSync(fullPath, "utf-8");
45
- content = content.replace(
46
- /from\s+['"](\.\/(?!as-is\/).*?)['"]/g,
47
- 'from "$1.js"'
48
- );
49
- content = content.replace(
50
- /import\(['"](\.\/(?!as-is\/).*?)['"]\)/g,
51
- 'import("$1.js")'
52
- );
34
+ content = content.replace(/from\s+['"](\.\/.*?)['"]/g, 'from "$1.js"');
35
+ content = content.replace(/import\(['"](\.\/.*?)['"]\)/g, 'import("$1.js")');
53
36
  fs.writeFileSync(fullPath, content, "utf-8");
54
37
  }
55
38
  });
56
39
  })(dir);
57
- console.log("\u{1F527} Post-processing done: .ts imports \u2192 .js (excluding as-is folder).");
40
+ console.log("\u{1F527} Post-processing done: relative imports \u2192 .js extensions.");
58
41
  }
59
42
  async function runBuild() {
60
43
  try {
61
44
  cleanDist(outDir);
62
- console.log("\u{1F680} Building main sources\u2026");
45
+ console.log("\u{1F680} Building sources\u2026");
63
46
  await build({
64
- entryPoints: collectFiles(srcDir, [AS_IS]),
47
+ entryPoints: collectFiles(srcDir),
65
48
  outdir: outDir,
66
49
  platform: "node",
67
50
  format: "esm",
@@ -69,21 +52,7 @@ async function runBuild() {
69
52
  packages: "external",
70
53
  sourcemap: true
71
54
  });
72
- console.log("\u2705 Main build done.");
73
- console.log("\u{1F680} Building as-is sources\u2026");
74
- await build({
75
- entryPoints: collectFiles(path.join(srcDir, AS_IS)),
76
- outdir: path.join(outDir, AS_IS),
77
- platform: "neutral",
78
- format: "esm",
79
- target: ["node20"],
80
- packages: "external",
81
- jsx: "automatic",
82
- sourcemap: true
83
- });
84
- console.log("\u2705 as-is build done.");
85
- copyDir(path.join(srcDir, AS_IS), path.join(outDir, AS_IS));
86
- console.log(`\u{1F4C1} Copied as-is sources \u2192 dist/${AS_IS}/`);
55
+ console.log("\u2705 Build done.");
87
56
  processDist(outDir);
88
57
  console.log("\u{1F4C4} Generating TypeScript declarations\u2026");
89
58
  execSync("tsc --emitDeclarationOnly --declaration --outDir dist", { stdio: "inherit" });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/builder.ts"],
4
- "sourcesContent": ["/**\n * builder.ts \u2014 NukeJS Package Build Script\n *\n * Compiles the NukeJS source into dist/ with two separate esbuild passes:\n *\n * Pass 1 (main): All src/ files excluding as-is/, compiled to Node ESM.\n * Pass 2 (as-is): Link.tsx + useRouter.ts compiled to browser-neutral ESM,\n * then the original .ts/.tsx sources are also copied into\n * dist/as-is/ so end-users can reference them directly.\n *\n * After both passes, processDist() rewrites bare relative imports\n * (e.g. `from './utils'`) to include .js extensions, which is required for\n * Node's strict ESM resolver.\n *\n * Finally, `tsc --emitDeclarationOnly` generates .d.ts files for consumers.\n */\n\nimport { build } from 'esbuild';\nimport fs from 'fs';\nimport { execSync } from 'child_process';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\nconst srcDir = path.resolve(__dirname, '');\nconst outDir = path.resolve(__dirname, '../dist');\nconst AS_IS = 'as-is';\n\n// \u2500\u2500\u2500 Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction cleanDist(dir: string): void {\n if (!fs.existsSync(dir)) return;\n fs.rmSync(dir, { recursive: true, force: true });\n console.log(`\uD83D\uDDD1\uFE0F Cleared ${dir}`);\n}\n\n/** Collects all .ts/.tsx/.js/.jsx files under `dir`, skipping `exclude` dirs. */\nfunction collectFiles(dir: string, exclude: string[] = []): string[] {\n const files: string[] = [];\n for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {\n const full = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n if (!exclude.includes(entry.name)) files.push(...collectFiles(full, exclude));\n } else if (/\\.[tj]sx?$/.test(entry.name)) {\n files.push(full);\n }\n }\n return files;\n}\n\n/**\n * Copies a directory recursively, preserving structure.\n * Used to place the original as-is .ts/.tsx sources into dist/as-is/\n * so end-users can read and copy them.\n */\nfunction copyDir(src: string, dest: string): void {\n if (!fs.existsSync(src)) return;\n fs.mkdirSync(dest, { recursive: true });\n for (const entry of fs.readdirSync(src, { withFileTypes: true })) {\n const s = path.join(src, entry.name);\n const d = path.join(dest, entry.name);\n entry.isDirectory() ? copyDir(s, d) : fs.copyFileSync(s, d);\n }\n}\n\n// --- Post-process .js files ---\nfunction processDist(dir: string) {\n const excludeFolder = \"as-is\";\n\n (function walk(currentDir: string) {\n fs.readdirSync(currentDir, { withFileTypes: true }).forEach((d) => {\n const fullPath = path.join(currentDir, d.name);\n\n if (d.isDirectory()) {\n if (d.name !== excludeFolder) walk(fullPath);\n } else if (fullPath.endsWith(\".js\")) {\n let content = fs.readFileSync(fullPath, \"utf-8\");\n\n // Replace import/export paths ending with .ts \u2192 .js, skip paths containing excludeFolder\n content = content.replace(\n /from\\s+['\"](\\.\\/(?!as-is\\/).*?)['\"]/g,\n 'from \"$1.js\"'\n );\n content = content.replace(\n /import\\(['\"](\\.\\/(?!as-is\\/).*?)['\"]\\)/g,\n 'import(\"$1.js\")'\n );\n\n fs.writeFileSync(fullPath, content, \"utf-8\");\n }\n });\n })(dir);\n\n console.log(\"\uD83D\uDD27 Post-processing done: .ts imports \u2192 .js (excluding as-is folder).\");\n}\n\n// \u2500\u2500\u2500 Build \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nasync function runBuild(): Promise<void> {\n try {\n cleanDist(outDir);\n\n // Pass 1: main source (Node platform, no JSX needed)\n console.log('\uD83D\uDE80 Building main sources\u2026');\n await build({\n entryPoints: collectFiles(srcDir, [AS_IS]),\n outdir: outDir,\n platform: 'node',\n format: 'esm',\n target: ['node20'],\n packages: 'external',\n sourcemap: true,\n });\n console.log('\u2705 Main build done.');\n\n // Pass 2: as-is sources (browser-neutral, needs JSX)\n console.log('\uD83D\uDE80 Building as-is sources\u2026');\n await build({\n entryPoints: collectFiles(path.join(srcDir, AS_IS)),\n outdir: path.join(outDir, AS_IS),\n platform: 'neutral',\n format: 'esm',\n target: ['node20'],\n packages: 'external',\n jsx: 'automatic',\n sourcemap: true,\n });\n console.log('\u2705 as-is build done.');\n\n // Copy original .ts/.tsx sources into dist/as-is/ for end-user reference\n copyDir(path.join(srcDir, AS_IS), path.join(outDir, AS_IS));\n console.log(`\uD83D\uDCC1 Copied as-is sources \u2192 dist/${AS_IS}/`);\n\n // Fix ESM import extensions across all compiled output\n processDist(outDir);\n\n // Emit .d.ts declaration files\n console.log('\uD83D\uDCC4 Generating TypeScript declarations\u2026');\n execSync('tsc --emitDeclarationOnly --declaration --outDir dist', { stdio: 'inherit' });\n\n console.log('\\n\uD83C\uDF89 Build complete \u2192 dist/');\n } catch (err) {\n console.error('\u274C Build failed:', err);\n process.exit(1);\n }\n}\n\nrunBuild();"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\r\n * builder.ts \u2014 NukeJS Package Build Script\r\n *\r\n * Compiles the NukeJS source into dist/ via a single esbuild pass targeting\r\n * Node ESM, followed by processDist() which rewrites bare relative imports\r\n * (e.g. `from './utils'`) to include .js extensions as required by Node's\r\n * strict ESM resolver.\r\n *\r\n * Finally, `tsc --emitDeclarationOnly` generates .d.ts files for consumers.\r\n */\r\n\r\nimport { build } from 'esbuild';\r\nimport fs from 'fs';\r\nimport { execSync } from 'child_process';\r\nimport path from 'path';\r\nimport { fileURLToPath } from 'url';\r\n\r\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\r\nconst srcDir = path.resolve(__dirname, '');\r\nconst outDir = path.resolve(__dirname, '../dist');\r\n\r\n// \u2500\u2500\u2500 Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nfunction cleanDist(dir: string): void {\r\n if (!fs.existsSync(dir)) return;\r\n fs.rmSync(dir, { recursive: true, force: true });\r\n console.log(`\uD83D\uDDD1\uFE0F Cleared ${dir}`);\r\n}\r\n\r\n/** Collects all .ts/.tsx/.js/.jsx files under `dir`, skipping `exclude` dirs. */\r\nfunction collectFiles(dir: string, exclude: string[] = []): string[] {\r\n const files: string[] = [];\r\n for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {\r\n const full = path.join(dir, entry.name);\r\n if (entry.isDirectory()) {\r\n if (!exclude.includes(entry.name)) files.push(...collectFiles(full, exclude));\r\n } else if (/\\.[tj]sx?$/.test(entry.name)) {\r\n files.push(full);\r\n }\r\n }\r\n return files;\r\n}\r\n\r\n// \u2500\u2500\u2500 Post-process .js files \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/** Rewrites bare relative imports to include .js extensions for Node ESM. */\r\nfunction processDist(dir: string) {\r\n (function walk(currentDir: string) {\r\n fs.readdirSync(currentDir, { withFileTypes: true }).forEach((d) => {\r\n const fullPath = path.join(currentDir, d.name);\r\n if (d.isDirectory()) {\r\n walk(fullPath);\r\n } else if (fullPath.endsWith('.js')) {\r\n let content = fs.readFileSync(fullPath, 'utf-8');\r\n content = content.replace(/from\\s+['\"](\\.\\/.*?)['\"]/g, 'from \"$1.js\"');\r\n content = content.replace(/import\\(['\"](\\.\\/.*?)['\"]\\)/g, 'import(\"$1.js\")');\r\n fs.writeFileSync(fullPath, content, 'utf-8');\r\n }\r\n });\r\n })(dir);\r\n\r\n console.log('\uD83D\uDD27 Post-processing done: relative imports \u2192 .js extensions.');\r\n}\r\n\r\n// \u2500\u2500\u2500 Build \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nasync function runBuild(): Promise<void> {\r\n try {\r\n cleanDist(outDir);\r\n\r\n console.log('\uD83D\uDE80 Building sources\u2026');\r\n await build({\r\n entryPoints: collectFiles(srcDir),\r\n outdir: outDir,\r\n platform: 'node',\r\n format: 'esm',\r\n target: ['node20'],\r\n packages: 'external',\r\n sourcemap: true,\r\n });\r\n console.log('\u2705 Build done.');\r\n\r\n processDist(outDir);\r\n\r\n console.log('\uD83D\uDCC4 Generating TypeScript declarations\u2026');\r\n execSync('tsc --emitDeclarationOnly --declaration --outDir dist', { stdio: 'inherit' });\r\n\r\n console.log('\\n\uD83C\uDF89 Build complete \u2192 dist/');\r\n } catch (err) {\r\n console.error('\u274C Build failed:', err);\r\n process.exit(1);\r\n }\r\n}\r\n\r\nrunBuild();"],
5
+ "mappings": "AAWA,SAAS,aAAa;AACtB,OAAO,QAAQ;AACf,SAAS,gBAAgB;AACzB,OAAO,UAAU;AACjB,SAAS,qBAAqB;AAE9B,MAAM,YAAY,KAAK,QAAQ,cAAc,YAAY,GAAG,CAAC;AAC7D,MAAM,SAAS,KAAK,QAAQ,WAAW,EAAE;AACzC,MAAM,SAAS,KAAK,QAAQ,WAAW,SAAS;AAIhD,SAAS,UAAU,KAAmB;AACpC,MAAI,CAAC,GAAG,WAAW,GAAG,EAAG;AACzB,KAAG,OAAO,KAAK,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAC/C,UAAQ,IAAI,4BAAgB,GAAG,EAAE;AACnC;AAGA,SAAS,aAAa,KAAa,UAAoB,CAAC,GAAa;AACnE,QAAM,QAAkB,CAAC;AACzB,aAAW,SAAS,GAAG,YAAY,KAAK,EAAE,eAAe,KAAK,CAAC,GAAG;AAChE,UAAM,OAAO,KAAK,KAAK,KAAK,MAAM,IAAI;AACtC,QAAI,MAAM,YAAY,GAAG;AACvB,UAAI,CAAC,QAAQ,SAAS,MAAM,IAAI,EAAG,OAAM,KAAK,GAAG,aAAa,MAAM,OAAO,CAAC;AAAA,IAC9E,WAAW,aAAa,KAAK,MAAM,IAAI,GAAG;AACxC,YAAM,KAAK,IAAI;AAAA,IACjB;AAAA,EACF;AACA,SAAO;AACT;AAKA,SAAS,YAAY,KAAa;AAChC,GAAC,SAAS,KAAK,YAAoB;AACjC,OAAG,YAAY,YAAY,EAAE,eAAe,KAAK,CAAC,EAAE,QAAQ,CAAC,MAAM;AACjE,YAAM,WAAW,KAAK,KAAK,YAAY,EAAE,IAAI;AAC7C,UAAI,EAAE,YAAY,GAAG;AACnB,aAAK,QAAQ;AAAA,MACf,WAAW,SAAS,SAAS,KAAK,GAAG;AACnC,YAAI,UAAU,GAAG,aAAa,UAAU,OAAO;AAC/C,kBAAU,QAAQ,QAAQ,6BAA6B,cAAc;AACrE,kBAAU,QAAQ,QAAQ,gCAAgC,iBAAiB;AAC3E,WAAG,cAAc,UAAU,SAAS,OAAO;AAAA,MAC7C;AAAA,IACF,CAAC;AAAA,EACH,GAAG,GAAG;AAEN,UAAQ,IAAI,yEAA6D;AAC3E;AAIA,eAAe,WAA0B;AACvC,MAAI;AACF,cAAU,MAAM;AAEhB,YAAQ,IAAI,mCAAuB;AACnC,UAAM,MAAM;AAAA,MACV,aAAa,aAAa,MAAM;AAAA,MAChC,QAAQ;AAAA,MACR,UAAU;AAAA,MACV,QAAQ;AAAA,MACR,QAAQ,CAAC,QAAQ;AAAA,MACjB,UAAU;AAAA,MACV,WAAW;AAAA,IACb,CAAC;AACD,YAAQ,IAAI,qBAAgB;AAE5B,gBAAY,MAAM;AAElB,YAAQ,IAAI,qDAAyC;AACrD,aAAS,yDAAyD,EAAE,OAAO,UAAU,CAAC;AAEtF,YAAQ,IAAI,0CAA8B;AAAA,EAC5C,SAAS,KAAK;AACZ,YAAQ,MAAM,yBAAoB,GAAG;AACrC,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF;AAEA,SAAS;",
6
6
  "names": []
7
7
  }
package/dist/bundle.js CHANGED
@@ -141,21 +141,76 @@ function fingerprint(el) {
141
141
  function syncHeadTags(doc) {
142
142
  const live = headBlock(document.head);
143
143
  const next = headBlock(doc.head);
144
- const liveMap = /* @__PURE__ */ new Map();
145
- for (const el of live.nodes) liveMap.set(fingerprint(el), el);
146
- const nextMap = /* @__PURE__ */ new Map();
147
- for (const el of next.nodes) nextMap.set(fingerprint(el), el);
148
144
  let anchor = live.closeComment;
149
145
  if (!anchor) {
150
146
  document.head.appendChild(document.createComment("n-head"));
151
147
  anchor = document.createComment("/n-head");
152
148
  document.head.appendChild(anchor);
153
149
  }
150
+ for (const el of live.nodes)
151
+ if (el.tagName === "SCRIPT") el.remove();
152
+ for (const el of next.nodes) {
153
+ if (el.tagName === "SCRIPT")
154
+ document.head.insertBefore(cloneScriptForExecution(el), anchor);
155
+ }
156
+ const liveMap = /* @__PURE__ */ new Map();
157
+ for (const el of live.nodes) if (el.tagName !== "SCRIPT") liveMap.set(fingerprint(el), el);
158
+ const nextMap = /* @__PURE__ */ new Map();
159
+ for (const el of next.nodes) if (el.tagName !== "SCRIPT") nextMap.set(fingerprint(el), el);
154
160
  for (const [fp, el] of nextMap)
155
161
  if (!liveMap.has(fp)) document.head.insertBefore(el, anchor);
156
162
  for (const [fp, el] of liveMap)
157
163
  if (!nextMap.has(fp)) el.remove();
158
164
  }
165
+ function bodyScriptsBlock(body) {
166
+ const nodes = [];
167
+ let closeComment = null;
168
+ let inside = false;
169
+ for (const child of Array.from(body.childNodes)) {
170
+ if (child.nodeType === Node.COMMENT_NODE) {
171
+ const text = child.data.trim();
172
+ if (text === "n-body-scripts") {
173
+ inside = true;
174
+ continue;
175
+ }
176
+ if (text === "/n-body-scripts") {
177
+ closeComment = child;
178
+ inside = false;
179
+ continue;
180
+ }
181
+ }
182
+ if (inside && child.nodeType === Node.ELEMENT_NODE)
183
+ nodes.push(child);
184
+ }
185
+ return { nodes, closeComment };
186
+ }
187
+ function cloneScriptForExecution(src) {
188
+ const el = document.createElement("script");
189
+ for (const { name, value } of Array.from(src.attributes)) {
190
+ if (name === "src") {
191
+ const url = new URL(value, location.href);
192
+ url.searchParams.set("t", String(Date.now()));
193
+ el.setAttribute("src", url.toString());
194
+ } else {
195
+ el.setAttribute(name, value);
196
+ }
197
+ }
198
+ if (src.textContent) el.textContent = src.textContent;
199
+ return el;
200
+ }
201
+ function syncBodyScripts(doc) {
202
+ const live = bodyScriptsBlock(document.body);
203
+ const next = bodyScriptsBlock(doc.body);
204
+ for (const el of live.nodes) el.remove();
205
+ let anchor = live.closeComment;
206
+ if (!anchor) {
207
+ document.body.appendChild(document.createComment("n-body-scripts"));
208
+ anchor = document.createComment("/n-body-scripts");
209
+ document.body.appendChild(anchor);
210
+ }
211
+ for (const el of next.nodes)
212
+ document.body.insertBefore(cloneScriptForExecution(el), anchor);
213
+ }
159
214
  function syncAttrs(live, next) {
160
215
  for (const { name, value } of Array.from(next.attributes))
161
216
  live.setAttribute(name, value);
@@ -177,6 +232,7 @@ function setupNavigation(log) {
177
232
  const currApp = document.getElementById("app");
178
233
  if (!newApp || !currApp) return;
179
234
  syncHeadTags(doc);
235
+ syncBodyScripts(doc);
180
236
  syncAttrs(document.documentElement, doc.documentElement);
181
237
  syncAttrs(document.body, doc.body);
182
238
  currApp.innerHTML = newApp.innerHTML;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/bundle.ts"],
4
- "sourcesContent": ["/**\r\n * bundle.ts \u2014 NukeJS Client Runtime\r\n *\r\n * This file is compiled by esbuild into /__n.js and served to every page.\r\n * It provides:\r\n *\r\n * initRuntime(data) \u2014 called once per page load to hydrate\r\n * \"use client\" components and wire up SPA nav\r\n * setupLocationChangeMonitor() \u2014 patches history.pushState/replaceState so\r\n * SPA navigation fires a 'locationchange' event\r\n *\r\n * Hydration model (partial hydration):\r\n * - The server renders the full page to HTML, wrapping each client component\r\n * in a <span data-hydrate-id=\"cc_\u2026\" data-hydrate-props=\"\u2026\"> marker.\r\n * - initRuntime loads the matching JS bundle for each marker and calls\r\n * hydrateRoot() on it, letting React take over just that subtree.\r\n * - Props serialized by the server may include nested React elements\r\n * (serialized as { __re: 'html'|'client', \u2026 }), which are reconstructed\r\n * back into React.createElement calls before mounting.\r\n *\r\n * SPA navigation:\r\n * - Link clicks / programmatic navigation dispatch a 'locationchange' event.\r\n * - The handler fetches the target URL as HTML, diffs the #app container,\r\n * unmounts the old React roots, and re-hydrates the new ones.\r\n * - HMR navigations add ?__hmr=1 so the server skips client-SSR (faster).\r\n *\r\n * Head tag management:\r\n * - The SSR renderer wraps every useHtml()-generated <meta>, <link>, <style>,\r\n * and <script> tag in <!--n-head-->\u2026<!--/n-head--> sentinel comments.\r\n * - On each navigation the client diffs the live sentinel block against the\r\n * incoming one by fingerprint, adding new tags and removing gone ones.\r\n * Tags shared between pages (e.g. a layout stylesheet) are left untouched\r\n * so there is no removal/re-insertion flash.\r\n * - New tags are always inserted before <!--/n-head--> so they stay inside\r\n * the tracked block and remain visible to the diff on subsequent navigations.\r\n */\r\n\r\n// \u2500\u2500\u2500 History patch \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Patches history.pushState and history.replaceState to fire a custom\r\n * 'locationchange' event on window. Also listens to 'popstate' for\r\n * back/forward navigation.\r\n *\r\n * Called after initRuntime sets up the navigation listener so there is no\r\n * race between the event firing and the listener being registered.\r\n */\r\nexport function setupLocationChangeMonitor(): void {\r\n const originalPushState = window.history.pushState.bind(window.history);\r\n const originalReplaceState = window.history.replaceState.bind(window.history);\r\n\r\n const dispatch = (href?: any) =>\r\n window.dispatchEvent(new CustomEvent('locationchange', { detail: { href } }));\r\n\r\n window.history.pushState = function (...args) {\r\n originalPushState(...args);\r\n dispatch(args[2]); // args[2] is the URL\r\n };\r\n\r\n window.history.replaceState = function (...args) {\r\n originalReplaceState(...args);\r\n dispatch(args[2]);\r\n };\r\n\r\n // Back/forward navigation via the browser's native UI.\r\n window.addEventListener('popstate', () => dispatch(window.location.pathname));\r\n}\r\n\r\n// \u2500\u2500\u2500 Logger \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\ntype ClientDebugLevel = 'silent' | 'error' | 'info' | 'verbose';\r\n\r\n/**\r\n * Returns a thin logger whose methods are no-ops unless `level` allows them.\r\n * The server embeds the active debug level in the __n_data JSON blob so the\r\n * client respects the same setting as the server.\r\n */\r\nfunction makeLogger(level: ClientDebugLevel) {\r\n return {\r\n verbose: (...a: any[]) => { if (level === 'verbose') console.log(...a); },\r\n info: (...a: any[]) => { if (level === 'verbose' || level === 'info') console.log(...a); },\r\n warn: (...a: any[]) => { if (level === 'verbose' || level === 'info') console.warn(...a); },\r\n error: (...a: any[]) => { if (level !== 'silent') console.error(...a); },\r\n };\r\n}\r\n\r\n// \u2500\u2500\u2500 Serialized node types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/** The wire format for React elements embedded in hydration props. */\r\ntype SerializedNode =\r\n | null\r\n | undefined\r\n | string\r\n | number\r\n | boolean\r\n | SerializedNode[]\r\n | { __re: 'html'; tag: string; props: Record<string, any> }\r\n | { __re: 'client'; componentId: string; props: Record<string, any> }\r\n | Record<string, any>;\r\n\r\ntype ModuleMap = Map<string, any>; // componentId \u2192 default export\r\n\r\n// \u2500\u2500\u2500 Prop reconstruction \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Recursively turns the server's serialized node tree back into real React\r\n * elements so they can be passed as props to hydrated components.\r\n *\r\n * The server serializes JSX passed as props (e.g. `<Button icon={<Icon />}>`)\r\n * into a JSON-safe format. This function reverses that process.\r\n */\r\nasync function reconstructElement(node: SerializedNode, mods: ModuleMap): Promise<any> {\r\n if (node === null || node === undefined) return node;\r\n if (typeof node !== 'object') return node; // primitive \u2014 pass through\r\n\r\n if (Array.isArray(node)) {\r\n const items = await Promise.all(node.map(n => reconstructElement(n, mods)));\r\n // Add index-based keys to React elements in the array to avoid the\r\n // \"Each child in a list should have a unique key prop\" warning.\r\n const React = await import('react');\r\n return items.map((el, i) =>\r\n el && typeof el === 'object' && el.$$typeof\r\n ? React.default.cloneElement(el, { key: el.key ?? i })\r\n : el,\r\n );\r\n }\r\n\r\n // Client component \u2014 look up the loaded module by ID.\r\n if ((node as any).__re === 'client') {\r\n const n = node as { __re: 'client'; componentId: string; props: Record<string, any> };\r\n const Comp = mods.get(n.componentId);\r\n if (!Comp) return null;\r\n const React = await import('react');\r\n return React.default.createElement(Comp, await reconstructProps(n.props, mods));\r\n }\r\n\r\n // Native HTML element (e.g. <div>, <span>).\r\n if ((node as any).__re === 'html') {\r\n const n = node as { __re: 'html'; tag: string; props: Record<string, any> };\r\n const React = await import('react');\r\n return React.default.createElement(n.tag, await reconstructProps(n.props, mods));\r\n }\r\n\r\n // Plain object \u2014 pass through as-is.\r\n return node;\r\n}\r\n\r\n/** Reconstructs every value in a props object, handling nested serialized nodes. */\r\nasync function reconstructProps(\r\n props: Record<string, any> | null | undefined,\r\n mods: ModuleMap,\r\n): Promise<Record<string, any>> {\r\n if (!props || typeof props !== 'object' || Array.isArray(props))\r\n return reconstructElement(props as any, mods);\r\n\r\n const out: Record<string, any> = {};\r\n for (const [k, v] of Object.entries(props))\r\n out[k] = await reconstructElement(v, mods);\r\n return out;\r\n}\r\n\r\n// \u2500\u2500\u2500 Module loading \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Dynamically imports each client component bundle from /__client-component/.\r\n * All fetches are issued in parallel; failures are logged but do not abort\r\n * the rest of the hydration pass.\r\n *\r\n * @param bust Optional cache-busting suffix appended as `?t=<bust>`.\r\n * Used during HMR navigation to bypass the module cache.\r\n */\r\nasync function loadModules(\r\n ids: string[],\r\n log: ReturnType<typeof makeLogger>,\r\n bust = '',\r\n): Promise<ModuleMap> {\r\n const mods: ModuleMap = new Map();\r\n await Promise.all(\r\n ids.map(async (id) => {\r\n try {\r\n const url = `/__client-component/${id}.js` + (bust ? `?t=${bust}` : '');\r\n const m = await import(url);\r\n mods.set(id, m.default);\r\n log.verbose('\u2713 Loaded:', id);\r\n } catch (err) {\r\n log.error('\u2717 Load failed:', id, err);\r\n }\r\n }),\r\n );\r\n return mods;\r\n}\r\n\r\n// \u2500\u2500\u2500 Root mounting \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/** All active React roots \u2014 tracked so they can be unmounted before navigation. */\r\ntype ReactRoot = { unmount(): void };\r\nconst activeRoots: ReactRoot[] = [];\r\n\r\n/**\r\n * Finds every `[data-hydrate-id]` span in the document and calls hydrateRoot()\r\n * on it. hydrateRoot reconciles React's virtual DOM against the existing server\r\n * HTML without discarding it, which avoids a visible flash on both initial load\r\n * and SPA navigation (where we set innerHTML to fresh SSR output before calling\r\n * mountNodes).\r\n *\r\n * Nested markers are skipped \u2014 the parent's React tree owns its children.\r\n */\r\nasync function mountNodes(\r\n mods: ModuleMap,\r\n log: ReturnType<typeof makeLogger>,\r\n): Promise<void> {\r\n const { hydrateRoot, createRoot } = await import('react-dom/client');\r\n const React = await import('react');\r\n\r\n const nodes = document.querySelectorAll<HTMLElement>('[data-hydrate-id]');\r\n log.verbose('Found', nodes.length, 'hydration point(s)');\r\n\r\n for (const node of nodes) {\r\n // Skip nested markers \u2014 the outer component owns its children.\r\n if (node.parentElement?.closest('[data-hydrate-id]')) continue;\r\n\r\n const id = node.getAttribute('data-hydrate-id')!;\r\n const Comp = mods.get(id);\r\n if (!Comp) { log.warn('No module for', id); continue; }\r\n\r\n let rawProps: Record<string, any> = {};\r\n try {\r\n rawProps = JSON.parse(node.getAttribute('data-hydrate-props') || '{}');\r\n } catch (e) {\r\n log.error('Props parse error for', id, e);\r\n }\r\n\r\n try {\r\n const element = React.default.createElement(Comp, await reconstructProps(rawProps, mods));\r\n\r\n // hydrateRoot reconciles against existing server HTML (initial page load).\r\n // createRoot renders fresh when the span is empty (HMR path \u2014 server sent\r\n // skipClientSSR=true so the span has no pre-rendered content to reconcile).\r\n let root: ReactRoot;\r\n if (node.innerHTML.trim()) {\r\n root = hydrateRoot(node, element);\r\n } else {\r\n const r = createRoot(node);\r\n r.render(element);\r\n root = r;\r\n }\r\n\r\n activeRoots.push(root);\r\n log.verbose('\u2713 Mounted:', id);\r\n } catch (err) {\r\n log.error('\u2717 Mount failed:', id, err);\r\n }\r\n }\r\n}\r\n\r\n// \u2500\u2500\u2500 Head tag sync \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Walks a <head> element and returns every Element node that lives between\r\n * the <!--n-head--> and <!--/n-head--> sentinel comments, plus the closing\r\n * comment node itself (used as the insertion anchor).\r\n *\r\n * The SSR renderer emits these sentinels around every useHtml()-generated tag\r\n * so the client can manage exactly that set without touching permanent tags\r\n * (charset, viewport, importmap, runtime <script>).\r\n */\r\nfunction headBlock(head: HTMLHeadElement): { nodes: Element[]; closeComment: Comment | null } {\r\n const nodes: Element[] = [];\r\n let closeComment: Comment | null = null;\r\n let inside = false;\r\n\r\n for (const child of Array.from(head.childNodes)) {\r\n if (child.nodeType === Node.COMMENT_NODE) {\r\n const text = (child as Comment).data.trim();\r\n if (text === 'n-head') { inside = true; continue; }\r\n if (text === '/n-head') { closeComment = child as Comment; inside = false; continue; }\r\n }\r\n if (inside && child.nodeType === Node.ELEMENT_NODE)\r\n nodes.push(child as Element);\r\n }\r\n\r\n return { nodes, closeComment };\r\n}\r\n\r\n/** Stable key for an Element: tag name + sorted attribute list (name=value pairs). */\r\nfunction fingerprint(el: Element): string {\r\n return el.tagName + '|' + Array.from(el.attributes)\r\n .sort((a, b) => a.name.localeCompare(b.name))\r\n .map(a => `${a.name}=${a.value}`)\r\n .join('&');\r\n}\r\n\r\n/**\r\n * Diffs the live <!--n-head--> block against the incoming document's block and\r\n * applies the minimal set of DOM mutations:\r\n *\r\n * - Tags present in `next` but not in `live` \u2192 inserted before <!--/n-head-->\r\n * so they remain inside the tracked block on future navigations.\r\n * - Tags present in `live` but not in `next` \u2192 removed.\r\n * - Tags present in both \u2192 left untouched (no removal/re-insertion flash).\r\n *\r\n * If the live head has no sentinel block yet (e.g. initial page had no useHtml\r\n * tags), both sentinel comments are created on the fly.\r\n */\r\nfunction syncHeadTags(doc: Document): void {\r\n const live = headBlock(document.head);\r\n const next = headBlock(doc.head);\r\n\r\n const liveMap = new Map<string, Element>();\r\n for (const el of live.nodes) liveMap.set(fingerprint(el), el);\r\n\r\n const nextMap = new Map<string, Element>();\r\n for (const el of next.nodes) nextMap.set(fingerprint(el), el);\r\n\r\n // Ensure we have an anchor to insert before.\r\n let anchor = live.closeComment;\r\n if (!anchor) {\r\n document.head.appendChild(document.createComment('n-head'));\r\n anchor = document.createComment('/n-head');\r\n document.head.appendChild(anchor);\r\n }\r\n\r\n for (const [fp, el] of nextMap)\r\n if (!liveMap.has(fp)) document.head.insertBefore(el, anchor);\r\n\r\n for (const [fp, el] of liveMap)\r\n if (!nextMap.has(fp)) el.remove();\r\n}\r\n\r\n// \u2500\u2500\u2500 SPA navigation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Syncs attributes from a parsed element onto the live document element.\r\n * Adds/updates attributes present in `next` and removes any that were set\r\n * on `live` but are absent in `next` (clears stale htmlAttrs/bodyAttrs).\r\n */\r\nfunction syncAttrs(live: Element, next: Element): void {\r\n for (const { name, value } of Array.from(next.attributes))\r\n live.setAttribute(name, value);\r\n for (const { name } of Array.from(live.attributes))\r\n if (!next.hasAttribute(name)) live.removeAttribute(name);\r\n}\r\n\r\n/**\r\n * Listens for 'locationchange' events and performs a soft navigation:\r\n *\r\n * 1. Fetch the target URL as HTML (?__hmr=1 skips client-SSR for HMR speed).\r\n * 2. Parse the response with DOMParser.\r\n * 3. Apply all visual DOM changes first (head tags, html/body attrs, #app\r\n * innerHTML, title, __n_data) so the new content is painted before React\r\n * cleanup effects run \u2014 prevents a useHtml restore from briefly undoing\r\n * the new document state.\r\n * 4. Unmount old React roots (runs cleanup effects against the already-updated DOM).\r\n * 5. Re-hydrate new client component markers.\r\n * 6. Scroll to top.\r\n *\r\n * Falls back to a full page reload if anything goes wrong.\r\n */\r\nfunction setupNavigation(log: ReturnType<typeof makeLogger>): void {\r\n window.addEventListener('locationchange', async ({ detail: { href, hmr } }: any) => {\r\n try {\r\n const fetchUrl = hmr\r\n ? href + (href.includes('?') ? '&' : '?') + '__hmr=1'\r\n : href;\r\n\r\n const response = await fetch(fetchUrl, { headers: { Accept: 'text/html' } });\r\n if (!response.ok) {\r\n log.error('Navigation fetch failed:', response.status);\r\n return;\r\n }\r\n\r\n const parser = new DOMParser();\r\n const doc = parser.parseFromString(await response.text(), 'text/html');\r\n const newApp = doc.getElementById('app');\r\n const currApp = document.getElementById('app');\r\n if (!newApp || !currApp) return;\r\n\r\n // \u2500\u2500 Visual update \u2014 all DOM mutations before React teardown \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n // Styles must be in place before new content appears to avoid an unstyled\r\n // flash. Unmounting runs useEffect cleanups (including useHtml restores)\r\n // which would temporarily revert document state if done first.\r\n\r\n // 1. Head tags \u2014 diff-based sync preserves shared layout tags untouched.\r\n syncHeadTags(doc);\r\n\r\n // 2. <html> and <body> attributes (lang, class, style, etc.).\r\n syncAttrs(document.documentElement, doc.documentElement);\r\n syncAttrs(document.body, doc.body);\r\n\r\n // 3. Page content.\r\n currApp.innerHTML = newApp.innerHTML;\r\n\r\n // 4. <title>.\r\n const newTitle = doc.querySelector('title');\r\n if (newTitle) document.title = newTitle.textContent ?? '';\r\n\r\n // 5. Runtime data blob \u2014 must come after innerHTML swap so the new\r\n // __n_data element is part of the live document.\r\n const newDataEl = doc.getElementById('__n_data');\r\n const currDataEl = document.getElementById('__n_data');\r\n if (newDataEl && currDataEl) currDataEl.textContent = newDataEl.textContent;\r\n\r\n // \u2500\u2500 React teardown \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n // Unmount after the visual update. Cleanup effects now run against an\r\n // already-updated document, so there is nothing left to visually undo.\r\n activeRoots.splice(0).forEach(r => r.unmount());\r\n\r\n // \u2500\u2500 Re-hydration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n const navData = JSON.parse(currDataEl?.textContent ?? '{}') as RuntimeData;\r\n log.info('\uD83D\uDD04 Route \u2192', href, '\u2014 mounting', navData.hydrateIds?.length ?? 0, 'component(s)');\r\n\r\n const mods = await loadModules(navData.allIds ?? [], log, String(Date.now()));\r\n await mountNodes(mods, log);\r\n\r\n window.scrollTo(0, 0);\r\n log.info('\uD83C\uDF89 Navigation complete:', href);\r\n } catch (err) {\r\n log.error('Navigation error, falling back to full reload:', err);\r\n window.location.href = href;\r\n }\r\n });\r\n}\r\n\r\n// \u2500\u2500\u2500 Public API \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/** Shape of the JSON blob embedded as #__n_data in every SSR page. */\r\nexport interface RuntimeData {\r\n /** IDs of client components actually rendered on this page (subset of allIds). */\r\n hydrateIds: string[];\r\n /** All client component IDs reachable from this page, including layouts.\r\n * Pre-loaded so SPA navigations to related pages feel instant. */\r\n allIds: string[];\r\n url: string;\r\n params: Record<string, any>;\r\n debug: ClientDebugLevel;\r\n}\r\n\r\n/**\r\n * Bootstraps the NukeJS client runtime.\r\n *\r\n * Called once per page load from the inline <script type=\"module\"> injected\r\n * by the SSR renderer:\r\n *\r\n * ```js\r\n * const { initRuntime } = await import('nukejs');\r\n * const data = JSON.parse(document.getElementById('__n_data').textContent);\r\n * await initRuntime(data);\r\n * ```\r\n *\r\n * Order of operations:\r\n * 1. Create the logger at the configured debug level.\r\n * 2. Wire up SPA navigation listener.\r\n * 3. Load all client component bundles in parallel.\r\n * 4. Hydrate every [data-hydrate-id] node.\r\n * 5. Patch history.pushState/replaceState so Link clicks trigger navigation.\r\n */\r\nexport async function initRuntime(data: RuntimeData): Promise<void> {\r\n const log = makeLogger(data.debug ?? 'silent');\r\n\r\n log.info('\uD83D\uDE80 Partial hydration:', data.hydrateIds.length, 'root component(s)');\r\n\r\n // Set up navigation first so any 'locationchange' fired during hydration\r\n // is captured (e.g. a redirect side-effect inside a component).\r\n setupNavigation(log);\r\n\r\n // Load all component bundles (not just hydrateIds) so SPA navigations to\r\n // related pages can mount their components without an extra network round-trip.\r\n const mods = await loadModules(data.allIds, log);\r\n await mountNodes(mods, log);\r\n\r\n log.info('\uD83C\uDF89 Done!');\r\n\r\n // Patch history last so pushState calls during hydration don't trigger a\r\n // navigation before roots are ready.\r\n setupLocationChangeMonitor();\r\n}"],
5
- "mappings": "AA+CO,SAAS,6BAAmC;AACjD,QAAM,oBAAuB,OAAO,QAAQ,UAAU,KAAK,OAAO,OAAO;AACzE,QAAM,uBAAuB,OAAO,QAAQ,aAAa,KAAK,OAAO,OAAO;AAE5E,QAAM,WAAW,CAAC,SAChB,OAAO,cAAc,IAAI,YAAY,kBAAkB,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;AAE9E,SAAO,QAAQ,YAAY,YAAa,MAAM;AAC5C,sBAAkB,GAAG,IAAI;AACzB,aAAS,KAAK,CAAC,CAAC;AAAA,EAClB;AAEA,SAAO,QAAQ,eAAe,YAAa,MAAM;AAC/C,yBAAqB,GAAG,IAAI;AAC5B,aAAS,KAAK,CAAC,CAAC;AAAA,EAClB;AAGA,SAAO,iBAAiB,YAAY,MAAM,SAAS,OAAO,SAAS,QAAQ,CAAC;AAC9E;AAWA,SAAS,WAAW,OAAyB;AAC3C,SAAO;AAAA,IACL,SAAS,IAAI,MAAa;AAAE,UAAI,UAAU,UAAW,SAAQ,IAAI,GAAG,CAAC;AAAA,IAAG;AAAA,IACxE,MAAS,IAAI,MAAa;AAAE,UAAI,UAAU,aAAa,UAAU,OAAQ,SAAQ,IAAI,GAAG,CAAC;AAAA,IAAG;AAAA,IAC5F,MAAS,IAAI,MAAa;AAAE,UAAI,UAAU,aAAa,UAAU,OAAQ,SAAQ,KAAK,GAAG,CAAC;AAAA,IAAG;AAAA,IAC7F,OAAS,IAAI,MAAa;AAAE,UAAI,UAAU,SAAU,SAAQ,MAAM,GAAG,CAAC;AAAA,IAAG;AAAA,EAC3E;AACF;AA2BA,eAAe,mBAAmB,MAAsB,MAA+B;AACrF,MAAI,SAAS,QAAQ,SAAS,OAAW,QAAO;AAChD,MAAI,OAAO,SAAS,SAAU,QAAO;AAErC,MAAI,MAAM,QAAQ,IAAI,GAAG;AACvB,UAAM,QAAQ,MAAM,QAAQ,IAAI,KAAK,IAAI,OAAK,mBAAmB,GAAG,IAAI,CAAC,CAAC;AAG1E,UAAM,QAAQ,MAAM,OAAO,OAAO;AAClC,WAAO,MAAM;AAAA,MAAI,CAAC,IAAI,MACpB,MAAM,OAAO,OAAO,YAAY,GAAG,WAC/B,MAAM,QAAQ,aAAa,IAAI,EAAE,KAAK,GAAG,OAAO,EAAE,CAAC,IACnD;AAAA,IACN;AAAA,EACF;AAGA,MAAK,KAAa,SAAS,UAAU;AACnC,UAAM,IAAI;AACV,UAAM,OAAO,KAAK,IAAI,EAAE,WAAW;AACnC,QAAI,CAAC,KAAM,QAAO;AAClB,UAAM,QAAQ,MAAM,OAAO,OAAO;AAClC,WAAO,MAAM,QAAQ,cAAc,MAAM,MAAM,iBAAiB,EAAE,OAAO,IAAI,CAAC;AAAA,EAChF;AAGA,MAAK,KAAa,SAAS,QAAQ;AACjC,UAAM,IAAI;AACV,UAAM,QAAQ,MAAM,OAAO,OAAO;AAClC,WAAO,MAAM,QAAQ,cAAc,EAAE,KAAK,MAAM,iBAAiB,EAAE,OAAO,IAAI,CAAC;AAAA,EACjF;AAGA,SAAO;AACT;AAGA,eAAe,iBACb,OACA,MAC8B;AAC9B,MAAI,CAAC,SAAS,OAAO,UAAU,YAAY,MAAM,QAAQ,KAAK;AAC5D,WAAO,mBAAmB,OAAc,IAAI;AAE9C,QAAM,MAA2B,CAAC;AAClC,aAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,KAAK;AACvC,QAAI,CAAC,IAAI,MAAM,mBAAmB,GAAG,IAAI;AAC3C,SAAO;AACT;AAYA,eAAe,YACb,KACA,KACA,OAAO,IACa;AACpB,QAAM,OAAkB,oBAAI,IAAI;AAChC,QAAM,QAAQ;AAAA,IACZ,IAAI,IAAI,OAAO,OAAO;AACpB,UAAI;AACF,cAAM,MAAM,uBAAuB,EAAE,SAAS,OAAO,MAAM,IAAI,KAAK;AACpE,cAAM,IAAI,MAAM,OAAO;AACvB,aAAK,IAAI,IAAI,EAAE,OAAO;AACtB,YAAI,QAAQ,kBAAa,EAAE;AAAA,MAC7B,SAAS,KAAK;AACZ,YAAI,MAAM,uBAAkB,IAAI,GAAG;AAAA,MACrC;AAAA,IACF,CAAC;AAAA,EACH;AACA,SAAO;AACT;AAMA,MAAM,cAA2B,CAAC;AAWlC,eAAe,WACb,MACA,KACe;AACf,QAAM,EAAE,aAAa,WAAW,IAAI,MAAM,OAAO,kBAAkB;AACnE,QAAM,QAAQ,MAAM,OAAO,OAAO;AAElC,QAAM,QAAQ,SAAS,iBAA8B,mBAAmB;AACxE,MAAI,QAAQ,SAAS,MAAM,QAAQ,oBAAoB;AAEvD,aAAW,QAAQ,OAAO;AAExB,QAAI,KAAK,eAAe,QAAQ,mBAAmB,EAAG;AAEtD,UAAM,KAAO,KAAK,aAAa,iBAAiB;AAChD,UAAM,OAAO,KAAK,IAAI,EAAE;AACxB,QAAI,CAAC,MAAM;AAAE,UAAI,KAAK,iBAAiB,EAAE;AAAG;AAAA,IAAU;AAEtD,QAAI,WAAgC,CAAC;AACrC,QAAI;AACF,iBAAW,KAAK,MAAM,KAAK,aAAa,oBAAoB,KAAK,IAAI;AAAA,IACvE,SAAS,GAAG;AACV,UAAI,MAAM,yBAAyB,IAAI,CAAC;AAAA,IAC1C;AAEA,QAAI;AACF,YAAM,UAAU,MAAM,QAAQ,cAAc,MAAM,MAAM,iBAAiB,UAAU,IAAI,CAAC;AAKxF,UAAI;AACJ,UAAI,KAAK,UAAU,KAAK,GAAG;AACzB,eAAO,YAAY,MAAM,OAAO;AAAA,MAClC,OAAO;AACL,cAAM,IAAI,WAAW,IAAI;AACzB,UAAE,OAAO,OAAO;AAChB,eAAO;AAAA,MACT;AAEA,kBAAY,KAAK,IAAI;AACrB,UAAI,QAAQ,mBAAc,EAAE;AAAA,IAC9B,SAAS,KAAK;AACZ,UAAI,MAAM,wBAAmB,IAAI,GAAG;AAAA,IACtC;AAAA,EACF;AACF;AAaA,SAAS,UAAU,MAA2E;AAC5F,QAAM,QAAmB,CAAC;AAC1B,MAAI,eAA+B;AACnC,MAAI,SAAS;AAEb,aAAW,SAAS,MAAM,KAAK,KAAK,UAAU,GAAG;AAC/C,QAAI,MAAM,aAAa,KAAK,cAAc;AACxC,YAAM,OAAQ,MAAkB,KAAK,KAAK;AAC1C,UAAI,SAAS,UAAW;AAAE,iBAAS;AAAO;AAAA,MAAU;AACpD,UAAI,SAAS,WAAW;AAAE,uBAAe;AAAkB,iBAAS;AAAO;AAAA,MAAU;AAAA,IACvF;AACA,QAAI,UAAU,MAAM,aAAa,KAAK;AACpC,YAAM,KAAK,KAAgB;AAAA,EAC/B;AAEA,SAAO,EAAE,OAAO,aAAa;AAC/B;AAGA,SAAS,YAAY,IAAqB;AACxC,SAAO,GAAG,UAAU,MAAM,MAAM,KAAK,GAAG,UAAU,EAC/C,KAAK,CAAC,GAAG,MAAM,EAAE,KAAK,cAAc,EAAE,IAAI,CAAC,EAC3C,IAAI,OAAK,GAAG,EAAE,IAAI,IAAI,EAAE,KAAK,EAAE,EAC/B,KAAK,GAAG;AACb;AAcA,SAAS,aAAa,KAAqB;AACzC,QAAM,OAAO,UAAU,SAAS,IAAI;AACpC,QAAM,OAAO,UAAU,IAAI,IAAI;AAE/B,QAAM,UAAU,oBAAI,IAAqB;AACzC,aAAW,MAAM,KAAK,MAAO,SAAQ,IAAI,YAAY,EAAE,GAAG,EAAE;AAE5D,QAAM,UAAU,oBAAI,IAAqB;AACzC,aAAW,MAAM,KAAK,MAAO,SAAQ,IAAI,YAAY,EAAE,GAAG,EAAE;AAG5D,MAAI,SAAS,KAAK;AAClB,MAAI,CAAC,QAAQ;AACX,aAAS,KAAK,YAAY,SAAS,cAAc,QAAQ,CAAC;AAC1D,aAAS,SAAS,cAAc,SAAS;AACzC,aAAS,KAAK,YAAY,MAAM;AAAA,EAClC;AAEA,aAAW,CAAC,IAAI,EAAE,KAAK;AACrB,QAAI,CAAC,QAAQ,IAAI,EAAE,EAAG,UAAS,KAAK,aAAa,IAAI,MAAM;AAE7D,aAAW,CAAC,IAAI,EAAE,KAAK;AACrB,QAAI,CAAC,QAAQ,IAAI,EAAE,EAAG,IAAG,OAAO;AACpC;AASA,SAAS,UAAU,MAAe,MAAqB;AACrD,aAAW,EAAE,MAAM,MAAM,KAAK,MAAM,KAAK,KAAK,UAAU;AACtD,SAAK,aAAa,MAAM,KAAK;AAC/B,aAAW,EAAE,KAAK,KAAK,MAAM,KAAK,KAAK,UAAU;AAC/C,QAAI,CAAC,KAAK,aAAa,IAAI,EAAG,MAAK,gBAAgB,IAAI;AAC3D;AAiBA,SAAS,gBAAgB,KAA0C;AACjE,SAAO,iBAAiB,kBAAkB,OAAO,EAAE,QAAQ,EAAE,MAAM,IAAI,EAAE,MAAW;AAClF,QAAI;AACF,YAAM,WAAW,MACb,QAAQ,KAAK,SAAS,GAAG,IAAI,MAAM,OAAO,YAC1C;AAEJ,YAAM,WAAW,MAAM,MAAM,UAAU,EAAE,SAAS,EAAE,QAAQ,YAAY,EAAE,CAAC;AAC3E,UAAI,CAAC,SAAS,IAAI;AAChB,YAAI,MAAM,4BAA4B,SAAS,MAAM;AACrD;AAAA,MACF;AAEA,YAAM,SAAU,IAAI,UAAU;AAC9B,YAAM,MAAU,OAAO,gBAAgB,MAAM,SAAS,KAAK,GAAG,WAAW;AACzE,YAAM,SAAU,IAAI,eAAe,KAAK;AACxC,YAAM,UAAU,SAAS,eAAe,KAAK;AAC7C,UAAI,CAAC,UAAU,CAAC,QAAS;AAQzB,mBAAa,GAAG;AAGhB,gBAAU,SAAS,iBAAiB,IAAI,eAAe;AACvD,gBAAU,SAAS,MAAM,IAAI,IAAI;AAGjC,cAAQ,YAAY,OAAO;AAG3B,YAAM,WAAW,IAAI,cAAc,OAAO;AAC1C,UAAI,SAAU,UAAS,QAAQ,SAAS,eAAe;AAIvD,YAAM,YAAa,IAAI,eAAe,UAAU;AAChD,YAAM,aAAa,SAAS,eAAe,UAAU;AACrD,UAAI,aAAa,WAAY,YAAW,cAAc,UAAU;AAKhE,kBAAY,OAAO,CAAC,EAAE,QAAQ,OAAK,EAAE,QAAQ,CAAC;AAG9C,YAAM,UAAU,KAAK,MAAM,YAAY,eAAe,IAAI;AAC1D,UAAI,KAAK,0BAAc,MAAM,mBAAc,QAAQ,YAAY,UAAU,GAAG,cAAc;AAE1F,YAAM,OAAO,MAAM,YAAY,QAAQ,UAAU,CAAC,GAAG,KAAK,OAAO,KAAK,IAAI,CAAC,CAAC;AAC5E,YAAM,WAAW,MAAM,GAAG;AAE1B,aAAO,SAAS,GAAG,CAAC;AACpB,UAAI,KAAK,kCAA2B,IAAI;AAAA,IAC1C,SAAS,KAAK;AACZ,UAAI,MAAM,kDAAkD,GAAG;AAC/D,aAAO,SAAS,OAAO;AAAA,IACzB;AAAA,EACF,CAAC;AACH;AAmCA,eAAsB,YAAY,MAAkC;AAClE,QAAM,MAAM,WAAW,KAAK,SAAS,QAAQ;AAE7C,MAAI,KAAK,gCAAyB,KAAK,WAAW,QAAQ,mBAAmB;AAI7E,kBAAgB,GAAG;AAInB,QAAM,OAAO,MAAM,YAAY,KAAK,QAAQ,GAAG;AAC/C,QAAM,WAAW,MAAM,GAAG;AAE1B,MAAI,KAAK,iBAAU;AAInB,6BAA2B;AAC7B;",
4
+ "sourcesContent": ["/**\r\n * bundle.ts \u2014 NukeJS Client Runtime\r\n *\r\n * This file is compiled by esbuild into /__n.js and served to every page.\r\n * It provides:\r\n *\r\n * initRuntime(data) \u2014 called once per page load to hydrate\r\n * \"use client\" components and wire up SPA nav\r\n * setupLocationChangeMonitor() \u2014 patches history.pushState/replaceState so\r\n * SPA navigation fires a 'locationchange' event\r\n *\r\n * Hydration model (partial hydration):\r\n * - The server renders the full page to HTML, wrapping each client component\r\n * in a <span data-hydrate-id=\"cc_\u2026\" data-hydrate-props=\"\u2026\"> marker.\r\n * - initRuntime loads the matching JS bundle for each marker and calls\r\n * hydrateRoot() on it, letting React take over just that subtree.\r\n * - Props serialized by the server may include nested React elements\r\n * (serialized as { __re: 'html'|'client', \u2026 }), which are reconstructed\r\n * back into React.createElement calls before mounting.\r\n *\r\n * SPA navigation:\r\n * - Link clicks / programmatic navigation dispatch a 'locationchange' event.\r\n * - The handler fetches the target URL as HTML, diffs the #app container,\r\n * unmounts the old React roots, and re-hydrates the new ones.\r\n * - HMR navigations add ?__hmr=1 so the server skips client-SSR (faster).\r\n *\r\n * Head tag management:\r\n * - The SSR renderer wraps every useHtml()-generated <meta>, <link>, <style>,\r\n * and <script> tag in <!--n-head-->\u2026<!--/n-head--> sentinel comments.\r\n * - On each navigation the client diffs the live sentinel block against the\r\n * incoming one by fingerprint, adding new tags and removing gone ones.\r\n * Tags shared between pages (e.g. a layout stylesheet) are left untouched\r\n * so there is no removal/re-insertion flash.\r\n * - New tags are always inserted before <!--/n-head--> so they stay inside\r\n * the tracked block and remain visible to the diff on subsequent navigations.\r\n */\r\n\r\n// \u2500\u2500\u2500 History patch \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Patches history.pushState and history.replaceState to fire a custom\r\n * 'locationchange' event on window. Also listens to 'popstate' for\r\n * back/forward navigation.\r\n *\r\n * Called after initRuntime sets up the navigation listener so there is no\r\n * race between the event firing and the listener being registered.\r\n */\r\nexport function setupLocationChangeMonitor(): void {\r\n const originalPushState = window.history.pushState.bind(window.history);\r\n const originalReplaceState = window.history.replaceState.bind(window.history);\r\n\r\n const dispatch = (href?: any) =>\r\n window.dispatchEvent(new CustomEvent('locationchange', { detail: { href } }));\r\n\r\n window.history.pushState = function (...args) {\r\n originalPushState(...args);\r\n dispatch(args[2]); // args[2] is the URL\r\n };\r\n\r\n window.history.replaceState = function (...args) {\r\n originalReplaceState(...args);\r\n dispatch(args[2]);\r\n };\r\n\r\n // Back/forward navigation via the browser's native UI.\r\n window.addEventListener('popstate', () => dispatch(window.location.pathname));\r\n}\r\n\r\n// \u2500\u2500\u2500 Logger \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\ntype ClientDebugLevel = 'silent' | 'error' | 'info' | 'verbose';\r\n\r\n/**\r\n * Returns a thin logger whose methods are no-ops unless `level` allows them.\r\n * The server embeds the active debug level in the __n_data JSON blob so the\r\n * client respects the same setting as the server.\r\n */\r\nfunction makeLogger(level: ClientDebugLevel) {\r\n return {\r\n verbose: (...a: any[]) => { if (level === 'verbose') console.log(...a); },\r\n info: (...a: any[]) => { if (level === 'verbose' || level === 'info') console.log(...a); },\r\n warn: (...a: any[]) => { if (level === 'verbose' || level === 'info') console.warn(...a); },\r\n error: (...a: any[]) => { if (level !== 'silent') console.error(...a); },\r\n };\r\n}\r\n\r\n// \u2500\u2500\u2500 Serialized node types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/** The wire format for React elements embedded in hydration props. */\r\ntype SerializedNode =\r\n | null\r\n | undefined\r\n | string\r\n | number\r\n | boolean\r\n | SerializedNode[]\r\n | { __re: 'html'; tag: string; props: Record<string, any> }\r\n | { __re: 'client'; componentId: string; props: Record<string, any> }\r\n | Record<string, any>;\r\n\r\ntype ModuleMap = Map<string, any>; // componentId \u2192 default export\r\n\r\n// \u2500\u2500\u2500 Prop reconstruction \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Recursively turns the server's serialized node tree back into real React\r\n * elements so they can be passed as props to hydrated components.\r\n *\r\n * The server serializes JSX passed as props (e.g. `<Button icon={<Icon />}>`)\r\n * into a JSON-safe format. This function reverses that process.\r\n */\r\nasync function reconstructElement(node: SerializedNode, mods: ModuleMap): Promise<any> {\r\n if (node === null || node === undefined) return node;\r\n if (typeof node !== 'object') return node; // primitive \u2014 pass through\r\n\r\n if (Array.isArray(node)) {\r\n const items = await Promise.all(node.map(n => reconstructElement(n, mods)));\r\n // Add index-based keys to React elements in the array to avoid the\r\n // \"Each child in a list should have a unique key prop\" warning.\r\n const React = await import('react');\r\n return items.map((el, i) =>\r\n el && typeof el === 'object' && el.$$typeof\r\n ? React.default.cloneElement(el, { key: el.key ?? i })\r\n : el,\r\n );\r\n }\r\n\r\n // Client component \u2014 look up the loaded module by ID.\r\n if ((node as any).__re === 'client') {\r\n const n = node as { __re: 'client'; componentId: string; props: Record<string, any> };\r\n const Comp = mods.get(n.componentId);\r\n if (!Comp) return null;\r\n const React = await import('react');\r\n return React.default.createElement(Comp, await reconstructProps(n.props, mods));\r\n }\r\n\r\n // Native HTML element (e.g. <div>, <span>).\r\n if ((node as any).__re === 'html') {\r\n const n = node as { __re: 'html'; tag: string; props: Record<string, any> };\r\n const React = await import('react');\r\n return React.default.createElement(n.tag, await reconstructProps(n.props, mods));\r\n }\r\n\r\n // Plain object \u2014 pass through as-is.\r\n return node;\r\n}\r\n\r\n/** Reconstructs every value in a props object, handling nested serialized nodes. */\r\nasync function reconstructProps(\r\n props: Record<string, any> | null | undefined,\r\n mods: ModuleMap,\r\n): Promise<Record<string, any>> {\r\n if (!props || typeof props !== 'object' || Array.isArray(props))\r\n return reconstructElement(props as any, mods);\r\n\r\n const out: Record<string, any> = {};\r\n for (const [k, v] of Object.entries(props))\r\n out[k] = await reconstructElement(v, mods);\r\n return out;\r\n}\r\n\r\n// \u2500\u2500\u2500 Module loading \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Dynamically imports each client component bundle from /__client-component/.\r\n * All fetches are issued in parallel; failures are logged but do not abort\r\n * the rest of the hydration pass.\r\n *\r\n * @param bust Optional cache-busting suffix appended as `?t=<bust>`.\r\n * Used during HMR navigation to bypass the module cache.\r\n */\r\nasync function loadModules(\r\n ids: string[],\r\n log: ReturnType<typeof makeLogger>,\r\n bust = '',\r\n): Promise<ModuleMap> {\r\n const mods: ModuleMap = new Map();\r\n await Promise.all(\r\n ids.map(async (id) => {\r\n try {\r\n const url = `/__client-component/${id}.js` + (bust ? `?t=${bust}` : '');\r\n const m = await import(url);\r\n mods.set(id, m.default);\r\n log.verbose('\u2713 Loaded:', id);\r\n } catch (err) {\r\n log.error('\u2717 Load failed:', id, err);\r\n }\r\n }),\r\n );\r\n return mods;\r\n}\r\n\r\n// \u2500\u2500\u2500 Root mounting \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/** All active React roots \u2014 tracked so they can be unmounted before navigation. */\r\ntype ReactRoot = { unmount(): void };\r\nconst activeRoots: ReactRoot[] = [];\r\n\r\n/**\r\n * Finds every `[data-hydrate-id]` span in the document and calls hydrateRoot()\r\n * on it. hydrateRoot reconciles React's virtual DOM against the existing server\r\n * HTML without discarding it, which avoids a visible flash on both initial load\r\n * and SPA navigation (where we set innerHTML to fresh SSR output before calling\r\n * mountNodes).\r\n *\r\n * Nested markers are skipped \u2014 the parent's React tree owns its children.\r\n */\r\nasync function mountNodes(\r\n mods: ModuleMap,\r\n log: ReturnType<typeof makeLogger>,\r\n): Promise<void> {\r\n const { hydrateRoot, createRoot } = await import('react-dom/client');\r\n const React = await import('react');\r\n\r\n const nodes = document.querySelectorAll<HTMLElement>('[data-hydrate-id]');\r\n log.verbose('Found', nodes.length, 'hydration point(s)');\r\n\r\n for (const node of nodes) {\r\n // Skip nested markers \u2014 the outer component owns its children.\r\n if (node.parentElement?.closest('[data-hydrate-id]')) continue;\r\n\r\n const id = node.getAttribute('data-hydrate-id')!;\r\n const Comp = mods.get(id);\r\n if (!Comp) { log.warn('No module for', id); continue; }\r\n\r\n let rawProps: Record<string, any> = {};\r\n try {\r\n rawProps = JSON.parse(node.getAttribute('data-hydrate-props') || '{}');\r\n } catch (e) {\r\n log.error('Props parse error for', id, e);\r\n }\r\n\r\n try {\r\n const element = React.default.createElement(Comp, await reconstructProps(rawProps, mods));\r\n\r\n // hydrateRoot reconciles against existing server HTML (initial page load).\r\n // createRoot renders fresh when the span is empty (HMR path \u2014 server sent\r\n // skipClientSSR=true so the span has no pre-rendered content to reconcile).\r\n let root: ReactRoot;\r\n if (node.innerHTML.trim()) {\r\n root = hydrateRoot(node, element);\r\n } else {\r\n const r = createRoot(node);\r\n r.render(element);\r\n root = r;\r\n }\r\n\r\n activeRoots.push(root);\r\n log.verbose('\u2713 Mounted:', id);\r\n } catch (err) {\r\n log.error('\u2717 Mount failed:', id, err);\r\n }\r\n }\r\n}\r\n\r\n// \u2500\u2500\u2500 Head tag sync \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Walks a <head> element and returns every Element node that lives between\r\n * the <!--n-head--> and <!--/n-head--> sentinel comments, plus the closing\r\n * comment node itself (used as the insertion anchor).\r\n *\r\n * The SSR renderer emits these sentinels around every useHtml()-generated tag\r\n * so the client can manage exactly that set without touching permanent tags\r\n * (charset, viewport, importmap, runtime <script>).\r\n */\r\nfunction headBlock(head: HTMLHeadElement): { nodes: Element[]; closeComment: Comment | null } {\r\n const nodes: Element[] = [];\r\n let closeComment: Comment | null = null;\r\n let inside = false;\r\n\r\n for (const child of Array.from(head.childNodes)) {\r\n if (child.nodeType === Node.COMMENT_NODE) {\r\n const text = (child as Comment).data.trim();\r\n if (text === 'n-head') { inside = true; continue; }\r\n if (text === '/n-head') { closeComment = child as Comment; inside = false; continue; }\r\n }\r\n if (inside && child.nodeType === Node.ELEMENT_NODE)\r\n nodes.push(child as Element);\r\n }\r\n\r\n return { nodes, closeComment };\r\n}\r\n\r\n/** Stable key for an Element: tag name + sorted attribute list (name=value pairs). */\r\nfunction fingerprint(el: Element): string {\r\n return el.tagName + '|' + Array.from(el.attributes)\r\n .sort((a, b) => a.name.localeCompare(b.name))\r\n .map(a => `${a.name}=${a.value}`)\r\n .join('&');\r\n}\r\n\r\n/**\r\n * Diffs the live <!--n-head--> block against the incoming document's block and\r\n * applies the minimal set of DOM mutations:\r\n *\r\n * - Non-script tags (meta, link, style): fingerprint-diffed so shared layout\r\n * tags are left untouched (avoids stylesheet flash on navigation).\r\n * - Script tags: always removed and re-inserted as fresh elements so the\r\n * browser re-executes them and re-fetches any changed src file.\r\n * (Fingerprint diffing silently skips re-execution when src is unchanged.)\r\n *\r\n * If the live head has no sentinel block yet (e.g. initial page had no useHtml\r\n * tags), both sentinel comments are created on the fly.\r\n */\r\nfunction syncHeadTags(doc: Document): void {\r\n const live = headBlock(document.head);\r\n const next = headBlock(doc.head);\r\n\r\n // Ensure we have an anchor to insert before.\r\n let anchor = live.closeComment;\r\n if (!anchor) {\r\n document.head.appendChild(document.createComment('n-head'));\r\n anchor = document.createComment('/n-head');\r\n document.head.appendChild(anchor);\r\n }\r\n\r\n // \u2500\u2500 Scripts: always replace \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n // Remove all live script tags and re-insert fresh ones so the browser\r\n // executes them. src gets cache-busted so the latest file is fetched.\r\n for (const el of live.nodes)\r\n if (el.tagName === 'SCRIPT') el.remove();\r\n\r\n for (const el of next.nodes) {\r\n if (el.tagName === 'SCRIPT')\r\n document.head.insertBefore(cloneScriptForExecution(el), anchor);\r\n }\r\n\r\n // \u2500\u2500 Everything else: fingerprint diff \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n const liveMap = new Map<string, Element>();\r\n for (const el of live.nodes) if (el.tagName !== 'SCRIPT') liveMap.set(fingerprint(el), el);\r\n\r\n const nextMap = new Map<string, Element>();\r\n for (const el of next.nodes) if (el.tagName !== 'SCRIPT') nextMap.set(fingerprint(el), el);\r\n\r\n for (const [fp, el] of nextMap)\r\n if (!liveMap.has(fp)) document.head.insertBefore(el, anchor);\r\n\r\n for (const [fp, el] of liveMap)\r\n if (!nextMap.has(fp)) el.remove();\r\n}\r\n\r\n/**\r\n * Walks a <body> element and returns every Element node that lives between\r\n * the <!--n-body-scripts--> and <!--/n-body-scripts--> sentinel comments,\r\n * plus the closing comment node used as the insertion anchor.\r\n *\r\n * The SSR renderer emits these sentinels around every useHtml() body script\r\n * so the client can manage exactly that set without touching permanent nodes.\r\n */\r\nfunction bodyScriptsBlock(body: HTMLBodyElement | Element): { nodes: Element[]; closeComment: Comment | null } {\r\n const nodes: Element[] = [];\r\n let closeComment: Comment | null = null;\r\n let inside = false;\r\n\r\n for (const child of Array.from(body.childNodes)) {\r\n if (child.nodeType === Node.COMMENT_NODE) {\r\n const text = (child as Comment).data.trim();\r\n if (text === 'n-body-scripts') { inside = true; continue; }\r\n if (text === '/n-body-scripts') { closeComment = child as Comment; inside = false; continue; }\r\n }\r\n if (inside && child.nodeType === Node.ELEMENT_NODE)\r\n nodes.push(child as Element);\r\n }\r\n\r\n return { nodes, closeComment };\r\n}\r\n\r\n/**\r\n * Creates a fresh <script> element from a parsed source element so the browser\r\n * actually executes it when inserted into the live document.\r\n *\r\n * Why: browsers only execute a <script> that is *created and inserted* into\r\n * the live document. Nodes moved from a DOMParser document are auto-adopted\r\n * but their script is silently skipped. Cloning via createElement is required.\r\n *\r\n * Cache-busting: src-based scripts get a ?t=<timestamp> query appended so the\r\n * browser always fetches the latest version from the server on HMR updates,\r\n * bypassing the module/response cache.\r\n */\r\nfunction cloneScriptForExecution(src: Element): HTMLScriptElement {\r\n const el = document.createElement('script');\r\n for (const { name, value } of Array.from(src.attributes)) {\r\n if (name === 'src') {\r\n // Append a timestamp to force the browser to re-fetch the script file.\r\n const url = new URL(value, location.href);\r\n url.searchParams.set('t', String(Date.now()));\r\n el.setAttribute('src', url.toString());\r\n } else {\r\n el.setAttribute(name, value);\r\n }\r\n }\r\n // Copy inline content (for content-based scripts).\r\n if (src.textContent) el.textContent = src.textContent;\r\n return el;\r\n}\r\n\r\n/**\r\n * Replaces all body scripts in the <!--n-body-scripts--> sentinel block with\r\n * fresh elements from the incoming document.\r\n *\r\n * Unlike syncHeadTags (which diffs by fingerprint to avoid removing shared\r\n * stylesheets), body scripts must ALWAYS be removed and re-inserted so that:\r\n * - File changes picked up by HMR are actually executed by the browser.\r\n * - src-based scripts are cache-busted so the browser re-fetches them.\r\n *\r\n * Fingerprint diffing would silently skip re-execution of any script whose\r\n * src/attributes haven't changed, even if the file contents changed on disk.\r\n */\r\nfunction syncBodyScripts(doc: Document): void {\r\n const live = bodyScriptsBlock(document.body);\r\n const next = bodyScriptsBlock(doc.body);\r\n\r\n // Always remove every existing body script \u2014 never leave stale ones.\r\n for (const el of live.nodes) el.remove();\r\n\r\n // Ensure we have a sentinel anchor to insert before.\r\n let anchor = live.closeComment;\r\n if (!anchor) {\r\n document.body.appendChild(document.createComment('n-body-scripts'));\r\n anchor = document.createComment('/n-body-scripts');\r\n document.body.appendChild(anchor);\r\n }\r\n\r\n // Insert every script from the incoming document as a brand-new element\r\n // so the browser executes it. src gets a timestamp to bust any cache.\r\n for (const el of next.nodes)\r\n document.body.insertBefore(cloneScriptForExecution(el), anchor);\r\n}\r\n\r\n\r\n\r\n/**\r\n * Syncs attributes from a parsed element onto the live document element.\r\n * Adds/updates attributes present in `next` and removes any that were set\r\n * on `live` but are absent in `next` (clears stale htmlAttrs/bodyAttrs).\r\n */\r\nfunction syncAttrs(live: Element, next: Element): void {\r\n for (const { name, value } of Array.from(next.attributes))\r\n live.setAttribute(name, value);\r\n for (const { name } of Array.from(live.attributes))\r\n if (!next.hasAttribute(name)) live.removeAttribute(name);\r\n}\r\n\r\n/**\r\n * Listens for 'locationchange' events and performs a soft navigation:\r\n *\r\n * 1. Fetch the target URL as HTML (?__hmr=1 skips client-SSR for HMR speed).\r\n * 2. Parse the response with DOMParser.\r\n * 3. Apply all visual DOM changes first (head tags, html/body attrs, #app\r\n * innerHTML, title, __n_data) so the new content is painted before React\r\n * cleanup effects run \u2014 prevents a useHtml restore from briefly undoing\r\n * the new document state.\r\n * 4. Unmount old React roots (runs cleanup effects against the already-updated DOM).\r\n * 5. Re-hydrate new client component markers.\r\n * 6. Scroll to top.\r\n *\r\n * Falls back to a full page reload if anything goes wrong.\r\n */\r\nfunction setupNavigation(log: ReturnType<typeof makeLogger>): void {\r\n window.addEventListener('locationchange', async ({ detail: { href, hmr } }: any) => {\r\n try {\r\n const fetchUrl = hmr\r\n ? href + (href.includes('?') ? '&' : '?') + '__hmr=1'\r\n : href;\r\n\r\n const response = await fetch(fetchUrl, { headers: { Accept: 'text/html' } });\r\n if (!response.ok) {\r\n log.error('Navigation fetch failed:', response.status);\r\n return;\r\n }\r\n\r\n const parser = new DOMParser();\r\n const doc = parser.parseFromString(await response.text(), 'text/html');\r\n const newApp = doc.getElementById('app');\r\n const currApp = document.getElementById('app');\r\n if (!newApp || !currApp) return;\r\n\r\n // \u2500\u2500 Visual update \u2014 all DOM mutations before React teardown \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n // Styles must be in place before new content appears to avoid an unstyled\r\n // flash. Unmounting runs useEffect cleanups (including useHtml restores)\r\n // which would temporarily revert document state if done first.\r\n\r\n // 1. Head tags \u2014 diff-based sync preserves shared layout tags untouched.\r\n syncHeadTags(doc);\r\n\r\n // 2. Body scripts (position='body') \u2014 diff-based sync mirrors head tag logic.\r\n syncBodyScripts(doc);\r\n\r\n // 3. <html> and <body> attributes (lang, class, style, etc.).\r\n syncAttrs(document.documentElement, doc.documentElement);\r\n syncAttrs(document.body, doc.body);\r\n\r\n // 4. Page content.\r\n currApp.innerHTML = newApp.innerHTML;\r\n\r\n // 5. <title>.\r\n const newTitle = doc.querySelector('title');\r\n if (newTitle) document.title = newTitle.textContent ?? '';\r\n\r\n // 6. Runtime data blob \u2014 must come after innerHTML swap so the new\r\n // __n_data element is part of the live document.\r\n const newDataEl = doc.getElementById('__n_data');\r\n const currDataEl = document.getElementById('__n_data');\r\n if (newDataEl && currDataEl) currDataEl.textContent = newDataEl.textContent;\r\n\r\n // \u2500\u2500 React teardown \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n // Unmount after the visual update. Cleanup effects now run against an\r\n // already-updated document, so there is nothing left to visually undo.\r\n activeRoots.splice(0).forEach(r => r.unmount());\r\n\r\n // \u2500\u2500 Re-hydration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n const navData = JSON.parse(currDataEl?.textContent ?? '{}') as RuntimeData;\r\n log.info('\uD83D\uDD04 Route \u2192', href, '\u2014 mounting', navData.hydrateIds?.length ?? 0, 'component(s)');\r\n\r\n const mods = await loadModules(navData.allIds ?? [], log, String(Date.now()));\r\n await mountNodes(mods, log);\r\n\r\n window.scrollTo(0, 0);\r\n log.info('\uD83C\uDF89 Navigation complete:', href);\r\n } catch (err) {\r\n log.error('Navigation error, falling back to full reload:', err);\r\n window.location.href = href;\r\n }\r\n });\r\n}\r\n\r\n// \u2500\u2500\u2500 Public API \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/** Shape of the JSON blob embedded as #__n_data in every SSR page. */\r\nexport interface RuntimeData {\r\n /** IDs of client components actually rendered on this page (subset of allIds). */\r\n hydrateIds: string[];\r\n /** All client component IDs reachable from this page, including layouts.\r\n * Pre-loaded so SPA navigations to related pages feel instant. */\r\n allIds: string[];\r\n url: string;\r\n params: Record<string, any>;\r\n debug: ClientDebugLevel;\r\n}\r\n\r\n/**\r\n * Bootstraps the NukeJS client runtime.\r\n *\r\n * Called once per page load from the inline <script type=\"module\"> injected\r\n * by the SSR renderer:\r\n *\r\n * ```js\r\n * const { initRuntime } = await import('nukejs');\r\n * const data = JSON.parse(document.getElementById('__n_data').textContent);\r\n * await initRuntime(data);\r\n * ```\r\n *\r\n * Order of operations:\r\n * 1. Create the logger at the configured debug level.\r\n * 2. Wire up SPA navigation listener.\r\n * 3. Load all client component bundles in parallel.\r\n * 4. Hydrate every [data-hydrate-id] node.\r\n * 5. Patch history.pushState/replaceState so Link clicks trigger navigation.\r\n */\r\nexport async function initRuntime(data: RuntimeData): Promise<void> {\r\n const log = makeLogger(data.debug ?? 'silent');\r\n\r\n log.info('\uD83D\uDE80 Partial hydration:', data.hydrateIds.length, 'root component(s)');\r\n\r\n // Set up navigation first so any 'locationchange' fired during hydration\r\n // is captured (e.g. a redirect side-effect inside a component).\r\n setupNavigation(log);\r\n\r\n // Load all component bundles (not just hydrateIds) so SPA navigations to\r\n // related pages can mount their components without an extra network round-trip.\r\n const mods = await loadModules(data.allIds, log);\r\n await mountNodes(mods, log);\r\n\r\n log.info('\uD83C\uDF89 Done!');\r\n\r\n // Patch history last so pushState calls during hydration don't trigger a\r\n // navigation before roots are ready.\r\n setupLocationChangeMonitor();\r\n}"],
5
+ "mappings": "AA+CO,SAAS,6BAAmC;AACjD,QAAM,oBAAuB,OAAO,QAAQ,UAAU,KAAK,OAAO,OAAO;AACzE,QAAM,uBAAuB,OAAO,QAAQ,aAAa,KAAK,OAAO,OAAO;AAE5E,QAAM,WAAW,CAAC,SAChB,OAAO,cAAc,IAAI,YAAY,kBAAkB,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;AAE9E,SAAO,QAAQ,YAAY,YAAa,MAAM;AAC5C,sBAAkB,GAAG,IAAI;AACzB,aAAS,KAAK,CAAC,CAAC;AAAA,EAClB;AAEA,SAAO,QAAQ,eAAe,YAAa,MAAM;AAC/C,yBAAqB,GAAG,IAAI;AAC5B,aAAS,KAAK,CAAC,CAAC;AAAA,EAClB;AAGA,SAAO,iBAAiB,YAAY,MAAM,SAAS,OAAO,SAAS,QAAQ,CAAC;AAC9E;AAWA,SAAS,WAAW,OAAyB;AAC3C,SAAO;AAAA,IACL,SAAS,IAAI,MAAa;AAAE,UAAI,UAAU,UAAW,SAAQ,IAAI,GAAG,CAAC;AAAA,IAAG;AAAA,IACxE,MAAS,IAAI,MAAa;AAAE,UAAI,UAAU,aAAa,UAAU,OAAQ,SAAQ,IAAI,GAAG,CAAC;AAAA,IAAG;AAAA,IAC5F,MAAS,IAAI,MAAa;AAAE,UAAI,UAAU,aAAa,UAAU,OAAQ,SAAQ,KAAK,GAAG,CAAC;AAAA,IAAG;AAAA,IAC7F,OAAS,IAAI,MAAa;AAAE,UAAI,UAAU,SAAU,SAAQ,MAAM,GAAG,CAAC;AAAA,IAAG;AAAA,EAC3E;AACF;AA2BA,eAAe,mBAAmB,MAAsB,MAA+B;AACrF,MAAI,SAAS,QAAQ,SAAS,OAAW,QAAO;AAChD,MAAI,OAAO,SAAS,SAAU,QAAO;AAErC,MAAI,MAAM,QAAQ,IAAI,GAAG;AACvB,UAAM,QAAQ,MAAM,QAAQ,IAAI,KAAK,IAAI,OAAK,mBAAmB,GAAG,IAAI,CAAC,CAAC;AAG1E,UAAM,QAAQ,MAAM,OAAO,OAAO;AAClC,WAAO,MAAM;AAAA,MAAI,CAAC,IAAI,MACpB,MAAM,OAAO,OAAO,YAAY,GAAG,WAC/B,MAAM,QAAQ,aAAa,IAAI,EAAE,KAAK,GAAG,OAAO,EAAE,CAAC,IACnD;AAAA,IACN;AAAA,EACF;AAGA,MAAK,KAAa,SAAS,UAAU;AACnC,UAAM,IAAI;AACV,UAAM,OAAO,KAAK,IAAI,EAAE,WAAW;AACnC,QAAI,CAAC,KAAM,QAAO;AAClB,UAAM,QAAQ,MAAM,OAAO,OAAO;AAClC,WAAO,MAAM,QAAQ,cAAc,MAAM,MAAM,iBAAiB,EAAE,OAAO,IAAI,CAAC;AAAA,EAChF;AAGA,MAAK,KAAa,SAAS,QAAQ;AACjC,UAAM,IAAI;AACV,UAAM,QAAQ,MAAM,OAAO,OAAO;AAClC,WAAO,MAAM,QAAQ,cAAc,EAAE,KAAK,MAAM,iBAAiB,EAAE,OAAO,IAAI,CAAC;AAAA,EACjF;AAGA,SAAO;AACT;AAGA,eAAe,iBACb,OACA,MAC8B;AAC9B,MAAI,CAAC,SAAS,OAAO,UAAU,YAAY,MAAM,QAAQ,KAAK;AAC5D,WAAO,mBAAmB,OAAc,IAAI;AAE9C,QAAM,MAA2B,CAAC;AAClC,aAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,KAAK;AACvC,QAAI,CAAC,IAAI,MAAM,mBAAmB,GAAG,IAAI;AAC3C,SAAO;AACT;AAYA,eAAe,YACb,KACA,KACA,OAAO,IACa;AACpB,QAAM,OAAkB,oBAAI,IAAI;AAChC,QAAM,QAAQ;AAAA,IACZ,IAAI,IAAI,OAAO,OAAO;AACpB,UAAI;AACF,cAAM,MAAM,uBAAuB,EAAE,SAAS,OAAO,MAAM,IAAI,KAAK;AACpE,cAAM,IAAI,MAAM,OAAO;AACvB,aAAK,IAAI,IAAI,EAAE,OAAO;AACtB,YAAI,QAAQ,kBAAa,EAAE;AAAA,MAC7B,SAAS,KAAK;AACZ,YAAI,MAAM,uBAAkB,IAAI,GAAG;AAAA,MACrC;AAAA,IACF,CAAC;AAAA,EACH;AACA,SAAO;AACT;AAMA,MAAM,cAA2B,CAAC;AAWlC,eAAe,WACb,MACA,KACe;AACf,QAAM,EAAE,aAAa,WAAW,IAAI,MAAM,OAAO,kBAAkB;AACnE,QAAM,QAAQ,MAAM,OAAO,OAAO;AAElC,QAAM,QAAQ,SAAS,iBAA8B,mBAAmB;AACxE,MAAI,QAAQ,SAAS,MAAM,QAAQ,oBAAoB;AAEvD,aAAW,QAAQ,OAAO;AAExB,QAAI,KAAK,eAAe,QAAQ,mBAAmB,EAAG;AAEtD,UAAM,KAAO,KAAK,aAAa,iBAAiB;AAChD,UAAM,OAAO,KAAK,IAAI,EAAE;AACxB,QAAI,CAAC,MAAM;AAAE,UAAI,KAAK,iBAAiB,EAAE;AAAG;AAAA,IAAU;AAEtD,QAAI,WAAgC,CAAC;AACrC,QAAI;AACF,iBAAW,KAAK,MAAM,KAAK,aAAa,oBAAoB,KAAK,IAAI;AAAA,IACvE,SAAS,GAAG;AACV,UAAI,MAAM,yBAAyB,IAAI,CAAC;AAAA,IAC1C;AAEA,QAAI;AACF,YAAM,UAAU,MAAM,QAAQ,cAAc,MAAM,MAAM,iBAAiB,UAAU,IAAI,CAAC;AAKxF,UAAI;AACJ,UAAI,KAAK,UAAU,KAAK,GAAG;AACzB,eAAO,YAAY,MAAM,OAAO;AAAA,MAClC,OAAO;AACL,cAAM,IAAI,WAAW,IAAI;AACzB,UAAE,OAAO,OAAO;AAChB,eAAO;AAAA,MACT;AAEA,kBAAY,KAAK,IAAI;AACrB,UAAI,QAAQ,mBAAc,EAAE;AAAA,IAC9B,SAAS,KAAK;AACZ,UAAI,MAAM,wBAAmB,IAAI,GAAG;AAAA,IACtC;AAAA,EACF;AACF;AAaA,SAAS,UAAU,MAA2E;AAC5F,QAAM,QAAmB,CAAC;AAC1B,MAAI,eAA+B;AACnC,MAAI,SAAS;AAEb,aAAW,SAAS,MAAM,KAAK,KAAK,UAAU,GAAG;AAC/C,QAAI,MAAM,aAAa,KAAK,cAAc;AACxC,YAAM,OAAQ,MAAkB,KAAK,KAAK;AAC1C,UAAI,SAAS,UAAW;AAAE,iBAAS;AAAO;AAAA,MAAU;AACpD,UAAI,SAAS,WAAW;AAAE,uBAAe;AAAkB,iBAAS;AAAO;AAAA,MAAU;AAAA,IACvF;AACA,QAAI,UAAU,MAAM,aAAa,KAAK;AACpC,YAAM,KAAK,KAAgB;AAAA,EAC/B;AAEA,SAAO,EAAE,OAAO,aAAa;AAC/B;AAGA,SAAS,YAAY,IAAqB;AACxC,SAAO,GAAG,UAAU,MAAM,MAAM,KAAK,GAAG,UAAU,EAC/C,KAAK,CAAC,GAAG,MAAM,EAAE,KAAK,cAAc,EAAE,IAAI,CAAC,EAC3C,IAAI,OAAK,GAAG,EAAE,IAAI,IAAI,EAAE,KAAK,EAAE,EAC/B,KAAK,GAAG;AACb;AAeA,SAAS,aAAa,KAAqB;AACzC,QAAM,OAAO,UAAU,SAAS,IAAI;AACpC,QAAM,OAAO,UAAU,IAAI,IAAI;AAG/B,MAAI,SAAS,KAAK;AAClB,MAAI,CAAC,QAAQ;AACX,aAAS,KAAK,YAAY,SAAS,cAAc,QAAQ,CAAC;AAC1D,aAAS,SAAS,cAAc,SAAS;AACzC,aAAS,KAAK,YAAY,MAAM;AAAA,EAClC;AAKA,aAAW,MAAM,KAAK;AACpB,QAAI,GAAG,YAAY,SAAU,IAAG,OAAO;AAEzC,aAAW,MAAM,KAAK,OAAO;AAC3B,QAAI,GAAG,YAAY;AACjB,eAAS,KAAK,aAAa,wBAAwB,EAAE,GAAG,MAAM;AAAA,EAClE;AAGA,QAAM,UAAU,oBAAI,IAAqB;AACzC,aAAW,MAAM,KAAK,MAAO,KAAI,GAAG,YAAY,SAAU,SAAQ,IAAI,YAAY,EAAE,GAAG,EAAE;AAEzF,QAAM,UAAU,oBAAI,IAAqB;AACzC,aAAW,MAAM,KAAK,MAAO,KAAI,GAAG,YAAY,SAAU,SAAQ,IAAI,YAAY,EAAE,GAAG,EAAE;AAEzF,aAAW,CAAC,IAAI,EAAE,KAAK;AACrB,QAAI,CAAC,QAAQ,IAAI,EAAE,EAAG,UAAS,KAAK,aAAa,IAAI,MAAM;AAE7D,aAAW,CAAC,IAAI,EAAE,KAAK;AACrB,QAAI,CAAC,QAAQ,IAAI,EAAE,EAAG,IAAG,OAAO;AACpC;AAUA,SAAS,iBAAiB,MAAqF;AAC7G,QAAM,QAAmB,CAAC;AAC1B,MAAI,eAA+B;AACnC,MAAI,SAAS;AAEb,aAAW,SAAS,MAAM,KAAK,KAAK,UAAU,GAAG;AAC/C,QAAI,MAAM,aAAa,KAAK,cAAc;AACxC,YAAM,OAAQ,MAAkB,KAAK,KAAK;AAC1C,UAAI,SAAS,kBAAmB;AAAE,iBAAS;AAAO;AAAA,MAAU;AAC5D,UAAI,SAAS,mBAAmB;AAAE,uBAAe;AAAkB,iBAAS;AAAO;AAAA,MAAU;AAAA,IAC/F;AACA,QAAI,UAAU,MAAM,aAAa,KAAK;AACpC,YAAM,KAAK,KAAgB;AAAA,EAC/B;AAEA,SAAO,EAAE,OAAO,aAAa;AAC/B;AAcA,SAAS,wBAAwB,KAAiC;AAChE,QAAM,KAAK,SAAS,cAAc,QAAQ;AAC1C,aAAW,EAAE,MAAM,MAAM,KAAK,MAAM,KAAK,IAAI,UAAU,GAAG;AACxD,QAAI,SAAS,OAAO;AAElB,YAAM,MAAM,IAAI,IAAI,OAAO,SAAS,IAAI;AACxC,UAAI,aAAa,IAAI,KAAK,OAAO,KAAK,IAAI,CAAC,CAAC;AAC5C,SAAG,aAAa,OAAO,IAAI,SAAS,CAAC;AAAA,IACvC,OAAO;AACL,SAAG,aAAa,MAAM,KAAK;AAAA,IAC7B;AAAA,EACF;AAEA,MAAI,IAAI,YAAa,IAAG,cAAc,IAAI;AAC1C,SAAO;AACT;AAcA,SAAS,gBAAgB,KAAqB;AAC5C,QAAM,OAAO,iBAAiB,SAAS,IAAI;AAC3C,QAAM,OAAO,iBAAiB,IAAI,IAAI;AAGtC,aAAW,MAAM,KAAK,MAAO,IAAG,OAAO;AAGvC,MAAI,SAAS,KAAK;AAClB,MAAI,CAAC,QAAQ;AACX,aAAS,KAAK,YAAY,SAAS,cAAc,gBAAgB,CAAC;AAClE,aAAS,SAAS,cAAc,iBAAiB;AACjD,aAAS,KAAK,YAAY,MAAM;AAAA,EAClC;AAIA,aAAW,MAAM,KAAK;AACpB,aAAS,KAAK,aAAa,wBAAwB,EAAE,GAAG,MAAM;AAClE;AASA,SAAS,UAAU,MAAe,MAAqB;AACrD,aAAW,EAAE,MAAM,MAAM,KAAK,MAAM,KAAK,KAAK,UAAU;AACtD,SAAK,aAAa,MAAM,KAAK;AAC/B,aAAW,EAAE,KAAK,KAAK,MAAM,KAAK,KAAK,UAAU;AAC/C,QAAI,CAAC,KAAK,aAAa,IAAI,EAAG,MAAK,gBAAgB,IAAI;AAC3D;AAiBA,SAAS,gBAAgB,KAA0C;AACjE,SAAO,iBAAiB,kBAAkB,OAAO,EAAE,QAAQ,EAAE,MAAM,IAAI,EAAE,MAAW;AAClF,QAAI;AACF,YAAM,WAAW,MACb,QAAQ,KAAK,SAAS,GAAG,IAAI,MAAM,OAAO,YAC1C;AAEJ,YAAM,WAAW,MAAM,MAAM,UAAU,EAAE,SAAS,EAAE,QAAQ,YAAY,EAAE,CAAC;AAC3E,UAAI,CAAC,SAAS,IAAI;AAChB,YAAI,MAAM,4BAA4B,SAAS,MAAM;AACrD;AAAA,MACF;AAEA,YAAM,SAAU,IAAI,UAAU;AAC9B,YAAM,MAAU,OAAO,gBAAgB,MAAM,SAAS,KAAK,GAAG,WAAW;AACzE,YAAM,SAAU,IAAI,eAAe,KAAK;AACxC,YAAM,UAAU,SAAS,eAAe,KAAK;AAC7C,UAAI,CAAC,UAAU,CAAC,QAAS;AAQzB,mBAAa,GAAG;AAGhB,sBAAgB,GAAG;AAGnB,gBAAU,SAAS,iBAAiB,IAAI,eAAe;AACvD,gBAAU,SAAS,MAAM,IAAI,IAAI;AAGjC,cAAQ,YAAY,OAAO;AAG3B,YAAM,WAAW,IAAI,cAAc,OAAO;AAC1C,UAAI,SAAU,UAAS,QAAQ,SAAS,eAAe;AAIvD,YAAM,YAAa,IAAI,eAAe,UAAU;AAChD,YAAM,aAAa,SAAS,eAAe,UAAU;AACrD,UAAI,aAAa,WAAY,YAAW,cAAc,UAAU;AAKhE,kBAAY,OAAO,CAAC,EAAE,QAAQ,OAAK,EAAE,QAAQ,CAAC;AAG9C,YAAM,UAAU,KAAK,MAAM,YAAY,eAAe,IAAI;AAC1D,UAAI,KAAK,0BAAc,MAAM,mBAAc,QAAQ,YAAY,UAAU,GAAG,cAAc;AAE1F,YAAM,OAAO,MAAM,YAAY,QAAQ,UAAU,CAAC,GAAG,KAAK,OAAO,KAAK,IAAI,CAAC,CAAC;AAC5E,YAAM,WAAW,MAAM,GAAG;AAE1B,aAAO,SAAS,GAAG,CAAC;AACpB,UAAI,KAAK,kCAA2B,IAAI;AAAA,IAC1C,SAAS,KAAK;AACZ,UAAI,MAAM,kDAAkD,GAAG;AAC/D,aAAO,SAAS,OAAO;AAAA,IACzB;AAAA,EACF,CAAC;AACH;AAmCA,eAAsB,YAAY,MAAkC;AAClE,QAAM,MAAM,WAAW,KAAK,SAAS,QAAQ;AAE7C,MAAI,KAAK,gCAAyB,KAAK,WAAW,QAAQ,mBAAmB;AAI7E,kBAAgB,GAAG;AAInB,QAAM,OAAO,MAAM,YAAY,KAAK,QAAQ,GAAG;AAC/C,QAAM,WAAW,MAAM,GAAG;AAE1B,MAAI,KAAK,iBAAU;AAInB,6BAA2B;AAC7B;",
6
6
  "names": []
7
7
  }
@@ -32,6 +32,12 @@ export interface ComponentInfo {
32
32
  isClientComponent: boolean;
33
33
  /** Stable hash-based ID, present only for client components. */
34
34
  clientComponentId?: string;
35
+ /**
36
+ * The name of the default-exported component function.
37
+ * Handles both source format (`export default Link`) and esbuild's compiled
38
+ * format (`var Link_default = Link; export { Link_default as default }`).
39
+ */
40
+ exportedName?: string;
35
41
  }
36
42
  /**
37
43
  * Analyses a component file and returns cached results on subsequent calls.
@@ -16,13 +16,24 @@ function isClientComponent(filePath) {
16
16
  function getClientComponentId(filePath, pagesDir) {
17
17
  return "cc_" + createHash("md5").update(path.relative(pagesDir, filePath)).digest("hex").substring(0, 8);
18
18
  }
19
+ function getExportedDefaultName(filePath) {
20
+ const content = fs.readFileSync(filePath, "utf-8");
21
+ let m = content.match(/export\s+default\s+(?:function\s+)?(\w+)/);
22
+ if (m?.[1]) return m[1];
23
+ m = content.match(/var\s+\w+_default\s*=\s*(\w+)/);
24
+ if (m?.[1]) return m[1];
25
+ m = content.match(/export\s*\{[^}]*\b(\w+)\s+as\s+default\b[^}]*\}/);
26
+ if (m?.[1] && !m[1].endsWith("_default")) return m[1];
27
+ return void 0;
28
+ }
19
29
  function analyzeComponent(filePath, pagesDir) {
20
30
  if (componentCache.has(filePath)) return componentCache.get(filePath);
21
31
  const isClient = isClientComponent(filePath);
22
32
  const info = {
23
33
  filePath,
24
34
  isClientComponent: isClient,
25
- clientComponentId: isClient ? getClientComponentId(filePath, pagesDir) : void 0
35
+ clientComponentId: isClient ? getClientComponentId(filePath, pagesDir) : void 0,
36
+ exportedName: isClient ? getExportedDefaultName(filePath) : void 0
26
37
  };
27
38
  componentCache.set(filePath, info);
28
39
  return info;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/component-analyzer.ts"],
4
- "sourcesContent": ["/**\n * component-analyzer.ts \u2014 Static Import Analyzer & Client Component Registry\n *\n * This module solves a core problem in NukeJS's partial hydration model:\n * the server needs to know *at render time* which components in a page's\n * import tree are \"use client\" boundaries so it can:\n *\n * 1. Emit <span data-hydrate-id=\"\u2026\"> markers instead of rendering them.\n * 2. Inject the matching bundle URLs into the page's runtime data blob.\n * 3. Serialize the props passed to those components so the browser can\n * reconstruct them after loading the bundle.\n *\n * How it works:\n * - analyzeComponent() checks whether a file starts with \"use client\"\n * and assigns a stable content-hash ID if it does.\n * - extractImports() parses `import \u2026 from '\u2026'` statements with a\n * regex and resolves relative/absolute paths.\n * - findClientComponentsInTree() recursively walks the import graph, stopping\n * at client boundaries (they own their subtree).\n *\n * Results are memoised in `componentCache` (process-lifetime) so repeated SSR\n * renders don't re-read and re-hash files they've already seen.\n *\n * ID scheme:\n * The ID for a client component is `cc_` + the first 8 hex chars of the MD5\n * hash of its path relative to pagesDir. This is stable across restarts and\n * matches what the browser will request from /__client-component/<id>.js.\n */\n\nimport path from 'path';\nimport fs from 'fs';\nimport { createHash } from 'node:crypto';\nimport { fileURLToPath } from 'url';\n\n// \u2500\u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport interface ComponentInfo {\n filePath: string;\n /** True when the file's first non-comment line is \"use client\". */\n isClientComponent: boolean;\n /** Stable hash-based ID, present only for client components. */\n clientComponentId?: string;\n}\n\n// \u2500\u2500\u2500 In-process cache \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n// Memoises analyze results for the lifetime of the dev server process.\n// In production builds the analysis runs once per build, so no cache is needed.\nconst componentCache = new Map<string, ComponentInfo>();\n\n// \u2500\u2500\u2500 Client boundary detection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Returns true when a file begins with a `\"use client\"` or `'use client'`\n * directive (ignoring blank lines and line/block comment prefixes).\n *\n * Only the first five lines are checked \u2014 the directive must appear before\n * any executable code.\n */\nfunction isClientComponent(filePath: string): boolean {\n const content = fs.readFileSync(filePath, 'utf-8');\n for (const line of content.split('\\n').slice(0, 5)) {\n const trimmed = line.trim();\n if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('/*')) continue;\n if (/^[\"']use client[\"'];?$/.test(trimmed)) return true;\n break; // First substantive line is not \"use client\"\n }\n return false;\n}\n\n/**\n * Generates a deterministic, short ID for a client component.\n * The path is made relative to pagesDir before hashing so the ID is\n * portable across machines (absolute paths differ per developer).\n */\nfunction getClientComponentId(filePath: string, pagesDir: string): string {\n return 'cc_' + createHash('md5')\n .update(path.relative(pagesDir, filePath))\n .digest('hex')\n .substring(0, 8);\n}\n\n// \u2500\u2500\u2500 Analysis \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Analyses a component file and returns cached results on subsequent calls.\n *\n * @param filePath Absolute path to the source file.\n * @param pagesDir Absolute path to the pages root (used for ID generation).\n */\nexport function analyzeComponent(filePath: string, pagesDir: string): ComponentInfo {\n if (componentCache.has(filePath)) return componentCache.get(filePath)!;\n\n const isClient = isClientComponent(filePath);\n const info: ComponentInfo = {\n filePath,\n isClientComponent: isClient,\n clientComponentId: isClient ? getClientComponentId(filePath, pagesDir) : undefined,\n };\n\n componentCache.set(filePath, info);\n return info;\n}\n\n// \u2500\u2500\u2500 Import extraction \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Parses `import \u2026 from '\u2026'` and `export \u2026 from '\u2026'` statements in a file\n * and returns a list of resolved absolute paths for all *local* imports.\n *\n * Non-local specifiers (npm packages) are skipped, except `nukejs` itself \u2014\n * which is resolved to our own index file so built-in \"use client\" components\n * like `<Link>` are included in the client component discovery walk.\n *\n * Extensions are tried in priority order if the specifier has none.\n */\nfunction extractImports(filePath: string): string[] {\n const content = fs.readFileSync(filePath, 'utf-8');\n const dir = path.dirname(filePath);\n const imports: string[] = [];\n\n const importRegex =\n /(?:import|export)\\s+(?:(?:\\{[^}]*\\}|\\*\\s+as\\s+\\w+|\\w+)\\s+from\\s+)?['\"]([^'\"]+)['\"]/g;\n let match: RegExpExecArray | null;\n\n while ((match = importRegex.exec(content)) !== null) {\n const spec = match[1];\n\n // Special case: resolve the 'nukejs' package to our own source so\n // built-in \"use client\" exports (Link, useRouter, etc.) are discovered.\n if (spec === 'nukejs') {\n const selfDir = path.dirname(fileURLToPath(import.meta.url));\n for (const candidate of [\n path.join(selfDir, 'index.ts'),\n path.join(selfDir, 'index.js'),\n ]) {\n if (fs.existsSync(candidate)) { imports.push(candidate); break; }\n }\n continue;\n }\n\n // Skip npm packages and other non-local specifiers.\n if (!spec.startsWith('.') && !spec.startsWith('/')) continue;\n\n // Resolve to an absolute path, trying extensions if needed.\n let resolved = path.resolve(dir, spec);\n const EXTS = ['.tsx', '.ts', '.jsx', '.js'] as const;\n const isFile = (p: string) => fs.existsSync(p) && fs.statSync(p).isFile();\n\n if (!isFile(resolved)) {\n let found = false;\n\n // 1. Try appending an extension (./Button \u2192 ./Button.tsx)\n for (const ext of EXTS) {\n if (isFile(resolved + ext)) { resolved += ext; found = true; break; }\n }\n\n // 2. Try an index file inside the directory (./components \u2192 ./components/index.tsx)\n if (!found && fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {\n for (const ext of EXTS) {\n const candidate = path.join(resolved, `index${ext}`);\n if (isFile(candidate)) { resolved = candidate; found = true; break; }\n }\n }\n\n if (!found) continue; // Unresolvable \u2014 skip silently\n }\n\n imports.push(resolved);\n }\n\n return imports;\n}\n\n// \u2500\u2500\u2500 Tree walk \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Recursively walks the import graph from `filePath`, collecting every\n * \"use client\" file encountered.\n *\n * The walk stops at client boundaries: a \"use client\" file is recorded and\n * its own imports are NOT walked (the client runtime handles their subtree).\n *\n * The `visited` set prevents infinite loops from circular imports.\n *\n * @returns Map<id, absoluteFilePath> for every client component reachable\n * from `filePath` (including `filePath` itself if it's a client).\n */\nexport function findClientComponentsInTree(\n filePath: string,\n pagesDir: string,\n visited = new Set<string>(),\n): Map<string, string> {\n const found = new Map<string, string>();\n if (visited.has(filePath)) return found;\n visited.add(filePath);\n\n const info = analyzeComponent(filePath, pagesDir);\n\n if (info.isClientComponent && info.clientComponentId) {\n found.set(info.clientComponentId, filePath);\n return found; // Stop \u2014 client boundary owns its subtree\n }\n\n for (const importPath of extractImports(filePath)) {\n for (const [id, p] of findClientComponentsInTree(importPath, pagesDir, visited)) {\n found.set(id, p);\n }\n }\n\n return found;\n}\n\n// \u2500\u2500\u2500 Cache access \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Looks up the absolute file path for a client component by its ID.\n * Returns undefined when the ID is not in the cache.\n */\nexport function getComponentById(id: string): string | undefined {\n for (const [filePath, info] of componentCache) {\n if (info.clientComponentId === id) return filePath;\n }\n return undefined;\n}\n\n/** Returns the live component cache (used by bundler.ts for ID\u2192path lookup). */\nexport function getComponentCache(): Map<string, ComponentInfo> {\n return componentCache;\n}\n\n/**\n * Removes a single file's analysis entry from the cache.\n * Call this whenever a source file changes in dev mode so the next render\n * re-analyses the file (picks up added/removed \"use client\" directives and\n * changed import graphs).\n */\nexport function invalidateComponentCache(filePath: string): void {\n componentCache.delete(filePath);\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\r\n * component-analyzer.ts \u2014 Static Import Analyzer & Client Component Registry\r\n *\r\n * This module solves a core problem in NukeJS's partial hydration model:\r\n * the server needs to know *at render time* which components in a page's\r\n * import tree are \"use client\" boundaries so it can:\r\n *\r\n * 1. Emit <span data-hydrate-id=\"\u2026\"> markers instead of rendering them.\r\n * 2. Inject the matching bundle URLs into the page's runtime data blob.\r\n * 3. Serialize the props passed to those components so the browser can\r\n * reconstruct them after loading the bundle.\r\n *\r\n * How it works:\r\n * - analyzeComponent() checks whether a file starts with \"use client\"\r\n * and assigns a stable content-hash ID if it does.\r\n * - extractImports() parses `import \u2026 from '\u2026'` statements with a\r\n * regex and resolves relative/absolute paths.\r\n * - findClientComponentsInTree() recursively walks the import graph, stopping\r\n * at client boundaries (they own their subtree).\r\n *\r\n * Results are memoised in `componentCache` (process-lifetime) so repeated SSR\r\n * renders don't re-read and re-hash files they've already seen.\r\n *\r\n * ID scheme:\r\n * The ID for a client component is `cc_` + the first 8 hex chars of the MD5\r\n * hash of its path relative to pagesDir. This is stable across restarts and\r\n * matches what the browser will request from /__client-component/<id>.js.\r\n */\r\n\r\nimport path from 'path';\r\nimport fs from 'fs';\r\nimport { createHash } from 'node:crypto';\r\nimport { fileURLToPath } from 'url';\r\n\r\n// \u2500\u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nexport interface ComponentInfo {\r\n filePath: string;\r\n /** True when the file's first non-comment line is \"use client\". */\r\n isClientComponent: boolean;\r\n /** Stable hash-based ID, present only for client components. */\r\n clientComponentId?: string;\r\n /**\r\n * The name of the default-exported component function.\r\n * Handles both source format (`export default Link`) and esbuild's compiled\r\n * format (`var Link_default = Link; export { Link_default as default }`).\r\n */\r\n exportedName?: string;\r\n}\r\n\r\n// \u2500\u2500\u2500 In-process cache \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n// Memoises analyze results for the lifetime of the dev server process.\r\n// In production builds the analysis runs once per build, so no cache is needed.\r\nconst componentCache = new Map<string, ComponentInfo>();\r\n\r\n// \u2500\u2500\u2500 Client boundary detection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Returns true when a file begins with a `\"use client\"` or `'use client'`\r\n * directive (ignoring blank lines and line/block comment prefixes).\r\n *\r\n * Only the first five lines are checked \u2014 the directive must appear before\r\n * any executable code.\r\n */\r\nfunction isClientComponent(filePath: string): boolean {\r\n const content = fs.readFileSync(filePath, 'utf-8');\r\n for (const line of content.split('\\n').slice(0, 5)) {\r\n const trimmed = line.trim();\r\n if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('/*')) continue;\r\n if (/^[\"']use client[\"'];?$/.test(trimmed)) return true;\r\n break; // First substantive line is not \"use client\"\r\n }\r\n return false;\r\n}\r\n\r\n/**\r\n * Generates a deterministic, short ID for a client component.\r\n * The path is made relative to pagesDir before hashing so the ID is\r\n * portable across machines (absolute paths differ per developer).\r\n */\r\nfunction getClientComponentId(filePath: string, pagesDir: string): string {\r\n return 'cc_' + createHash('md5')\r\n .update(path.relative(pagesDir, filePath))\r\n .digest('hex')\r\n .substring(0, 8);\r\n}\r\n\r\n// \u2500\u2500\u2500 Default export name extraction \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Extracts the name of the default-exported function from a component file.\r\n *\r\n * Handles three formats:\r\n * 1. Source: `export default function Link(\u2026)` or `export default Link`\r\n * 2. esbuild: `var Link_default = Link;` (compiled arrow-function component)\r\n * 3. Re-export: `export { Link as default }`\r\n */\r\nfunction getExportedDefaultName(filePath: string): string | undefined {\r\n const content = fs.readFileSync(filePath, 'utf-8');\r\n\r\n // Format 1 \u2013 source files: `export default function Foo` or `export default Foo`\r\n let m = content.match(/export\\s+default\\s+(?:function\\s+)?(\\w+)/);\r\n if (m?.[1]) return m[1];\r\n\r\n // Format 2 \u2013 esbuild compiled arrow components: `var Foo_default = Foo`\r\n // esbuild renames the variable to `<name>_default` and keeps the original name.\r\n m = content.match(/var\\s+\\w+_default\\s*=\\s*(\\w+)/);\r\n if (m?.[1]) return m[1];\r\n\r\n // Format 3 \u2013 explicit re-export: `export { Foo as default }`\r\n m = content.match(/export\\s*\\{[^}]*\\b(\\w+)\\s+as\\s+default\\b[^}]*\\}/);\r\n if (m?.[1] && !m[1].endsWith('_default')) return m[1];\r\n\r\n return undefined;\r\n}\r\n\r\n// \u2500\u2500\u2500 Analysis \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Analyses a component file and returns cached results on subsequent calls.\r\n *\r\n * @param filePath Absolute path to the source file.\r\n * @param pagesDir Absolute path to the pages root (used for ID generation).\r\n */\r\nexport function analyzeComponent(filePath: string, pagesDir: string): ComponentInfo {\r\n if (componentCache.has(filePath)) return componentCache.get(filePath)!;\r\n\r\n const isClient = isClientComponent(filePath);\r\n const info: ComponentInfo = {\r\n filePath,\r\n isClientComponent: isClient,\r\n clientComponentId: isClient ? getClientComponentId(filePath, pagesDir) : undefined,\r\n exportedName: isClient ? getExportedDefaultName(filePath) : undefined,\r\n };\r\n\r\n componentCache.set(filePath, info);\r\n return info;\r\n}\r\n\r\n// \u2500\u2500\u2500 Import extraction \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Parses `import \u2026 from '\u2026'` and `export \u2026 from '\u2026'` statements in a file\r\n * and returns a list of resolved absolute paths for all *local* imports.\r\n *\r\n * Non-local specifiers (npm packages) are skipped, except `nukejs` itself \u2014\r\n * which is resolved to our own index file so built-in \"use client\" components\r\n * like `<Link>` are included in the client component discovery walk.\r\n *\r\n * Extensions are tried in priority order if the specifier has none.\r\n */\r\nfunction extractImports(filePath: string): string[] {\r\n const content = fs.readFileSync(filePath, 'utf-8');\r\n const dir = path.dirname(filePath);\r\n const imports: string[] = [];\r\n\r\n const importRegex =\r\n /(?:import|export)\\s+(?:(?:\\{[^}]*\\}|\\*\\s+as\\s+\\w+|\\w+)\\s+from\\s+)?['\"]([^'\"]+)['\"]/g;\r\n let match: RegExpExecArray | null;\r\n\r\n while ((match = importRegex.exec(content)) !== null) {\r\n const spec = match[1];\r\n\r\n // Special case: resolve the 'nukejs' package to our own source so\r\n // built-in \"use client\" exports (Link, useRouter, etc.) are discovered.\r\n if (spec === 'nukejs') {\r\n const selfDir = path.dirname(fileURLToPath(import.meta.url));\r\n for (const candidate of [\r\n path.join(selfDir, 'index.ts'),\r\n path.join(selfDir, 'index.js'),\r\n ]) {\r\n if (fs.existsSync(candidate)) { imports.push(candidate); break; }\r\n }\r\n continue;\r\n }\r\n\r\n // Skip npm packages and other non-local specifiers.\r\n if (!spec.startsWith('.') && !spec.startsWith('/')) continue;\r\n\r\n // Resolve to an absolute path, trying extensions if needed.\r\n let resolved = path.resolve(dir, spec);\r\n const EXTS = ['.tsx', '.ts', '.jsx', '.js'] as const;\r\n const isFile = (p: string) => fs.existsSync(p) && fs.statSync(p).isFile();\r\n\r\n if (!isFile(resolved)) {\r\n let found = false;\r\n\r\n // 1. Try appending an extension (./Button \u2192 ./Button.tsx)\r\n for (const ext of EXTS) {\r\n if (isFile(resolved + ext)) { resolved += ext; found = true; break; }\r\n }\r\n\r\n // 2. Try an index file inside the directory (./components \u2192 ./components/index.tsx)\r\n if (!found && fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {\r\n for (const ext of EXTS) {\r\n const candidate = path.join(resolved, `index${ext}`);\r\n if (isFile(candidate)) { resolved = candidate; found = true; break; }\r\n }\r\n }\r\n\r\n if (!found) continue; // Unresolvable \u2014 skip silently\r\n }\r\n\r\n imports.push(resolved);\r\n }\r\n\r\n return imports;\r\n}\r\n\r\n// \u2500\u2500\u2500 Tree walk \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Recursively walks the import graph from `filePath`, collecting every\r\n * \"use client\" file encountered.\r\n *\r\n * The walk stops at client boundaries: a \"use client\" file is recorded and\r\n * its own imports are NOT walked (the client runtime handles their subtree).\r\n *\r\n * The `visited` set prevents infinite loops from circular imports.\r\n *\r\n * @returns Map<id, absoluteFilePath> for every client component reachable\r\n * from `filePath` (including `filePath` itself if it's a client).\r\n */\r\nexport function findClientComponentsInTree(\r\n filePath: string,\r\n pagesDir: string,\r\n visited = new Set<string>(),\r\n): Map<string, string> {\r\n const found = new Map<string, string>();\r\n if (visited.has(filePath)) return found;\r\n visited.add(filePath);\r\n\r\n const info = analyzeComponent(filePath, pagesDir);\r\n\r\n if (info.isClientComponent && info.clientComponentId) {\r\n found.set(info.clientComponentId, filePath);\r\n return found; // Stop \u2014 client boundary owns its subtree\r\n }\r\n\r\n for (const importPath of extractImports(filePath)) {\r\n for (const [id, p] of findClientComponentsInTree(importPath, pagesDir, visited)) {\r\n found.set(id, p);\r\n }\r\n }\r\n\r\n return found;\r\n}\r\n\r\n// \u2500\u2500\u2500 Cache access \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Looks up the absolute file path for a client component by its ID.\r\n * Returns undefined when the ID is not in the cache.\r\n */\r\nexport function getComponentById(id: string): string | undefined {\r\n for (const [filePath, info] of componentCache) {\r\n if (info.clientComponentId === id) return filePath;\r\n }\r\n return undefined;\r\n}\r\n\r\n/** Returns the live component cache (used by bundler.ts for ID\u2192path lookup). */\r\nexport function getComponentCache(): Map<string, ComponentInfo> {\r\n return componentCache;\r\n}\r\n\r\n/**\r\n * Removes a single file's analysis entry from the cache.\r\n * Call this whenever a source file changes in dev mode so the next render\r\n * re-analyses the file (picks up added/removed \"use client\" directives and\r\n * changed import graphs).\r\n */\r\nexport function invalidateComponentCache(filePath: string): void {\r\n componentCache.delete(filePath);\r\n}"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -22,15 +22,15 @@ function hmr() {
22
22
  reloadStylesheets();
23
23
  return;
24
24
  }
25
- if (msg.url === window.location.pathname) {
25
+ if (patternMatchesPathname(msg.url, window.location.pathname)) {
26
26
  log.info("[HMR] Page changed:", msg.url);
27
- navigate(window.location.pathname);
27
+ navigate(window.location.pathname + window.location.search);
28
28
  }
29
29
  return;
30
30
  }
31
31
  if (msg.type === "replace") {
32
32
  log.info("[HMR] Component changed:", msg.component);
33
- navigate(window.location.pathname);
33
+ navigate(window.location.pathname + window.location.search);
34
34
  return;
35
35
  }
36
36
  } catch (err) {
@@ -41,7 +41,20 @@ function hmr() {
41
41
  function navigate(href) {
42
42
  window.dispatchEvent(new CustomEvent("locationchange", { detail: { href, hmr: true } }));
43
43
  }
44
- function waitForReconnect(intervalMs = 500, maxAttempts = 30) {
44
+ function patternMatchesPathname(pattern, pathname) {
45
+ const normPattern = pattern.length > 1 ? pattern.replace(/\/+$/, "") : pattern;
46
+ const normPathname = pathname.length > 1 ? pathname.replace(/\/+$/, "") : pathname;
47
+ const segments = normPattern.replace(/^\//, "").split("/");
48
+ const regexParts = segments.map((seg) => {
49
+ if (/^\[\[\.\.\..+\]\]$/.test(seg)) return "(?:/.*)?";
50
+ if (/^\[\.\.\./.test(seg)) return "(?:/.+)";
51
+ if (/^\[\[/.test(seg)) return "(?:/[^/]*)?";
52
+ if (/^\[/.test(seg)) return "/[^/]+";
53
+ return "/" + seg.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
54
+ });
55
+ return new RegExp("^" + regexParts.join("") + "$").test(normPathname);
56
+ }
57
+ function waitForReconnect(intervalMs = 3e3, maxAttempts = 10) {
45
58
  let attempts = 0;
46
59
  const id = setInterval(async () => {
47
60
  attempts++;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/hmr-bundle.ts"],
4
- "sourcesContent": ["/**\r\n * hmr-bundle.ts \u2014 HMR Client Script\r\n *\r\n * This file is compiled on-demand by middleware.ts and served to the browser\r\n * as /__hmr.js (injected into every dev-mode page as a module script).\r\n *\r\n * It opens an EventSource connection to /__hmr and reacts to three message\r\n * types from the server:\r\n *\r\n * 'reload' \u2014 A page or stylesheet changed.\r\n * url === '*' \u2192 reload stylesheets in-place (no flicker)\r\n * url === window.location.pathname \u2192 soft-navigate the current page\r\n *\r\n * 'replace' \u2014 A component/utility changed.\r\n * Re-navigate the current page so SSR picks up the new code.\r\n *\r\n * 'restart' \u2014 The server is restarting (config or middleware changed).\r\n * Close the SSE connection and poll /__hmr_ping until the\r\n * server is back, then hard-reload the page.\r\n *\r\n * The same reconnect polling is used when the SSE connection drops unexpectedly\r\n * (e.g. the dev server crashed).\r\n */\r\n\r\nimport { log } from './logger';\r\n\r\n// \u2500\u2500\u2500 Entry point \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/** Opens the SSE connection and starts listening for HMR events. */\r\nexport default function hmr(): void {\r\n const es = new EventSource('/__hmr');\r\n\r\n es.onopen = () => {\r\n log.info('[HMR] Connected');\r\n };\r\n\r\n es.onerror = () => {\r\n // Connection dropped without a restart message (e.g. crash or network\r\n // blip). Close cleanly and poll until the server is back.\r\n es.close();\r\n waitForReconnect();\r\n };\r\n\r\n es.onmessage = async (event) => {\r\n try {\r\n const msg = JSON.parse(event.data);\r\n\r\n if (msg.type === 'restart') {\r\n log.info('[HMR] Server restarting \u2014 waiting to reconnect...');\r\n es.close();\r\n waitForReconnect();\r\n return;\r\n }\r\n\r\n if (msg.type === 'reload') {\r\n if (msg.url === '*') {\r\n // CSS / global style change \u2014 bust stylesheet hrefs in-place.\r\n // This avoids a full page reload and its associated FOUC.\r\n reloadStylesheets();\r\n return;\r\n }\r\n // A specific page changed \u2014 only navigate if we're on that page.\r\n if (msg.url === window.location.pathname) {\r\n log.info('[HMR] Page changed:', msg.url);\r\n navigate(window.location.pathname);\r\n }\r\n return;\r\n }\r\n\r\n if (msg.type === 'replace') {\r\n // A shared component or utility changed. The current page might use\r\n // it, so we re-navigate to pick up the latest server render.\r\n log.info('[HMR] Component changed:', msg.component);\r\n navigate(window.location.pathname);\r\n return;\r\n }\r\n } catch (err) {\r\n log.error('[HMR] Message parse error:', err);\r\n }\r\n };\r\n}\r\n\r\n// \u2500\u2500\u2500 Soft navigation helper \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Triggers a soft (SPA-style) navigation via the locationchange event that\r\n * bundle.ts listens to. Adds `hmr: true` in the detail so the navigation\r\n * handler appends `?__hmr=1`, which tells SSR to skip client-component\r\n * renderToString (faster HMR round-trips).\r\n */\r\nfunction navigate(href: string): void {\r\n window.dispatchEvent(new CustomEvent('locationchange', { detail: { href, hmr: true } }));\r\n}\r\n\r\n// \u2500\u2500\u2500 Reconnect polling \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Polls /__hmr_ping at `intervalMs` until the server responds with a 200\r\n * (meaning it's back up), then triggers a full page reload to pick up any\r\n * changes that happened during the downtime.\r\n *\r\n * Gives up after `maxAttempts` (default ~15 seconds at 500 ms intervals).\r\n */\r\nfunction waitForReconnect(intervalMs = 500, maxAttempts = 30): void {\r\n let attempts = 0;\r\n\r\n const id = setInterval(async () => {\r\n attempts++;\r\n try {\r\n const res = await fetch('/__hmr_ping', { cache: 'no-store' });\r\n if (res.ok) {\r\n clearInterval(id);\r\n log.info('[HMR] Server back \u2014 reloading');\r\n window.location.reload();\r\n }\r\n } catch {\r\n // Server still down \u2014 keep polling silently.\r\n }\r\n\r\n if (attempts >= maxAttempts) {\r\n clearInterval(id);\r\n log.error('[HMR] Server did not come back after restart');\r\n }\r\n }, intervalMs);\r\n}\r\n\r\n// \u2500\u2500\u2500 Stylesheet cache-buster \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Appends a `?t=<timestamp>` query to every `<link rel=\"stylesheet\">` href.\r\n * The browser treats the new URL as a different resource and re-fetches it,\r\n * updating styles without a page reload or visible flash.\r\n */\r\nfunction reloadStylesheets(): void {\r\n const links = document.querySelectorAll<HTMLLinkElement>('link[rel=\"stylesheet\"]');\r\n log.info(`[HMR] CSS changed \u2014 reloading ${links.length} stylesheet(s)`);\r\n links.forEach(link => {\r\n const url = new URL(link.href);\r\n url.searchParams.set('t', String(Date.now()));\r\n link.href = url.toString();\r\n });\r\n}\r\n\r\n// Auto-start when this module is loaded.\r\nhmr();\r\n"],
5
- "mappings": "AAwBA,SAAS,WAAW;AAKL,SAAR,MAA6B;AAClC,QAAM,KAAK,IAAI,YAAY,QAAQ;AAEnC,KAAG,SAAS,MAAM;AAChB,QAAI,KAAK,iBAAiB;AAAA,EAC5B;AAEA,KAAG,UAAU,MAAM;AAGjB,OAAG,MAAM;AACT,qBAAiB;AAAA,EACnB;AAEA,KAAG,YAAY,OAAO,UAAU;AAC9B,QAAI;AACF,YAAM,MAAM,KAAK,MAAM,MAAM,IAAI;AAEjC,UAAI,IAAI,SAAS,WAAW;AAC1B,YAAI,KAAK,wDAAmD;AAC5D,WAAG,MAAM;AACT,yBAAiB;AACjB;AAAA,MACF;AAEA,UAAI,IAAI,SAAS,UAAU;AACzB,YAAI,IAAI,QAAQ,KAAK;AAGnB,4BAAkB;AAClB;AAAA,QACF;AAEA,YAAI,IAAI,QAAQ,OAAO,SAAS,UAAU;AACxC,cAAI,KAAK,uBAAuB,IAAI,GAAG;AACvC,mBAAS,OAAO,SAAS,QAAQ;AAAA,QACnC;AACA;AAAA,MACF;AAEA,UAAI,IAAI,SAAS,WAAW;AAG1B,YAAI,KAAK,4BAA4B,IAAI,SAAS;AAClD,iBAAS,OAAO,SAAS,QAAQ;AACjC;AAAA,MACF;AAAA,IACF,SAAS,KAAK;AACZ,UAAI,MAAM,8BAA8B,GAAG;AAAA,IAC7C;AAAA,EACF;AACF;AAUA,SAAS,SAAS,MAAoB;AACpC,SAAO,cAAc,IAAI,YAAY,kBAAkB,EAAE,QAAQ,EAAE,MAAM,KAAK,KAAK,EAAE,CAAC,CAAC;AACzF;AAWA,SAAS,iBAAiB,aAAa,KAAK,cAAc,IAAU;AAClE,MAAI,WAAW;AAEf,QAAM,KAAK,YAAY,YAAY;AACjC;AACA,QAAI;AACF,YAAM,MAAM,MAAM,MAAM,eAAe,EAAE,OAAO,WAAW,CAAC;AAC5D,UAAI,IAAI,IAAI;AACV,sBAAc,EAAE;AAChB,YAAI,KAAK,oCAA+B;AACxC,eAAO,SAAS,OAAO;AAAA,MACzB;AAAA,IACF,QAAQ;AAAA,IAER;AAEA,QAAI,YAAY,aAAa;AAC3B,oBAAc,EAAE;AAChB,UAAI,MAAM,8CAA8C;AAAA,IAC1D;AAAA,EACF,GAAG,UAAU;AACf;AASA,SAAS,oBAA0B;AACjC,QAAM,QAAQ,SAAS,iBAAkC,wBAAwB;AACjF,MAAI,KAAK,sCAAiC,MAAM,MAAM,gBAAgB;AACtE,QAAM,QAAQ,UAAQ;AACpB,UAAM,MAAM,IAAI,IAAI,KAAK,IAAI;AAC7B,QAAI,aAAa,IAAI,KAAK,OAAO,KAAK,IAAI,CAAC,CAAC;AAC5C,SAAK,OAAO,IAAI,SAAS;AAAA,EAC3B,CAAC;AACH;AAGA,IAAI;",
4
+ "sourcesContent": ["/**\r\n * hmr-bundle.ts \u2014 HMR Client Script\r\n *\r\n * This file is compiled on-demand by middleware.ts and served to the browser\r\n * as /__hmr.js (injected into every dev-mode page as a module script).\r\n *\r\n * It opens an EventSource connection to /__hmr and reacts to three message\r\n * types from the server:\r\n *\r\n * 'reload' \u2014 A page or stylesheet changed.\r\n * url === '*' \u2192 reload stylesheets in-place (no flicker)\r\n * url === window.location.pathname \u2192 soft-navigate the current page\r\n *\r\n * 'replace' \u2014 A component/utility changed.\r\n * Re-navigate the current page so SSR picks up the new code.\r\n *\r\n * 'restart' \u2014 The server is restarting (config or middleware changed).\r\n * Close the SSE connection and poll /__hmr_ping until the\r\n * server is back, then hard-reload the page.\r\n *\r\n * The same reconnect polling is used when the SSE connection drops unexpectedly\r\n * (e.g. the dev server crashed).\r\n */\r\n\r\nimport { log } from './logger';\r\n\r\n// \u2500\u2500\u2500 Entry point \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/** Opens the SSE connection and starts listening for HMR events. */\r\nexport default function hmr(): void {\r\n const es = new EventSource('/__hmr');\r\n\r\n es.onopen = () => {\r\n log.info('[HMR] Connected');\r\n };\r\n\r\n es.onerror = () => {\r\n // Connection dropped without a restart message (e.g. crash or network\r\n // blip). Close cleanly and poll until the server is back.\r\n es.close();\r\n waitForReconnect();\r\n };\r\n\r\n es.onmessage = async (event) => {\r\n try {\r\n const msg = JSON.parse(event.data);\r\n\r\n if (msg.type === 'restart') {\r\n log.info('[HMR] Server restarting \u2014 waiting to reconnect...');\r\n es.close();\r\n waitForReconnect();\r\n return;\r\n }\r\n\r\n if (msg.type === 'reload') {\r\n if (msg.url === '*') {\r\n // CSS / global style change \u2014 bust stylesheet hrefs in-place.\r\n // This avoids a full page reload and its associated FOUC.\r\n reloadStylesheets();\r\n return;\r\n }\r\n // A specific page changed \u2014 only navigate if we're on that page.\r\n if (patternMatchesPathname(msg.url, window.location.pathname)) {\r\n log.info('[HMR] Page changed:', msg.url);\r\n navigate(window.location.pathname + window.location.search);\r\n }\r\n return;\r\n }\r\n\r\n if (msg.type === 'replace') {\r\n // A shared component or utility changed. The current page might use\r\n // it, so we re-navigate to pick up the latest server render.\r\n log.info('[HMR] Component changed:', msg.component);\r\n navigate(window.location.pathname + window.location.search);\r\n return;\r\n }\r\n } catch (err) {\r\n log.error('[HMR] Message parse error:', err);\r\n }\r\n };\r\n}\r\n\r\n// \u2500\u2500\u2500 Soft navigation helper \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Triggers a soft (SPA-style) navigation via the locationchange event that\r\n * bundle.ts listens to. Adds `hmr: true` in the detail so the navigation\r\n * handler appends `?__hmr=1`, which tells SSR to skip client-component\r\n * renderToString (faster HMR round-trips).\r\n */\r\nfunction navigate(href: string): void {\r\n window.dispatchEvent(new CustomEvent('locationchange', { detail: { href, hmr: true } }));\r\n}\r\n\r\n// \u2500\u2500\u2500 Dynamic route pattern matching \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Returns true when `pathname` matches the route `pattern` emitted by the\r\n * server. Patterns use the file-system conventions:\r\n * [param] \u2192 any single non-slash segment\r\n * [...slug] \u2192 one or more segments\r\n * [[...slug]] \u2192 zero or more segments\r\n * [[param]] \u2192 zero or one segment\r\n *\r\n * Each segment is classified before any escaping so that bracket characters\r\n * in param names are never mistaken for regex metacharacters.\r\n */\r\nfunction patternMatchesPathname(pattern: string, pathname: string): boolean {\r\n // Normalise trailing slashes so /a/ matches pattern /a and vice versa.\r\n const normPattern = pattern.length > 1 ? pattern.replace(/\\/+$/, '') : pattern;\r\n const normPathname = pathname.length > 1 ? pathname.replace(/\\/+$/, '') : pathname;\r\n const segments = normPattern.replace(/^\\//, '').split('/');\r\n const regexParts = segments.map(seg => {\r\n if (/^\\[\\[\\.\\.\\..+\\]\\]$/.test(seg)) return '(?:\\/.*)?' ; // [[...x]] optional catch-all\r\n if (/^\\[\\.\\.\\./.test(seg)) return '(?:\\/.+)' ; // [...x] required catch-all\r\n if (/^\\[\\[/.test(seg)) return '(?:\\/[^/]*)?' ;// [[x]] optional single\r\n if (/^\\[/.test(seg)) return '\\/[^/]+' ; // [x] required single\r\n return '\\/' + seg.replace(/[.+?^${}()|[\\]\\\\]/g, '\\\\$&'); // static \u2014 escape metacharacters\r\n });\r\n return new RegExp('^' + regexParts.join('') + '$').test(normPathname);\r\n}\r\n\r\n// \u2500\u2500\u2500 Reconnect polling \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Polls /__hmr_ping at `intervalMs` until the server responds with a 200\r\n * (meaning it's back up), then triggers a full page reload to pick up any\r\n * changes that happened during the downtime.\r\n *\r\n * Gives up after `maxAttempts` (default ~15 seconds at 500 ms intervals).\r\n */\r\nfunction waitForReconnect(intervalMs = 3000, maxAttempts = 10): void {\r\n let attempts = 0;\r\n\r\n const id = setInterval(async () => {\r\n attempts++;\r\n try {\r\n const res = await fetch('/__hmr_ping', { cache: 'no-store' });\r\n if (res.ok) {\r\n clearInterval(id);\r\n log.info('[HMR] Server back \u2014 reloading');\r\n window.location.reload();\r\n }\r\n } catch {\r\n // Server still down \u2014 keep polling silently.\r\n }\r\n\r\n if (attempts >= maxAttempts) {\r\n clearInterval(id);\r\n log.error('[HMR] Server did not come back after restart');\r\n }\r\n }, intervalMs);\r\n}\r\n\r\n// \u2500\u2500\u2500 Stylesheet cache-buster \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Appends a `?t=<timestamp>` query to every `<link rel=\"stylesheet\">` href.\r\n * The browser treats the new URL as a different resource and re-fetches it,\r\n * updating styles without a page reload or visible flash.\r\n */\r\nfunction reloadStylesheets(): void {\r\n const links = document.querySelectorAll<HTMLLinkElement>('link[rel=\"stylesheet\"]');\r\n log.info(`[HMR] CSS changed \u2014 reloading ${links.length} stylesheet(s)`);\r\n links.forEach(link => {\r\n const url = new URL(link.href);\r\n url.searchParams.set('t', String(Date.now()));\r\n link.href = url.toString();\r\n });\r\n}\r\n\r\n// Auto-start when this module is loaded.\r\nhmr();"],
5
+ "mappings": "AAwBA,SAAS,WAAW;AAKL,SAAR,MAA6B;AAClC,QAAM,KAAK,IAAI,YAAY,QAAQ;AAEnC,KAAG,SAAS,MAAM;AAChB,QAAI,KAAK,iBAAiB;AAAA,EAC5B;AAEA,KAAG,UAAU,MAAM;AAGjB,OAAG,MAAM;AACT,qBAAiB;AAAA,EACnB;AAEA,KAAG,YAAY,OAAO,UAAU;AAC9B,QAAI;AACF,YAAM,MAAM,KAAK,MAAM,MAAM,IAAI;AAEjC,UAAI,IAAI,SAAS,WAAW;AAC1B,YAAI,KAAK,wDAAmD;AAC5D,WAAG,MAAM;AACT,yBAAiB;AACjB;AAAA,MACF;AAEA,UAAI,IAAI,SAAS,UAAU;AACzB,YAAI,IAAI,QAAQ,KAAK;AAGnB,4BAAkB;AAClB;AAAA,QACF;AAEA,YAAI,uBAAuB,IAAI,KAAK,OAAO,SAAS,QAAQ,GAAG;AAC7D,cAAI,KAAK,uBAAuB,IAAI,GAAG;AACvC,mBAAS,OAAO,SAAS,WAAW,OAAO,SAAS,MAAM;AAAA,QAC5D;AACA;AAAA,MACF;AAEA,UAAI,IAAI,SAAS,WAAW;AAG1B,YAAI,KAAK,4BAA4B,IAAI,SAAS;AAClD,iBAAS,OAAO,SAAS,WAAW,OAAO,SAAS,MAAM;AAC1D;AAAA,MACF;AAAA,IACF,SAAS,KAAK;AACZ,UAAI,MAAM,8BAA8B,GAAG;AAAA,IAC7C;AAAA,EACF;AACF;AAUA,SAAS,SAAS,MAAoB;AACpC,SAAO,cAAc,IAAI,YAAY,kBAAkB,EAAE,QAAQ,EAAE,MAAM,KAAK,KAAK,EAAE,CAAC,CAAC;AACzF;AAeA,SAAS,uBAAuB,SAAiB,UAA2B;AAE1E,QAAM,cAAe,QAAQ,SAAU,IAAI,QAAQ,QAAQ,QAAQ,EAAE,IAAK;AAC1E,QAAM,eAAe,SAAS,SAAS,IAAI,SAAS,QAAQ,QAAQ,EAAE,IAAI;AAC1E,QAAM,WAAa,YAAY,QAAQ,OAAO,EAAE,EAAE,MAAM,GAAG;AAC3D,QAAM,aAAa,SAAS,IAAI,SAAO;AACrC,QAAI,qBAAqB,KAAK,GAAG,EAAG,QAAO;AAC3C,QAAI,YAAY,KAAK,GAAG,EAAc,QAAO;AAC7C,QAAI,QAAQ,KAAK,GAAG,EAAoB,QAAO;AAC/C,QAAI,MAAM,KAAK,GAAG,EAAsB,QAAO;AAC/C,WAAO,MAAO,IAAI,QAAQ,sBAAsB,MAAM;AAAA,EACxD,CAAC;AACD,SAAO,IAAI,OAAO,MAAM,WAAW,KAAK,EAAE,IAAI,GAAG,EAAE,KAAK,YAAY;AACtE;AAWA,SAAS,iBAAiB,aAAa,KAAM,cAAc,IAAU;AACnE,MAAI,WAAW;AAEf,QAAM,KAAK,YAAY,YAAY;AACjC;AACA,QAAI;AACF,YAAM,MAAM,MAAM,MAAM,eAAe,EAAE,OAAO,WAAW,CAAC;AAC5D,UAAI,IAAI,IAAI;AACV,sBAAc,EAAE;AAChB,YAAI,KAAK,oCAA+B;AACxC,eAAO,SAAS,OAAO;AAAA,MACzB;AAAA,IACF,QAAQ;AAAA,IAER;AAEA,QAAI,YAAY,aAAa;AAC3B,oBAAc,EAAE;AAChB,UAAI,MAAM,8CAA8C;AAAA,IAC1D;AAAA,EACF,GAAG,UAAU;AACf;AASA,SAAS,oBAA0B;AACjC,QAAM,QAAQ,SAAS,iBAAkC,wBAAwB;AACjF,MAAI,KAAK,sCAAiC,MAAM,MAAM,gBAAgB;AACtE,QAAM,QAAQ,UAAQ;AACpB,UAAM,MAAM,IAAI,IAAI,KAAK,IAAI;AAC7B,QAAI,aAAa,IAAI,KAAK,OAAO,KAAK,IAAI,CAAC,CAAC;AAC5C,SAAK,OAAO,IAAI,SAAS;AAAA,EAC3B,CAAC;AACH;AAGA,IAAI;",
6
6
  "names": []
7
7
  }