nukejs 0.0.4 → 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,36 +431,37 @@ 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>\`,
448
- ...store.meta.map(renderMetaTag),
449
- ...store.link.map(renderLinkTag),
450
- ...store.style.map(renderStyleTag),
451
- ...store.script.map(renderScriptTag),
453
+ ...(store.meta.length || store.link.length || store.style.length || store.script.length ? [
454
+ ' <!--n-head-->',
455
+ ...store.meta.map(renderMetaTag),
456
+ ...store.link.map(renderLinkTag),
457
+ ...store.style.map(renderStyleTag),
458
+ ...store.script.map(renderScriptTag),
459
+ ' <!--/n-head-->',
460
+ ] : []),
452
461
  ];
453
462
 
454
463
  const runtimeData = JSON.stringify({
455
- hydrateIds: [...hydrated],
456
- allIds: ALL_CLIENT_IDS,
457
- url,
458
- params,
459
- debug: 'silent',
464
+ hydrateIds: [...hydrated], allIds: ALL_CLIENT_IDS, url, params, debug: 'silent',
460
465
  }).replace(/</g, '\\u003c').replace(/>/g, '\\u003e').replace(/&/g, '\\u0026');
461
466
 
462
467
  const html = \`<!DOCTYPE html>
@@ -472,9 +477,9 @@ export default async function handler(req: IncomingMessage, res: ServerResponse)
472
477
  <script type="importmap">
473
478
  {
474
479
  "imports": {
475
- "react": "/__react.js",
476
- "react-dom/client": "/__react.js",
477
- "react/jsx-runtime": "/__react.js",
480
+ "react": "/__n.js",
481
+ "react-dom/client": "/__n.js",
482
+ "react/jsx-runtime": "/__n.js",
478
483
  "nukejs": "/__n.js"
479
484
  }
480
485
  }
@@ -498,10 +503,10 @@ export default async function handler(req: IncomingMessage, res: ServerResponse)
498
503
  res.end('Internal Server Error');
499
504
  }
500
505
  }
501
- `.trimStart();
506
+ `;
502
507
  }
503
508
  async function bundleApiHandler(absPath) {
504
- const adapterName = `_api_adapter_${crypto.randomBytes(4).toString("hex")}.ts`;
509
+ const adapterName = `_api_adapter_${randomBytes(4).toString("hex")}.ts`;
505
510
  const adapterPath = path.join(path.dirname(absPath), adapterName);
506
511
  fs.writeFileSync(adapterPath, makeApiAdapterSource(path.basename(absPath)));
507
512
  let text;
@@ -522,23 +527,28 @@ async function bundleApiHandler(absPath) {
522
527
  return text;
523
528
  }
524
529
  async function bundlePageHandler(opts) {
525
- const { absPath, clientComponentNames, allClientIds, layoutPaths, prerenderedHtml } = opts;
526
- 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;
527
538
  const adapterDir = path.dirname(absPath);
528
- const adapterPath = path.join(adapterDir, adapterName);
539
+ const adapterPath = path.join(adapterDir, `_page_adapter_${randomBytes(4).toString("hex")}.ts`);
529
540
  const layoutImports = layoutPaths.map((lp, i) => {
530
541
  const rel = path.relative(adapterDir, lp).replace(/\\/g, "/");
531
- const importPath = rel.startsWith(".") ? rel : "./" + rel;
532
- return `import __layout_${i}__ from ${JSON.stringify(importPath)};`;
542
+ return `import __layout_${i}__ from ${JSON.stringify(rel.startsWith(".") ? rel : "./" + rel)};`;
533
543
  }).join("\n");
534
- const layoutArrayItems = layoutPaths.map((_, i) => `__layout_${i}__`).join(", ");
535
544
  fs.writeFileSync(adapterPath, makePageAdapterSource({
536
545
  pageImport: JSON.stringify("./" + path.basename(absPath)),
537
546
  layoutImports,
538
547
  clientComponentNames,
539
548
  allClientIds,
540
- layoutArrayItems,
541
- prerenderedHtml
549
+ layoutArrayItems: layoutPaths.map((_, i) => `__layout_${i}__`).join(", "),
550
+ prerenderedHtml,
551
+ catchAllNames
542
552
  }));
543
553
  let text;
544
554
  try {
@@ -549,38 +559,8 @@ async function bundlePageHandler(opts) {
549
559
  platform: "node",
550
560
  target: "node20",
551
561
  jsx: "automatic",
552
- external: [
553
- // Node built-ins only — all npm packages (react, nukejs, …) are inlined
554
- "node:*",
555
- "http",
556
- "https",
557
- "fs",
558
- "path",
559
- "url",
560
- "crypto",
561
- "stream",
562
- "buffer",
563
- "events",
564
- "util",
565
- "os",
566
- "net",
567
- "tls",
568
- "child_process",
569
- "worker_threads",
570
- "cluster",
571
- "dgram",
572
- "dns",
573
- "readline",
574
- "zlib",
575
- "assert",
576
- "module",
577
- "perf_hooks",
578
- "string_decoder",
579
- "timers",
580
- "async_hooks",
581
- "v8",
582
- "vm"
583
- ],
562
+ packages: "external",
563
+ external: NODE_BUILTINS,
584
564
  define: { "process.env.NODE_ENV": '"production"' },
585
565
  write: false
586
566
  });
@@ -611,7 +591,7 @@ async function bundleClientComponents(globalRegistry, pagesDir, staticDir) {
611
591
  fs.writeFileSync(path.join(outDir, `${id}.js`), browserResult.outputFiles[0].text);
612
592
  const ssrTmp = path.join(
613
593
  path.dirname(filePath),
614
- `_ssr_${id}_${crypto.randomBytes(4).toString("hex")}.mjs`
594
+ `_ssr_${id}_${randomBytes(4).toString("hex")}.mjs`
615
595
  );
616
596
  try {
617
597
  const ssrResult = await build({
@@ -631,7 +611,7 @@ async function bundleClientComponents(globalRegistry, pagesDir, staticDir) {
631
611
  const { renderToString } = await import("react-dom/server");
632
612
  prerendered.set(id, renderToString(createElement(Component, {})));
633
613
  console.log(` prerendered ${id}`);
634
- } catch (e) {
614
+ } catch {
635
615
  prerendered.set(id, "");
636
616
  } finally {
637
617
  if (fs.existsSync(ssrTmp)) fs.unlinkSync(ssrTmp);
@@ -640,7 +620,9 @@ async function bundleClientComponents(globalRegistry, pagesDir, staticDir) {
640
620
  console.log(` bundled ${globalRegistry.size} client component(s) \u2192 ${path.relative(process.cwd(), outDir)}/`);
641
621
  return prerendered;
642
622
  }
643
- async function buildReactBundle(staticDir) {
623
+ async function buildCombinedBundle(staticDir) {
624
+ const nukeDir = path.dirname(fileURLToPath(import.meta.url));
625
+ const bundleFile = nukeDir.endsWith("dist") ? "bundle" : "bundle.ts";
644
626
  const result = await build({
645
627
  stdin: {
646
628
  contents: `
@@ -653,7 +635,7 @@ import React, {
653
635
  } from 'react';
654
636
  import { jsx, jsxs } from 'react/jsx-runtime';
655
637
  import { hydrateRoot, createRoot } from 'react-dom/client';
656
-
638
+ export { initRuntime, setupLocationChangeMonitor } from "./${bundleFile}.js";
657
639
  export {
658
640
  useState, useEffect, useContext, useReducer, useCallback, useMemo,
659
641
  useRef, useImperativeHandle, useLayoutEffect, useDebugValue,
@@ -664,7 +646,8 @@ export {
664
646
  };
665
647
  export default React;
666
648
  `,
667
- loader: "ts"
649
+ loader: "ts",
650
+ resolveDir: nukeDir
668
651
  },
669
652
  bundle: true,
670
653
  write: false,
@@ -678,21 +661,8 @@ export default React;
678
661
  },
679
662
  define: { "process.env.NODE_ENV": '"production"' }
680
663
  });
681
- fs.writeFileSync(path.join(staticDir, "__react.js"), result.outputFiles[0].text);
682
- console.log(" built __react.js");
683
- }
684
- async function buildNukeBundle(staticDir) {
685
- const nukeDir = path.dirname(fileURLToPath(import.meta.url));
686
- const result = await build({
687
- entryPoints: [path.join(nukeDir, "bundle.js")],
688
- bundle: true,
689
- write: false,
690
- format: "esm",
691
- minify: true,
692
- external: ["react", "react-dom/client"]
693
- });
694
664
  fs.writeFileSync(path.join(staticDir, "__n.js"), result.outputFiles[0].text);
695
- console.log(" built __n.js");
665
+ console.log(" built __n.js (react + runtime)");
696
666
  }
697
667
  function copyPublicFiles(publicDir, destDir) {
698
668
  if (!fs.existsSync(publicDir)) return;
@@ -700,26 +670,24 @@ function copyPublicFiles(publicDir, destDir) {
700
670
  (function walk(src, dest) {
701
671
  fs.mkdirSync(dest, { recursive: true });
702
672
  for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
703
- const srcPath = path.join(src, entry.name);
704
- const destPath = path.join(dest, entry.name);
673
+ const s = path.join(src, entry.name);
674
+ const d = path.join(dest, entry.name);
705
675
  if (entry.isDirectory()) {
706
- walk(srcPath, destPath);
676
+ walk(s, d);
707
677
  } else {
708
- fs.copyFileSync(srcPath, destPath);
678
+ fs.copyFileSync(s, d);
709
679
  count++;
710
680
  }
711
681
  }
712
682
  })(publicDir, destDir);
713
- if (count > 0) {
683
+ if (count > 0)
714
684
  console.log(` copied ${count} public file(s) \u2192 ${path.relative(process.cwd(), destDir)}/`);
715
- }
716
685
  }
717
686
  export {
718
687
  analyzeFile,
719
- buildNukeBundle,
688
+ buildCombinedBundle,
720
689
  buildPages,
721
690
  buildPerPageRegistry,
722
- buildReactBundle,
723
691
  bundleApiHandler,
724
692
  bundleClientComponents,
725
693
  bundlePageHandler,