nukejs 0.0.5 → 0.0.7

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 (42) hide show
  1. package/README.md +96 -8
  2. package/dist/Link.js +16 -0
  3. package/dist/Link.js.map +7 -0
  4. package/dist/build-common.d.ts +57 -80
  5. package/dist/build-common.js +156 -168
  6. package/dist/build-common.js.map +2 -2
  7. package/dist/build-node.d.ts +14 -0
  8. package/dist/build-node.js +50 -51
  9. package/dist/build-node.js.map +2 -2
  10. package/dist/build-vercel.d.ts +18 -0
  11. package/dist/build-vercel.js +76 -63
  12. package/dist/build-vercel.js.map +2 -2
  13. package/dist/builder.d.ts +10 -0
  14. package/dist/builder.js +31 -62
  15. package/dist/builder.js.map +3 -3
  16. package/dist/bundle.js +69 -6
  17. package/dist/bundle.js.map +2 -2
  18. package/dist/component-analyzer.d.ts +13 -10
  19. package/dist/component-analyzer.js +26 -17
  20. package/dist/component-analyzer.js.map +2 -2
  21. package/dist/hmr-bundle.js +17 -4
  22. package/dist/hmr-bundle.js.map +2 -2
  23. package/dist/html-store.d.ts +7 -0
  24. package/dist/html-store.js.map +2 -2
  25. package/dist/index.d.ts +2 -2
  26. package/dist/index.js +2 -2
  27. package/dist/index.js.map +1 -1
  28. package/dist/renderer.js +2 -7
  29. package/dist/renderer.js.map +2 -2
  30. package/dist/router.js +16 -4
  31. package/dist/router.js.map +2 -2
  32. package/dist/ssr.js +21 -4
  33. package/dist/ssr.js.map +2 -2
  34. package/dist/use-html.js +5 -1
  35. package/dist/use-html.js.map +2 -2
  36. package/dist/use-router.js +28 -0
  37. package/dist/use-router.js.map +7 -0
  38. package/package.json +1 -1
  39. package/dist/as-is/Link.tsx +0 -20
  40. package/dist/as-is/useRouter.ts +0 -33
  41. /package/dist/{as-is/Link.d.ts → Link.d.ts} +0 -0
  42. /package/dist/{as-is/useRouter.d.ts → use-router.d.ts} +0 -0
@@ -1,9 +1,40 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
- import crypto from "crypto";
3
+ import { randomBytes } from "node:crypto";
4
4
  import { fileURLToPath, pathToFileURL } from "url";
5
5
  import { build } from "esbuild";
6
6
  import { findClientComponentsInTree } from "./component-analyzer.js";
7
+ const NODE_BUILTINS = [
8
+ "node:*",
9
+ "http",
10
+ "https",
11
+ "fs",
12
+ "path",
13
+ "url",
14
+ "crypto",
15
+ "stream",
16
+ "buffer",
17
+ "events",
18
+ "util",
19
+ "os",
20
+ "net",
21
+ "tls",
22
+ "child_process",
23
+ "worker_threads",
24
+ "cluster",
25
+ "dgram",
26
+ "dns",
27
+ "readline",
28
+ "zlib",
29
+ "assert",
30
+ "module",
31
+ "perf_hooks",
32
+ "string_decoder",
33
+ "timers",
34
+ "async_hooks",
35
+ "v8",
36
+ "vm"
37
+ ];
7
38
  function walkFiles(dir, base = dir) {
8
39
  if (!fs.existsSync(dir)) return [];
9
40
  const results = [];
@@ -22,12 +53,14 @@ function analyzeFile(relPath, prefix = "api") {
22
53
  let segments = normalized.split("/");
23
54
  if (segments.at(-1) === "index") segments = segments.slice(0, -1);
24
55
  const paramNames = [];
56
+ const catchAllNames = [];
25
57
  const regexParts = [];
26
58
  let specificity = 0;
27
59
  for (const seg of segments) {
28
60
  const optCatchAll = seg.match(/^\[\[\.\.\.(.+)\]\]$/);
29
61
  if (optCatchAll) {
30
62
  paramNames.push(optCatchAll[1]);
63
+ catchAllNames.push(optCatchAll[1]);
31
64
  regexParts.push("(.*)");
32
65
  specificity += 1;
33
66
  continue;
@@ -35,10 +68,18 @@ function analyzeFile(relPath, prefix = "api") {
35
68
  const catchAll = seg.match(/^\[\.\.\.(.+)\]$/);
36
69
  if (catchAll) {
37
70
  paramNames.push(catchAll[1]);
71
+ catchAllNames.push(catchAll[1]);
38
72
  regexParts.push("(.+)");
39
73
  specificity += 10;
40
74
  continue;
41
75
  }
76
+ const optDynamic = seg.match(/^\[\[([^.][^\]]*)\]\]$/);
77
+ if (optDynamic) {
78
+ paramNames.push(optDynamic[1]);
79
+ regexParts.push("__OPT__([^/]*)");
80
+ specificity += 30;
81
+ continue;
82
+ }
42
83
  const dynamic = seg.match(/^\[(.+)\]$/);
43
84
  if (dynamic) {
44
85
  paramNames.push(dynamic[1]);
@@ -49,11 +90,26 @@ function analyzeFile(relPath, prefix = "api") {
49
90
  regexParts.push(seg.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
50
91
  specificity += 1e3;
51
92
  }
52
- const srcRegex = segments.length === 0 ? "^/$" : "^/" + regexParts.join("/") + "$";
93
+ let srcRegex;
94
+ if (segments.length === 0) {
95
+ srcRegex = "^/$";
96
+ } else {
97
+ let body = "";
98
+ for (let i = 0; i < regexParts.length; i++) {
99
+ const part = regexParts[i];
100
+ if (part.startsWith("__OPT__")) {
101
+ const cap = part.slice(7);
102
+ body += i === 0 ? cap : `(?:/${cap})?`;
103
+ } else {
104
+ body += (i === 0 ? "" : "/") + part;
105
+ }
106
+ }
107
+ srcRegex = "^/" + body + "$";
108
+ }
53
109
  const funcSegments = normalized.split("/");
54
110
  if (funcSegments.at(-1) === "index") funcSegments.pop();
55
111
  const funcPath = funcSegments.length === 0 ? `/${prefix}/_index` : `/${prefix}/` + funcSegments.join("/");
56
- return { srcRegex, paramNames, funcPath, specificity };
112
+ return { srcRegex, paramNames, catchAllNames, funcPath, specificity };
57
113
  }
58
114
  function isServerComponent(filePath) {
59
115
  const content = fs.readFileSync(filePath, "utf-8");
@@ -80,14 +136,18 @@ function findPageLayouts(routeFilePath, pagesDir) {
80
136
  }
81
137
  function extractDefaultExportName(filePath) {
82
138
  const content = fs.readFileSync(filePath, "utf-8");
83
- const match = content.match(/export\s+default\s+(?:function\s+)?(\w+)/);
84
- return match?.[1] ?? null;
139
+ let m = content.match(/export\s+default\s+(?:function\s+)?(\w+)/);
140
+ if (m?.[1]) return m[1];
141
+ m = content.match(/var\s+\w+_default\s*=\s*(\w+)/);
142
+ if (m?.[1]) return m[1];
143
+ m = content.match(/export\s*\{[^}]*\b(\w+)\s+as\s+default\b[^}]*\}/);
144
+ if (m?.[1] && !m[1].endsWith("_default")) return m[1];
145
+ return null;
85
146
  }
86
147
  function collectServerPages(pagesDir) {
87
148
  if (!fs.existsSync(pagesDir)) return [];
88
149
  return walkFiles(pagesDir).filter((relPath) => {
89
- const base = path.basename(relPath, path.extname(relPath));
90
- if (base === "layout") return false;
150
+ if (path.basename(relPath, path.extname(relPath)) === "layout") return false;
91
151
  return isServerComponent(path.join(pagesDir, relPath));
92
152
  }).map((relPath) => ({
93
153
  ...analyzeFile(relPath, "page"),
@@ -97,27 +157,21 @@ function collectServerPages(pagesDir) {
97
157
  function collectGlobalClientRegistry(serverPages, pagesDir) {
98
158
  const registry = /* @__PURE__ */ new Map();
99
159
  for (const { absPath } of serverPages) {
100
- for (const [id, p] of findClientComponentsInTree(absPath, pagesDir)) {
160
+ for (const [id, p] of findClientComponentsInTree(absPath, pagesDir))
101
161
  registry.set(id, p);
102
- }
103
- for (const layoutPath of findPageLayouts(absPath, pagesDir)) {
104
- for (const [id, p] of findClientComponentsInTree(layoutPath, pagesDir)) {
162
+ for (const layoutPath of findPageLayouts(absPath, pagesDir))
163
+ for (const [id, p] of findClientComponentsInTree(layoutPath, pagesDir))
105
164
  registry.set(id, p);
106
- }
107
- }
108
165
  }
109
166
  return registry;
110
167
  }
111
168
  function buildPerPageRegistry(absPath, layoutPaths, pagesDir) {
112
169
  const registry = /* @__PURE__ */ new Map();
113
- for (const [id, p] of findClientComponentsInTree(absPath, pagesDir)) {
170
+ for (const [id, p] of findClientComponentsInTree(absPath, pagesDir))
114
171
  registry.set(id, p);
115
- }
116
- for (const lp of layoutPaths) {
117
- for (const [id, p] of findClientComponentsInTree(lp, pagesDir)) {
172
+ for (const lp of layoutPaths)
173
+ for (const [id, p] of findClientComponentsInTree(lp, pagesDir))
118
174
  registry.set(id, p);
119
- }
120
- }
121
175
  const clientComponentNames = {};
122
176
  for (const [id, filePath] of registry) {
123
177
  const name = extractDefaultExportName(filePath);
@@ -131,30 +185,29 @@ async function buildPages(pagesDir, staticDir) {
131
185
  console.warn(`\u26A0 Pages found in ${pagesDir} but none are server components`);
132
186
  }
133
187
  if (serverPages.length === 0) return [];
134
- const globalClientRegistry = collectGlobalClientRegistry(serverPages, pagesDir);
135
- const prerenderedHtml = await bundleClientComponents(globalClientRegistry, pagesDir, staticDir);
136
- const prerenderedHtmlRecord = Object.fromEntries(prerenderedHtml);
188
+ const globalRegistry = collectGlobalClientRegistry(serverPages, pagesDir);
189
+ const prerenderedHtml = await bundleClientComponents(globalRegistry, pagesDir, staticDir);
190
+ const prerenderedRecord = Object.fromEntries(prerenderedHtml);
137
191
  const builtPages = [];
138
192
  for (const page of serverPages) {
139
- const { funcPath, absPath } = page;
140
- console.log(` building ${fs.existsSync(absPath) ? absPath : absPath} \u2192 ${funcPath} [page]`);
141
- const layoutPaths = findPageLayouts(absPath, pagesDir);
142
- const { registry, clientComponentNames } = buildPerPageRegistry(absPath, layoutPaths, pagesDir);
193
+ console.log(` building ${page.absPath} \u2192 ${page.funcPath} [page]`);
194
+ const layoutPaths = findPageLayouts(page.absPath, pagesDir);
195
+ const { registry, clientComponentNames } = buildPerPageRegistry(page.absPath, layoutPaths, pagesDir);
143
196
  const bundleText = await bundlePageHandler({
144
- absPath,
197
+ absPath: page.absPath,
145
198
  pagesDir,
146
199
  clientComponentNames,
147
200
  allClientIds: [...registry.keys()],
148
201
  layoutPaths,
149
- prerenderedHtml: prerenderedHtmlRecord
202
+ prerenderedHtml: prerenderedRecord,
203
+ catchAllNames: page.catchAllNames
150
204
  });
151
205
  builtPages.push({ ...page, bundleText });
152
206
  }
153
207
  return builtPages;
154
208
  }
155
209
  function makeApiAdapterSource(handlerFilename) {
156
- return `
157
- import type { IncomingMessage, ServerResponse } from 'http';
210
+ return `import type { IncomingMessage, ServerResponse } from 'http';
158
211
  import * as mod from ${JSON.stringify("./" + handlerFilename)};
159
212
 
160
213
  function enhance(res: ServerResponse) {
@@ -197,7 +250,7 @@ export default async function handler(req: IncomingMessage, res: ServerResponse)
197
250
  }
198
251
  await fn(apiReq, apiRes);
199
252
  }
200
- `.trimStart();
253
+ `;
201
254
  }
202
255
  function makePageAdapterSource(opts) {
203
256
  const {
@@ -206,32 +259,21 @@ function makePageAdapterSource(opts) {
206
259
  clientComponentNames,
207
260
  allClientIds,
208
261
  layoutArrayItems,
209
- prerenderedHtml
262
+ prerenderedHtml,
263
+ catchAllNames
210
264
  } = opts;
211
- return `
212
- import type { IncomingMessage, ServerResponse } from 'http';
265
+ return `import type { IncomingMessage, ServerResponse } from 'http';
266
+ import { createElement as __createElement__ } from 'react';
267
+ import { renderToString as __renderToString__ } from 'react-dom/server';
213
268
  import * as __page__ from ${pageImport};
214
269
  ${layoutImports}
215
270
 
216
- // \u2500\u2500\u2500 Pre-built client component registry \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
217
- // Computed at BUILD TIME from the import tree. Source files are not deployed,
218
- // so we must not read them with fs.readFileSync at runtime.
219
- // Key: default-export function name \u2192 Value: stable content-hash ID
220
271
  const CLIENT_COMPONENTS: Record<string, string> = ${JSON.stringify(clientComponentNames)};
221
-
222
- // All client component IDs reachable from this page (page + layouts).
223
- // Sent to initRuntime so the browser pre-loads all bundles for SPA navigation.
224
272
  const ALL_CLIENT_IDS: string[] = ${JSON.stringify(allClientIds)};
225
-
226
- // Pre-rendered HTML for each client component, produced at BUILD TIME by
227
- // renderToString with default props. Used directly in the span wrapper so
228
- // the server response contains real markup and React hydration never sees a
229
- // mismatch. No react-dom/server is needed at runtime.
230
273
  const PRERENDERED_HTML: Record<string, string> = ${JSON.stringify(prerenderedHtml)};
274
+ const CATCH_ALL_NAMES = new Set(${JSON.stringify(catchAllNames)});
231
275
 
232
- // \u2500\u2500\u2500 html-store (inlined \u2014 no external refs) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
233
- // Uses the same globalThis Symbol key as html-store.ts so any useHtml() call
234
- // (however imported) writes into the same store that runWithHtmlStore reads.
276
+ // \u2500\u2500\u2500 html-store (inlined) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
235
277
  type TitleValue = string | ((prev: string) => string);
236
278
  interface HtmlStore {
237
279
  titleOps: TitleValue[];
@@ -243,11 +285,10 @@ interface HtmlStore {
243
285
  style: { content?: string; media?: string }[];
244
286
  }
245
287
  const __STORE_KEY__ = Symbol.for('__nukejs_html_store__');
246
- function __getStore(): HtmlStore | null { return (globalThis as any)[__STORE_KEY__] ?? null; }
247
- function __setStore(s: HtmlStore | null): void { (globalThis as any)[__STORE_KEY__] = s; }
248
- function __emptyStore(): HtmlStore {
249
- return { titleOps: [], htmlAttrs: {}, bodyAttrs: {}, meta: [], link: [], script: [], style: [] };
250
- }
288
+ const __getStore = (): HtmlStore | null => (globalThis as any)[__STORE_KEY__] ?? null;
289
+ const __setStore = (s: HtmlStore | null): void => { (globalThis as any)[__STORE_KEY__] = s; };
290
+ const __emptyStore = (): HtmlStore =>
291
+ ({ titleOps: [], htmlAttrs: {}, bodyAttrs: {}, meta: [], link: [], script: [], style: [] });
251
292
  async function runWithHtmlStore(fn: () => Promise<void>): Promise<HtmlStore> {
252
293
  __setStore(__emptyStore());
253
294
  try { await fn(); return { ...(__getStore() ?? __emptyStore()) }; }
@@ -280,59 +321,44 @@ function openTag(tag: string, attrs: Record<string, string | undefined>): string
280
321
  const s = renderAttrs(attrs as Record<string, string | boolean | undefined>);
281
322
  return s ? \`<\${tag} \${s}>\` : \`<\${tag}>\`;
282
323
  }
283
- function metaKey(k: string): string { return k === 'httpEquiv' ? 'http-equiv' : k; }
284
- function linkKey(k: string): string {
285
- if (k === 'hrefLang') return 'hreflang';
286
- if (k === 'crossOrigin') return 'crossorigin';
287
- return k;
288
- }
289
324
  function renderMetaTag(tag: Record<string, string | undefined>): string {
325
+ const key = (k: string) => k === 'httpEquiv' ? 'http-equiv' : k;
290
326
  const attrs: Record<string, string | undefined> = {};
291
- for (const [k, v] of Object.entries(tag)) if (v !== undefined) attrs[metaKey(k)] = v;
327
+ for (const [k, v] of Object.entries(tag)) if (v !== undefined) attrs[key(k)] = v;
292
328
  return \` <meta \${renderAttrs(attrs as any)} />\`;
293
329
  }
294
330
  function renderLinkTag(tag: Record<string, string | undefined>): string {
331
+ const key = (k: string) => k === 'hrefLang' ? 'hreflang' : k === 'crossOrigin' ? 'crossorigin' : k;
295
332
  const attrs: Record<string, string | undefined> = {};
296
- for (const [k, v] of Object.entries(tag)) if (v !== undefined) attrs[linkKey(k)] = v;
333
+ for (const [k, v] of Object.entries(tag)) if (v !== undefined) attrs[key(k)] = v;
297
334
  return \` <link \${renderAttrs(attrs as any)} />\`;
298
335
  }
299
336
  function renderScriptTag(tag: any): string {
300
- const attrs: Record<string, any> = {
301
- src: tag.src, type: tag.type, crossorigin: tag.crossOrigin,
302
- integrity: tag.integrity, defer: tag.defer, async: tag.async, nomodule: tag.noModule,
303
- };
304
- const s = renderAttrs(attrs);
305
- const open = s ? \`<script \${s}>\` : '<script>';
306
- return \` \${open}\${tag.src ? '' : (tag.content ?? '')}</script>\`;
337
+ const s = renderAttrs({ src: tag.src, type: tag.type, crossorigin: tag.crossOrigin,
338
+ integrity: tag.integrity, defer: tag.defer, async: tag.async, nomodule: tag.noModule });
339
+ return \` \${s ? \`<script \${s}>\` : '<script>'}\${tag.src ? '' : (tag.content ?? '')}</script>\`;
307
340
  }
308
341
  function renderStyleTag(tag: any): string {
309
342
  const media = tag.media ? \` media="\${escapeAttr(tag.media)}"\` : '';
310
343
  return \` <style\${media}>\${tag.content ?? ''}</style>\`;
311
344
  }
312
345
 
313
- // \u2500\u2500\u2500 Void element set \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
346
+ // \u2500\u2500\u2500 Renderer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
314
347
  const VOID_TAGS = new Set([
315
348
  'area','base','br','col','embed','hr','img','input',
316
349
  'link','meta','param','source','track','wbr',
317
350
  ]);
318
351
 
319
- // \u2500\u2500\u2500 Prop serialization \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
320
- // Converts React element trees in props to a JSON-safe format that
321
- // bundle.ts / initRuntime can reconstruct on the client.
322
352
  function serializeProps(value: any): any {
323
353
  if (value == null || typeof value !== 'object') return value;
324
354
  if (typeof value === 'function') return undefined;
325
- if (Array.isArray(value)) {
326
- return value.map(serializeProps).filter((v: any) => v !== undefined);
327
- }
355
+ if (Array.isArray(value)) return value.map(serializeProps).filter((v: any) => v !== undefined);
328
356
  if ((value as any).$$typeof) {
329
- const { type, props: elProps } = value as any;
330
- if (typeof type === 'string') {
331
- return { __re: 'html', tag: type, props: serializeProps(elProps) };
332
- }
357
+ const { type, props: p } = value as any;
358
+ if (typeof type === 'string') return { __re: 'html', tag: type, props: serializeProps(p) };
333
359
  if (typeof type === 'function') {
334
360
  const cid = CLIENT_COMPONENTS[type.name];
335
- if (cid) return { __re: 'client', componentId: cid, props: serializeProps(elProps) };
361
+ if (cid) return { __re: 'client', componentId: cid, props: serializeProps(p) };
336
362
  }
337
363
  return undefined;
338
364
  }
@@ -344,24 +370,16 @@ function serializeProps(value: any): any {
344
370
  return out;
345
371
  }
346
372
 
347
- // \u2500\u2500\u2500 Async recursive renderer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
348
- // Handles: null/undefined/boolean \u2192 '', strings/numbers \u2192 escaped text, arrays,
349
- // Fragment, void/non-void HTML elements, class components, sync + async functions.
350
- // Client components \u2192 <span data-hydrate-id="\u2026"> markers for browser hydration.
351
373
  async function renderNode(node: any, hydrated: Set<string>): Promise<string> {
352
374
  if (node == null || typeof node === 'boolean') return '';
353
375
  if (typeof node === 'string') return escapeHtml(node);
354
376
  if (typeof node === 'number') return String(node);
355
- if (Array.isArray(node)) {
356
- return (await Promise.all(node.map(n => renderNode(n, hydrated)))).join('');
357
- }
377
+ if (Array.isArray(node)) return (await Promise.all(node.map(n => renderNode(n, hydrated)))).join('');
358
378
 
359
379
  const { type, props } = node as { type: any; props: Record<string, any> };
360
380
  if (!type) return '';
361
381
 
362
- if (type === Symbol.for('react.fragment')) {
363
- return renderNode(props?.children ?? null, hydrated);
364
- }
382
+ if (type === Symbol.for('react.fragment')) return renderNode(props?.children ?? null, hydrated);
365
383
 
366
384
  if (typeof type === 'string') {
367
385
  const { children, dangerouslySetInnerHTML, ...rest } = props || {};
@@ -392,21 +410,16 @@ async function renderNode(node: any, hydrated: Set<string>): Promise<string> {
392
410
  if (clientId) {
393
411
  hydrated.add(clientId);
394
412
  const serializedProps = serializeProps(props ?? {});
395
- // Render with actual props so children/content appear in the SSR HTML.
396
- // Fall back to the build-time pre-rendered HTML if the component throws
397
- // (e.g. it references browser-only APIs during render).
398
413
  let ssrHtml: string;
399
414
  try {
400
- const result = await (type as Function)(props || {});
401
- ssrHtml = await renderNode(result, new Set());
415
+ ssrHtml = __renderToString__(__createElement__(type as any, props || {}));
402
416
  } catch {
403
417
  ssrHtml = PRERENDERED_HTML[clientId] ?? '';
404
418
  }
405
419
  return \`<span data-hydrate-id="\${clientId}" data-hydrate-props="\${escapeHtml(JSON.stringify(serializedProps))}">\${ssrHtml}</span>\`;
406
420
  }
407
421
  const instance = type.prototype?.isReactComponent ? new (type as any)(props) : null;
408
- const result = instance ? instance.render() : await (type as Function)(props || {});
409
- return renderNode(result, hydrated);
422
+ return renderNode(instance ? instance.render() : await (type as Function)(props || {}), hydrated);
410
423
  }
411
424
 
412
425
  return '';
@@ -417,9 +430,8 @@ const LAYOUT_COMPONENTS: Array<(props: any) => any> = [${layoutArrayItems}];
417
430
 
418
431
  function wrapWithLayouts(element: any): any {
419
432
  let el = element;
420
- for (let i = LAYOUT_COMPONENTS.length - 1; i >= 0; i--) {
433
+ for (let i = LAYOUT_COMPONENTS.length - 1; i >= 0; i--)
421
434
  el = { type: LAYOUT_COMPONENTS[i], props: { children: el }, key: null, ref: null };
422
- }
423
435
  return el;
424
436
  }
425
437
 
@@ -427,40 +439,43 @@ function wrapWithLayouts(element: any): any {
427
439
  export default async function handler(req: IncomingMessage, res: ServerResponse): Promise<void> {
428
440
  try {
429
441
  const parsed = new URL(req.url || '/', 'http://localhost');
430
- const params: Record<string, string> = {};
431
- parsed.searchParams.forEach((v, k) => { params[k] = v; });
442
+ const params: Record<string, string | string[]> = {};
443
+ parsed.searchParams.forEach((_, k) => {
444
+ params[k] = CATCH_ALL_NAMES.has(k)
445
+ ? parsed.searchParams.getAll(k)
446
+ : parsed.searchParams.get(k) as string;
447
+ });
432
448
  const url = req.url || '/';
433
449
 
434
450
  const hydrated = new Set<string>();
435
- const pageElement = { type: __page__.default, props: params as any, key: null, ref: null };
436
- const wrapped = wrapWithLayouts(pageElement);
451
+ const wrapped = wrapWithLayouts({ type: __page__.default, props: params as any, key: null, ref: null });
437
452
 
438
453
  let appHtml = '';
439
- const store = await runWithHtmlStore(async () => {
440
- appHtml = await renderNode(wrapped, hydrated);
441
- });
454
+ const store = await runWithHtmlStore(async () => { appHtml = await renderNode(wrapped, hydrated); });
442
455
 
443
- const pageTitle = resolveTitle(store.titleOps, 'SSR App');
444
- const headLines: string[] = [
456
+ const pageTitle = resolveTitle(store.titleOps, 'NukeJS');
457
+ const headScripts = store.script.filter((s: any) => (s.position ?? 'head') === 'head');
458
+ const bodyScripts = store.script.filter((s: any) => s.position === 'body');
459
+ const headLines = [
445
460
  ' <meta charset="utf-8" />',
446
461
  ' <meta name="viewport" content="width=device-width, initial-scale=1" />',
447
462
  \` <title>\${escapeHtml(pageTitle)}</title>\`,
448
- ...(store.meta.length || store.link.length || store.style.length || store.script.length ? [
463
+ ...(store.meta.length || store.link.length || store.style.length || headScripts.length ? [
449
464
  ' <!--n-head-->',
450
465
  ...store.meta.map(renderMetaTag),
451
466
  ...store.link.map(renderLinkTag),
452
467
  ...store.style.map(renderStyleTag),
453
- ...store.script.map(renderScriptTag),
468
+ ...headScripts.map(renderScriptTag),
454
469
  ' <!--/n-head-->',
455
470
  ] : []),
456
471
  ];
472
+ const bodyScriptLines = bodyScripts.length
473
+ ? [' <!--n-body-scripts-->', ...bodyScripts.map(renderScriptTag), ' <!--/n-body-scripts-->']
474
+ : [];
475
+ const bodyScriptsHtml = bodyScriptLines.length ? '\\n' + bodyScriptLines.join('\\n') + '\\n' : '';
457
476
 
458
477
  const runtimeData = JSON.stringify({
459
- hydrateIds: [...hydrated],
460
- allIds: ALL_CLIENT_IDS,
461
- url,
462
- params,
463
- debug: 'silent',
478
+ hydrateIds: [...hydrated], allIds: ALL_CLIENT_IDS, url, params, debug: 'silent',
464
479
  }).replace(/</g, '\\u003c').replace(/>/g, '\\u003e').replace(/&/g, '\\u0026');
465
480
 
466
481
  const html = \`<!DOCTYPE html>
@@ -489,7 +504,7 @@ export default async function handler(req: IncomingMessage, res: ServerResponse)
489
504
  const data = JSON.parse(document.getElementById('__n_data').textContent);
490
505
  await initRuntime(data);
491
506
  </script>
492
- </body>
507
+ \${bodyScriptsHtml}</body>
493
508
  </html>\`;
494
509
 
495
510
  res.statusCode = 200;
@@ -502,10 +517,10 @@ export default async function handler(req: IncomingMessage, res: ServerResponse)
502
517
  res.end('Internal Server Error');
503
518
  }
504
519
  }
505
- `.trimStart();
520
+ `;
506
521
  }
507
522
  async function bundleApiHandler(absPath) {
508
- const adapterName = `_api_adapter_${crypto.randomBytes(4).toString("hex")}.ts`;
523
+ const adapterName = `_api_adapter_${randomBytes(4).toString("hex")}.ts`;
509
524
  const adapterPath = path.join(path.dirname(absPath), adapterName);
510
525
  fs.writeFileSync(adapterPath, makeApiAdapterSource(path.basename(absPath)));
511
526
  let text;
@@ -526,23 +541,28 @@ async function bundleApiHandler(absPath) {
526
541
  return text;
527
542
  }
528
543
  async function bundlePageHandler(opts) {
529
- const { absPath, clientComponentNames, allClientIds, layoutPaths, prerenderedHtml } = opts;
530
- const adapterName = `_page_adapter_${crypto.randomBytes(4).toString("hex")}.ts`;
544
+ const {
545
+ absPath,
546
+ clientComponentNames,
547
+ allClientIds,
548
+ layoutPaths,
549
+ prerenderedHtml,
550
+ catchAllNames
551
+ } = opts;
531
552
  const adapterDir = path.dirname(absPath);
532
- const adapterPath = path.join(adapterDir, adapterName);
553
+ const adapterPath = path.join(adapterDir, `_page_adapter_${randomBytes(4).toString("hex")}.ts`);
533
554
  const layoutImports = layoutPaths.map((lp, i) => {
534
555
  const rel = path.relative(adapterDir, lp).replace(/\\/g, "/");
535
- const importPath = rel.startsWith(".") ? rel : "./" + rel;
536
- return `import __layout_${i}__ from ${JSON.stringify(importPath)};`;
556
+ return `import __layout_${i}__ from ${JSON.stringify(rel.startsWith(".") ? rel : "./" + rel)};`;
537
557
  }).join("\n");
538
- const layoutArrayItems = layoutPaths.map((_, i) => `__layout_${i}__`).join(", ");
539
558
  fs.writeFileSync(adapterPath, makePageAdapterSource({
540
559
  pageImport: JSON.stringify("./" + path.basename(absPath)),
541
560
  layoutImports,
542
561
  clientComponentNames,
543
562
  allClientIds,
544
- layoutArrayItems,
545
- prerenderedHtml
563
+ layoutArrayItems: layoutPaths.map((_, i) => `__layout_${i}__`).join(", "),
564
+ prerenderedHtml,
565
+ catchAllNames
546
566
  }));
547
567
  let text;
548
568
  try {
@@ -553,38 +573,8 @@ async function bundlePageHandler(opts) {
553
573
  platform: "node",
554
574
  target: "node20",
555
575
  jsx: "automatic",
556
- external: [
557
- // Node built-ins only — all npm packages (react, nukejs, …) are inlined
558
- "node:*",
559
- "http",
560
- "https",
561
- "fs",
562
- "path",
563
- "url",
564
- "crypto",
565
- "stream",
566
- "buffer",
567
- "events",
568
- "util",
569
- "os",
570
- "net",
571
- "tls",
572
- "child_process",
573
- "worker_threads",
574
- "cluster",
575
- "dgram",
576
- "dns",
577
- "readline",
578
- "zlib",
579
- "assert",
580
- "module",
581
- "perf_hooks",
582
- "string_decoder",
583
- "timers",
584
- "async_hooks",
585
- "v8",
586
- "vm"
587
- ],
576
+ packages: "external",
577
+ external: NODE_BUILTINS,
588
578
  define: { "process.env.NODE_ENV": '"production"' },
589
579
  write: false
590
580
  });
@@ -615,7 +605,7 @@ async function bundleClientComponents(globalRegistry, pagesDir, staticDir) {
615
605
  fs.writeFileSync(path.join(outDir, `${id}.js`), browserResult.outputFiles[0].text);
616
606
  const ssrTmp = path.join(
617
607
  path.dirname(filePath),
618
- `_ssr_${id}_${crypto.randomBytes(4).toString("hex")}.mjs`
608
+ `_ssr_${id}_${randomBytes(4).toString("hex")}.mjs`
619
609
  );
620
610
  try {
621
611
  const ssrResult = await build({
@@ -635,7 +625,7 @@ async function bundleClientComponents(globalRegistry, pagesDir, staticDir) {
635
625
  const { renderToString } = await import("react-dom/server");
636
626
  prerendered.set(id, renderToString(createElement(Component, {})));
637
627
  console.log(` prerendered ${id}`);
638
- } catch (e) {
628
+ } catch {
639
629
  prerendered.set(id, "");
640
630
  } finally {
641
631
  if (fs.existsSync(ssrTmp)) fs.unlinkSync(ssrTmp);
@@ -660,7 +650,6 @@ import React, {
660
650
  import { jsx, jsxs } from 'react/jsx-runtime';
661
651
  import { hydrateRoot, createRoot } from 'react-dom/client';
662
652
  export { initRuntime, setupLocationChangeMonitor } from "./${bundleFile}.js";
663
-
664
653
  export {
665
654
  useState, useEffect, useContext, useReducer, useCallback, useMemo,
666
655
  useRef, useImperativeHandle, useLayoutEffect, useDebugValue,
@@ -695,19 +684,18 @@ function copyPublicFiles(publicDir, destDir) {
695
684
  (function walk(src, dest) {
696
685
  fs.mkdirSync(dest, { recursive: true });
697
686
  for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
698
- const srcPath = path.join(src, entry.name);
699
- const destPath = path.join(dest, entry.name);
687
+ const s = path.join(src, entry.name);
688
+ const d = path.join(dest, entry.name);
700
689
  if (entry.isDirectory()) {
701
- walk(srcPath, destPath);
690
+ walk(s, d);
702
691
  } else {
703
- fs.copyFileSync(srcPath, destPath);
692
+ fs.copyFileSync(s, d);
704
693
  count++;
705
694
  }
706
695
  }
707
696
  })(publicDir, destDir);
708
- if (count > 0) {
697
+ if (count > 0)
709
698
  console.log(` copied ${count} public file(s) \u2192 ${path.relative(process.cwd(), destDir)}/`);
710
- }
711
699
  }
712
700
  export {
713
701
  analyzeFile,