nukejs 0.0.5 → 0.0.6

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.
@@ -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,12 @@ 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
+ return content.match(/export\s+default\s+(?:function\s+)?(\w+)/)?.[1] ?? null;
85
140
  }
86
141
  function collectServerPages(pagesDir) {
87
142
  if (!fs.existsSync(pagesDir)) return [];
88
143
  return walkFiles(pagesDir).filter((relPath) => {
89
- const base = path.basename(relPath, path.extname(relPath));
90
- if (base === "layout") return false;
144
+ if (path.basename(relPath, path.extname(relPath)) === "layout") return false;
91
145
  return isServerComponent(path.join(pagesDir, relPath));
92
146
  }).map((relPath) => ({
93
147
  ...analyzeFile(relPath, "page"),
@@ -97,27 +151,21 @@ function collectServerPages(pagesDir) {
97
151
  function collectGlobalClientRegistry(serverPages, pagesDir) {
98
152
  const registry = /* @__PURE__ */ new Map();
99
153
  for (const { absPath } of serverPages) {
100
- for (const [id, p] of findClientComponentsInTree(absPath, pagesDir)) {
154
+ for (const [id, p] of findClientComponentsInTree(absPath, pagesDir))
101
155
  registry.set(id, p);
102
- }
103
- for (const layoutPath of findPageLayouts(absPath, pagesDir)) {
104
- for (const [id, p] of findClientComponentsInTree(layoutPath, pagesDir)) {
156
+ for (const layoutPath of findPageLayouts(absPath, pagesDir))
157
+ for (const [id, p] of findClientComponentsInTree(layoutPath, pagesDir))
105
158
  registry.set(id, p);
106
- }
107
- }
108
159
  }
109
160
  return registry;
110
161
  }
111
162
  function buildPerPageRegistry(absPath, layoutPaths, pagesDir) {
112
163
  const registry = /* @__PURE__ */ new Map();
113
- for (const [id, p] of findClientComponentsInTree(absPath, pagesDir)) {
164
+ for (const [id, p] of findClientComponentsInTree(absPath, pagesDir))
114
165
  registry.set(id, p);
115
- }
116
- for (const lp of layoutPaths) {
117
- for (const [id, p] of findClientComponentsInTree(lp, pagesDir)) {
166
+ for (const lp of layoutPaths)
167
+ for (const [id, p] of findClientComponentsInTree(lp, pagesDir))
118
168
  registry.set(id, p);
119
- }
120
- }
121
169
  const clientComponentNames = {};
122
170
  for (const [id, filePath] of registry) {
123
171
  const name = extractDefaultExportName(filePath);
@@ -131,30 +179,29 @@ async function buildPages(pagesDir, staticDir) {
131
179
  console.warn(`\u26A0 Pages found in ${pagesDir} but none are server components`);
132
180
  }
133
181
  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);
182
+ const globalRegistry = collectGlobalClientRegistry(serverPages, pagesDir);
183
+ const prerenderedHtml = await bundleClientComponents(globalRegistry, pagesDir, staticDir);
184
+ const prerenderedRecord = Object.fromEntries(prerenderedHtml);
137
185
  const builtPages = [];
138
186
  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);
187
+ console.log(` building ${page.absPath} \u2192 ${page.funcPath} [page]`);
188
+ const layoutPaths = findPageLayouts(page.absPath, pagesDir);
189
+ const { registry, clientComponentNames } = buildPerPageRegistry(page.absPath, layoutPaths, pagesDir);
143
190
  const bundleText = await bundlePageHandler({
144
- absPath,
191
+ absPath: page.absPath,
145
192
  pagesDir,
146
193
  clientComponentNames,
147
194
  allClientIds: [...registry.keys()],
148
195
  layoutPaths,
149
- prerenderedHtml: prerenderedHtmlRecord
196
+ prerenderedHtml: prerenderedRecord,
197
+ catchAllNames: page.catchAllNames
150
198
  });
151
199
  builtPages.push({ ...page, bundleText });
152
200
  }
153
201
  return builtPages;
154
202
  }
155
203
  function makeApiAdapterSource(handlerFilename) {
156
- return `
157
- import type { IncomingMessage, ServerResponse } from 'http';
204
+ return `import type { IncomingMessage, ServerResponse } from 'http';
158
205
  import * as mod from ${JSON.stringify("./" + handlerFilename)};
159
206
 
160
207
  function enhance(res: ServerResponse) {
@@ -197,7 +244,7 @@ export default async function handler(req: IncomingMessage, res: ServerResponse)
197
244
  }
198
245
  await fn(apiReq, apiRes);
199
246
  }
200
- `.trimStart();
247
+ `;
201
248
  }
202
249
  function makePageAdapterSource(opts) {
203
250
  const {
@@ -206,32 +253,19 @@ function makePageAdapterSource(opts) {
206
253
  clientComponentNames,
207
254
  allClientIds,
208
255
  layoutArrayItems,
209
- prerenderedHtml
256
+ prerenderedHtml,
257
+ catchAllNames
210
258
  } = opts;
211
- return `
212
- import type { IncomingMessage, ServerResponse } from 'http';
259
+ return `import type { IncomingMessage, ServerResponse } from 'http';
213
260
  import * as __page__ from ${pageImport};
214
261
  ${layoutImports}
215
262
 
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
263
  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
264
  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
265
  const PRERENDERED_HTML: Record<string, string> = ${JSON.stringify(prerenderedHtml)};
266
+ const CATCH_ALL_NAMES = new Set(${JSON.stringify(catchAllNames)});
231
267
 
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.
268
+ // \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
269
  type TitleValue = string | ((prev: string) => string);
236
270
  interface HtmlStore {
237
271
  titleOps: TitleValue[];
@@ -243,11 +277,10 @@ interface HtmlStore {
243
277
  style: { content?: string; media?: string }[];
244
278
  }
245
279
  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
- }
280
+ const __getStore = (): HtmlStore | null => (globalThis as any)[__STORE_KEY__] ?? null;
281
+ const __setStore = (s: HtmlStore | null): void => { (globalThis as any)[__STORE_KEY__] = s; };
282
+ const __emptyStore = (): HtmlStore =>
283
+ ({ titleOps: [], htmlAttrs: {}, bodyAttrs: {}, meta: [], link: [], script: [], style: [] });
251
284
  async function runWithHtmlStore(fn: () => Promise<void>): Promise<HtmlStore> {
252
285
  __setStore(__emptyStore());
253
286
  try { await fn(); return { ...(__getStore() ?? __emptyStore()) }; }
@@ -280,59 +313,44 @@ function openTag(tag: string, attrs: Record<string, string | undefined>): string
280
313
  const s = renderAttrs(attrs as Record<string, string | boolean | undefined>);
281
314
  return s ? \`<\${tag} \${s}>\` : \`<\${tag}>\`;
282
315
  }
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
316
  function renderMetaTag(tag: Record<string, string | undefined>): string {
317
+ const key = (k: string) => k === 'httpEquiv' ? 'http-equiv' : k;
290
318
  const attrs: Record<string, string | undefined> = {};
291
- for (const [k, v] of Object.entries(tag)) if (v !== undefined) attrs[metaKey(k)] = v;
319
+ for (const [k, v] of Object.entries(tag)) if (v !== undefined) attrs[key(k)] = v;
292
320
  return \` <meta \${renderAttrs(attrs as any)} />\`;
293
321
  }
294
322
  function renderLinkTag(tag: Record<string, string | undefined>): string {
323
+ const key = (k: string) => k === 'hrefLang' ? 'hreflang' : k === 'crossOrigin' ? 'crossorigin' : k;
295
324
  const attrs: Record<string, string | undefined> = {};
296
- for (const [k, v] of Object.entries(tag)) if (v !== undefined) attrs[linkKey(k)] = v;
325
+ for (const [k, v] of Object.entries(tag)) if (v !== undefined) attrs[key(k)] = v;
297
326
  return \` <link \${renderAttrs(attrs as any)} />\`;
298
327
  }
299
328
  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>\`;
329
+ const s = renderAttrs({ src: tag.src, type: tag.type, crossorigin: tag.crossOrigin,
330
+ integrity: tag.integrity, defer: tag.defer, async: tag.async, nomodule: tag.noModule });
331
+ return \` \${s ? \`<script \${s}>\` : '<script>'}\${tag.src ? '' : (tag.content ?? '')}</script>\`;
307
332
  }
308
333
  function renderStyleTag(tag: any): string {
309
334
  const media = tag.media ? \` media="\${escapeAttr(tag.media)}"\` : '';
310
335
  return \` <style\${media}>\${tag.content ?? ''}</style>\`;
311
336
  }
312
337
 
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
338
+ // \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
339
  const VOID_TAGS = new Set([
315
340
  'area','base','br','col','embed','hr','img','input',
316
341
  'link','meta','param','source','track','wbr',
317
342
  ]);
318
343
 
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
344
  function serializeProps(value: any): any {
323
345
  if (value == null || typeof value !== 'object') return value;
324
346
  if (typeof value === 'function') return undefined;
325
- if (Array.isArray(value)) {
326
- return value.map(serializeProps).filter((v: any) => v !== undefined);
327
- }
347
+ if (Array.isArray(value)) return value.map(serializeProps).filter((v: any) => v !== undefined);
328
348
  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
- }
349
+ const { type, props: p } = value as any;
350
+ if (typeof type === 'string') return { __re: 'html', tag: type, props: serializeProps(p) };
333
351
  if (typeof type === 'function') {
334
352
  const cid = CLIENT_COMPONENTS[type.name];
335
- if (cid) return { __re: 'client', componentId: cid, props: serializeProps(elProps) };
353
+ if (cid) return { __re: 'client', componentId: cid, props: serializeProps(p) };
336
354
  }
337
355
  return undefined;
338
356
  }
@@ -344,24 +362,16 @@ function serializeProps(value: any): any {
344
362
  return out;
345
363
  }
346
364
 
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
365
  async function renderNode(node: any, hydrated: Set<string>): Promise<string> {
352
366
  if (node == null || typeof node === 'boolean') return '';
353
367
  if (typeof node === 'string') return escapeHtml(node);
354
368
  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
- }
369
+ if (Array.isArray(node)) return (await Promise.all(node.map(n => renderNode(n, hydrated)))).join('');
358
370
 
359
371
  const { type, props } = node as { type: any; props: Record<string, any> };
360
372
  if (!type) return '';
361
373
 
362
- if (type === Symbol.for('react.fragment')) {
363
- return renderNode(props?.children ?? null, hydrated);
364
- }
374
+ if (type === Symbol.for('react.fragment')) return renderNode(props?.children ?? null, hydrated);
365
375
 
366
376
  if (typeof type === 'string') {
367
377
  const { children, dangerouslySetInnerHTML, ...rest } = props || {};
@@ -392,21 +402,16 @@ async function renderNode(node: any, hydrated: Set<string>): Promise<string> {
392
402
  if (clientId) {
393
403
  hydrated.add(clientId);
394
404
  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
405
  let ssrHtml: string;
399
406
  try {
400
- const result = await (type as Function)(props || {});
401
- ssrHtml = await renderNode(result, new Set());
407
+ ssrHtml = await renderNode(await (type as Function)(props || {}), new Set());
402
408
  } catch {
403
409
  ssrHtml = PRERENDERED_HTML[clientId] ?? '';
404
410
  }
405
411
  return \`<span data-hydrate-id="\${clientId}" data-hydrate-props="\${escapeHtml(JSON.stringify(serializedProps))}">\${ssrHtml}</span>\`;
406
412
  }
407
413
  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);
414
+ return renderNode(instance ? instance.render() : await (type as Function)(props || {}), hydrated);
410
415
  }
411
416
 
412
417
  return '';
@@ -417,9 +422,8 @@ const LAYOUT_COMPONENTS: Array<(props: any) => any> = [${layoutArrayItems}];
417
422
 
418
423
  function wrapWithLayouts(element: any): any {
419
424
  let el = element;
420
- for (let i = LAYOUT_COMPONENTS.length - 1; i >= 0; i--) {
425
+ for (let i = LAYOUT_COMPONENTS.length - 1; i >= 0; i--)
421
426
  el = { type: LAYOUT_COMPONENTS[i], props: { children: el }, key: null, ref: null };
422
- }
423
427
  return el;
424
428
  }
425
429
 
@@ -427,21 +431,22 @@ function wrapWithLayouts(element: any): any {
427
431
  export default async function handler(req: IncomingMessage, res: ServerResponse): Promise<void> {
428
432
  try {
429
433
  const parsed = new URL(req.url || '/', 'http://localhost');
430
- const params: Record<string, string> = {};
431
- parsed.searchParams.forEach((v, k) => { params[k] = v; });
434
+ const params: Record<string, string | string[]> = {};
435
+ parsed.searchParams.forEach((_, k) => {
436
+ params[k] = CATCH_ALL_NAMES.has(k)
437
+ ? parsed.searchParams.getAll(k)
438
+ : parsed.searchParams.get(k) as string;
439
+ });
432
440
  const url = req.url || '/';
433
441
 
434
442
  const hydrated = new Set<string>();
435
- const pageElement = { type: __page__.default, props: params as any, key: null, ref: null };
436
- const wrapped = wrapWithLayouts(pageElement);
443
+ const wrapped = wrapWithLayouts({ type: __page__.default, props: params as any, key: null, ref: null });
437
444
 
438
445
  let appHtml = '';
439
- const store = await runWithHtmlStore(async () => {
440
- appHtml = await renderNode(wrapped, hydrated);
441
- });
446
+ const store = await runWithHtmlStore(async () => { appHtml = await renderNode(wrapped, hydrated); });
442
447
 
443
- const pageTitle = resolveTitle(store.titleOps, 'SSR App');
444
- const headLines: string[] = [
448
+ const pageTitle = resolveTitle(store.titleOps, 'Nuke');
449
+ const headLines = [
445
450
  ' <meta charset="utf-8" />',
446
451
  ' <meta name="viewport" content="width=device-width, initial-scale=1" />',
447
452
  \` <title>\${escapeHtml(pageTitle)}</title>\`,
@@ -456,11 +461,7 @@ export default async function handler(req: IncomingMessage, res: ServerResponse)
456
461
  ];
457
462
 
458
463
  const runtimeData = JSON.stringify({
459
- hydrateIds: [...hydrated],
460
- allIds: ALL_CLIENT_IDS,
461
- url,
462
- params,
463
- debug: 'silent',
464
+ hydrateIds: [...hydrated], allIds: ALL_CLIENT_IDS, url, params, debug: 'silent',
464
465
  }).replace(/</g, '\\u003c').replace(/>/g, '\\u003e').replace(/&/g, '\\u0026');
465
466
 
466
467
  const html = \`<!DOCTYPE html>
@@ -502,10 +503,10 @@ export default async function handler(req: IncomingMessage, res: ServerResponse)
502
503
  res.end('Internal Server Error');
503
504
  }
504
505
  }
505
- `.trimStart();
506
+ `;
506
507
  }
507
508
  async function bundleApiHandler(absPath) {
508
- const adapterName = `_api_adapter_${crypto.randomBytes(4).toString("hex")}.ts`;
509
+ const adapterName = `_api_adapter_${randomBytes(4).toString("hex")}.ts`;
509
510
  const adapterPath = path.join(path.dirname(absPath), adapterName);
510
511
  fs.writeFileSync(adapterPath, makeApiAdapterSource(path.basename(absPath)));
511
512
  let text;
@@ -526,23 +527,28 @@ async function bundleApiHandler(absPath) {
526
527
  return text;
527
528
  }
528
529
  async function bundlePageHandler(opts) {
529
- const { absPath, clientComponentNames, allClientIds, layoutPaths, prerenderedHtml } = opts;
530
- const adapterName = `_page_adapter_${crypto.randomBytes(4).toString("hex")}.ts`;
530
+ const {
531
+ absPath,
532
+ clientComponentNames,
533
+ allClientIds,
534
+ layoutPaths,
535
+ prerenderedHtml,
536
+ catchAllNames
537
+ } = opts;
531
538
  const adapterDir = path.dirname(absPath);
532
- const adapterPath = path.join(adapterDir, adapterName);
539
+ const adapterPath = path.join(adapterDir, `_page_adapter_${randomBytes(4).toString("hex")}.ts`);
533
540
  const layoutImports = layoutPaths.map((lp, i) => {
534
541
  const rel = path.relative(adapterDir, lp).replace(/\\/g, "/");
535
- const importPath = rel.startsWith(".") ? rel : "./" + rel;
536
- return `import __layout_${i}__ from ${JSON.stringify(importPath)};`;
542
+ return `import __layout_${i}__ from ${JSON.stringify(rel.startsWith(".") ? rel : "./" + rel)};`;
537
543
  }).join("\n");
538
- const layoutArrayItems = layoutPaths.map((_, i) => `__layout_${i}__`).join(", ");
539
544
  fs.writeFileSync(adapterPath, makePageAdapterSource({
540
545
  pageImport: JSON.stringify("./" + path.basename(absPath)),
541
546
  layoutImports,
542
547
  clientComponentNames,
543
548
  allClientIds,
544
- layoutArrayItems,
545
- prerenderedHtml
549
+ layoutArrayItems: layoutPaths.map((_, i) => `__layout_${i}__`).join(", "),
550
+ prerenderedHtml,
551
+ catchAllNames
546
552
  }));
547
553
  let text;
548
554
  try {
@@ -553,38 +559,8 @@ async function bundlePageHandler(opts) {
553
559
  platform: "node",
554
560
  target: "node20",
555
561
  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
- ],
562
+ packages: "external",
563
+ external: NODE_BUILTINS,
588
564
  define: { "process.env.NODE_ENV": '"production"' },
589
565
  write: false
590
566
  });
@@ -615,7 +591,7 @@ async function bundleClientComponents(globalRegistry, pagesDir, staticDir) {
615
591
  fs.writeFileSync(path.join(outDir, `${id}.js`), browserResult.outputFiles[0].text);
616
592
  const ssrTmp = path.join(
617
593
  path.dirname(filePath),
618
- `_ssr_${id}_${crypto.randomBytes(4).toString("hex")}.mjs`
594
+ `_ssr_${id}_${randomBytes(4).toString("hex")}.mjs`
619
595
  );
620
596
  try {
621
597
  const ssrResult = await build({
@@ -635,7 +611,7 @@ async function bundleClientComponents(globalRegistry, pagesDir, staticDir) {
635
611
  const { renderToString } = await import("react-dom/server");
636
612
  prerendered.set(id, renderToString(createElement(Component, {})));
637
613
  console.log(` prerendered ${id}`);
638
- } catch (e) {
614
+ } catch {
639
615
  prerendered.set(id, "");
640
616
  } finally {
641
617
  if (fs.existsSync(ssrTmp)) fs.unlinkSync(ssrTmp);
@@ -660,7 +636,6 @@ import React, {
660
636
  import { jsx, jsxs } from 'react/jsx-runtime';
661
637
  import { hydrateRoot, createRoot } from 'react-dom/client';
662
638
  export { initRuntime, setupLocationChangeMonitor } from "./${bundleFile}.js";
663
-
664
639
  export {
665
640
  useState, useEffect, useContext, useReducer, useCallback, useMemo,
666
641
  useRef, useImperativeHandle, useLayoutEffect, useDebugValue,
@@ -695,19 +670,18 @@ function copyPublicFiles(publicDir, destDir) {
695
670
  (function walk(src, dest) {
696
671
  fs.mkdirSync(dest, { recursive: true });
697
672
  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);
673
+ const s = path.join(src, entry.name);
674
+ const d = path.join(dest, entry.name);
700
675
  if (entry.isDirectory()) {
701
- walk(srcPath, destPath);
676
+ walk(s, d);
702
677
  } else {
703
- fs.copyFileSync(srcPath, destPath);
678
+ fs.copyFileSync(s, d);
704
679
  count++;
705
680
  }
706
681
  }
707
682
  })(publicDir, destDir);
708
- if (count > 0) {
683
+ if (count > 0)
709
684
  console.log(` copied ${count} public file(s) \u2192 ${path.relative(process.cwd(), destDir)}/`);
710
- }
711
685
  }
712
686
  export {
713
687
  analyzeFile,