nukejs 0.0.1 → 0.0.3

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 (80) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +529 -0
  3. package/bin/index.mjs +126 -0
  4. package/dist/app.d.ts +18 -0
  5. package/dist/app.js +124 -0
  6. package/dist/app.js.map +7 -0
  7. package/dist/as-is/Link.d.ts +6 -0
  8. package/dist/as-is/Link.tsx +20 -0
  9. package/dist/as-is/useRouter.d.ts +7 -0
  10. package/dist/as-is/useRouter.ts +33 -0
  11. package/dist/build-common.d.ts +192 -0
  12. package/dist/build-common.js +737 -0
  13. package/dist/build-common.js.map +7 -0
  14. package/dist/build-node.d.ts +1 -0
  15. package/dist/build-node.js +170 -0
  16. package/dist/build-node.js.map +7 -0
  17. package/dist/build-vercel.d.ts +1 -0
  18. package/dist/build-vercel.js +65 -0
  19. package/dist/build-vercel.js.map +7 -0
  20. package/dist/builder.d.ts +1 -0
  21. package/dist/builder.js +97 -0
  22. package/dist/builder.js.map +7 -0
  23. package/dist/bundle.d.ts +68 -0
  24. package/dist/bundle.js +166 -0
  25. package/dist/bundle.js.map +7 -0
  26. package/dist/bundler.d.ts +58 -0
  27. package/dist/bundler.js +100 -0
  28. package/dist/bundler.js.map +7 -0
  29. package/dist/component-analyzer.d.ts +72 -0
  30. package/dist/component-analyzer.js +102 -0
  31. package/dist/component-analyzer.js.map +7 -0
  32. package/dist/config.d.ts +35 -0
  33. package/dist/config.js +30 -0
  34. package/dist/config.js.map +7 -0
  35. package/dist/hmr-bundle.d.ts +25 -0
  36. package/dist/hmr-bundle.js +76 -0
  37. package/dist/hmr-bundle.js.map +7 -0
  38. package/dist/hmr.d.ts +55 -0
  39. package/dist/hmr.js +62 -0
  40. package/dist/hmr.js.map +7 -0
  41. package/dist/html-store.d.ts +121 -0
  42. package/dist/html-store.js +42 -0
  43. package/dist/html-store.js.map +7 -0
  44. package/dist/http-server.d.ts +99 -0
  45. package/dist/http-server.js +166 -0
  46. package/dist/http-server.js.map +7 -0
  47. package/dist/index.d.ts +9 -0
  48. package/dist/index.js +20 -0
  49. package/dist/index.js.map +7 -0
  50. package/dist/logger.d.ts +58 -0
  51. package/dist/logger.js +53 -0
  52. package/dist/logger.js.map +7 -0
  53. package/dist/metadata.d.ts +50 -0
  54. package/dist/metadata.js +43 -0
  55. package/dist/metadata.js.map +7 -0
  56. package/dist/middleware-loader.d.ts +50 -0
  57. package/dist/middleware-loader.js +50 -0
  58. package/dist/middleware-loader.js.map +7 -0
  59. package/dist/middleware.d.ts +22 -0
  60. package/dist/middleware.example.d.ts +8 -0
  61. package/dist/middleware.example.js +58 -0
  62. package/dist/middleware.example.js.map +7 -0
  63. package/dist/middleware.js +72 -0
  64. package/dist/middleware.js.map +7 -0
  65. package/dist/renderer.d.ts +44 -0
  66. package/dist/renderer.js +130 -0
  67. package/dist/renderer.js.map +7 -0
  68. package/dist/router.d.ts +84 -0
  69. package/dist/router.js +104 -0
  70. package/dist/router.js.map +7 -0
  71. package/dist/ssr.d.ts +39 -0
  72. package/dist/ssr.js +168 -0
  73. package/dist/ssr.js.map +7 -0
  74. package/dist/use-html.d.ts +64 -0
  75. package/dist/use-html.js +125 -0
  76. package/dist/use-html.js.map +7 -0
  77. package/dist/utils.d.ts +26 -0
  78. package/dist/utils.js +62 -0
  79. package/dist/utils.js.map +7 -0
  80. package/package.json +63 -12
@@ -0,0 +1,737 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import crypto from "crypto";
4
+ import { fileURLToPath, pathToFileURL } from "url";
5
+ import { build } from "esbuild";
6
+ import { findClientComponentsInTree } from "./component-analyzer.js";
7
+ function walkFiles(dir, base = dir) {
8
+ if (!fs.existsSync(dir)) return [];
9
+ const results = [];
10
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
11
+ const full = path.join(dir, entry.name);
12
+ if (entry.isDirectory()) {
13
+ results.push(...walkFiles(full, base));
14
+ } else if (entry.name.endsWith(".ts") || entry.name.endsWith(".tsx")) {
15
+ results.push(path.relative(base, full));
16
+ }
17
+ }
18
+ return results;
19
+ }
20
+ function analyzeFile(relPath, prefix = "api") {
21
+ const normalized = relPath.replace(/\\/g, "/").replace(/\.(tsx?)$/, "");
22
+ let segments = normalized.split("/");
23
+ if (segments.at(-1) === "index") segments = segments.slice(0, -1);
24
+ const paramNames = [];
25
+ const regexParts = [];
26
+ let specificity = 0;
27
+ for (const seg of segments) {
28
+ const optCatchAll = seg.match(/^\[\[\.\.\.(.+)\]\]$/);
29
+ if (optCatchAll) {
30
+ paramNames.push(optCatchAll[1]);
31
+ regexParts.push("(.*)");
32
+ specificity += 1;
33
+ continue;
34
+ }
35
+ const catchAll = seg.match(/^\[\.\.\.(.+)\]$/);
36
+ if (catchAll) {
37
+ paramNames.push(catchAll[1]);
38
+ regexParts.push("(.+)");
39
+ specificity += 10;
40
+ continue;
41
+ }
42
+ const dynamic = seg.match(/^\[(.+)\]$/);
43
+ if (dynamic) {
44
+ paramNames.push(dynamic[1]);
45
+ regexParts.push("([^/]+)");
46
+ specificity += 100;
47
+ continue;
48
+ }
49
+ regexParts.push(seg.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
50
+ specificity += 1e3;
51
+ }
52
+ const srcRegex = segments.length === 0 ? "^/$" : "^/" + regexParts.join("/") + "$";
53
+ const funcSegments = normalized.split("/");
54
+ if (funcSegments.at(-1) === "index") funcSegments.pop();
55
+ const funcPath = funcSegments.length === 0 ? `/${prefix}/_index` : `/${prefix}/` + funcSegments.join("/");
56
+ return { srcRegex, paramNames, funcPath, specificity };
57
+ }
58
+ function isServerComponent(filePath) {
59
+ const content = fs.readFileSync(filePath, "utf-8");
60
+ for (const line of content.split("\n").slice(0, 5)) {
61
+ const trimmed = line.trim();
62
+ if (!trimmed || trimmed.startsWith("//") || trimmed.startsWith("/*")) continue;
63
+ if (/^["']use client["'];?$/.test(trimmed)) return false;
64
+ break;
65
+ }
66
+ return true;
67
+ }
68
+ function findPageLayouts(routeFilePath, pagesDir) {
69
+ const layouts = [];
70
+ const rootLayout = path.join(pagesDir, "layout.tsx");
71
+ if (fs.existsSync(rootLayout)) layouts.push(rootLayout);
72
+ const relativePath = path.relative(pagesDir, path.dirname(routeFilePath));
73
+ if (!relativePath || relativePath === ".") return layouts;
74
+ const segments = relativePath.split(path.sep).filter(Boolean);
75
+ for (let i = 1; i <= segments.length; i++) {
76
+ const layoutPath = path.join(pagesDir, ...segments.slice(0, i), "layout.tsx");
77
+ if (fs.existsSync(layoutPath)) layouts.push(layoutPath);
78
+ }
79
+ return layouts;
80
+ }
81
+ function extractDefaultExportName(filePath) {
82
+ const content = fs.readFileSync(filePath, "utf-8");
83
+ const match = content.match(/export\s+default\s+(?:function\s+)?(\w+)/);
84
+ return match?.[1] ?? null;
85
+ }
86
+ function collectServerPages(pagesDir) {
87
+ if (!fs.existsSync(pagesDir)) return [];
88
+ return walkFiles(pagesDir).filter((relPath) => {
89
+ const base = path.basename(relPath, path.extname(relPath));
90
+ if (base === "layout") return false;
91
+ return isServerComponent(path.join(pagesDir, relPath));
92
+ }).map((relPath) => ({
93
+ ...analyzeFile(relPath, "page"),
94
+ absPath: path.join(pagesDir, relPath)
95
+ })).sort((a, b) => b.specificity - a.specificity);
96
+ }
97
+ function collectGlobalClientRegistry(serverPages, pagesDir) {
98
+ const registry = /* @__PURE__ */ new Map();
99
+ for (const { absPath } of serverPages) {
100
+ for (const [id, p] of findClientComponentsInTree(absPath, pagesDir)) {
101
+ registry.set(id, p);
102
+ }
103
+ for (const layoutPath of findPageLayouts(absPath, pagesDir)) {
104
+ for (const [id, p] of findClientComponentsInTree(layoutPath, pagesDir)) {
105
+ registry.set(id, p);
106
+ }
107
+ }
108
+ }
109
+ return registry;
110
+ }
111
+ function buildPerPageRegistry(absPath, layoutPaths, pagesDir) {
112
+ const registry = /* @__PURE__ */ new Map();
113
+ for (const [id, p] of findClientComponentsInTree(absPath, pagesDir)) {
114
+ registry.set(id, p);
115
+ }
116
+ for (const lp of layoutPaths) {
117
+ for (const [id, p] of findClientComponentsInTree(lp, pagesDir)) {
118
+ registry.set(id, p);
119
+ }
120
+ }
121
+ const clientComponentNames = {};
122
+ for (const [id, filePath] of registry) {
123
+ const name = extractDefaultExportName(filePath);
124
+ if (name) clientComponentNames[name] = id;
125
+ }
126
+ return { registry, clientComponentNames };
127
+ }
128
+ async function buildPages(pagesDir, staticDir) {
129
+ const serverPages = collectServerPages(pagesDir);
130
+ if (fs.existsSync(pagesDir) && walkFiles(pagesDir).length > 0 && serverPages.length === 0) {
131
+ console.warn(`\u26A0 Pages found in ${pagesDir} but none are server components`);
132
+ }
133
+ 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);
137
+ const builtPages = [];
138
+ 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);
143
+ const bundleText = await bundlePageHandler({
144
+ absPath,
145
+ pagesDir,
146
+ clientComponentNames,
147
+ allClientIds: [...registry.keys()],
148
+ layoutPaths,
149
+ prerenderedHtml: prerenderedHtmlRecord
150
+ });
151
+ builtPages.push({ ...page, bundleText });
152
+ }
153
+ return builtPages;
154
+ }
155
+ function makeApiAdapterSource(handlerFilename) {
156
+ return `
157
+ import type { IncomingMessage, ServerResponse } from 'http';
158
+ import * as mod from ${JSON.stringify("./" + handlerFilename)};
159
+
160
+ function enhance(res: ServerResponse) {
161
+ (res as any).json = function (data: any, status = 200) {
162
+ this.statusCode = status;
163
+ this.setHeader('Content-Type', 'application/json');
164
+ this.end(JSON.stringify(data));
165
+ };
166
+ (res as any).status = function (code: number) { this.statusCode = code; return this; };
167
+ return res;
168
+ }
169
+
170
+ async function parseBody(req: IncomingMessage): Promise<any> {
171
+ return new Promise((resolve, reject) => {
172
+ let body = '';
173
+ req.on('data', chunk => { body += chunk.toString(); });
174
+ req.on('end', () => {
175
+ try {
176
+ resolve(body && req.headers['content-type']?.includes('application/json')
177
+ ? JSON.parse(body) : body);
178
+ } catch (e) { reject(e); }
179
+ });
180
+ req.on('error', reject);
181
+ });
182
+ }
183
+
184
+ export default async function handler(req: IncomingMessage, res: ServerResponse) {
185
+ const method = (req.method || 'GET').toUpperCase();
186
+ const apiRes = enhance(res);
187
+ const apiReq = req as any;
188
+
189
+ apiReq.body = await parseBody(req);
190
+ apiReq.query = Object.fromEntries(new URL(req.url || '/', 'http://localhost').searchParams);
191
+ apiReq.params = apiReq.query;
192
+
193
+ const fn = (mod as any)[method] ?? (mod as any).default;
194
+ if (typeof fn !== 'function') {
195
+ (apiRes as any).json({ error: \`Method \${method} not allowed\` }, 405);
196
+ return;
197
+ }
198
+ await fn(apiReq, apiRes);
199
+ }
200
+ `.trimStart();
201
+ }
202
+ function makePageAdapterSource(opts) {
203
+ const {
204
+ pageImport,
205
+ layoutImports,
206
+ clientComponentNames,
207
+ allClientIds,
208
+ layoutArrayItems,
209
+ prerenderedHtml
210
+ } = opts;
211
+ return `
212
+ import type { IncomingMessage, ServerResponse } from 'http';
213
+ import * as __page__ from ${pageImport};
214
+ ${layoutImports}
215
+
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
+ 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
+ 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
+ const PRERENDERED_HTML: Record<string, string> = ${JSON.stringify(prerenderedHtml)};
231
+
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.
235
+ type TitleValue = string | ((prev: string) => string);
236
+ interface HtmlStore {
237
+ titleOps: TitleValue[];
238
+ htmlAttrs: Record<string, string | undefined>;
239
+ bodyAttrs: Record<string, string | undefined>;
240
+ meta: Record<string, string | undefined>[];
241
+ link: Record<string, string | undefined>[];
242
+ script: Record<string, any>[];
243
+ style: { content?: string; media?: string }[];
244
+ }
245
+ 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
+ }
251
+ async function runWithHtmlStore(fn: () => Promise<void>): Promise<HtmlStore> {
252
+ __setStore(__emptyStore());
253
+ try { await fn(); return { ...(__getStore() ?? __emptyStore()) }; }
254
+ finally { __setStore(null); }
255
+ }
256
+ function resolveTitle(ops: TitleValue[], fallback = ''): string {
257
+ let t = fallback;
258
+ for (let i = ops.length - 1; i >= 0; i--) {
259
+ const op = ops[i]; t = typeof op === 'string' ? op : op(t);
260
+ }
261
+ return t;
262
+ }
263
+
264
+ // \u2500\u2500\u2500 HTML 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
265
+ function escapeHtml(s: string): string {
266
+ return String(s)
267
+ .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
268
+ .replace(/"/g, '&quot;').replace(/'/g, '&#039;');
269
+ }
270
+ function escapeAttr(s: string): string {
271
+ return String(s).replace(/&/g, '&amp;').replace(/"/g, '&quot;');
272
+ }
273
+ function renderAttrs(attrs: Record<string, string | boolean | undefined>): string {
274
+ return Object.entries(attrs)
275
+ .filter(([, v]) => v !== undefined && v !== false)
276
+ .map(([k, v]) => v === true ? k : \`\${k}="\${escapeAttr(String(v))}"\`)
277
+ .join(' ');
278
+ }
279
+ function openTag(tag: string, attrs: Record<string, string | undefined>): string {
280
+ const s = renderAttrs(attrs as Record<string, string | boolean | undefined>);
281
+ return s ? \`<\${tag} \${s}>\` : \`<\${tag}>\`;
282
+ }
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
+ function renderMetaTag(tag: Record<string, string | undefined>): string {
290
+ const attrs: Record<string, string | undefined> = {};
291
+ for (const [k, v] of Object.entries(tag)) if (v !== undefined) attrs[metaKey(k)] = v;
292
+ return \` <meta \${renderAttrs(attrs as any)} />\`;
293
+ }
294
+ function renderLinkTag(tag: Record<string, string | undefined>): string {
295
+ const attrs: Record<string, string | undefined> = {};
296
+ for (const [k, v] of Object.entries(tag)) if (v !== undefined) attrs[linkKey(k)] = v;
297
+ return \` <link \${renderAttrs(attrs as any)} />\`;
298
+ }
299
+ 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>\`;
307
+ }
308
+ function renderStyleTag(tag: any): string {
309
+ const media = tag.media ? \` media="\${escapeAttr(tag.media)}"\` : '';
310
+ return \` <style\${media}>\${tag.content ?? ''}</style>\`;
311
+ }
312
+
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
314
+ const VOID_TAGS = new Set([
315
+ 'area','base','br','col','embed','hr','img','input',
316
+ 'link','meta','param','source','track','wbr',
317
+ ]);
318
+
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
+ function serializeProps(value: any): any {
323
+ if (value == null || typeof value !== 'object') return value;
324
+ if (typeof value === 'function') return undefined;
325
+ if (Array.isArray(value)) {
326
+ return value.map(serializeProps).filter((v: any) => v !== undefined);
327
+ }
328
+ 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
+ }
333
+ if (typeof type === 'function') {
334
+ const cid = CLIENT_COMPONENTS[type.name];
335
+ if (cid) return { __re: 'client', componentId: cid, props: serializeProps(elProps) };
336
+ }
337
+ return undefined;
338
+ }
339
+ const out: any = {};
340
+ for (const [k, v] of Object.entries(value as Record<string, any>)) {
341
+ const s = serializeProps(v);
342
+ if (s !== undefined) out[k] = s;
343
+ }
344
+ return out;
345
+ }
346
+
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
+ async function renderNode(node: any, hydrated: Set<string>): Promise<string> {
352
+ if (node == null || typeof node === 'boolean') return '';
353
+ if (typeof node === 'string') return escapeHtml(node);
354
+ 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
+ }
358
+
359
+ const { type, props } = node as { type: any; props: Record<string, any> };
360
+ if (!type) return '';
361
+
362
+ if (type === Symbol.for('react.fragment')) {
363
+ return renderNode(props?.children ?? null, hydrated);
364
+ }
365
+
366
+ if (typeof type === 'string') {
367
+ const { children, dangerouslySetInnerHTML, ...rest } = props || {};
368
+ const attrParts: string[] = [];
369
+ for (const [k, v] of Object.entries(rest as Record<string, any>)) {
370
+ const name = k === 'className' ? 'class' : k === 'htmlFor' ? 'for' : k;
371
+ if (typeof v === 'boolean') { if (v) attrParts.push(name); continue; }
372
+ if (v == null) continue;
373
+ if (k === 'style' && typeof v === 'object') {
374
+ const css = Object.entries(v as Record<string, any>)
375
+ .map(([p, val]) => \`\${p.replace(/[A-Z]/g, m => \`-\${m.toLowerCase()}\`)}:\${escapeHtml(String(val))}\`)
376
+ .join(';');
377
+ attrParts.push(\`style="\${css}"\`);
378
+ continue;
379
+ }
380
+ attrParts.push(\`\${name}="\${escapeHtml(String(v))}"\`);
381
+ }
382
+ const attrStr = attrParts.length ? ' ' + attrParts.join(' ') : '';
383
+ if (VOID_TAGS.has(type)) return \`<\${type}\${attrStr} />\`;
384
+ const inner = dangerouslySetInnerHTML
385
+ ? (dangerouslySetInnerHTML as any).__html
386
+ : await renderNode(children ?? null, hydrated);
387
+ return \`<\${type}\${attrStr}>\${inner}</\${type}>\`;
388
+ }
389
+
390
+ if (typeof type === 'function') {
391
+ const clientId = CLIENT_COMPONENTS[type.name];
392
+ if (clientId) {
393
+ hydrated.add(clientId);
394
+ 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
+ let ssrHtml: string;
399
+ try {
400
+ const result = await (type as Function)(props || {});
401
+ ssrHtml = await renderNode(result, new Set());
402
+ } catch {
403
+ ssrHtml = PRERENDERED_HTML[clientId] ?? '';
404
+ }
405
+ return \`<span data-hydrate-id="\${clientId}" data-hydrate-props="\${escapeHtml(JSON.stringify(serializedProps))}">\${ssrHtml}</span>\`;
406
+ }
407
+ 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);
410
+ }
411
+
412
+ return '';
413
+ }
414
+
415
+ // \u2500\u2500\u2500 Layout wrapping \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
416
+ const LAYOUT_COMPONENTS: Array<(props: any) => any> = [${layoutArrayItems}];
417
+
418
+ function wrapWithLayouts(element: any): any {
419
+ let el = element;
420
+ for (let i = LAYOUT_COMPONENTS.length - 1; i >= 0; i--) {
421
+ el = { type: LAYOUT_COMPONENTS[i], props: { children: el }, key: null, ref: null };
422
+ }
423
+ return el;
424
+ }
425
+
426
+ // \u2500\u2500\u2500 Handler \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
427
+ export default async function handler(req: IncomingMessage, res: ServerResponse): Promise<void> {
428
+ try {
429
+ const parsed = new URL(req.url || '/', 'http://localhost');
430
+ const params: Record<string, string> = {};
431
+ parsed.searchParams.forEach((v, k) => { params[k] = v; });
432
+ const url = req.url || '/';
433
+
434
+ const hydrated = new Set<string>();
435
+ const pageElement = { type: __page__.default, props: params as any, key: null, ref: null };
436
+ const wrapped = wrapWithLayouts(pageElement);
437
+
438
+ let appHtml = '';
439
+ const store = await runWithHtmlStore(async () => {
440
+ appHtml = await renderNode(wrapped, hydrated);
441
+ });
442
+
443
+ const pageTitle = resolveTitle(store.titleOps, 'SSR App');
444
+ const headLines: string[] = [
445
+ ' <meta charset="utf-8" />',
446
+ ' <meta name="viewport" content="width=device-width, initial-scale=1" />',
447
+ \` <title>\${escapeHtml(pageTitle)}</title>\`,
448
+ ...store.meta.map(renderMetaTag),
449
+ ...store.link.map(renderLinkTag),
450
+ ...store.style.map(renderStyleTag),
451
+ ...store.script.map(renderScriptTag),
452
+ ];
453
+
454
+ const runtimeData = JSON.stringify({
455
+ hydrateIds: [...hydrated],
456
+ allIds: ALL_CLIENT_IDS,
457
+ url,
458
+ params,
459
+ debug: 'silent',
460
+ }).replace(/</g, '\\u003c').replace(/>/g, '\\u003e').replace(/&/g, '\\u0026');
461
+
462
+ const html = \`<!DOCTYPE html>
463
+ \${openTag('html', store.htmlAttrs)}
464
+ <head>
465
+ \${headLines.join('\\n')}
466
+ </head>
467
+ \${openTag('body', store.bodyAttrs)}
468
+ <div id="app">\${appHtml}</div>
469
+
470
+ <script id="__n_data" type="application/json">\${runtimeData}</script>
471
+
472
+ <script type="importmap">
473
+ {
474
+ "imports": {
475
+ "react": "/__react.js",
476
+ "react-dom/client": "/__react.js",
477
+ "react/jsx-runtime": "/__react.js",
478
+ "nukejs": "/__n.js"
479
+ }
480
+ }
481
+ </script>
482
+
483
+ <script type="module">
484
+ const { initRuntime } = await import('nukejs');
485
+ const data = JSON.parse(document.getElementById('__n_data').textContent);
486
+ await initRuntime(data);
487
+ </script>
488
+ </body>
489
+ </html>\`;
490
+
491
+ res.statusCode = 200;
492
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
493
+ res.end(html);
494
+ } catch (err: any) {
495
+ console.error('[page render error]', err);
496
+ res.statusCode = 500;
497
+ res.setHeader('Content-Type', 'text/plain; charset=utf-8');
498
+ res.end('Internal Server Error');
499
+ }
500
+ }
501
+ `.trimStart();
502
+ }
503
+ async function bundleApiHandler(absPath) {
504
+ const adapterName = `_api_adapter_${crypto.randomBytes(4).toString("hex")}.ts`;
505
+ const adapterPath = path.join(path.dirname(absPath), adapterName);
506
+ fs.writeFileSync(adapterPath, makeApiAdapterSource(path.basename(absPath)));
507
+ let text;
508
+ try {
509
+ const result = await build({
510
+ entryPoints: [adapterPath],
511
+ bundle: true,
512
+ format: "esm",
513
+ platform: "node",
514
+ target: "node20",
515
+ packages: "external",
516
+ write: false
517
+ });
518
+ text = result.outputFiles[0].text;
519
+ } finally {
520
+ fs.unlinkSync(adapterPath);
521
+ }
522
+ return text;
523
+ }
524
+ async function bundlePageHandler(opts) {
525
+ const { absPath, clientComponentNames, allClientIds, layoutPaths, prerenderedHtml } = opts;
526
+ const adapterName = `_page_adapter_${crypto.randomBytes(4).toString("hex")}.ts`;
527
+ const adapterDir = path.dirname(absPath);
528
+ const adapterPath = path.join(adapterDir, adapterName);
529
+ const layoutImports = layoutPaths.map((lp, i) => {
530
+ const rel = path.relative(adapterDir, lp).replace(/\\/g, "/");
531
+ const importPath = rel.startsWith(".") ? rel : "./" + rel;
532
+ return `import __layout_${i}__ from ${JSON.stringify(importPath)};`;
533
+ }).join("\n");
534
+ const layoutArrayItems = layoutPaths.map((_, i) => `__layout_${i}__`).join(", ");
535
+ fs.writeFileSync(adapterPath, makePageAdapterSource({
536
+ pageImport: JSON.stringify("./" + path.basename(absPath)),
537
+ layoutImports,
538
+ clientComponentNames,
539
+ allClientIds,
540
+ layoutArrayItems,
541
+ prerenderedHtml
542
+ }));
543
+ let text;
544
+ try {
545
+ const result = await build({
546
+ entryPoints: [adapterPath],
547
+ bundle: true,
548
+ format: "esm",
549
+ platform: "node",
550
+ target: "node20",
551
+ 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
+ ],
584
+ define: { "process.env.NODE_ENV": '"production"' },
585
+ write: false
586
+ });
587
+ text = result.outputFiles[0].text;
588
+ } finally {
589
+ fs.unlinkSync(adapterPath);
590
+ }
591
+ return text;
592
+ }
593
+ async function bundleClientComponents(globalRegistry, pagesDir, staticDir) {
594
+ if (globalRegistry.size === 0) return /* @__PURE__ */ new Map();
595
+ const outDir = path.join(staticDir, "__client-component");
596
+ fs.mkdirSync(outDir, { recursive: true });
597
+ const prerendered = /* @__PURE__ */ new Map();
598
+ for (const [id, filePath] of globalRegistry) {
599
+ console.log(` bundling client ${id} (${path.relative(pagesDir, filePath)})`);
600
+ const browserResult = await build({
601
+ entryPoints: [filePath],
602
+ bundle: true,
603
+ format: "esm",
604
+ platform: "browser",
605
+ jsx: "automatic",
606
+ minify: true,
607
+ external: ["react", "react-dom/client", "react/jsx-runtime"],
608
+ define: { "process.env.NODE_ENV": '"production"' },
609
+ write: false
610
+ });
611
+ fs.writeFileSync(path.join(outDir, `${id}.js`), browserResult.outputFiles[0].text);
612
+ const ssrTmp = path.join(
613
+ path.dirname(filePath),
614
+ `_ssr_${id}_${crypto.randomBytes(4).toString("hex")}.mjs`
615
+ );
616
+ try {
617
+ const ssrResult = await build({
618
+ entryPoints: [filePath],
619
+ bundle: true,
620
+ format: "esm",
621
+ platform: "node",
622
+ target: "node20",
623
+ jsx: "automatic",
624
+ packages: "external",
625
+ define: { "process.env.NODE_ENV": '"production"' },
626
+ write: false
627
+ });
628
+ fs.writeFileSync(ssrTmp, ssrResult.outputFiles[0].text);
629
+ const { default: Component } = await import(pathToFileURL(ssrTmp).href);
630
+ const { createElement } = await import("react");
631
+ const { renderToString } = await import("react-dom/server");
632
+ prerendered.set(id, renderToString(createElement(Component, {})));
633
+ console.log(` prerendered ${id}`);
634
+ } catch (e) {
635
+ console.warn(` [SSR prerender failed for ${id}]`, e);
636
+ prerendered.set(id, "");
637
+ } finally {
638
+ if (fs.existsSync(ssrTmp)) fs.unlinkSync(ssrTmp);
639
+ }
640
+ }
641
+ console.log(` bundled ${globalRegistry.size} client component(s) \u2192 ${path.relative(process.cwd(), outDir)}/`);
642
+ return prerendered;
643
+ }
644
+ async function buildReactBundle(staticDir) {
645
+ const result = await build({
646
+ stdin: {
647
+ contents: `
648
+ import React, {
649
+ useState, useEffect, useContext, useReducer, useCallback, useMemo,
650
+ useRef, useImperativeHandle, useLayoutEffect, useDebugValue,
651
+ useDeferredValue, useTransition, useId, useSyncExternalStore,
652
+ useInsertionEffect, createContext, forwardRef, memo, lazy,
653
+ Suspense, Fragment, StrictMode, Component, PureComponent
654
+ } from 'react';
655
+ import { jsx, jsxs } from 'react/jsx-runtime';
656
+ import { hydrateRoot, createRoot } from 'react-dom/client';
657
+
658
+ export {
659
+ useState, useEffect, useContext, useReducer, useCallback, useMemo,
660
+ useRef, useImperativeHandle, useLayoutEffect, useDebugValue,
661
+ useDeferredValue, useTransition, useId, useSyncExternalStore,
662
+ useInsertionEffect, createContext, forwardRef, memo, lazy,
663
+ Suspense, Fragment, StrictMode, Component, PureComponent,
664
+ hydrateRoot, createRoot, jsx, jsxs
665
+ };
666
+ export default React;
667
+ `,
668
+ loader: "ts"
669
+ },
670
+ bundle: true,
671
+ write: false,
672
+ treeShaking: true,
673
+ minify: true,
674
+ format: "esm",
675
+ jsx: "automatic",
676
+ alias: {
677
+ react: path.dirname(fileURLToPath(import.meta.resolve("react/package.json"))),
678
+ "react-dom": path.dirname(fileURLToPath(import.meta.resolve("react-dom/package.json")))
679
+ },
680
+ define: { "process.env.NODE_ENV": '"production"' }
681
+ });
682
+ fs.writeFileSync(path.join(staticDir, "__react.js"), result.outputFiles[0].text);
683
+ console.log(" built __react.js");
684
+ }
685
+ async function buildNukeBundle(staticDir) {
686
+ const nukeDir = path.dirname(fileURLToPath(import.meta.url));
687
+ const result = await build({
688
+ entryPoints: [path.join(nukeDir, "bundle.js")],
689
+ bundle: true,
690
+ write: false,
691
+ format: "esm",
692
+ minify: true,
693
+ external: ["react", "react-dom/client"]
694
+ });
695
+ fs.writeFileSync(path.join(staticDir, "__n.js"), result.outputFiles[0].text);
696
+ console.log(" built __n.js");
697
+ }
698
+ function copyPublicFiles(publicDir, destDir) {
699
+ if (!fs.existsSync(publicDir)) return;
700
+ let count = 0;
701
+ (function walk(src, dest) {
702
+ fs.mkdirSync(dest, { recursive: true });
703
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
704
+ const srcPath = path.join(src, entry.name);
705
+ const destPath = path.join(dest, entry.name);
706
+ if (entry.isDirectory()) {
707
+ walk(srcPath, destPath);
708
+ } else {
709
+ fs.copyFileSync(srcPath, destPath);
710
+ count++;
711
+ }
712
+ }
713
+ })(publicDir, destDir);
714
+ if (count > 0) {
715
+ console.log(` copied ${count} public file(s) \u2192 ${path.relative(process.cwd(), destDir)}/`);
716
+ }
717
+ }
718
+ export {
719
+ analyzeFile,
720
+ buildNukeBundle,
721
+ buildPages,
722
+ buildPerPageRegistry,
723
+ buildReactBundle,
724
+ bundleApiHandler,
725
+ bundleClientComponents,
726
+ bundlePageHandler,
727
+ collectGlobalClientRegistry,
728
+ collectServerPages,
729
+ copyPublicFiles,
730
+ extractDefaultExportName,
731
+ findPageLayouts,
732
+ isServerComponent,
733
+ makeApiAdapterSource,
734
+ makePageAdapterSource,
735
+ walkFiles
736
+ };
737
+ //# sourceMappingURL=build-common.js.map