nukejs 0.0.8 → 0.0.10
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.
- package/dist/build-common.js +42 -3
- package/dist/build-common.js.map +2 -2
- package/dist/http-server.js +5 -12
- package/dist/http-server.js.map +2 -2
- package/dist/renderer.js +34 -3
- package/dist/renderer.js.map +2 -2
- package/package.json +1 -1
package/dist/build-common.js
CHANGED
|
@@ -349,6 +349,43 @@ const VOID_TAGS = new Set([
|
|
|
349
349
|
'link','meta','param','source','track','wbr',
|
|
350
350
|
]);
|
|
351
351
|
|
|
352
|
+
// \u2500\u2500\u2500 Wrapper attribute 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
|
|
353
|
+
function isWrapperAttr(key: string): boolean {
|
|
354
|
+
return (
|
|
355
|
+
key === 'className' ||
|
|
356
|
+
key === 'style' ||
|
|
357
|
+
key === 'id' ||
|
|
358
|
+
key.startsWith('data-') ||
|
|
359
|
+
key.startsWith('aria-')
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
function splitWrapperAttrs(props: any): { wrapperAttrs: Record<string, any>; componentProps: Record<string, any> } {
|
|
363
|
+
const wrapperAttrs: Record<string, any> = {};
|
|
364
|
+
const componentProps: Record<string, any> = {};
|
|
365
|
+
for (const [key, value] of Object.entries((props || {}) as Record<string, any>)) {
|
|
366
|
+
if (isWrapperAttr(key)) wrapperAttrs[key] = value;
|
|
367
|
+
else componentProps[key] = value;
|
|
368
|
+
}
|
|
369
|
+
return { wrapperAttrs, componentProps };
|
|
370
|
+
}
|
|
371
|
+
function buildWrapperAttrString(attrs: Record<string, any>): string {
|
|
372
|
+
const parts = Object.entries(attrs)
|
|
373
|
+
.map(([key, value]) => {
|
|
374
|
+
if (key === 'className') key = 'class';
|
|
375
|
+
if (key === 'style' && typeof value === 'object') {
|
|
376
|
+
const css = Object.entries(value as Record<string, any>)
|
|
377
|
+
.map(([p, val]) => \`\${p.replace(/[A-Z]/g, m => \`-\${m.toLowerCase()}\`)}:\${escapeHtml(String(val))}\`)
|
|
378
|
+
.join(';');
|
|
379
|
+
return \`style="\${css}"\`;
|
|
380
|
+
}
|
|
381
|
+
if (typeof value === 'boolean') return value ? key : '';
|
|
382
|
+
if (value == null) return '';
|
|
383
|
+
return \`\${key}="\${escapeHtml(String(value))}"\`;
|
|
384
|
+
})
|
|
385
|
+
.filter(Boolean);
|
|
386
|
+
return parts.length ? ' ' + parts.join(' ') : '';
|
|
387
|
+
}
|
|
388
|
+
|
|
352
389
|
function serializeProps(value: any): any {
|
|
353
390
|
if (value == null || typeof value !== 'object') return value;
|
|
354
391
|
if (typeof value === 'function') return undefined;
|
|
@@ -409,14 +446,16 @@ async function renderNode(node: any, hydrated: Set<string>): Promise<string> {
|
|
|
409
446
|
const clientId = CLIENT_COMPONENTS[type.name];
|
|
410
447
|
if (clientId) {
|
|
411
448
|
hydrated.add(clientId);
|
|
412
|
-
const
|
|
449
|
+
const { wrapperAttrs, componentProps } = splitWrapperAttrs(props);
|
|
450
|
+
const wrapperAttrStr = buildWrapperAttrString(wrapperAttrs);
|
|
451
|
+
const serializedProps = serializeProps(componentProps ?? {});
|
|
413
452
|
let ssrHtml: string;
|
|
414
453
|
try {
|
|
415
|
-
ssrHtml = __renderToString__(__createElement__(type as any,
|
|
454
|
+
ssrHtml = __renderToString__(__createElement__(type as any, componentProps || {}));
|
|
416
455
|
} catch {
|
|
417
456
|
ssrHtml = PRERENDERED_HTML[clientId] ?? '';
|
|
418
457
|
}
|
|
419
|
-
return \`<span data-hydrate-id="\${clientId}" data-hydrate-props="\${escapeHtml(JSON.stringify(serializedProps))}">\${ssrHtml}</span>\`;
|
|
458
|
+
return \`<span data-hydrate-id="\${clientId}"\${wrapperAttrStr} data-hydrate-props="\${escapeHtml(JSON.stringify(serializedProps))}">\${ssrHtml}</span>\`;
|
|
420
459
|
}
|
|
421
460
|
const instance = type.prototype?.isReactComponent ? new (type as any)(props) : null;
|
|
422
461
|
return renderNode(instance ? instance.render() : await (type as Function)(props || {}), hydrated);
|
package/dist/build-common.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/build-common.ts"],
|
|
4
|
-
"sourcesContent": ["/**\r\n * build-common.ts \u2014 Shared Build Logic\r\n *\r\n * Used by both build-node.ts and build-vercel.ts.\r\n *\r\n * Exports:\r\n * \u2014 types : AnalyzedRoute, ServerPage, BuiltPage,\r\n * PageAdapterOptions, PageBundleOptions\r\n * \u2014 utility helpers : walkFiles, analyzeFile, isServerComponent,\r\n * findPageLayouts, extractDefaultExportName\r\n * \u2014 collection : collectServerPages, collectGlobalClientRegistry,\r\n * buildPerPageRegistry\r\n * \u2014 template codegen : makeApiAdapterSource, makePageAdapterSource\r\n * \u2014 bundle ops : bundleApiHandler, bundlePageHandler,\r\n * bundleClientComponents, buildPages,\r\n * buildCombinedBundle, copyPublicFiles\r\n */\r\n\r\nimport fs from 'fs';\r\nimport path from 'path';\r\nimport { randomBytes } from 'node:crypto';\r\nimport { fileURLToPath, pathToFileURL } from 'url';\r\nimport { build } from 'esbuild';\r\nimport { findClientComponentsInTree } from './component-analyzer';\r\n\r\n// \u2500\u2500\u2500 Node built-in externals \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * All Node.js built-in module names.\r\n * Used as the `external` list when bundling for Node so esbuild never tries\r\n * to inline them, which would produce broken `require()` shims in ESM output.\r\n */\r\nconst NODE_BUILTINS = [\r\n 'node:*',\r\n 'http', 'https', 'fs', 'path', 'url', 'crypto', 'stream', 'buffer',\r\n 'events', 'util', 'os', 'net', 'tls', 'child_process', 'worker_threads',\r\n 'cluster', 'dgram', 'dns', 'readline', 'zlib', 'assert', 'module',\r\n 'perf_hooks', 'string_decoder', 'timers', 'async_hooks', 'v8', 'vm',\r\n];\r\n\r\n// \u2500\u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nexport interface AnalyzedRoute {\r\n /** Regex string matching the URL path, e.g. '^/users/([^/]+)$' */\r\n srcRegex: string;\r\n /** Names of captured groups in srcRegex order */\r\n paramNames: string[];\r\n /**\r\n * Subset of paramNames that are catch-all ([...slug] or [[...path]]).\r\n * Their runtime values are string[] not string.\r\n */\r\n catchAllNames: string[];\r\n /** Function namespace path, e.g. '/api/users' or '/page/about' */\r\n funcPath: string;\r\n specificity: number;\r\n}\r\n\r\nexport interface ServerPage extends AnalyzedRoute {\r\n absPath: string;\r\n}\r\n\r\nexport interface BuiltPage extends ServerPage {\r\n bundleText: string;\r\n}\r\n\r\n// \u2500\u2500\u2500 File walker \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nexport function walkFiles(dir: string, base: string = dir): string[] {\r\n if (!fs.existsSync(dir)) return [];\r\n const results: string[] = [];\r\n for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {\r\n const full = path.join(dir, entry.name);\r\n if (entry.isDirectory()) {\r\n results.push(...walkFiles(full, base));\r\n } else if (entry.name.endsWith('.ts') || entry.name.endsWith('.tsx')) {\r\n results.push(path.relative(base, full));\r\n }\r\n }\r\n return results;\r\n}\r\n\r\n// \u2500\u2500\u2500 Route analysis \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Parses dynamic-route segments from a relative file path and returns a regex,\r\n * captured param names, catch-all param names, a function path, and a\r\n * specificity score.\r\n *\r\n * Supported patterns per segment:\r\n * [[...name]] optional catch-all \u2192 regex (.*) \u2192 string[]\r\n * [...name] required catch-all \u2192 regex (.+) \u2192 string[]\r\n * [[name]] optional single \u2192 regex ([^/]*)? \u2192 string\r\n * [name] required single \u2192 regex ([^/]+) \u2192 string\r\n * literal static \u2192 escaped literal\r\n *\r\n * @param relPath Relative path from the dir root (e.g. 'users/[id].tsx').\r\n * @param prefix Namespace for funcPath ('api' | 'page').\r\n */\r\nexport function analyzeFile(relPath: string, prefix = 'api'): AnalyzedRoute {\r\n const normalized = relPath.replace(/\\\\/g, '/').replace(/\\.(tsx?)$/, '');\r\n let segments = normalized.split('/');\r\n if (segments.at(-1) === 'index') segments = segments.slice(0, -1);\r\n\r\n const paramNames: string[] = [];\r\n const catchAllNames: string[] = [];\r\n const regexParts: string[] = [];\r\n let specificity = 0;\r\n\r\n for (const seg of segments) {\r\n const optCatchAll = seg.match(/^\\[\\[\\.\\.\\.(.+)\\]\\]$/);\r\n if (optCatchAll) {\r\n paramNames.push(optCatchAll[1]);\r\n catchAllNames.push(optCatchAll[1]);\r\n regexParts.push('(.*)');\r\n specificity += 1;\r\n continue;\r\n }\r\n const catchAll = seg.match(/^\\[\\.\\.\\.(.+)\\]$/);\r\n if (catchAll) {\r\n paramNames.push(catchAll[1]);\r\n catchAllNames.push(catchAll[1]);\r\n regexParts.push('(.+)');\r\n specificity += 10;\r\n continue;\r\n }\r\n const optDynamic = seg.match(/^\\[\\[([^.][^\\]]*)\\]\\]$/);\r\n if (optDynamic) {\r\n paramNames.push(optDynamic[1]);\r\n regexParts.push('__OPT__([^/]*)'); // marker \u2014 resolved below\r\n specificity += 30;\r\n continue;\r\n }\r\n const dynamic = seg.match(/^\\[(.+)\\]$/);\r\n if (dynamic) {\r\n paramNames.push(dynamic[1]);\r\n regexParts.push('([^/]+)');\r\n specificity += 100;\r\n continue;\r\n }\r\n regexParts.push(seg.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&'));\r\n specificity += 1000;\r\n }\r\n\r\n // Build the regex string.\r\n // __OPT__(...) markers indicate optional single segments where the preceding\r\n // slash must also be optional (e.g. users/[[id]] should match /users).\r\n let srcRegex: string;\r\n if (segments.length === 0) {\r\n srcRegex = '^/$';\r\n } else {\r\n let body = '';\r\n for (let i = 0; i < regexParts.length; i++) {\r\n const part = regexParts[i];\r\n if (part.startsWith('__OPT__')) {\r\n const cap = part.slice(7);\r\n // At position 0, ^/ already provides the leading slash\r\n body += i === 0 ? cap : `(?:/${cap})?`;\r\n } else {\r\n body += (i === 0 ? '' : '/') + part;\r\n }\r\n }\r\n srcRegex = '^/' + body + '$';\r\n }\r\n\r\n const funcSegments = normalized.split('/');\r\n if (funcSegments.at(-1) === 'index') funcSegments.pop();\r\n const funcPath = funcSegments.length === 0\r\n ? `/${prefix}/_index`\r\n : `/${prefix}/` + funcSegments.join('/');\r\n\r\n return { srcRegex, paramNames, catchAllNames, funcPath, specificity };\r\n}\r\n\r\n// \u2500\u2500\u2500 Server-component detection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Returns true when a file does NOT begin with a \"use client\" directive,\r\n * i.e. it is a server component.\r\n */\r\nexport function isServerComponent(filePath: string): boolean {\r\n const content = fs.readFileSync(filePath, 'utf-8');\r\n for (const line of content.split('\\n').slice(0, 5)) {\r\n const trimmed = line.trim();\r\n if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('/*')) continue;\r\n if (/^[\"']use client[\"'];?$/.test(trimmed)) return false;\r\n break;\r\n }\r\n return true;\r\n}\r\n\r\n// \u2500\u2500\u2500 Layout discovery \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Walks from the pages root to the directory containing `routeFilePath` and\r\n * returns every layout.tsx found, in outermost-first order.\r\n */\r\nexport function findPageLayouts(routeFilePath: string, pagesDir: string): string[] {\r\n const layouts: string[] = [];\r\n\r\n const rootLayout = path.join(pagesDir, 'layout.tsx');\r\n if (fs.existsSync(rootLayout)) layouts.push(rootLayout);\r\n\r\n const relativePath = path.relative(pagesDir, path.dirname(routeFilePath));\r\n if (!relativePath || relativePath === '.') return layouts;\r\n\r\n const segments = relativePath.split(path.sep).filter(Boolean);\r\n for (let i = 1; i <= segments.length; i++) {\r\n const layoutPath = path.join(pagesDir, ...segments.slice(0, i), 'layout.tsx');\r\n if (fs.existsSync(layoutPath)) layouts.push(layoutPath);\r\n }\r\n\r\n return layouts;\r\n}\r\n\r\n/**\r\n * Extracts the identifier used as the default export from a component file.\r\n * Returns null when no default export is found.\r\n *\r\n * Handles three formats so that components compiled by esbuild are recognised\r\n * alongside hand-written source files:\r\n * 1. Source: `export default function Foo` / `export default Foo`\r\n * 2. esbuild: `var Foo_default = Foo` (compiled arrow-function component)\r\n * 3. Re-export: `export { Foo as default }`\r\n */\r\nexport function extractDefaultExportName(filePath: string): string | null {\r\n const content = fs.readFileSync(filePath, 'utf-8');\r\n\r\n // Format 1 \u2013 source: `export default function Foo` or `export default Foo`\r\n let m = content.match(/export\\s+default\\s+(?:function\\s+)?(\\w+)/);\r\n if (m?.[1]) return m[1];\r\n\r\n // Format 2 \u2013 esbuild compiled: `var Foo_default = Foo`\r\n m = content.match(/var\\s+\\w+_default\\s*=\\s*(\\w+)/);\r\n if (m?.[1]) return m[1];\r\n\r\n // Format 3 \u2013 explicit re-export: `export { Foo as default }`\r\n m = content.match(/export\\s*\\{[^}]*\\b(\\w+)\\s+as\\s+default\\b[^}]*\\}/);\r\n if (m?.[1] && !m[1].endsWith('_default')) return m[1];\r\n\r\n return null;\r\n}\r\n\r\n// \u2500\u2500\u2500 Server page collection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Returns all server-component pages inside `pagesDir`, sorted most-specific\r\n * first so precise routes shadow catch-alls in routers.\r\n * layout.tsx files and \"use client\" files are excluded.\r\n */\r\nexport function collectServerPages(pagesDir: string): ServerPage[] {\r\n if (!fs.existsSync(pagesDir)) return [];\r\n return walkFiles(pagesDir)\r\n .filter(relPath => {\r\n if (path.basename(relPath, path.extname(relPath)) === 'layout') return false;\r\n return isServerComponent(path.join(pagesDir, relPath));\r\n })\r\n .map(relPath => ({\r\n ...analyzeFile(relPath, 'page'),\r\n absPath: path.join(pagesDir, relPath),\r\n }))\r\n .sort((a, b) => b.specificity - a.specificity);\r\n}\r\n\r\n/**\r\n * Walks every server page and its layout chain to collect all client component\r\n * IDs reachable anywhere in the app.\r\n */\r\nexport function collectGlobalClientRegistry(\r\n serverPages: ServerPage[],\r\n pagesDir: string,\r\n): Map<string, string> {\r\n const registry = new Map<string, string>();\r\n for (const { absPath } of serverPages) {\r\n for (const [id, p] of findClientComponentsInTree(absPath, pagesDir))\r\n registry.set(id, p);\r\n for (const layoutPath of findPageLayouts(absPath, pagesDir))\r\n for (const [id, p] of findClientComponentsInTree(layoutPath, pagesDir))\r\n registry.set(id, p);\r\n }\r\n return registry;\r\n}\r\n\r\n// \u2500\u2500\u2500 Per-page 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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Builds the per-page client component registry (page + its layout chain)\r\n * and returns both the id\u2192path map and the name\u2192id map needed by\r\n * bundlePageHandler.\r\n */\r\nexport function buildPerPageRegistry(\r\n absPath: string,\r\n layoutPaths: string[],\r\n pagesDir: string,\r\n): { registry: Map<string, string>; clientComponentNames: Record<string, string> } {\r\n const registry = new Map<string, string>();\r\n\r\n for (const [id, p] of findClientComponentsInTree(absPath, pagesDir))\r\n registry.set(id, p);\r\n for (const lp of layoutPaths)\r\n for (const [id, p] of findClientComponentsInTree(lp, pagesDir))\r\n registry.set(id, p);\r\n\r\n const clientComponentNames: Record<string, string> = {};\r\n for (const [id, filePath] of registry) {\r\n const name = extractDefaultExportName(filePath);\r\n if (name) clientComponentNames[name] = id;\r\n }\r\n\r\n return { registry, clientComponentNames };\r\n}\r\n\r\n// \u2500\u2500\u2500 High-level page builder \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Runs both passes of the page build:\r\n *\r\n * Pass 1 \u2014 bundles all client components to `staticDir/__client-component/`\r\n * and collects pre-rendered HTML for each.\r\n * Pass 2 \u2014 bundles every server-component page into a self-contained ESM\r\n * handler and returns the results as `BuiltPage[]`.\r\n */\r\nexport async function buildPages(\r\n pagesDir: string,\r\n staticDir: string,\r\n): Promise<BuiltPage[]> {\r\n const serverPages = collectServerPages(pagesDir);\r\n\r\n if (fs.existsSync(pagesDir) && walkFiles(pagesDir).length > 0 && serverPages.length === 0) {\r\n console.warn(`\u26A0 Pages found in ${pagesDir} but none are server components`);\r\n }\r\n\r\n if (serverPages.length === 0) return [];\r\n\r\n const globalRegistry = collectGlobalClientRegistry(serverPages, pagesDir);\r\n const prerenderedHtml = await bundleClientComponents(globalRegistry, pagesDir, staticDir);\r\n const prerenderedRecord = Object.fromEntries(prerenderedHtml);\r\n\r\n const builtPages: BuiltPage[] = [];\r\n\r\n for (const page of serverPages) {\r\n console.log(` building ${page.absPath} \u2192 ${page.funcPath} [page]`);\r\n\r\n const layoutPaths = findPageLayouts(page.absPath, pagesDir);\r\n const { registry, clientComponentNames } = buildPerPageRegistry(page.absPath, layoutPaths, pagesDir);\r\n\r\n const bundleText = await bundlePageHandler({\r\n absPath: page.absPath,\r\n pagesDir,\r\n clientComponentNames,\r\n allClientIds: [...registry.keys()],\r\n layoutPaths,\r\n prerenderedHtml: prerenderedRecord,\r\n catchAllNames: page.catchAllNames,\r\n });\r\n\r\n builtPages.push({ ...page, bundleText });\r\n }\r\n\r\n return builtPages;\r\n}\r\n\r\n// \u2500\u2500\u2500 API adapter template \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Returns the TypeScript source for a thin HTTP adapter that wraps an API\r\n * route module and exposes a single `handler(req, res)` default export.\r\n */\r\nexport function makeApiAdapterSource(handlerFilename: string): string {\r\n return `\\\r\nimport type { IncomingMessage, ServerResponse } from 'http';\r\nimport * as mod from ${JSON.stringify('./' + handlerFilename)};\r\n\r\nfunction enhance(res: ServerResponse) {\r\n (res as any).json = function (data: any, status = 200) {\r\n this.statusCode = status;\r\n this.setHeader('Content-Type', 'application/json');\r\n this.end(JSON.stringify(data));\r\n };\r\n (res as any).status = function (code: number) { this.statusCode = code; return this; };\r\n return res;\r\n}\r\n\r\nasync function parseBody(req: IncomingMessage): Promise<any> {\r\n return new Promise((resolve, reject) => {\r\n let body = '';\r\n req.on('data', chunk => { body += chunk.toString(); });\r\n req.on('end', () => {\r\n try {\r\n resolve(body && req.headers['content-type']?.includes('application/json')\r\n ? JSON.parse(body) : body);\r\n } catch (e) { reject(e); }\r\n });\r\n req.on('error', reject);\r\n });\r\n}\r\n\r\nexport default async function handler(req: IncomingMessage, res: ServerResponse) {\r\n const method = (req.method || 'GET').toUpperCase();\r\n const apiRes = enhance(res);\r\n const apiReq = req as any;\r\n\r\n apiReq.body = await parseBody(req);\r\n apiReq.query = Object.fromEntries(new URL(req.url || '/', 'http://localhost').searchParams);\r\n apiReq.params = apiReq.query;\r\n\r\n const fn = (mod as any)[method] ?? (mod as any).default;\r\n if (typeof fn !== 'function') {\r\n (apiRes as any).json({ error: \\`Method \\${method} not allowed\\` }, 405);\r\n return;\r\n }\r\n await fn(apiReq, apiRes);\r\n}\r\n`;\r\n}\r\n\r\n// \u2500\u2500\u2500 Page adapter template \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nexport interface PageAdapterOptions {\r\n /** e.g. './home.tsx' \u2014 relative import for the page default export */\r\n pageImport: string;\r\n /** Newline-joined import statements for layout components */\r\n layoutImports: string;\r\n /** function-name \u2192 cc_id map, computed at build time */\r\n clientComponentNames: Record<string, string>;\r\n /** All client component IDs reachable from this page */\r\n allClientIds: string[];\r\n /** Comma-separated list of __layout_N__ identifiers */\r\n layoutArrayItems: string;\r\n /** Pre-rendered HTML per client component ID, computed at build time */\r\n prerenderedHtml: Record<string, string>;\r\n /** Catch-all param names whose runtime values are string[] not string */\r\n catchAllNames: string[];\r\n}\r\n\r\n/**\r\n * Returns the TypeScript source for a fully self-contained page handler.\r\n *\r\n * The adapter:\r\n * \u2022 Inlines the html-store so useHtml() works without external deps.\r\n * \u2022 Contains an async recursive renderer for server + client components.\r\n * \u2022 Client components are identified via the pre-computed CLIENT_COMPONENTS\r\n * map \u2014 no fs.readFileSync at runtime.\r\n * \u2022 Emits the full HTML document including the __n_data blob and bootstrap.\r\n */\r\nexport function makePageAdapterSource(opts: PageAdapterOptions): string {\r\n const {\r\n pageImport, layoutImports, clientComponentNames, allClientIds,\r\n layoutArrayItems, prerenderedHtml, catchAllNames,\r\n } = opts;\r\n\r\n return `\\\r\nimport type { IncomingMessage, ServerResponse } from 'http';\r\nimport { createElement as __createElement__ } from 'react';\r\nimport { renderToString as __renderToString__ } from 'react-dom/server';\r\nimport * as __page__ from ${pageImport};\r\n${layoutImports}\r\n\r\nconst CLIENT_COMPONENTS: Record<string, string> = ${JSON.stringify(clientComponentNames)};\r\nconst ALL_CLIENT_IDS: string[] = ${JSON.stringify(allClientIds)};\r\nconst PRERENDERED_HTML: Record<string, string> = ${JSON.stringify(prerenderedHtml)};\r\nconst CATCH_ALL_NAMES = new Set(${JSON.stringify(catchAllNames)});\r\n\r\n// \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\r\ntype TitleValue = string | ((prev: string) => string);\r\ninterface HtmlStore {\r\n titleOps: TitleValue[];\r\n htmlAttrs: Record<string, string | undefined>;\r\n bodyAttrs: Record<string, string | undefined>;\r\n meta: Record<string, string | undefined>[];\r\n link: Record<string, string | undefined>[];\r\n script: Record<string, any>[];\r\n style: { content?: string; media?: string }[];\r\n}\r\nconst __STORE_KEY__ = Symbol.for('__nukejs_html_store__');\r\nconst __getStore = (): HtmlStore | null => (globalThis as any)[__STORE_KEY__] ?? null;\r\nconst __setStore = (s: HtmlStore | null): void => { (globalThis as any)[__STORE_KEY__] = s; };\r\nconst __emptyStore = (): HtmlStore =>\r\n ({ titleOps: [], htmlAttrs: {}, bodyAttrs: {}, meta: [], link: [], script: [], style: [] });\r\nasync function runWithHtmlStore(fn: () => Promise<void>): Promise<HtmlStore> {\r\n __setStore(__emptyStore());\r\n try { await fn(); return { ...(__getStore() ?? __emptyStore()) }; }\r\n finally { __setStore(null); }\r\n}\r\nfunction resolveTitle(ops: TitleValue[], fallback = ''): string {\r\n let t = fallback;\r\n for (let i = ops.length - 1; i >= 0; i--) {\r\n const op = ops[i]; t = typeof op === 'string' ? op : op(t);\r\n }\r\n return t;\r\n}\r\n\r\n// \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\r\nfunction escapeHtml(s: string): string {\r\n return String(s)\r\n .replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')\r\n .replace(/\"/g, '"').replace(/'/g, ''');\r\n}\r\nfunction escapeAttr(s: string): string {\r\n return String(s).replace(/&/g, '&').replace(/\"/g, '"');\r\n}\r\nfunction renderAttrs(attrs: Record<string, string | boolean | undefined>): string {\r\n return Object.entries(attrs)\r\n .filter(([, v]) => v !== undefined && v !== false)\r\n .map(([k, v]) => v === true ? k : \\`\\${k}=\"\\${escapeAttr(String(v))}\"\\`)\r\n .join(' ');\r\n}\r\nfunction openTag(tag: string, attrs: Record<string, string | undefined>): string {\r\n const s = renderAttrs(attrs as Record<string, string | boolean | undefined>);\r\n return s ? \\`<\\${tag} \\${s}>\\` : \\`<\\${tag}>\\`;\r\n}\r\nfunction renderMetaTag(tag: Record<string, string | undefined>): string {\r\n const key = (k: string) => k === 'httpEquiv' ? 'http-equiv' : k;\r\n const attrs: Record<string, string | undefined> = {};\r\n for (const [k, v] of Object.entries(tag)) if (v !== undefined) attrs[key(k)] = v;\r\n return \\` <meta \\${renderAttrs(attrs as any)} />\\`;\r\n}\r\nfunction renderLinkTag(tag: Record<string, string | undefined>): string {\r\n const key = (k: string) => k === 'hrefLang' ? 'hreflang' : k === 'crossOrigin' ? 'crossorigin' : k;\r\n const attrs: Record<string, string | undefined> = {};\r\n for (const [k, v] of Object.entries(tag)) if (v !== undefined) attrs[key(k)] = v;\r\n return \\` <link \\${renderAttrs(attrs as any)} />\\`;\r\n}\r\nfunction renderScriptTag(tag: any): string {\r\n const s = renderAttrs({ src: tag.src, type: tag.type, crossorigin: tag.crossOrigin,\r\n integrity: tag.integrity, defer: tag.defer, async: tag.async, nomodule: tag.noModule });\r\n return \\` \\${s ? \\`<script \\${s}>\\` : '<script>'}\\${tag.src ? '' : (tag.content ?? '')}</script>\\`;\r\n}\r\nfunction renderStyleTag(tag: any): string {\r\n const media = tag.media ? \\` media=\"\\${escapeAttr(tag.media)}\"\\` : '';\r\n return \\` <style\\${media}>\\${tag.content ?? ''}</style>\\`;\r\n}\r\n\r\n// \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\r\nconst VOID_TAGS = new Set([\r\n 'area','base','br','col','embed','hr','img','input',\r\n 'link','meta','param','source','track','wbr',\r\n]);\r\n\r\nfunction serializeProps(value: any): any {\r\n if (value == null || typeof value !== 'object') return value;\r\n if (typeof value === 'function') return undefined;\r\n if (Array.isArray(value)) return value.map(serializeProps).filter((v: any) => v !== undefined);\r\n if ((value as any).$$typeof) {\r\n const { type, props: p } = value as any;\r\n if (typeof type === 'string') return { __re: 'html', tag: type, props: serializeProps(p) };\r\n if (typeof type === 'function') {\r\n const cid = CLIENT_COMPONENTS[type.name];\r\n if (cid) return { __re: 'client', componentId: cid, props: serializeProps(p) };\r\n }\r\n return undefined;\r\n }\r\n const out: any = {};\r\n for (const [k, v] of Object.entries(value as Record<string, any>)) {\r\n const s = serializeProps(v);\r\n if (s !== undefined) out[k] = s;\r\n }\r\n return out;\r\n}\r\n\r\nasync function renderNode(node: any, hydrated: Set<string>): Promise<string> {\r\n if (node == null || typeof node === 'boolean') return '';\r\n if (typeof node === 'string') return escapeHtml(node);\r\n if (typeof node === 'number') return String(node);\r\n if (Array.isArray(node)) return (await Promise.all(node.map(n => renderNode(n, hydrated)))).join('');\r\n\r\n const { type, props } = node as { type: any; props: Record<string, any> };\r\n if (!type) return '';\r\n\r\n if (type === Symbol.for('react.fragment')) return renderNode(props?.children ?? null, hydrated);\r\n\r\n if (typeof type === 'string') {\r\n const { children, dangerouslySetInnerHTML, ...rest } = props || {};\r\n const attrParts: string[] = [];\r\n for (const [k, v] of Object.entries(rest as Record<string, any>)) {\r\n const name = k === 'className' ? 'class' : k === 'htmlFor' ? 'for' : k;\r\n if (typeof v === 'boolean') { if (v) attrParts.push(name); continue; }\r\n if (v == null) continue;\r\n if (k === 'style' && typeof v === 'object') {\r\n const css = Object.entries(v as Record<string, any>)\r\n .map(([p, val]) => \\`\\${p.replace(/[A-Z]/g, m => \\`-\\${m.toLowerCase()}\\`)}:\\${escapeHtml(String(val))}\\`)\r\n .join(';');\r\n attrParts.push(\\`style=\"\\${css}\"\\`);\r\n continue;\r\n }\r\n attrParts.push(\\`\\${name}=\"\\${escapeHtml(String(v))}\"\\`);\r\n }\r\n const attrStr = attrParts.length ? ' ' + attrParts.join(' ') : '';\r\n if (VOID_TAGS.has(type)) return \\`<\\${type}\\${attrStr} />\\`;\r\n const inner = dangerouslySetInnerHTML\r\n ? (dangerouslySetInnerHTML as any).__html\r\n : await renderNode(children ?? null, hydrated);\r\n return \\`<\\${type}\\${attrStr}>\\${inner}</\\${type}>\\`;\r\n }\r\n\r\n if (typeof type === 'function') {\r\n const clientId = CLIENT_COMPONENTS[type.name];\r\n if (clientId) {\r\n hydrated.add(clientId);\r\n const serializedProps = serializeProps(props ?? {});\r\n let ssrHtml: string;\r\n try {\r\n ssrHtml = __renderToString__(__createElement__(type as any, props || {}));\r\n } catch {\r\n ssrHtml = PRERENDERED_HTML[clientId] ?? '';\r\n }\r\n return \\`<span data-hydrate-id=\"\\${clientId}\" data-hydrate-props=\"\\${escapeHtml(JSON.stringify(serializedProps))}\">\\${ssrHtml}</span>\\`;\r\n }\r\n const instance = type.prototype?.isReactComponent ? new (type as any)(props) : null;\r\n return renderNode(instance ? instance.render() : await (type as Function)(props || {}), hydrated);\r\n }\r\n\r\n return '';\r\n}\r\n\r\n// \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\r\nconst LAYOUT_COMPONENTS: Array<(props: any) => any> = [${layoutArrayItems}];\r\n\r\nfunction wrapWithLayouts(element: any): any {\r\n let el = element;\r\n for (let i = LAYOUT_COMPONENTS.length - 1; i >= 0; i--)\r\n el = { type: LAYOUT_COMPONENTS[i], props: { children: el }, key: null, ref: null };\r\n return el;\r\n}\r\n\r\n// \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\r\nexport default async function handler(req: IncomingMessage, res: ServerResponse): Promise<void> {\r\n try {\r\n const parsed = new URL(req.url || '/', 'http://localhost');\r\n const params: Record<string, string | string[]> = {};\r\n parsed.searchParams.forEach((_, k) => {\r\n params[k] = CATCH_ALL_NAMES.has(k)\r\n ? parsed.searchParams.getAll(k)\r\n : parsed.searchParams.get(k) as string;\r\n });\r\n const url = req.url || '/';\r\n\r\n const hydrated = new Set<string>();\r\n const wrapped = wrapWithLayouts({ type: __page__.default, props: params as any, key: null, ref: null });\r\n\r\n let appHtml = '';\r\n const store = await runWithHtmlStore(async () => { appHtml = await renderNode(wrapped, hydrated); });\r\n\r\n const pageTitle = resolveTitle(store.titleOps, 'NukeJS');\r\n const headScripts = store.script.filter((s: any) => (s.position ?? 'head') === 'head');\r\n const bodyScripts = store.script.filter((s: any) => s.position === 'body');\r\n const headLines = [\r\n ' <meta charset=\"utf-8\" />',\r\n ' <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />',\r\n \\` <title>\\${escapeHtml(pageTitle)}</title>\\`,\r\n ...(store.meta.length || store.link.length || store.style.length || headScripts.length ? [\r\n ' <!--n-head-->',\r\n ...store.meta.map(renderMetaTag),\r\n ...store.link.map(renderLinkTag),\r\n ...store.style.map(renderStyleTag),\r\n ...headScripts.map(renderScriptTag),\r\n ' <!--/n-head-->',\r\n ] : []),\r\n ];\r\n const bodyScriptLines = bodyScripts.length\r\n ? [' <!--n-body-scripts-->', ...bodyScripts.map(renderScriptTag), ' <!--/n-body-scripts-->']\r\n : [];\r\n const bodyScriptsHtml = bodyScriptLines.length ? '\\\\n' + bodyScriptLines.join('\\\\n') + '\\\\n' : '';\r\n\r\n const runtimeData = JSON.stringify({\r\n hydrateIds: [...hydrated], allIds: ALL_CLIENT_IDS, url, params, debug: 'silent',\r\n }).replace(/</g, '\\\\u003c').replace(/>/g, '\\\\u003e').replace(/&/g, '\\\\u0026');\r\n\r\n const html = \\`<!DOCTYPE html>\r\n\\${openTag('html', store.htmlAttrs)}\r\n<head>\r\n\\${headLines.join('\\\\n')}\r\n</head>\r\n\\${openTag('body', store.bodyAttrs)}\r\n <div id=\"app\">\\${appHtml}</div>\r\n\r\n <script id=\"__n_data\" type=\"application/json\">\\${runtimeData}</script>\r\n\r\n <script type=\"importmap\">\r\n {\r\n \"imports\": {\r\n \"react\": \"/__n.js\",\r\n \"react-dom/client\": \"/__n.js\",\r\n \"react/jsx-runtime\": \"/__n.js\",\r\n \"nukejs\": \"/__n.js\"\r\n }\r\n }\r\n </script>\r\n\r\n <script type=\"module\">\r\n const { initRuntime } = await import('nukejs');\r\n const data = JSON.parse(document.getElementById('__n_data').textContent);\r\n await initRuntime(data);\r\n </script>\r\n\\${bodyScriptsHtml}</body>\r\n</html>\\`;\r\n\r\n res.statusCode = 200;\r\n res.setHeader('Content-Type', 'text/html; charset=utf-8');\r\n res.end(html);\r\n } catch (err: any) {\r\n console.error('[page render error]', err);\r\n res.statusCode = 500;\r\n res.setHeader('Content-Type', 'text/plain; charset=utf-8');\r\n res.end('Internal Server Error');\r\n }\r\n}\r\n`;\r\n}\r\n\r\n// \u2500\u2500\u2500 Bundle operations \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Bundles an API route handler into a single self-contained ESM string.\r\n * node_modules are kept external \u2014 they exist at runtime on both Node and\r\n * Vercel (Vercel bundles them separately via the pages dispatcher).\r\n */\r\nexport async function bundleApiHandler(absPath: string): Promise<string> {\r\n const adapterName = `_api_adapter_${randomBytes(4).toString('hex')}.ts`;\r\n const adapterPath = path.join(path.dirname(absPath), adapterName);\r\n fs.writeFileSync(adapterPath, makeApiAdapterSource(path.basename(absPath)));\r\n\r\n let text: string;\r\n try {\r\n const result = await build({\r\n entryPoints: [adapterPath],\r\n bundle: true,\r\n format: 'esm',\r\n platform: 'node',\r\n target: 'node20',\r\n packages: 'external',\r\n write: false,\r\n });\r\n text = result.outputFiles[0].text;\r\n } finally {\r\n fs.unlinkSync(adapterPath);\r\n }\r\n return text;\r\n}\r\n\r\nexport interface PageBundleOptions {\r\n absPath: string;\r\n pagesDir: string;\r\n clientComponentNames: Record<string, string>;\r\n allClientIds: string[];\r\n layoutPaths: string[];\r\n prerenderedHtml: Record<string, string>;\r\n catchAllNames: string[];\r\n}\r\n\r\n/**\r\n * Bundles a server-component page into a single self-contained ESM string.\r\n * All npm packages are kept external \u2014 the Node production server has\r\n * node_modules available at runtime.\r\n */\r\nexport async function bundlePageHandler(opts: PageBundleOptions): Promise<string> {\r\n const {\r\n absPath, clientComponentNames, allClientIds,\r\n layoutPaths, prerenderedHtml, catchAllNames,\r\n } = opts;\r\n\r\n const adapterDir = path.dirname(absPath);\r\n const adapterPath = path.join(adapterDir, `_page_adapter_${randomBytes(4).toString('hex')}.ts`);\r\n\r\n const layoutImports = layoutPaths\r\n .map((lp, i) => {\r\n const rel = path.relative(adapterDir, lp).replace(/\\\\/g, '/');\r\n return `import __layout_${i}__ from ${JSON.stringify(rel.startsWith('.') ? rel : './' + rel)};`;\r\n })\r\n .join('\\n');\r\n\r\n fs.writeFileSync(adapterPath, makePageAdapterSource({\r\n pageImport: JSON.stringify('./' + path.basename(absPath)),\r\n layoutImports,\r\n clientComponentNames,\r\n allClientIds,\r\n layoutArrayItems: layoutPaths.map((_, i) => `__layout_${i}__`).join(', '),\r\n prerenderedHtml,\r\n catchAllNames,\r\n }));\r\n\r\n let text: string;\r\n try {\r\n const result = await build({\r\n entryPoints: [adapterPath],\r\n bundle: true,\r\n format: 'esm',\r\n platform: 'node',\r\n target: 'node20',\r\n jsx: 'automatic',\r\n packages: 'external',\r\n external: NODE_BUILTINS,\r\n define: { 'process.env.NODE_ENV': '\"production\"' },\r\n write: false,\r\n });\r\n text = result.outputFiles[0].text;\r\n } finally {\r\n fs.unlinkSync(adapterPath);\r\n }\r\n return text;\r\n}\r\n\r\n/**\r\n * Bundles every client component in `globalRegistry` to\r\n * `<staticDir>/__client-component/<id>.js` and pre-renders each to HTML.\r\n */\r\nexport async function bundleClientComponents(\r\n globalRegistry: Map<string, string>,\r\n pagesDir: string,\r\n staticDir: string,\r\n): Promise<Map<string, string>> {\r\n if (globalRegistry.size === 0) return new Map();\r\n\r\n const outDir = path.join(staticDir, '__client-component');\r\n fs.mkdirSync(outDir, { recursive: true });\r\n\r\n const prerendered = new Map<string, string>();\r\n\r\n for (const [id, filePath] of globalRegistry) {\r\n console.log(` bundling client ${id} (${path.relative(pagesDir, filePath)})`);\r\n\r\n // Browser bundle \u2014 served to the client for hydration\r\n const browserResult = await build({\r\n entryPoints: [filePath],\r\n bundle: true,\r\n format: 'esm',\r\n platform: 'browser',\r\n jsx: 'automatic',\r\n minify: true,\r\n external: ['react', 'react-dom/client', 'react/jsx-runtime'],\r\n define: { 'process.env.NODE_ENV': '\"production\"' },\r\n write: false,\r\n });\r\n fs.writeFileSync(path.join(outDir, `${id}.js`), browserResult.outputFiles[0].text);\r\n\r\n // SSR pre-render \u2014 bundle for Node, import, renderToString, discard\r\n const ssrTmp = path.join(\r\n path.dirname(filePath),\r\n `_ssr_${id}_${randomBytes(4).toString('hex')}.mjs`,\r\n );\r\n try {\r\n const ssrResult = await build({\r\n entryPoints: [filePath],\r\n bundle: true,\r\n format: 'esm',\r\n platform: 'node',\r\n target: 'node20',\r\n jsx: 'automatic',\r\n packages: 'external',\r\n define: { 'process.env.NODE_ENV': '\"production\"' },\r\n write: false,\r\n });\r\n fs.writeFileSync(ssrTmp, ssrResult.outputFiles[0].text);\r\n\r\n const { default: Component } = await import(pathToFileURL(ssrTmp).href);\r\n const { createElement } = await import('react');\r\n const { renderToString } = await import('react-dom/server');\r\n\r\n prerendered.set(id, renderToString(createElement(Component, {})));\r\n console.log(` prerendered ${id}`);\r\n } catch {\r\n prerendered.set(id, '');\r\n } finally {\r\n if (fs.existsSync(ssrTmp)) fs.unlinkSync(ssrTmp);\r\n }\r\n }\r\n\r\n console.log(` bundled ${globalRegistry.size} client component(s) \u2192 ${path.relative(process.cwd(), outDir)}/`);\r\n return prerendered;\r\n}\r\n\r\n/**\r\n * Builds the combined browser bundle (__n.js) that contains the full React\r\n * runtime + NukeJS client runtime in a single file.\r\n */\r\nexport async function buildCombinedBundle(staticDir: string): Promise<void> {\r\n const nukeDir = path.dirname(fileURLToPath(import.meta.url));\r\n const bundleFile = nukeDir.endsWith('dist') ? 'bundle' : 'bundle.ts';\r\n\r\n const result = await build({\r\n stdin: {\r\n contents: `\r\nimport React, {\r\n useState, useEffect, useContext, useReducer, useCallback, useMemo,\r\n useRef, useImperativeHandle, useLayoutEffect, useDebugValue,\r\n useDeferredValue, useTransition, useId, useSyncExternalStore,\r\n useInsertionEffect, createContext, forwardRef, memo, lazy,\r\n Suspense, Fragment, StrictMode, Component, PureComponent\r\n} from 'react';\r\nimport { jsx, jsxs } from 'react/jsx-runtime';\r\nimport { hydrateRoot, createRoot } from 'react-dom/client';\r\nexport { initRuntime, setupLocationChangeMonitor } from './${bundleFile}';\r\nexport {\r\n useState, useEffect, useContext, useReducer, useCallback, useMemo,\r\n useRef, useImperativeHandle, useLayoutEffect, useDebugValue,\r\n useDeferredValue, useTransition, useId, useSyncExternalStore,\r\n useInsertionEffect, createContext, forwardRef, memo, lazy,\r\n Suspense, Fragment, StrictMode, Component, PureComponent,\r\n hydrateRoot, createRoot, jsx, jsxs\r\n};\r\nexport default React;\r\n`,\r\n loader: 'ts',\r\n resolveDir: nukeDir,\r\n },\r\n bundle: true,\r\n write: false,\r\n treeShaking: true,\r\n minify: true,\r\n format: 'esm',\r\n jsx: 'automatic',\r\n alias: {\r\n react: path.dirname(fileURLToPath(import.meta.resolve('react/package.json'))),\r\n 'react-dom': path.dirname(fileURLToPath(import.meta.resolve('react-dom/package.json'))),\r\n },\r\n define: { 'process.env.NODE_ENV': '\"production\"' },\r\n });\r\n\r\n fs.writeFileSync(path.join(staticDir, '__n.js'), result.outputFiles[0].text);\r\n console.log(' built __n.js (react + runtime)');\r\n}\r\n\r\n// \u2500\u2500\u2500 Public file copying \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Recursively copies every file from `publicDir` into `destDir`, preserving\r\n * the directory structure. Skips silently when `publicDir` does not exist.\r\n */\r\nexport function copyPublicFiles(publicDir: string, destDir: string): void {\r\n if (!fs.existsSync(publicDir)) return;\r\n\r\n let count = 0;\r\n (function walk(src: string, dest: string) {\r\n fs.mkdirSync(dest, { recursive: true });\r\n for (const entry of fs.readdirSync(src, { withFileTypes: true })) {\r\n const s = path.join(src, entry.name);\r\n const d = path.join(dest, entry.name);\r\n if (entry.isDirectory()) { walk(s, d); } else { fs.copyFileSync(s, d); count++; }\r\n }\r\n })(publicDir, destDir);\r\n\r\n if (count > 0)\r\n console.log(` copied ${count} public file(s) \u2192 ${path.relative(process.cwd(), destDir)}/`);\r\n}"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["/**\r\n * build-common.ts \u2014 Shared Build Logic\r\n *\r\n * Used by both build-node.ts and build-vercel.ts.\r\n *\r\n * Exports:\r\n * \u2014 types : AnalyzedRoute, ServerPage, BuiltPage,\r\n * PageAdapterOptions, PageBundleOptions\r\n * \u2014 utility helpers : walkFiles, analyzeFile, isServerComponent,\r\n * findPageLayouts, extractDefaultExportName\r\n * \u2014 collection : collectServerPages, collectGlobalClientRegistry,\r\n * buildPerPageRegistry\r\n * \u2014 template codegen : makeApiAdapterSource, makePageAdapterSource\r\n * \u2014 bundle ops : bundleApiHandler, bundlePageHandler,\r\n * bundleClientComponents, buildPages,\r\n * buildCombinedBundle, copyPublicFiles\r\n */\r\n\r\nimport fs from 'fs';\r\nimport path from 'path';\r\nimport { randomBytes } from 'node:crypto';\r\nimport { fileURLToPath, pathToFileURL } from 'url';\r\nimport { build } from 'esbuild';\r\nimport { findClientComponentsInTree } from './component-analyzer';\r\n\r\n// \u2500\u2500\u2500 Node built-in externals \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * All Node.js built-in module names.\r\n * Used as the `external` list when bundling for Node so esbuild never tries\r\n * to inline them, which would produce broken `require()` shims in ESM output.\r\n */\r\nconst NODE_BUILTINS = [\r\n 'node:*',\r\n 'http', 'https', 'fs', 'path', 'url', 'crypto', 'stream', 'buffer',\r\n 'events', 'util', 'os', 'net', 'tls', 'child_process', 'worker_threads',\r\n 'cluster', 'dgram', 'dns', 'readline', 'zlib', 'assert', 'module',\r\n 'perf_hooks', 'string_decoder', 'timers', 'async_hooks', 'v8', 'vm',\r\n];\r\n\r\n// \u2500\u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nexport interface AnalyzedRoute {\r\n /** Regex string matching the URL path, e.g. '^/users/([^/]+)$' */\r\n srcRegex: string;\r\n /** Names of captured groups in srcRegex order */\r\n paramNames: string[];\r\n /**\r\n * Subset of paramNames that are catch-all ([...slug] or [[...path]]).\r\n * Their runtime values are string[] not string.\r\n */\r\n catchAllNames: string[];\r\n /** Function namespace path, e.g. '/api/users' or '/page/about' */\r\n funcPath: string;\r\n specificity: number;\r\n}\r\n\r\nexport interface ServerPage extends AnalyzedRoute {\r\n absPath: string;\r\n}\r\n\r\nexport interface BuiltPage extends ServerPage {\r\n bundleText: string;\r\n}\r\n\r\n// \u2500\u2500\u2500 File walker \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nexport function walkFiles(dir: string, base: string = dir): string[] {\r\n if (!fs.existsSync(dir)) return [];\r\n const results: string[] = [];\r\n for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {\r\n const full = path.join(dir, entry.name);\r\n if (entry.isDirectory()) {\r\n results.push(...walkFiles(full, base));\r\n } else if (entry.name.endsWith('.ts') || entry.name.endsWith('.tsx')) {\r\n results.push(path.relative(base, full));\r\n }\r\n }\r\n return results;\r\n}\r\n\r\n// \u2500\u2500\u2500 Route analysis \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Parses dynamic-route segments from a relative file path and returns a regex,\r\n * captured param names, catch-all param names, a function path, and a\r\n * specificity score.\r\n *\r\n * Supported patterns per segment:\r\n * [[...name]] optional catch-all \u2192 regex (.*) \u2192 string[]\r\n * [...name] required catch-all \u2192 regex (.+) \u2192 string[]\r\n * [[name]] optional single \u2192 regex ([^/]*)? \u2192 string\r\n * [name] required single \u2192 regex ([^/]+) \u2192 string\r\n * literal static \u2192 escaped literal\r\n *\r\n * @param relPath Relative path from the dir root (e.g. 'users/[id].tsx').\r\n * @param prefix Namespace for funcPath ('api' | 'page').\r\n */\r\nexport function analyzeFile(relPath: string, prefix = 'api'): AnalyzedRoute {\r\n const normalized = relPath.replace(/\\\\/g, '/').replace(/\\.(tsx?)$/, '');\r\n let segments = normalized.split('/');\r\n if (segments.at(-1) === 'index') segments = segments.slice(0, -1);\r\n\r\n const paramNames: string[] = [];\r\n const catchAllNames: string[] = [];\r\n const regexParts: string[] = [];\r\n let specificity = 0;\r\n\r\n for (const seg of segments) {\r\n const optCatchAll = seg.match(/^\\[\\[\\.\\.\\.(.+)\\]\\]$/);\r\n if (optCatchAll) {\r\n paramNames.push(optCatchAll[1]);\r\n catchAllNames.push(optCatchAll[1]);\r\n regexParts.push('(.*)');\r\n specificity += 1;\r\n continue;\r\n }\r\n const catchAll = seg.match(/^\\[\\.\\.\\.(.+)\\]$/);\r\n if (catchAll) {\r\n paramNames.push(catchAll[1]);\r\n catchAllNames.push(catchAll[1]);\r\n regexParts.push('(.+)');\r\n specificity += 10;\r\n continue;\r\n }\r\n const optDynamic = seg.match(/^\\[\\[([^.][^\\]]*)\\]\\]$/);\r\n if (optDynamic) {\r\n paramNames.push(optDynamic[1]);\r\n regexParts.push('__OPT__([^/]*)'); // marker \u2014 resolved below\r\n specificity += 30;\r\n continue;\r\n }\r\n const dynamic = seg.match(/^\\[(.+)\\]$/);\r\n if (dynamic) {\r\n paramNames.push(dynamic[1]);\r\n regexParts.push('([^/]+)');\r\n specificity += 100;\r\n continue;\r\n }\r\n regexParts.push(seg.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&'));\r\n specificity += 1000;\r\n }\r\n\r\n // Build the regex string.\r\n // __OPT__(...) markers indicate optional single segments where the preceding\r\n // slash must also be optional (e.g. users/[[id]] should match /users).\r\n let srcRegex: string;\r\n if (segments.length === 0) {\r\n srcRegex = '^/$';\r\n } else {\r\n let body = '';\r\n for (let i = 0; i < regexParts.length; i++) {\r\n const part = regexParts[i];\r\n if (part.startsWith('__OPT__')) {\r\n const cap = part.slice(7);\r\n // At position 0, ^/ already provides the leading slash\r\n body += i === 0 ? cap : `(?:/${cap})?`;\r\n } else {\r\n body += (i === 0 ? '' : '/') + part;\r\n }\r\n }\r\n srcRegex = '^/' + body + '$';\r\n }\r\n\r\n const funcSegments = normalized.split('/');\r\n if (funcSegments.at(-1) === 'index') funcSegments.pop();\r\n const funcPath = funcSegments.length === 0\r\n ? `/${prefix}/_index`\r\n : `/${prefix}/` + funcSegments.join('/');\r\n\r\n return { srcRegex, paramNames, catchAllNames, funcPath, specificity };\r\n}\r\n\r\n// \u2500\u2500\u2500 Server-component detection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Returns true when a file does NOT begin with a \"use client\" directive,\r\n * i.e. it is a server component.\r\n */\r\nexport function isServerComponent(filePath: string): boolean {\r\n const content = fs.readFileSync(filePath, 'utf-8');\r\n for (const line of content.split('\\n').slice(0, 5)) {\r\n const trimmed = line.trim();\r\n if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('/*')) continue;\r\n if (/^[\"']use client[\"'];?$/.test(trimmed)) return false;\r\n break;\r\n }\r\n return true;\r\n}\r\n\r\n// \u2500\u2500\u2500 Layout discovery \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Walks from the pages root to the directory containing `routeFilePath` and\r\n * returns every layout.tsx found, in outermost-first order.\r\n */\r\nexport function findPageLayouts(routeFilePath: string, pagesDir: string): string[] {\r\n const layouts: string[] = [];\r\n\r\n const rootLayout = path.join(pagesDir, 'layout.tsx');\r\n if (fs.existsSync(rootLayout)) layouts.push(rootLayout);\r\n\r\n const relativePath = path.relative(pagesDir, path.dirname(routeFilePath));\r\n if (!relativePath || relativePath === '.') return layouts;\r\n\r\n const segments = relativePath.split(path.sep).filter(Boolean);\r\n for (let i = 1; i <= segments.length; i++) {\r\n const layoutPath = path.join(pagesDir, ...segments.slice(0, i), 'layout.tsx');\r\n if (fs.existsSync(layoutPath)) layouts.push(layoutPath);\r\n }\r\n\r\n return layouts;\r\n}\r\n\r\n/**\r\n * Extracts the identifier used as the default export from a component file.\r\n * Returns null when no default export is found.\r\n *\r\n * Handles three formats so that components compiled by esbuild are recognised\r\n * alongside hand-written source files:\r\n * 1. Source: `export default function Foo` / `export default Foo`\r\n * 2. esbuild: `var Foo_default = Foo` (compiled arrow-function component)\r\n * 3. Re-export: `export { Foo as default }`\r\n */\r\nexport function extractDefaultExportName(filePath: string): string | null {\r\n const content = fs.readFileSync(filePath, 'utf-8');\r\n\r\n // Format 1 \u2013 source: `export default function Foo` or `export default Foo`\r\n let m = content.match(/export\\s+default\\s+(?:function\\s+)?(\\w+)/);\r\n if (m?.[1]) return m[1];\r\n\r\n // Format 2 \u2013 esbuild compiled: `var Foo_default = Foo`\r\n m = content.match(/var\\s+\\w+_default\\s*=\\s*(\\w+)/);\r\n if (m?.[1]) return m[1];\r\n\r\n // Format 3 \u2013 explicit re-export: `export { Foo as default }`\r\n m = content.match(/export\\s*\\{[^}]*\\b(\\w+)\\s+as\\s+default\\b[^}]*\\}/);\r\n if (m?.[1] && !m[1].endsWith('_default')) return m[1];\r\n\r\n return null;\r\n}\r\n\r\n// \u2500\u2500\u2500 Server page collection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Returns all server-component pages inside `pagesDir`, sorted most-specific\r\n * first so precise routes shadow catch-alls in routers.\r\n * layout.tsx files and \"use client\" files are excluded.\r\n */\r\nexport function collectServerPages(pagesDir: string): ServerPage[] {\r\n if (!fs.existsSync(pagesDir)) return [];\r\n return walkFiles(pagesDir)\r\n .filter(relPath => {\r\n if (path.basename(relPath, path.extname(relPath)) === 'layout') return false;\r\n return isServerComponent(path.join(pagesDir, relPath));\r\n })\r\n .map(relPath => ({\r\n ...analyzeFile(relPath, 'page'),\r\n absPath: path.join(pagesDir, relPath),\r\n }))\r\n .sort((a, b) => b.specificity - a.specificity);\r\n}\r\n\r\n/**\r\n * Walks every server page and its layout chain to collect all client component\r\n * IDs reachable anywhere in the app.\r\n */\r\nexport function collectGlobalClientRegistry(\r\n serverPages: ServerPage[],\r\n pagesDir: string,\r\n): Map<string, string> {\r\n const registry = new Map<string, string>();\r\n for (const { absPath } of serverPages) {\r\n for (const [id, p] of findClientComponentsInTree(absPath, pagesDir))\r\n registry.set(id, p);\r\n for (const layoutPath of findPageLayouts(absPath, pagesDir))\r\n for (const [id, p] of findClientComponentsInTree(layoutPath, pagesDir))\r\n registry.set(id, p);\r\n }\r\n return registry;\r\n}\r\n\r\n// \u2500\u2500\u2500 Per-page 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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Builds the per-page client component registry (page + its layout chain)\r\n * and returns both the id\u2192path map and the name\u2192id map needed by\r\n * bundlePageHandler.\r\n */\r\nexport function buildPerPageRegistry(\r\n absPath: string,\r\n layoutPaths: string[],\r\n pagesDir: string,\r\n): { registry: Map<string, string>; clientComponentNames: Record<string, string> } {\r\n const registry = new Map<string, string>();\r\n\r\n for (const [id, p] of findClientComponentsInTree(absPath, pagesDir))\r\n registry.set(id, p);\r\n for (const lp of layoutPaths)\r\n for (const [id, p] of findClientComponentsInTree(lp, pagesDir))\r\n registry.set(id, p);\r\n\r\n const clientComponentNames: Record<string, string> = {};\r\n for (const [id, filePath] of registry) {\r\n const name = extractDefaultExportName(filePath);\r\n if (name) clientComponentNames[name] = id;\r\n }\r\n\r\n return { registry, clientComponentNames };\r\n}\r\n\r\n// \u2500\u2500\u2500 High-level page builder \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Runs both passes of the page build:\r\n *\r\n * Pass 1 \u2014 bundles all client components to `staticDir/__client-component/`\r\n * and collects pre-rendered HTML for each.\r\n * Pass 2 \u2014 bundles every server-component page into a self-contained ESM\r\n * handler and returns the results as `BuiltPage[]`.\r\n */\r\nexport async function buildPages(\r\n pagesDir: string,\r\n staticDir: string,\r\n): Promise<BuiltPage[]> {\r\n const serverPages = collectServerPages(pagesDir);\r\n\r\n if (fs.existsSync(pagesDir) && walkFiles(pagesDir).length > 0 && serverPages.length === 0) {\r\n console.warn(`\u26A0 Pages found in ${pagesDir} but none are server components`);\r\n }\r\n\r\n if (serverPages.length === 0) return [];\r\n\r\n const globalRegistry = collectGlobalClientRegistry(serverPages, pagesDir);\r\n const prerenderedHtml = await bundleClientComponents(globalRegistry, pagesDir, staticDir);\r\n const prerenderedRecord = Object.fromEntries(prerenderedHtml);\r\n\r\n const builtPages: BuiltPage[] = [];\r\n\r\n for (const page of serverPages) {\r\n console.log(` building ${page.absPath} \u2192 ${page.funcPath} [page]`);\r\n\r\n const layoutPaths = findPageLayouts(page.absPath, pagesDir);\r\n const { registry, clientComponentNames } = buildPerPageRegistry(page.absPath, layoutPaths, pagesDir);\r\n\r\n const bundleText = await bundlePageHandler({\r\n absPath: page.absPath,\r\n pagesDir,\r\n clientComponentNames,\r\n allClientIds: [...registry.keys()],\r\n layoutPaths,\r\n prerenderedHtml: prerenderedRecord,\r\n catchAllNames: page.catchAllNames,\r\n });\r\n\r\n builtPages.push({ ...page, bundleText });\r\n }\r\n\r\n return builtPages;\r\n}\r\n\r\n// \u2500\u2500\u2500 API adapter template \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Returns the TypeScript source for a thin HTTP adapter that wraps an API\r\n * route module and exposes a single `handler(req, res)` default export.\r\n */\r\nexport function makeApiAdapterSource(handlerFilename: string): string {\r\n return `\\\r\nimport type { IncomingMessage, ServerResponse } from 'http';\r\nimport * as mod from ${JSON.stringify('./' + handlerFilename)};\r\n\r\nfunction enhance(res: ServerResponse) {\r\n (res as any).json = function (data: any, status = 200) {\r\n this.statusCode = status;\r\n this.setHeader('Content-Type', 'application/json');\r\n this.end(JSON.stringify(data));\r\n };\r\n (res as any).status = function (code: number) { this.statusCode = code; return this; };\r\n return res;\r\n}\r\n\r\nasync function parseBody(req: IncomingMessage): Promise<any> {\r\n return new Promise((resolve, reject) => {\r\n let body = '';\r\n req.on('data', chunk => { body += chunk.toString(); });\r\n req.on('end', () => {\r\n try {\r\n resolve(body && req.headers['content-type']?.includes('application/json')\r\n ? JSON.parse(body) : body);\r\n } catch (e) { reject(e); }\r\n });\r\n req.on('error', reject);\r\n });\r\n}\r\n\r\nexport default async function handler(req: IncomingMessage, res: ServerResponse) {\r\n const method = (req.method || 'GET').toUpperCase();\r\n const apiRes = enhance(res);\r\n const apiReq = req as any;\r\n\r\n apiReq.body = await parseBody(req);\r\n apiReq.query = Object.fromEntries(new URL(req.url || '/', 'http://localhost').searchParams);\r\n apiReq.params = apiReq.query;\r\n\r\n const fn = (mod as any)[method] ?? (mod as any).default;\r\n if (typeof fn !== 'function') {\r\n (apiRes as any).json({ error: \\`Method \\${method} not allowed\\` }, 405);\r\n return;\r\n }\r\n await fn(apiReq, apiRes);\r\n}\r\n`;\r\n}\r\n\r\n// \u2500\u2500\u2500 Page adapter template \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nexport interface PageAdapterOptions {\r\n /** e.g. './home.tsx' \u2014 relative import for the page default export */\r\n pageImport: string;\r\n /** Newline-joined import statements for layout components */\r\n layoutImports: string;\r\n /** function-name \u2192 cc_id map, computed at build time */\r\n clientComponentNames: Record<string, string>;\r\n /** All client component IDs reachable from this page */\r\n allClientIds: string[];\r\n /** Comma-separated list of __layout_N__ identifiers */\r\n layoutArrayItems: string;\r\n /** Pre-rendered HTML per client component ID, computed at build time */\r\n prerenderedHtml: Record<string, string>;\r\n /** Catch-all param names whose runtime values are string[] not string */\r\n catchAllNames: string[];\r\n}\r\n\r\n/**\r\n * Returns the TypeScript source for a fully self-contained page handler.\r\n *\r\n * The adapter:\r\n * \u2022 Inlines the html-store so useHtml() works without external deps.\r\n * \u2022 Contains an async recursive renderer for server + client components.\r\n * \u2022 Client components are identified via the pre-computed CLIENT_COMPONENTS\r\n * map \u2014 no fs.readFileSync at runtime.\r\n * \u2022 Emits the full HTML document including the __n_data blob and bootstrap.\r\n */\r\nexport function makePageAdapterSource(opts: PageAdapterOptions): string {\r\n const {\r\n pageImport, layoutImports, clientComponentNames, allClientIds,\r\n layoutArrayItems, prerenderedHtml, catchAllNames,\r\n } = opts;\r\n\r\n return `\\\r\nimport type { IncomingMessage, ServerResponse } from 'http';\r\nimport { createElement as __createElement__ } from 'react';\r\nimport { renderToString as __renderToString__ } from 'react-dom/server';\r\nimport * as __page__ from ${pageImport};\r\n${layoutImports}\r\n\r\nconst CLIENT_COMPONENTS: Record<string, string> = ${JSON.stringify(clientComponentNames)};\r\nconst ALL_CLIENT_IDS: string[] = ${JSON.stringify(allClientIds)};\r\nconst PRERENDERED_HTML: Record<string, string> = ${JSON.stringify(prerenderedHtml)};\r\nconst CATCH_ALL_NAMES = new Set(${JSON.stringify(catchAllNames)});\r\n\r\n// \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\r\ntype TitleValue = string | ((prev: string) => string);\r\ninterface HtmlStore {\r\n titleOps: TitleValue[];\r\n htmlAttrs: Record<string, string | undefined>;\r\n bodyAttrs: Record<string, string | undefined>;\r\n meta: Record<string, string | undefined>[];\r\n link: Record<string, string | undefined>[];\r\n script: Record<string, any>[];\r\n style: { content?: string; media?: string }[];\r\n}\r\nconst __STORE_KEY__ = Symbol.for('__nukejs_html_store__');\r\nconst __getStore = (): HtmlStore | null => (globalThis as any)[__STORE_KEY__] ?? null;\r\nconst __setStore = (s: HtmlStore | null): void => { (globalThis as any)[__STORE_KEY__] = s; };\r\nconst __emptyStore = (): HtmlStore =>\r\n ({ titleOps: [], htmlAttrs: {}, bodyAttrs: {}, meta: [], link: [], script: [], style: [] });\r\nasync function runWithHtmlStore(fn: () => Promise<void>): Promise<HtmlStore> {\r\n __setStore(__emptyStore());\r\n try { await fn(); return { ...(__getStore() ?? __emptyStore()) }; }\r\n finally { __setStore(null); }\r\n}\r\nfunction resolveTitle(ops: TitleValue[], fallback = ''): string {\r\n let t = fallback;\r\n for (let i = ops.length - 1; i >= 0; i--) {\r\n const op = ops[i]; t = typeof op === 'string' ? op : op(t);\r\n }\r\n return t;\r\n}\r\n\r\n// \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\r\nfunction escapeHtml(s: string): string {\r\n return String(s)\r\n .replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')\r\n .replace(/\"/g, '"').replace(/'/g, ''');\r\n}\r\nfunction escapeAttr(s: string): string {\r\n return String(s).replace(/&/g, '&').replace(/\"/g, '"');\r\n}\r\nfunction renderAttrs(attrs: Record<string, string | boolean | undefined>): string {\r\n return Object.entries(attrs)\r\n .filter(([, v]) => v !== undefined && v !== false)\r\n .map(([k, v]) => v === true ? k : \\`\\${k}=\"\\${escapeAttr(String(v))}\"\\`)\r\n .join(' ');\r\n}\r\nfunction openTag(tag: string, attrs: Record<string, string | undefined>): string {\r\n const s = renderAttrs(attrs as Record<string, string | boolean | undefined>);\r\n return s ? \\`<\\${tag} \\${s}>\\` : \\`<\\${tag}>\\`;\r\n}\r\nfunction renderMetaTag(tag: Record<string, string | undefined>): string {\r\n const key = (k: string) => k === 'httpEquiv' ? 'http-equiv' : k;\r\n const attrs: Record<string, string | undefined> = {};\r\n for (const [k, v] of Object.entries(tag)) if (v !== undefined) attrs[key(k)] = v;\r\n return \\` <meta \\${renderAttrs(attrs as any)} />\\`;\r\n}\r\nfunction renderLinkTag(tag: Record<string, string | undefined>): string {\r\n const key = (k: string) => k === 'hrefLang' ? 'hreflang' : k === 'crossOrigin' ? 'crossorigin' : k;\r\n const attrs: Record<string, string | undefined> = {};\r\n for (const [k, v] of Object.entries(tag)) if (v !== undefined) attrs[key(k)] = v;\r\n return \\` <link \\${renderAttrs(attrs as any)} />\\`;\r\n}\r\nfunction renderScriptTag(tag: any): string {\r\n const s = renderAttrs({ src: tag.src, type: tag.type, crossorigin: tag.crossOrigin,\r\n integrity: tag.integrity, defer: tag.defer, async: tag.async, nomodule: tag.noModule });\r\n return \\` \\${s ? \\`<script \\${s}>\\` : '<script>'}\\${tag.src ? '' : (tag.content ?? '')}</script>\\`;\r\n}\r\nfunction renderStyleTag(tag: any): string {\r\n const media = tag.media ? \\` media=\"\\${escapeAttr(tag.media)}\"\\` : '';\r\n return \\` <style\\${media}>\\${tag.content ?? ''}</style>\\`;\r\n}\r\n\r\n// \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\r\nconst VOID_TAGS = new Set([\r\n 'area','base','br','col','embed','hr','img','input',\r\n 'link','meta','param','source','track','wbr',\r\n]);\r\n\r\n// \u2500\u2500\u2500 Wrapper attribute 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\r\nfunction isWrapperAttr(key: string): boolean {\r\n return (\r\n key === 'className' ||\r\n key === 'style' ||\r\n key === 'id' ||\r\n key.startsWith('data-') ||\r\n key.startsWith('aria-')\r\n );\r\n}\r\nfunction splitWrapperAttrs(props: any): { wrapperAttrs: Record<string, any>; componentProps: Record<string, any> } {\r\n const wrapperAttrs: Record<string, any> = {};\r\n const componentProps: Record<string, any> = {};\r\n for (const [key, value] of Object.entries((props || {}) as Record<string, any>)) {\r\n if (isWrapperAttr(key)) wrapperAttrs[key] = value;\r\n else componentProps[key] = value;\r\n }\r\n return { wrapperAttrs, componentProps };\r\n}\r\nfunction buildWrapperAttrString(attrs: Record<string, any>): string {\r\n const parts = Object.entries(attrs)\r\n .map(([key, value]) => {\r\n if (key === 'className') key = 'class';\r\n if (key === 'style' && typeof value === 'object') {\r\n const css = Object.entries(value as Record<string, any>)\r\n .map(([p, val]) => \\`\\${p.replace(/[A-Z]/g, m => \\`-\\${m.toLowerCase()}\\`)}:\\${escapeHtml(String(val))}\\`)\r\n .join(';');\r\n return \\`style=\"\\${css}\"\\`;\r\n }\r\n if (typeof value === 'boolean') return value ? key : '';\r\n if (value == null) return '';\r\n return \\`\\${key}=\"\\${escapeHtml(String(value))}\"\\`;\r\n })\r\n .filter(Boolean);\r\n return parts.length ? ' ' + parts.join(' ') : '';\r\n}\r\n\r\nfunction serializeProps(value: any): any {\r\n if (value == null || typeof value !== 'object') return value;\r\n if (typeof value === 'function') return undefined;\r\n if (Array.isArray(value)) return value.map(serializeProps).filter((v: any) => v !== undefined);\r\n if ((value as any).$$typeof) {\r\n const { type, props: p } = value as any;\r\n if (typeof type === 'string') return { __re: 'html', tag: type, props: serializeProps(p) };\r\n if (typeof type === 'function') {\r\n const cid = CLIENT_COMPONENTS[type.name];\r\n if (cid) return { __re: 'client', componentId: cid, props: serializeProps(p) };\r\n }\r\n return undefined;\r\n }\r\n const out: any = {};\r\n for (const [k, v] of Object.entries(value as Record<string, any>)) {\r\n const s = serializeProps(v);\r\n if (s !== undefined) out[k] = s;\r\n }\r\n return out;\r\n}\r\n\r\nasync function renderNode(node: any, hydrated: Set<string>): Promise<string> {\r\n if (node == null || typeof node === 'boolean') return '';\r\n if (typeof node === 'string') return escapeHtml(node);\r\n if (typeof node === 'number') return String(node);\r\n if (Array.isArray(node)) return (await Promise.all(node.map(n => renderNode(n, hydrated)))).join('');\r\n\r\n const { type, props } = node as { type: any; props: Record<string, any> };\r\n if (!type) return '';\r\n\r\n if (type === Symbol.for('react.fragment')) return renderNode(props?.children ?? null, hydrated);\r\n\r\n if (typeof type === 'string') {\r\n const { children, dangerouslySetInnerHTML, ...rest } = props || {};\r\n const attrParts: string[] = [];\r\n for (const [k, v] of Object.entries(rest as Record<string, any>)) {\r\n const name = k === 'className' ? 'class' : k === 'htmlFor' ? 'for' : k;\r\n if (typeof v === 'boolean') { if (v) attrParts.push(name); continue; }\r\n if (v == null) continue;\r\n if (k === 'style' && typeof v === 'object') {\r\n const css = Object.entries(v as Record<string, any>)\r\n .map(([p, val]) => \\`\\${p.replace(/[A-Z]/g, m => \\`-\\${m.toLowerCase()}\\`)}:\\${escapeHtml(String(val))}\\`)\r\n .join(';');\r\n attrParts.push(\\`style=\"\\${css}\"\\`);\r\n continue;\r\n }\r\n attrParts.push(\\`\\${name}=\"\\${escapeHtml(String(v))}\"\\`);\r\n }\r\n const attrStr = attrParts.length ? ' ' + attrParts.join(' ') : '';\r\n if (VOID_TAGS.has(type)) return \\`<\\${type}\\${attrStr} />\\`;\r\n const inner = dangerouslySetInnerHTML\r\n ? (dangerouslySetInnerHTML as any).__html\r\n : await renderNode(children ?? null, hydrated);\r\n return \\`<\\${type}\\${attrStr}>\\${inner}</\\${type}>\\`;\r\n }\r\n\r\n if (typeof type === 'function') {\r\n const clientId = CLIENT_COMPONENTS[type.name];\r\n if (clientId) {\r\n hydrated.add(clientId);\r\n const { wrapperAttrs, componentProps } = splitWrapperAttrs(props);\r\n const wrapperAttrStr = buildWrapperAttrString(wrapperAttrs);\r\n const serializedProps = serializeProps(componentProps ?? {});\r\n let ssrHtml: string;\r\n try {\r\n ssrHtml = __renderToString__(__createElement__(type as any, componentProps || {}));\r\n } catch {\r\n ssrHtml = PRERENDERED_HTML[clientId] ?? '';\r\n }\r\n return \\`<span data-hydrate-id=\"\\${clientId}\"\\${wrapperAttrStr} data-hydrate-props=\"\\${escapeHtml(JSON.stringify(serializedProps))}\">\\${ssrHtml}</span>\\`;\r\n }\r\n const instance = type.prototype?.isReactComponent ? new (type as any)(props) : null;\r\n return renderNode(instance ? instance.render() : await (type as Function)(props || {}), hydrated);\r\n }\r\n\r\n return '';\r\n}\r\n\r\n// \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\r\nconst LAYOUT_COMPONENTS: Array<(props: any) => any> = [${layoutArrayItems}];\r\n\r\nfunction wrapWithLayouts(element: any): any {\r\n let el = element;\r\n for (let i = LAYOUT_COMPONENTS.length - 1; i >= 0; i--)\r\n el = { type: LAYOUT_COMPONENTS[i], props: { children: el }, key: null, ref: null };\r\n return el;\r\n}\r\n\r\n// \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\r\nexport default async function handler(req: IncomingMessage, res: ServerResponse): Promise<void> {\r\n try {\r\n const parsed = new URL(req.url || '/', 'http://localhost');\r\n const params: Record<string, string | string[]> = {};\r\n parsed.searchParams.forEach((_, k) => {\r\n params[k] = CATCH_ALL_NAMES.has(k)\r\n ? parsed.searchParams.getAll(k)\r\n : parsed.searchParams.get(k) as string;\r\n });\r\n const url = req.url || '/';\r\n\r\n const hydrated = new Set<string>();\r\n const wrapped = wrapWithLayouts({ type: __page__.default, props: params as any, key: null, ref: null });\r\n\r\n let appHtml = '';\r\n const store = await runWithHtmlStore(async () => { appHtml = await renderNode(wrapped, hydrated); });\r\n\r\n const pageTitle = resolveTitle(store.titleOps, 'NukeJS');\r\n const headScripts = store.script.filter((s: any) => (s.position ?? 'head') === 'head');\r\n const bodyScripts = store.script.filter((s: any) => s.position === 'body');\r\n const headLines = [\r\n ' <meta charset=\"utf-8\" />',\r\n ' <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />',\r\n \\` <title>\\${escapeHtml(pageTitle)}</title>\\`,\r\n ...(store.meta.length || store.link.length || store.style.length || headScripts.length ? [\r\n ' <!--n-head-->',\r\n ...store.meta.map(renderMetaTag),\r\n ...store.link.map(renderLinkTag),\r\n ...store.style.map(renderStyleTag),\r\n ...headScripts.map(renderScriptTag),\r\n ' <!--/n-head-->',\r\n ] : []),\r\n ];\r\n const bodyScriptLines = bodyScripts.length\r\n ? [' <!--n-body-scripts-->', ...bodyScripts.map(renderScriptTag), ' <!--/n-body-scripts-->']\r\n : [];\r\n const bodyScriptsHtml = bodyScriptLines.length ? '\\\\n' + bodyScriptLines.join('\\\\n') + '\\\\n' : '';\r\n\r\n const runtimeData = JSON.stringify({\r\n hydrateIds: [...hydrated], allIds: ALL_CLIENT_IDS, url, params, debug: 'silent',\r\n }).replace(/</g, '\\\\u003c').replace(/>/g, '\\\\u003e').replace(/&/g, '\\\\u0026');\r\n\r\n const html = \\`<!DOCTYPE html>\r\n\\${openTag('html', store.htmlAttrs)}\r\n<head>\r\n\\${headLines.join('\\\\n')}\r\n</head>\r\n\\${openTag('body', store.bodyAttrs)}\r\n <div id=\"app\">\\${appHtml}</div>\r\n\r\n <script id=\"__n_data\" type=\"application/json\">\\${runtimeData}</script>\r\n\r\n <script type=\"importmap\">\r\n {\r\n \"imports\": {\r\n \"react\": \"/__n.js\",\r\n \"react-dom/client\": \"/__n.js\",\r\n \"react/jsx-runtime\": \"/__n.js\",\r\n \"nukejs\": \"/__n.js\"\r\n }\r\n }\r\n </script>\r\n\r\n <script type=\"module\">\r\n const { initRuntime } = await import('nukejs');\r\n const data = JSON.parse(document.getElementById('__n_data').textContent);\r\n await initRuntime(data);\r\n </script>\r\n\\${bodyScriptsHtml}</body>\r\n</html>\\`;\r\n\r\n res.statusCode = 200;\r\n res.setHeader('Content-Type', 'text/html; charset=utf-8');\r\n res.end(html);\r\n } catch (err: any) {\r\n console.error('[page render error]', err);\r\n res.statusCode = 500;\r\n res.setHeader('Content-Type', 'text/plain; charset=utf-8');\r\n res.end('Internal Server Error');\r\n }\r\n}\r\n`;\r\n}\r\n\r\n// \u2500\u2500\u2500 Bundle operations \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Bundles an API route handler into a single self-contained ESM string.\r\n * node_modules are kept external \u2014 they exist at runtime on both Node and\r\n * Vercel (Vercel bundles them separately via the pages dispatcher).\r\n */\r\nexport async function bundleApiHandler(absPath: string): Promise<string> {\r\n const adapterName = `_api_adapter_${randomBytes(4).toString('hex')}.ts`;\r\n const adapterPath = path.join(path.dirname(absPath), adapterName);\r\n fs.writeFileSync(adapterPath, makeApiAdapterSource(path.basename(absPath)));\r\n\r\n let text: string;\r\n try {\r\n const result = await build({\r\n entryPoints: [adapterPath],\r\n bundle: true,\r\n format: 'esm',\r\n platform: 'node',\r\n target: 'node20',\r\n packages: 'external',\r\n write: false,\r\n });\r\n text = result.outputFiles[0].text;\r\n } finally {\r\n fs.unlinkSync(adapterPath);\r\n }\r\n return text;\r\n}\r\n\r\nexport interface PageBundleOptions {\r\n absPath: string;\r\n pagesDir: string;\r\n clientComponentNames: Record<string, string>;\r\n allClientIds: string[];\r\n layoutPaths: string[];\r\n prerenderedHtml: Record<string, string>;\r\n catchAllNames: string[];\r\n}\r\n\r\n/**\r\n * Bundles a server-component page into a single self-contained ESM string.\r\n * All npm packages are kept external \u2014 the Node production server has\r\n * node_modules available at runtime.\r\n */\r\nexport async function bundlePageHandler(opts: PageBundleOptions): Promise<string> {\r\n const {\r\n absPath, clientComponentNames, allClientIds,\r\n layoutPaths, prerenderedHtml, catchAllNames,\r\n } = opts;\r\n\r\n const adapterDir = path.dirname(absPath);\r\n const adapterPath = path.join(adapterDir, `_page_adapter_${randomBytes(4).toString('hex')}.ts`);\r\n\r\n const layoutImports = layoutPaths\r\n .map((lp, i) => {\r\n const rel = path.relative(adapterDir, lp).replace(/\\\\/g, '/');\r\n return `import __layout_${i}__ from ${JSON.stringify(rel.startsWith('.') ? rel : './' + rel)};`;\r\n })\r\n .join('\\n');\r\n\r\n fs.writeFileSync(adapterPath, makePageAdapterSource({\r\n pageImport: JSON.stringify('./' + path.basename(absPath)),\r\n layoutImports,\r\n clientComponentNames,\r\n allClientIds,\r\n layoutArrayItems: layoutPaths.map((_, i) => `__layout_${i}__`).join(', '),\r\n prerenderedHtml,\r\n catchAllNames,\r\n }));\r\n\r\n let text: string;\r\n try {\r\n const result = await build({\r\n entryPoints: [adapterPath],\r\n bundle: true,\r\n format: 'esm',\r\n platform: 'node',\r\n target: 'node20',\r\n jsx: 'automatic',\r\n packages: 'external',\r\n external: NODE_BUILTINS,\r\n define: { 'process.env.NODE_ENV': '\"production\"' },\r\n write: false,\r\n });\r\n text = result.outputFiles[0].text;\r\n } finally {\r\n fs.unlinkSync(adapterPath);\r\n }\r\n return text;\r\n}\r\n\r\n/**\r\n * Bundles every client component in `globalRegistry` to\r\n * `<staticDir>/__client-component/<id>.js` and pre-renders each to HTML.\r\n */\r\nexport async function bundleClientComponents(\r\n globalRegistry: Map<string, string>,\r\n pagesDir: string,\r\n staticDir: string,\r\n): Promise<Map<string, string>> {\r\n if (globalRegistry.size === 0) return new Map();\r\n\r\n const outDir = path.join(staticDir, '__client-component');\r\n fs.mkdirSync(outDir, { recursive: true });\r\n\r\n const prerendered = new Map<string, string>();\r\n\r\n for (const [id, filePath] of globalRegistry) {\r\n console.log(` bundling client ${id} (${path.relative(pagesDir, filePath)})`);\r\n\r\n // Browser bundle \u2014 served to the client for hydration\r\n const browserResult = await build({\r\n entryPoints: [filePath],\r\n bundle: true,\r\n format: 'esm',\r\n platform: 'browser',\r\n jsx: 'automatic',\r\n minify: true,\r\n external: ['react', 'react-dom/client', 'react/jsx-runtime'],\r\n define: { 'process.env.NODE_ENV': '\"production\"' },\r\n write: false,\r\n });\r\n fs.writeFileSync(path.join(outDir, `${id}.js`), browserResult.outputFiles[0].text);\r\n\r\n // SSR pre-render \u2014 bundle for Node, import, renderToString, discard\r\n const ssrTmp = path.join(\r\n path.dirname(filePath),\r\n `_ssr_${id}_${randomBytes(4).toString('hex')}.mjs`,\r\n );\r\n try {\r\n const ssrResult = await build({\r\n entryPoints: [filePath],\r\n bundle: true,\r\n format: 'esm',\r\n platform: 'node',\r\n target: 'node20',\r\n jsx: 'automatic',\r\n packages: 'external',\r\n define: { 'process.env.NODE_ENV': '\"production\"' },\r\n write: false,\r\n });\r\n fs.writeFileSync(ssrTmp, ssrResult.outputFiles[0].text);\r\n\r\n const { default: Component } = await import(pathToFileURL(ssrTmp).href);\r\n const { createElement } = await import('react');\r\n const { renderToString } = await import('react-dom/server');\r\n\r\n prerendered.set(id, renderToString(createElement(Component, {})));\r\n console.log(` prerendered ${id}`);\r\n } catch {\r\n prerendered.set(id, '');\r\n } finally {\r\n if (fs.existsSync(ssrTmp)) fs.unlinkSync(ssrTmp);\r\n }\r\n }\r\n\r\n console.log(` bundled ${globalRegistry.size} client component(s) \u2192 ${path.relative(process.cwd(), outDir)}/`);\r\n return prerendered;\r\n}\r\n\r\n/**\r\n * Builds the combined browser bundle (__n.js) that contains the full React\r\n * runtime + NukeJS client runtime in a single file.\r\n */\r\nexport async function buildCombinedBundle(staticDir: string): Promise<void> {\r\n const nukeDir = path.dirname(fileURLToPath(import.meta.url));\r\n const bundleFile = nukeDir.endsWith('dist') ? 'bundle' : 'bundle.ts';\r\n\r\n const result = await build({\r\n stdin: {\r\n contents: `\r\nimport React, {\r\n useState, useEffect, useContext, useReducer, useCallback, useMemo,\r\n useRef, useImperativeHandle, useLayoutEffect, useDebugValue,\r\n useDeferredValue, useTransition, useId, useSyncExternalStore,\r\n useInsertionEffect, createContext, forwardRef, memo, lazy,\r\n Suspense, Fragment, StrictMode, Component, PureComponent\r\n} from 'react';\r\nimport { jsx, jsxs } from 'react/jsx-runtime';\r\nimport { hydrateRoot, createRoot } from 'react-dom/client';\r\nexport { initRuntime, setupLocationChangeMonitor } from './${bundleFile}';\r\nexport {\r\n useState, useEffect, useContext, useReducer, useCallback, useMemo,\r\n useRef, useImperativeHandle, useLayoutEffect, useDebugValue,\r\n useDeferredValue, useTransition, useId, useSyncExternalStore,\r\n useInsertionEffect, createContext, forwardRef, memo, lazy,\r\n Suspense, Fragment, StrictMode, Component, PureComponent,\r\n hydrateRoot, createRoot, jsx, jsxs\r\n};\r\nexport default React;\r\n`,\r\n loader: 'ts',\r\n resolveDir: nukeDir,\r\n },\r\n bundle: true,\r\n write: false,\r\n treeShaking: true,\r\n minify: true,\r\n format: 'esm',\r\n jsx: 'automatic',\r\n alias: {\r\n react: path.dirname(fileURLToPath(import.meta.resolve('react/package.json'))),\r\n 'react-dom': path.dirname(fileURLToPath(import.meta.resolve('react-dom/package.json'))),\r\n },\r\n define: { 'process.env.NODE_ENV': '\"production\"' },\r\n });\r\n\r\n fs.writeFileSync(path.join(staticDir, '__n.js'), result.outputFiles[0].text);\r\n console.log(' built __n.js (react + runtime)');\r\n}\r\n\r\n// \u2500\u2500\u2500 Public file copying \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Recursively copies every file from `publicDir` into `destDir`, preserving\r\n * the directory structure. Skips silently when `publicDir` does not exist.\r\n */\r\nexport function copyPublicFiles(publicDir: string, destDir: string): void {\r\n if (!fs.existsSync(publicDir)) return;\r\n\r\n let count = 0;\r\n (function walk(src: string, dest: string) {\r\n fs.mkdirSync(dest, { recursive: true });\r\n for (const entry of fs.readdirSync(src, { withFileTypes: true })) {\r\n const s = path.join(src, entry.name);\r\n const d = path.join(dest, entry.name);\r\n if (entry.isDirectory()) { walk(s, d); } else { fs.copyFileSync(s, d); count++; }\r\n }\r\n })(publicDir, destDir);\r\n\r\n if (count > 0)\r\n console.log(` copied ${count} public file(s) \u2192 ${path.relative(process.cwd(), destDir)}/`);\r\n}"],
|
|
5
|
+
"mappings": "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AA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,yDAkMN,gBAAgB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA4FzE;AASA,eAAsB,iBAAiB,SAAkC;AACvE,QAAM,cAAc,gBAAgB,YAAY,CAAC,EAAE,SAAS,KAAK,CAAC;AAClE,QAAM,cAAc,KAAK,KAAK,KAAK,QAAQ,OAAO,GAAG,WAAW;AAChE,KAAG,cAAc,aAAa,qBAAqB,KAAK,SAAS,OAAO,CAAC,CAAC;AAE1E,MAAI;AACJ,MAAI;AACF,UAAM,SAAS,MAAM,MAAM;AAAA,MACzB,aAAa,CAAC,WAAW;AAAA,MACzB,QAAa;AAAA,MACb,QAAa;AAAA,MACb,UAAa;AAAA,MACb,QAAa;AAAA,MACb,UAAa;AAAA,MACb,OAAa;AAAA,IACf,CAAC;AACD,WAAO,OAAO,YAAY,CAAC,EAAE;AAAA,EAC/B,UAAE;AACA,OAAG,WAAW,WAAW;AAAA,EAC3B;AACA,SAAO;AACT;AAiBA,eAAsB,kBAAkB,MAA0C;AAChF,QAAM;AAAA,IACJ;AAAA,IAAS;AAAA,IAAsB;AAAA,IAC/B;AAAA,IAAa;AAAA,IAAiB;AAAA,EAChC,IAAI;AAEJ,QAAM,aAAc,KAAK,QAAQ,OAAO;AACxC,QAAM,cAAc,KAAK,KAAK,YAAY,iBAAiB,YAAY,CAAC,EAAE,SAAS,KAAK,CAAC,KAAK;AAE9F,QAAM,gBAAgB,YACnB,IAAI,CAAC,IAAI,MAAM;AACd,UAAM,MAAM,KAAK,SAAS,YAAY,EAAE,EAAE,QAAQ,OAAO,GAAG;AAC5D,WAAO,mBAAmB,CAAC,WAAW,KAAK,UAAU,IAAI,WAAW,GAAG,IAAI,MAAM,OAAO,GAAG,CAAC;AAAA,EAC9F,CAAC,EACA,KAAK,IAAI;AAEZ,KAAG,cAAc,aAAa,sBAAsB;AAAA,IAClD,YAAsB,KAAK,UAAU,OAAO,KAAK,SAAS,OAAO,CAAC;AAAA,IAClE;AAAA,IACA;AAAA,IACA;AAAA,IACA,kBAAsB,YAAY,IAAI,CAAC,GAAG,MAAM,YAAY,CAAC,IAAI,EAAE,KAAK,IAAI;AAAA,IAC5E;AAAA,IACA;AAAA,EACF,CAAC,CAAC;AAEF,MAAI;AACJ,MAAI;AACF,UAAM,SAAS,MAAM,MAAM;AAAA,MACzB,aAAa,CAAC,WAAW;AAAA,MACzB,QAAa;AAAA,MACb,QAAa;AAAA,MACb,UAAa;AAAA,MACb,QAAa;AAAA,MACb,KAAa;AAAA,MACb,UAAa;AAAA,MACb,UAAa;AAAA,MACb,QAAa,EAAE,wBAAwB,eAAe;AAAA,MACtD,OAAa;AAAA,IACf,CAAC;AACD,WAAO,OAAO,YAAY,CAAC,EAAE;AAAA,EAC/B,UAAE;AACA,OAAG,WAAW,WAAW;AAAA,EAC3B;AACA,SAAO;AACT;AAMA,eAAsB,uBACpB,gBACA,UACA,WAC8B;AAC9B,MAAI,eAAe,SAAS,EAAG,QAAO,oBAAI,IAAI;AAE9C,QAAM,SAAS,KAAK,KAAK,WAAW,oBAAoB;AACxD,KAAG,UAAU,QAAQ,EAAE,WAAW,KAAK,CAAC;AAExC,QAAM,cAAc,oBAAI,IAAoB;AAE5C,aAAW,CAAC,IAAI,QAAQ,KAAK,gBAAgB;AAC3C,YAAQ,IAAI,uBAAuB,EAAE,MAAM,KAAK,SAAS,UAAU,QAAQ,CAAC,GAAG;AAG/E,UAAM,gBAAgB,MAAM,MAAM;AAAA,MAChC,aAAa,CAAC,QAAQ;AAAA,MACtB,QAAa;AAAA,MACb,QAAa;AAAA,MACb,UAAa;AAAA,MACb,KAAa;AAAA,MACb,QAAa;AAAA,MACb,UAAa,CAAC,SAAS,oBAAoB,mBAAmB;AAAA,MAC9D,QAAa,EAAE,wBAAwB,eAAe;AAAA,MACtD,OAAa;AAAA,IACf,CAAC;AACD,OAAG,cAAc,KAAK,KAAK,QAAQ,GAAG,EAAE,KAAK,GAAG,cAAc,YAAY,CAAC,EAAE,IAAI;AAGjF,UAAM,SAAS,KAAK;AAAA,MAClB,KAAK,QAAQ,QAAQ;AAAA,MACrB,QAAQ,EAAE,IAAI,YAAY,CAAC,EAAE,SAAS,KAAK,CAAC;AAAA,IAC9C;AACA,QAAI;AACF,YAAM,YAAY,MAAM,MAAM;AAAA,QAC5B,aAAa,CAAC,QAAQ;AAAA,QACtB,QAAa;AAAA,QACb,QAAa;AAAA,QACb,UAAa;AAAA,QACb,QAAa;AAAA,QACb,KAAa;AAAA,QACb,UAAa;AAAA,QACb,QAAa,EAAE,wBAAwB,eAAe;AAAA,QACtD,OAAa;AAAA,MACf,CAAC;AACD,SAAG,cAAc,QAAQ,UAAU,YAAY,CAAC,EAAE,IAAI;AAEtD,YAAM,EAAE,SAAS,UAAU,IAAI,MAAM,OAAO,cAAc,MAAM,EAAE;AAClE,YAAM,EAAE,cAAc,IAAS,MAAM,OAAO,OAAO;AACnD,YAAM,EAAE,eAAe,IAAQ,MAAM,OAAO,kBAAkB;AAE9D,kBAAY,IAAI,IAAI,eAAe,cAAc,WAAW,CAAC,CAAC,CAAC,CAAC;AAChE,cAAQ,IAAI,uBAAuB,EAAE,EAAE;AAAA,IACzC,QAAQ;AACN,kBAAY,IAAI,IAAI,EAAE;AAAA,IACxB,UAAE;AACA,UAAI,GAAG,WAAW,MAAM,EAAG,IAAG,WAAW,MAAM;AAAA,IACjD;AAAA,EACF;AAEA,UAAQ,IAAI,eAAe,eAAe,IAAI,+BAA0B,KAAK,SAAS,QAAQ,IAAI,GAAG,MAAM,CAAC,GAAG;AAC/G,SAAO;AACT;AAMA,eAAsB,oBAAoB,WAAkC;AAC1E,QAAM,UAAa,KAAK,QAAQ,cAAc,YAAY,GAAG,CAAC;AAC9D,QAAM,aAAa,QAAQ,SAAS,MAAM,IAAI,WAAW;AAEzD,QAAM,SAAS,MAAM,MAAM;AAAA,IACzB,OAAO;AAAA,MACL,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,6DAU6C,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAWjE,QAAY;AAAA,MACZ,YAAY;AAAA,IACd;AAAA,IACA,QAAa;AAAA,IACb,OAAa;AAAA,IACb,aAAa;AAAA,IACb,QAAa;AAAA,IACb,QAAa;AAAA,IACb,KAAa;AAAA,IACb,OAAO;AAAA,MACL,OAAa,KAAK,QAAQ,cAAc,YAAY,QAAQ,oBAAoB,CAAC,CAAC;AAAA,MAClF,aAAa,KAAK,QAAQ,cAAc,YAAY,QAAQ,wBAAwB,CAAC,CAAC;AAAA,IACxF;AAAA,IACA,QAAQ,EAAE,wBAAwB,eAAe;AAAA,EACnD,CAAC;AAED,KAAG,cAAc,KAAK,KAAK,WAAW,QAAQ,GAAG,OAAO,YAAY,CAAC,EAAE,IAAI;AAC3E,UAAQ,IAAI,uCAAuC;AACrD;AAQO,SAAS,gBAAgB,WAAmB,SAAuB;AACxE,MAAI,CAAC,GAAG,WAAW,SAAS,EAAG;AAE/B,MAAI,QAAQ;AACZ,GAAC,SAAS,KAAK,KAAa,MAAc;AACxC,OAAG,UAAU,MAAM,EAAE,WAAW,KAAK,CAAC;AACtC,eAAW,SAAS,GAAG,YAAY,KAAK,EAAE,eAAe,KAAK,CAAC,GAAG;AAChE,YAAM,IAAI,KAAK,KAAK,KAAK,MAAM,IAAI;AACnC,YAAM,IAAI,KAAK,KAAK,MAAM,MAAM,IAAI;AACpC,UAAI,MAAM,YAAY,GAAG;AAAE,aAAK,GAAG,CAAC;AAAA,MAAG,OAAO;AAAE,WAAG,aAAa,GAAG,CAAC;AAAG;AAAA,MAAS;AAAA,IAClF;AAAA,EACF,GAAG,WAAW,OAAO;AAErB,MAAI,QAAQ;AACV,YAAQ,IAAI,eAAe,KAAK,0BAAqB,KAAK,SAAS,QAAQ,IAAI,GAAG,OAAO,CAAC,GAAG;AACjG;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/http-server.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import fs from "fs";
|
|
3
3
|
import { pathToFileURL } from "url";
|
|
4
|
-
import { build } from "esbuild";
|
|
5
4
|
import { log } from "./logger.js";
|
|
6
5
|
import { matchRoute } from "./router.js";
|
|
7
6
|
function discoverApiPrefixes(serverDir) {
|
|
@@ -104,17 +103,11 @@ function matchApiPrefix(url, apiPrefixes) {
|
|
|
104
103
|
return null;
|
|
105
104
|
}
|
|
106
105
|
async function importFreshInDev(filePath) {
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
target: "node20",
|
|
113
|
-
packages: "external",
|
|
114
|
-
write: false
|
|
115
|
-
});
|
|
116
|
-
const dataUrl = `data:text/javascript,${encodeURIComponent(result.outputFiles[0].text)}`;
|
|
117
|
-
return await import(dataUrl);
|
|
106
|
+
const { tsImport } = await import("tsx/esm/api");
|
|
107
|
+
return await tsImport(
|
|
108
|
+
pathToFileURL(filePath).href,
|
|
109
|
+
{ parentURL: import.meta.url }
|
|
110
|
+
);
|
|
118
111
|
}
|
|
119
112
|
function createApiHandler({ apiPrefixes, port, isDev }) {
|
|
120
113
|
return async function handleApiRoute(url, req, res) {
|
package/dist/http-server.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/http-server.ts"],
|
|
4
|
-
"sourcesContent": ["/**\r\n * http-server.ts \u2014 API Route Dispatcher\r\n *\r\n * Handles discovery and dispatch of API routes inside `serverDir`.\r\n *\r\n * Directory conventions (mirrors Next.js):\r\n * server/\r\n * users/ \u2192 prefix /users (directory)\r\n * index.ts \u2192 GET /users (method exports: GET, POST, \u2026)\r\n * [id].ts \u2192 GET /users/:id\r\n * auth.ts \u2192 prefix /auth (top-level file)\r\n * index.ts \u2192 prefix / (root handler)\r\n *\r\n * Route handler exports:\r\n * export function GET(req, res) { \u2026 }\r\n * export function POST(req, res) { \u2026 }\r\n * export default function(req, res) { \u2026 } // matches any method\r\n *\r\n * Request augmentation:\r\n * req.body \u2014 parsed JSON or raw string (10 MB limit)\r\n * req.params \u2014 dynamic route segments (e.g. { id: '42' })\r\n * req.query \u2014 URL search params\r\n *\r\n * Response augmentation:\r\n * res.json(data, status?) \u2014 JSON response shorthand\r\n * res.status(code) \u2014 sets statusCode, returns res for chaining\r\n */\r\n\r\nimport path from 'path';\r\nimport fs from 'fs';\r\nimport { pathToFileURL } from 'url';\r\nimport { build } from 'esbuild';\r\nimport type { IncomingMessage, ServerResponse } from 'http';\r\nimport { log } from './logger';\r\nimport { matchRoute } from './router';\r\n\r\n// \u2500\u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/** Describes a single API prefix discovered in serverDir. */\r\nexport interface ApiPrefixInfo {\r\n /** URL prefix this entry handles (e.g. '/users', ''). */\r\n prefix: string;\r\n /** Directory to scan for route files. */\r\n directory: string;\r\n /** Set when the prefix comes from a top-level file (not a directory). */\r\n filePath?: string;\r\n}\r\n\r\n/** Node's IncomingMessage with parsed body, params, and query. */\r\nexport interface ApiRequest extends IncomingMessage {\r\n params?: Record<string, string | string[]>;\r\n query?: Record<string, string>;\r\n body?: any;\r\n}\r\n\r\n/** Node's ServerResponse with json() and status() convenience methods. */\r\nexport interface ApiResponse extends ServerResponse {\r\n json: (data: any, status?: number) => void;\r\n status: (code: number) => ApiResponse;\r\n}\r\n\r\ntype HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'OPTIONS';\r\ntype ApiHandler = (req: ApiRequest, res: ApiResponse) => void | Promise<void>;\r\n\r\ninterface ApiModule {\r\n default?: ApiHandler;\r\n GET?: ApiHandler;\r\n POST?: ApiHandler;\r\n PUT?: ApiHandler;\r\n DELETE?: ApiHandler;\r\n PATCH?: ApiHandler;\r\n OPTIONS?: ApiHandler;\r\n}\r\n\r\n// \u2500\u2500\u2500 Route discovery \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Scans `serverDir` and returns one ApiPrefixInfo per directory, top-level\r\n * file, and root index.ts. Directories are returned before same-stem files\r\n * so `/a/b` routes resolve to the directory tree before any flat file.\r\n *\r\n * Called at startup and again whenever the server directory changes (in dev).\r\n */\r\nexport function discoverApiPrefixes(serverDir: string): ApiPrefixInfo[] {\r\n if (!fs.existsSync(serverDir)) {\r\n log.warn('Server directory not found:', serverDir);\r\n return [];\r\n }\r\n\r\n const entries = fs.readdirSync(serverDir, { withFileTypes: true });\r\n const prefixes: ApiPrefixInfo[] = [];\r\n\r\n // Directories first (higher specificity than same-stem files).\r\n for (const e of entries) {\r\n if (e.isDirectory()) {\r\n prefixes.push({ prefix: `/${e.name}`, directory: path.join(serverDir, e.name) });\r\n }\r\n }\r\n\r\n // Top-level .ts/.tsx files (excluding index which is handled separately below).\r\n for (const e of entries) {\r\n if (\r\n e.isFile() &&\r\n (e.name.endsWith('.ts') || e.name.endsWith('.tsx')) &&\r\n e.name !== 'index.ts' &&\r\n e.name !== 'index.tsx'\r\n ) {\r\n const stem = e.name.replace(/\\.tsx?$/, '');\r\n prefixes.push({\r\n prefix: `/${stem}`,\r\n directory: serverDir,\r\n filePath: path.join(serverDir, e.name),\r\n });\r\n }\r\n }\r\n\r\n // index.ts/tsx at the root of serverDir handles unmatched paths (prefix '').\r\n if (fs.existsSync(path.join(serverDir, 'index.ts'))) {\r\n prefixes.push({ prefix: '', directory: serverDir });\r\n }\r\n\r\n return prefixes;\r\n}\r\n\r\n// \u2500\u2500\u2500 Body parsing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nconst MAX_BODY_BYTES = 10 * 1024 * 1024; // 10 MB\r\n\r\n/**\r\n * Buffers the request body and returns:\r\n * - Parsed JSON object if Content-Type is application/json.\r\n * - Raw string otherwise.\r\n *\r\n * Rejects with an error if the body exceeds MAX_BODY_BYTES to prevent\r\n * memory exhaustion attacks. Deletes __proto__ and constructor from parsed\r\n * JSON objects to guard against prototype pollution.\r\n */\r\nexport async function parseBody(req: IncomingMessage): Promise<any> {\r\n return new Promise((resolve, reject) => {\r\n let body = '';\r\n let bytes = 0;\r\n\r\n req.on('data', chunk => {\r\n bytes += chunk.length;\r\n if (bytes > MAX_BODY_BYTES) {\r\n req.destroy();\r\n return reject(new Error('Request body too large'));\r\n }\r\n body += chunk.toString();\r\n });\r\n\r\n req.on('end', () => {\r\n try {\r\n if (body && req.headers['content-type']?.includes('application/json')) {\r\n const parsed = JSON.parse(body);\r\n // Guard against prototype pollution via __proto__ / constructor.\r\n if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {\r\n delete parsed.__proto__;\r\n delete parsed.constructor;\r\n }\r\n resolve(parsed);\r\n } else {\r\n resolve(body);\r\n }\r\n } catch (err) {\r\n reject(err);\r\n }\r\n });\r\n\r\n req.on('error', reject);\r\n });\r\n}\r\n\r\n// \u2500\u2500\u2500 Query parsing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/** Extracts URL search params into a plain string map. */\r\nexport function parseQuery(url: string, port: number): Record<string, string> {\r\n const query: Record<string, string> = {};\r\n new URL(url, `http://localhost:${port}`)\r\n .searchParams\r\n .forEach((v, k) => { query[k] = v; });\r\n return query;\r\n}\r\n\r\n// \u2500\u2500\u2500 Response enhancement \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Adds `json()` and `status()` convenience methods to a raw ServerResponse,\r\n * mirroring the Express API surface that most API handlers expect.\r\n */\r\nexport function enhanceResponse(res: ServerResponse): ApiResponse {\r\n const apiRes = res as ApiResponse;\r\n apiRes.json = function (data, statusCode = 200) {\r\n this.statusCode = statusCode;\r\n this.setHeader('Content-Type', 'application/json');\r\n this.end(JSON.stringify(data));\r\n };\r\n apiRes.status = function (code) {\r\n this.statusCode = code;\r\n return this;\r\n };\r\n return apiRes;\r\n}\r\n\r\n/** Responds to an OPTIONS preflight with permissive CORS headers. */\r\nfunction respondOptions(res: ApiResponse): void {\r\n res.setHeader('Access-Control-Allow-Origin', '*');\r\n res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');\r\n res.setHeader('Access-Control-Allow-Headers', 'Content-Type');\r\n res.statusCode = 204;\r\n res.end();\r\n}\r\n\r\n// \u2500\u2500\u2500 Prefix matching \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Finds the first ApiPrefixInfo whose prefix is a prefix of `url`.\r\n *\r\n * The empty-string prefix ('') acts as a catch-all and only matches when no\r\n * other prefix claims the URL.\r\n *\r\n * Returns `null` when no prefix matches (request should fall through to SSR).\r\n */\r\nexport function matchApiPrefix(\r\n url: string,\r\n apiPrefixes: ApiPrefixInfo[],\r\n): { prefix: ApiPrefixInfo; apiPath: string } | null {\r\n for (const prefix of apiPrefixes) {\r\n if (prefix.prefix === '') {\r\n // Empty prefix \u2014 only match if no other prefix has claimed this URL.\r\n const claimedByOther = apiPrefixes.some(\r\n p => p.prefix !== '' && url.startsWith(p.prefix),\r\n );\r\n if (!claimedByOther) return { prefix, apiPath: url || '/' };\r\n } else if (url.startsWith(prefix.prefix)) {\r\n return { prefix, apiPath: url.slice(prefix.prefix.length) || '/' };\r\n }\r\n }\r\n return null;\r\n}\r\n\r\n// \u2500\u2500\u2500 Dev-mode handler importer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n// \u2500\u2500\u2500 Dev-mode fresh importer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Bundles `filePath` + all its local transitive imports into a single ESM\r\n * string via esbuild on every call, writes it to a unique temp file, imports\r\n * it, then deletes the temp file.\r\n *\r\n * Why esbuild bundling and not ?t=timestamp:\r\n * Node's ESM cache is keyed on the full URL string. ?t=timestamp busts only\r\n * the entry file \u2014 every `import './other'` inside it resolves from cache as\r\n * normal. Bundling inlines all local deps so there are no transitive cache\r\n * hits at all. npm packages are kept external (packages: 'external') because\r\n * they live in node_modules and never change between requests.\r\n */\r\nasync function importFreshInDev(filePath: string): Promise<ApiModule> {\r\n const result = await build({\r\n entryPoints: [filePath],\r\n bundle: true,\r\n format: 'esm',\r\n platform: 'node',\r\n target: 'node20',\r\n packages: 'external',\r\n write: false,\r\n });\r\n\r\n const dataUrl = `data:text/javascript,${encodeURIComponent(result.outputFiles[0].text)}`;\r\n return await import(dataUrl) as ApiModule;\r\n}\r\n\r\n// \u2500\u2500\u2500 Request handler factory \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\ninterface ApiHandlerOptions {\r\n apiPrefixes: ApiPrefixInfo[];\r\n port: number;\r\n isDev: boolean;\r\n}\r\n\r\nexport function createApiHandler({ apiPrefixes, port, isDev }: ApiHandlerOptions) {\r\n return async function handleApiRoute(\r\n url: string,\r\n req: IncomingMessage,\r\n res: ServerResponse,\r\n ): Promise<void> {\r\n const apiRes = enhanceResponse(res);\r\n const apiMatch = matchApiPrefix(url, apiPrefixes);\r\n\r\n if (!apiMatch) {\r\n apiRes.json({ error: 'API endpoint not found' }, 404);\r\n return;\r\n }\r\n\r\n const { prefix, apiPath } = apiMatch;\r\n let filePath: string | null = null;\r\n let params: Record<string, string | string[]> = {};\r\n\r\n // 1. Direct file match (top-level file prefix, e.g. server/auth.ts \u2192 /auth).\r\n if (prefix.filePath) {\r\n filePath = prefix.filePath;\r\n }\r\n\r\n // 2. Root index.ts (prefix === '' and path === '/').\r\n if (!filePath && prefix.prefix === '' && apiPath === '/') {\r\n const indexPath = path.join(prefix.directory, 'index.ts');\r\n if (fs.existsSync(indexPath)) filePath = indexPath;\r\n }\r\n\r\n // 3. Dynamic route matching inside the prefix directory.\r\n if (!filePath) {\r\n const routeMatch =\r\n matchRoute(apiPath, prefix.directory, '.ts') ??\r\n matchRoute(apiPath, prefix.directory, '.tsx');\r\n if (routeMatch) { filePath = routeMatch.filePath; params = routeMatch.params; }\r\n }\r\n\r\n if (!filePath) {\r\n apiRes.json({ error: 'API endpoint not found' }, 404);\r\n return;\r\n }\r\n\r\n try {\r\n const method = (req.method || 'GET').toUpperCase() as HttpMethod;\r\n log.verbose(`API ${method} ${url} -> ${path.relative(process.cwd(), filePath)}`);\r\n\r\n // OPTIONS preflight \u2014 respond immediately with CORS headers.\r\n if (method === 'OPTIONS') { respondOptions(apiRes); return; }\r\n\r\n // Augment the request object with parsed body, params, and query.\r\n const apiReq = req as ApiRequest;\r\n apiReq.body = await parseBody(req);\r\n apiReq.params = params;\r\n apiReq.query = parseQuery(url, port);\r\n\r\n const apiModule: ApiModule = isDev\r\n ? await importFreshInDev(filePath)\r\n : await import(pathToFileURL(filePath).href);\r\n const handler = apiModule[method] ?? apiModule.default;\r\n\r\n if (!handler) {\r\n apiRes.json({ error: `Method ${method} not allowed` }, 405);\r\n return;\r\n }\r\n\r\n await handler(apiReq, apiRes);\r\n } catch (error) {\r\n log.error('API Error:', error);\r\n apiRes.json({ error: 'Internal server error' }, 500);\r\n }\r\n };\r\n}"],
|
|
5
|
-
"mappings": "AA4BA,OAAO,UAAU;AACjB,OAAO,QAAQ;AACf,SAAS,qBAAqB;
|
|
4
|
+
"sourcesContent": ["/**\r\n * http-server.ts \u2014 API Route Dispatcher\r\n *\r\n * Handles discovery and dispatch of API routes inside `serverDir`.\r\n *\r\n * Directory conventions (mirrors Next.js):\r\n * server/\r\n * users/ \u2192 prefix /users (directory)\r\n * index.ts \u2192 GET /users (method exports: GET, POST, \u2026)\r\n * [id].ts \u2192 GET /users/:id\r\n * auth.ts \u2192 prefix /auth (top-level file)\r\n * index.ts \u2192 prefix / (root handler)\r\n *\r\n * Route handler exports:\r\n * export function GET(req, res) { \u2026 }\r\n * export function POST(req, res) { \u2026 }\r\n * export default function(req, res) { \u2026 } // matches any method\r\n *\r\n * Request augmentation:\r\n * req.body \u2014 parsed JSON or raw string (10 MB limit)\r\n * req.params \u2014 dynamic route segments (e.g. { id: '42' })\r\n * req.query \u2014 URL search params\r\n *\r\n * Response augmentation:\r\n * res.json(data, status?) \u2014 JSON response shorthand\r\n * res.status(code) \u2014 sets statusCode, returns res for chaining\r\n */\r\n\r\nimport path from 'path';\r\nimport fs from 'fs';\r\nimport { pathToFileURL } from 'url';\r\nimport type { IncomingMessage, ServerResponse } from 'http';\r\nimport { log } from './logger';\r\nimport { matchRoute } from './router';\r\n\r\n// \u2500\u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/** Describes a single API prefix discovered in serverDir. */\r\nexport interface ApiPrefixInfo {\r\n /** URL prefix this entry handles (e.g. '/users', ''). */\r\n prefix: string;\r\n /** Directory to scan for route files. */\r\n directory: string;\r\n /** Set when the prefix comes from a top-level file (not a directory). */\r\n filePath?: string;\r\n}\r\n\r\n/** Node's IncomingMessage with parsed body, params, and query. */\r\nexport interface ApiRequest extends IncomingMessage {\r\n params?: Record<string, string | string[]>;\r\n query?: Record<string, string>;\r\n body?: any;\r\n}\r\n\r\n/** Node's ServerResponse with json() and status() convenience methods. */\r\nexport interface ApiResponse extends ServerResponse {\r\n json: (data: any, status?: number) => void;\r\n status: (code: number) => ApiResponse;\r\n}\r\n\r\ntype HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'OPTIONS';\r\ntype ApiHandler = (req: ApiRequest, res: ApiResponse) => void | Promise<void>;\r\n\r\ninterface ApiModule {\r\n default?: ApiHandler;\r\n GET?: ApiHandler;\r\n POST?: ApiHandler;\r\n PUT?: ApiHandler;\r\n DELETE?: ApiHandler;\r\n PATCH?: ApiHandler;\r\n OPTIONS?: ApiHandler;\r\n}\r\n\r\n// \u2500\u2500\u2500 Route discovery \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Scans `serverDir` and returns one ApiPrefixInfo per directory, top-level\r\n * file, and root index.ts. Directories are returned before same-stem files\r\n * so `/a/b` routes resolve to the directory tree before any flat file.\r\n *\r\n * Called at startup and again whenever the server directory changes (in dev).\r\n */\r\nexport function discoverApiPrefixes(serverDir: string): ApiPrefixInfo[] {\r\n if (!fs.existsSync(serverDir)) {\r\n log.warn('Server directory not found:', serverDir);\r\n return [];\r\n }\r\n\r\n const entries = fs.readdirSync(serverDir, { withFileTypes: true });\r\n const prefixes: ApiPrefixInfo[] = [];\r\n\r\n // Directories first (higher specificity than same-stem files).\r\n for (const e of entries) {\r\n if (e.isDirectory()) {\r\n prefixes.push({ prefix: `/${e.name}`, directory: path.join(serverDir, e.name) });\r\n }\r\n }\r\n\r\n // Top-level .ts/.tsx files (excluding index which is handled separately below).\r\n for (const e of entries) {\r\n if (\r\n e.isFile() &&\r\n (e.name.endsWith('.ts') || e.name.endsWith('.tsx')) &&\r\n e.name !== 'index.ts' &&\r\n e.name !== 'index.tsx'\r\n ) {\r\n const stem = e.name.replace(/\\.tsx?$/, '');\r\n prefixes.push({\r\n prefix: `/${stem}`,\r\n directory: serverDir,\r\n filePath: path.join(serverDir, e.name),\r\n });\r\n }\r\n }\r\n\r\n // index.ts/tsx at the root of serverDir handles unmatched paths (prefix '').\r\n if (fs.existsSync(path.join(serverDir, 'index.ts'))) {\r\n prefixes.push({ prefix: '', directory: serverDir });\r\n }\r\n\r\n return prefixes;\r\n}\r\n\r\n// \u2500\u2500\u2500 Body parsing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nconst MAX_BODY_BYTES = 10 * 1024 * 1024; // 10 MB\r\n\r\n/**\r\n * Buffers the request body and returns:\r\n * - Parsed JSON object if Content-Type is application/json.\r\n * - Raw string otherwise.\r\n *\r\n * Rejects with an error if the body exceeds MAX_BODY_BYTES to prevent\r\n * memory exhaustion attacks. Deletes __proto__ and constructor from parsed\r\n * JSON objects to guard against prototype pollution.\r\n */\r\nexport async function parseBody(req: IncomingMessage): Promise<any> {\r\n return new Promise((resolve, reject) => {\r\n let body = '';\r\n let bytes = 0;\r\n\r\n req.on('data', chunk => {\r\n bytes += chunk.length;\r\n if (bytes > MAX_BODY_BYTES) {\r\n req.destroy();\r\n return reject(new Error('Request body too large'));\r\n }\r\n body += chunk.toString();\r\n });\r\n\r\n req.on('end', () => {\r\n try {\r\n if (body && req.headers['content-type']?.includes('application/json')) {\r\n const parsed = JSON.parse(body);\r\n // Guard against prototype pollution via __proto__ / constructor.\r\n if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {\r\n delete parsed.__proto__;\r\n delete parsed.constructor;\r\n }\r\n resolve(parsed);\r\n } else {\r\n resolve(body);\r\n }\r\n } catch (err) {\r\n reject(err);\r\n }\r\n });\r\n\r\n req.on('error', reject);\r\n });\r\n}\r\n\r\n// \u2500\u2500\u2500 Query parsing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/** Extracts URL search params into a plain string map. */\r\nexport function parseQuery(url: string, port: number): Record<string, string> {\r\n const query: Record<string, string> = {};\r\n new URL(url, `http://localhost:${port}`)\r\n .searchParams\r\n .forEach((v, k) => { query[k] = v; });\r\n return query;\r\n}\r\n\r\n// \u2500\u2500\u2500 Response enhancement \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Adds `json()` and `status()` convenience methods to a raw ServerResponse,\r\n * mirroring the Express API surface that most API handlers expect.\r\n */\r\nexport function enhanceResponse(res: ServerResponse): ApiResponse {\r\n const apiRes = res as ApiResponse;\r\n apiRes.json = function (data, statusCode = 200) {\r\n this.statusCode = statusCode;\r\n this.setHeader('Content-Type', 'application/json');\r\n this.end(JSON.stringify(data));\r\n };\r\n apiRes.status = function (code) {\r\n this.statusCode = code;\r\n return this;\r\n };\r\n return apiRes;\r\n}\r\n\r\n/** Responds to an OPTIONS preflight with permissive CORS headers. */\r\nfunction respondOptions(res: ApiResponse): void {\r\n res.setHeader('Access-Control-Allow-Origin', '*');\r\n res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');\r\n res.setHeader('Access-Control-Allow-Headers', 'Content-Type');\r\n res.statusCode = 204;\r\n res.end();\r\n}\r\n\r\n// \u2500\u2500\u2500 Prefix matching \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Finds the first ApiPrefixInfo whose prefix is a prefix of `url`.\r\n *\r\n * The empty-string prefix ('') acts as a catch-all and only matches when no\r\n * other prefix claims the URL.\r\n *\r\n * Returns `null` when no prefix matches (request should fall through to SSR).\r\n */\r\nexport function matchApiPrefix(\r\n url: string,\r\n apiPrefixes: ApiPrefixInfo[],\r\n): { prefix: ApiPrefixInfo; apiPath: string } | null {\r\n for (const prefix of apiPrefixes) {\r\n if (prefix.prefix === '') {\r\n // Empty prefix \u2014 only match if no other prefix has claimed this URL.\r\n const claimedByOther = apiPrefixes.some(\r\n p => p.prefix !== '' && url.startsWith(p.prefix),\r\n );\r\n if (!claimedByOther) return { prefix, apiPath: url || '/' };\r\n } else if (url.startsWith(prefix.prefix)) {\r\n return { prefix, apiPath: url.slice(prefix.prefix.length) || '/' };\r\n }\r\n }\r\n return null;\r\n}\r\n\r\n// \u2500\u2500\u2500 Dev-mode fresh importer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Imports `filePath` fresh on every call using tsx's tsImport, which creates\r\n * an isolated module namespace that bypasses Node's ESM cache entirely.\r\n *\r\n * This is identical to how ssr.ts loads page and layout modules in dev mode.\r\n * tsx handles TypeScript and TSX natively, and bare specifiers (e.g.\r\n * \"@orpc/server/node\") resolve normally through the standard node_modules\r\n * chain \u2014 no bundling, no temp files, no watchers needed.\r\n */\r\nasync function importFreshInDev(filePath: string): Promise<ApiModule> {\r\n const { tsImport } = await import('tsx/esm/api');\r\n return await tsImport(\r\n pathToFileURL(filePath).href,\r\n { parentURL: import.meta.url },\r\n ) as ApiModule;\r\n}\r\n\r\n// \u2500\u2500\u2500 Request handler factory \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\ninterface ApiHandlerOptions {\r\n apiPrefixes: ApiPrefixInfo[];\r\n port: number;\r\n isDev: boolean;\r\n}\r\n\r\nexport function createApiHandler({ apiPrefixes, port, isDev }: ApiHandlerOptions) {\r\n return async function handleApiRoute(\r\n url: string,\r\n req: IncomingMessage,\r\n res: ServerResponse,\r\n ): Promise<void> {\r\n const apiRes = enhanceResponse(res);\r\n const apiMatch = matchApiPrefix(url, apiPrefixes);\r\n\r\n if (!apiMatch) {\r\n apiRes.json({ error: 'API endpoint not found' }, 404);\r\n return;\r\n }\r\n\r\n const { prefix, apiPath } = apiMatch;\r\n let filePath: string | null = null;\r\n let params: Record<string, string | string[]> = {};\r\n\r\n // 1. Direct file match (top-level file prefix, e.g. server/auth.ts \u2192 /auth).\r\n if (prefix.filePath) {\r\n filePath = prefix.filePath;\r\n }\r\n\r\n // 2. Root index.ts (prefix === '' and path === '/').\r\n if (!filePath && prefix.prefix === '' && apiPath === '/') {\r\n const indexPath = path.join(prefix.directory, 'index.ts');\r\n if (fs.existsSync(indexPath)) filePath = indexPath;\r\n }\r\n\r\n // 3. Dynamic route matching inside the prefix directory.\r\n if (!filePath) {\r\n const routeMatch =\r\n matchRoute(apiPath, prefix.directory, '.ts') ??\r\n matchRoute(apiPath, prefix.directory, '.tsx');\r\n if (routeMatch) { filePath = routeMatch.filePath; params = routeMatch.params; }\r\n }\r\n\r\n if (!filePath) {\r\n apiRes.json({ error: 'API endpoint not found' }, 404);\r\n return;\r\n }\r\n\r\n try {\r\n const method = (req.method || 'GET').toUpperCase() as HttpMethod;\r\n log.verbose(`API ${method} ${url} -> ${path.relative(process.cwd(), filePath)}`);\r\n\r\n // OPTIONS preflight \u2014 respond immediately with CORS headers.\r\n if (method === 'OPTIONS') { respondOptions(apiRes); return; }\r\n\r\n // Augment the request object with parsed body, params, and query.\r\n const apiReq = req as ApiRequest;\r\n apiReq.body = await parseBody(req);\r\n apiReq.params = params;\r\n apiReq.query = parseQuery(url, port);\r\n\r\n const apiModule: ApiModule = isDev\r\n ? await importFreshInDev(filePath)\r\n : await import(pathToFileURL(filePath).href);\r\n const handler = apiModule[method] ?? apiModule.default;\r\n\r\n if (!handler) {\r\n apiRes.json({ error: `Method ${method} not allowed` }, 405);\r\n return;\r\n }\r\n\r\n await handler(apiReq, apiRes);\r\n } catch (error) {\r\n log.error('API Error:', error);\r\n apiRes.json({ error: 'Internal server error' }, 500);\r\n }\r\n };\r\n}"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/renderer.js
CHANGED
|
@@ -4,6 +4,35 @@ import { renderToString } from "react-dom/server";
|
|
|
4
4
|
import { log } from "./logger.js";
|
|
5
5
|
import { getComponentCache } from "./component-analyzer.js";
|
|
6
6
|
import { escapeHtml } from "./utils.js";
|
|
7
|
+
function isWrapperAttr(key) {
|
|
8
|
+
return key === "className" || key === "style" || key === "id" || key.startsWith("data-") || key.startsWith("aria-");
|
|
9
|
+
}
|
|
10
|
+
function splitWrapperAttrs(props) {
|
|
11
|
+
const wrapperAttrs = {};
|
|
12
|
+
const componentProps = {};
|
|
13
|
+
for (const [key, value] of Object.entries(props || {})) {
|
|
14
|
+
if (isWrapperAttr(key)) wrapperAttrs[key] = value;
|
|
15
|
+
else componentProps[key] = value;
|
|
16
|
+
}
|
|
17
|
+
return { wrapperAttrs, componentProps };
|
|
18
|
+
}
|
|
19
|
+
function buildWrapperAttrString(attrs) {
|
|
20
|
+
const parts = Object.entries(attrs).map(([key, value]) => {
|
|
21
|
+
if (key === "className") key = "class";
|
|
22
|
+
if (key === "style" && typeof value === "object") {
|
|
23
|
+
const css = Object.entries(value).map(([k, v]) => {
|
|
24
|
+
const prop = k.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
|
|
25
|
+
const safeVal = String(v).replace(/[<>"'`\\]/g, "");
|
|
26
|
+
return `${prop}:${safeVal}`;
|
|
27
|
+
}).join(";");
|
|
28
|
+
return `style="${css}"`;
|
|
29
|
+
}
|
|
30
|
+
if (typeof value === "boolean") return value ? key : "";
|
|
31
|
+
if (value == null) return "";
|
|
32
|
+
return `${key}="${escapeHtml(String(value))}"`;
|
|
33
|
+
}).filter(Boolean);
|
|
34
|
+
return parts.length ? " " + parts.join(" ") : "";
|
|
35
|
+
}
|
|
7
36
|
async function renderElementToHtml(element, ctx) {
|
|
8
37
|
if (element === null || element === void 0 || typeof element === "boolean") return "";
|
|
9
38
|
if (typeof element === "string" || typeof element === "number")
|
|
@@ -54,10 +83,12 @@ async function renderFunctionComponent(type, props, ctx) {
|
|
|
54
83
|
if (!info.exportedName || type.name !== info.exportedName) continue;
|
|
55
84
|
try {
|
|
56
85
|
ctx.hydrated.add(id);
|
|
57
|
-
const
|
|
86
|
+
const { wrapperAttrs, componentProps } = splitWrapperAttrs(props);
|
|
87
|
+
const wrapperAttrStr = buildWrapperAttrString(wrapperAttrs);
|
|
88
|
+
const serializedProps = serializePropsForHydration(componentProps, ctx.registry);
|
|
58
89
|
log.verbose(`Client component rendered for hydration: ${id} (${path.basename(filePath)})`);
|
|
59
|
-
const html = ctx.skipClientSSR ? "" : renderToString(createElement(type,
|
|
60
|
-
return `<span data-hydrate-id="${id}" data-hydrate-props="${escapeHtml(
|
|
90
|
+
const html = ctx.skipClientSSR ? "" : renderToString(createElement(type, componentProps));
|
|
91
|
+
return `<span data-hydrate-id="${id}"${wrapperAttrStr} data-hydrate-props="${escapeHtml(
|
|
61
92
|
JSON.stringify(serializedProps)
|
|
62
93
|
)}">${html}</span>`;
|
|
63
94
|
} catch (err) {
|
package/dist/renderer.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/renderer.ts"],
|
|
4
|
-
"sourcesContent": ["/**\r\n * renderer.ts \u2014 Dev-Mode Async SSR Renderer\r\n *\r\n * Implements a recursive async renderer used in `nuke dev` to convert a React\r\n * element tree into an HTML string. It is a lighter alternative to\r\n * react-dom/server.renderToString that:\r\n *\r\n * - Supports async server components (components that return Promises).\r\n * - Emits <span data-hydrate-id=\"\u2026\"> markers for \"use client\" boundaries\r\n * instead of trying to render them server-side without their browser APIs.\r\n * - Serializes props passed to client components into the marker's\r\n * data-hydrate-props attribute so the browser can reconstruct them.\r\n *\r\n * In production (nuke build), the equivalent renderer is inlined into each\r\n * page's standalone bundle by build-common.ts (makePageAdapterSource).\r\n *\r\n * RenderContext:\r\n * registry \u2014 Map<id, filePath> of all client components for this page.\r\n * Populated by component-analyzer.ts before rendering.\r\n * hydrated \u2014 Set<id> populated during render; used to tell the browser\r\n * which components to hydrate on this specific request.\r\n * skipClientSSR \u2014 When true (HMR request), client components emit an empty\r\n * marker instead of running renderToString (faster dev reload).\r\n */\r\n\r\nimport path from 'path';\r\nimport { createElement, Fragment } from 'react';\r\nimport { renderToString } from 'react-dom/server';\r\nimport { log } from './logger';\r\nimport { getComponentCache } from './component-analyzer';\r\nimport { escapeHtml } from './utils';\r\n\r\n// \u2500\u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nexport interface RenderContext {\r\n /** id \u2192 absolute file path for every client component reachable from this page. */\r\n registry: Map<string, string>;\r\n /** Populated during render: IDs of client components actually encountered. */\r\n hydrated: Set<string>;\r\n /** When true, skip renderToString for client components (faster HMR). */\r\n skipClientSSR?: boolean;\r\n}\r\n\r\n// \u2500\u2500\u2500 Top-level 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\r\n\r\n/**\r\n * Recursively renders a React element (or primitive) to an HTML string.\r\n *\r\n * Handles:\r\n * null / undefined / boolean \u2192 ''\r\n * string / number \u2192 HTML-escaped text\r\n * array \u2192 rendered in parallel, joined\r\n * Fragment \u2192 renders children directly\r\n * HTML element string \u2192 renderHtmlElement()\r\n * function component \u2192 renderFunctionComponent()\r\n */\r\nexport async function renderElementToHtml(\r\n element: any,\r\n ctx: RenderContext,\r\n): Promise<string> {\r\n if (element === null || element === undefined || typeof element === 'boolean') return '';\r\n if (typeof element === 'string' || typeof element === 'number')\r\n return escapeHtml(String(element));\r\n\r\n if (Array.isArray(element)) {\r\n const parts = await Promise.all(element.map(e => renderElementToHtml(e, ctx)));\r\n return parts.join('');\r\n }\r\n\r\n if (!element.type) return '';\r\n\r\n const { type, props } = element;\r\n\r\n if (type === Fragment) return renderElementToHtml(props.children, ctx);\r\n if (typeof type === 'string') return renderHtmlElement(type, props, ctx);\r\n if (typeof type === 'function') return renderFunctionComponent(type, props, ctx);\r\n\r\n return '';\r\n}\r\n\r\n// \u2500\u2500\u2500 HTML element 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\r\n\r\n/**\r\n * Renders a native HTML element (e.g. `<div className=\"foo\">`).\r\n *\r\n * Attribute conversion:\r\n * className \u2192 class\r\n * htmlFor \u2192 for\r\n * style \u2192 converted from camelCase object to CSS string\r\n * boolean \u2192 omitted when false, rendered as name-only attribute when true\r\n * dangerouslySetInnerHTML \u2192 inner HTML set verbatim (no escaping)\r\n *\r\n * Void elements (img, br, input, etc.) are self-closed.\r\n */\r\nasync function renderHtmlElement(\r\n type: string,\r\n props: any,\r\n ctx: RenderContext,\r\n): Promise<string> {\r\n const { children, ...attributes } = (props || {}) as Record<string, any>;\r\n\r\n const attrs = Object.entries(attributes as Record<string, any>)\r\n .map(([key, value]) => {\r\n // React prop name \u2192 HTML attribute name.\r\n if (key === 'className') key = 'class';\r\n if (key === 'htmlFor') key = 'for';\r\n if (key === 'dangerouslySetInnerHTML') return ''; // handled separately below\r\n\r\n if (typeof value === 'boolean') return value ? key : '';\r\n\r\n // camelCase style object \u2192 \"prop:value;\u2026\" CSS string.\r\n if (key === 'style' && typeof value === 'object') {\r\n const styleStr = Object.entries(value)\r\n .map(([k, v]) => {\r\n const prop = k.replace(/[A-Z]/g, m => `-${m.toLowerCase()}`);\r\n // Strip characters that could break out of the attribute value.\r\n const safeVal = String(v).replace(/[<>\"'`\\\\]/g, '');\r\n return `${prop}:${safeVal}`;\r\n })\r\n .join(';');\r\n return `style=\"${styleStr}\"`;\r\n }\r\n\r\n return `${key}=\"${escapeHtml(String(value))}\"`;\r\n })\r\n .filter(Boolean)\r\n .join(' ');\r\n\r\n const attrStr = attrs ? ` ${attrs}` : '';\r\n\r\n if (props?.dangerouslySetInnerHTML) {\r\n return `<${type}${attrStr}>${props.dangerouslySetInnerHTML.__html}</${type}>`;\r\n }\r\n\r\n // Void elements cannot have children.\r\n if (['img', 'br', 'hr', 'input', 'meta', 'link'].includes(type)) {\r\n return `<${type}${attrStr} />`;\r\n }\r\n\r\n const childrenHtml = children ? await renderElementToHtml(children, ctx) : '';\r\n return `<${type}${attrStr}>${childrenHtml}</${type}>`;\r\n}\r\n\r\n// \u2500\u2500\u2500 Function component 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\r\n\r\n/**\r\n * Renders a function (or class) component.\r\n *\r\n * Client boundary detection:\r\n * The component cache maps file paths to ComponentInfo. We match the\r\n * component's function name against the default export of each registered\r\n * client file to determine whether this component is a client boundary.\r\n *\r\n * If it is, we emit a hydration marker and optionally run renderToString\r\n * to produce the initial HTML inside the marker (skipped when skipClientSSR\r\n * is set, e.g. during HMR navigation).\r\n *\r\n * Class components:\r\n * Instantiated via `new type(props)` and their render() method called.\r\n *\r\n * Async components:\r\n * Awaited if the return value is a Promise (standard server component pattern).\r\n */\r\nasync function renderFunctionComponent(\r\n type: Function,\r\n props: any,\r\n ctx: RenderContext,\r\n): Promise<string> {\r\n const componentCache = getComponentCache();\r\n\r\n // Check whether this component function is a registered client component.\r\n for (const [id, filePath] of ctx.registry.entries()) {\r\n const info = componentCache.get(filePath);\r\n if (!info?.isClientComponent) continue;\r\n\r\n // Match by default export function name (cached \u2014 handles both source and\r\n // esbuild-compiled formats; see component-analyzer.getExportedDefaultName).\r\n if (!info.exportedName || type.name !== info.exportedName) continue;\r\n\r\n // This is a client boundary.\r\n try {\r\n ctx.hydrated.add(id);\r\n const serializedProps = serializePropsForHydration(props, ctx.registry);\r\n log.verbose(`Client component rendered for hydration: ${id} (${path.basename(filePath)})`);\r\n\r\n // Optionally SSR the component so the initial HTML is meaningful\r\n // (improves perceived performance and avoids layout shift).\r\n const html = ctx.skipClientSSR\r\n ? ''\r\n : renderToString(createElement(type as React.ComponentType<any>, props));\r\n\r\n return `<span data-hydrate-id=\"${id}\" data-hydrate-props=\"${escapeHtml(\r\n JSON.stringify(serializedProps),\r\n )}\">${html}</span>`;\r\n } catch (err) {\r\n log.error('Error rendering client component:', err);\r\n return `<div style=\"color:red\">Error rendering client component: ${escapeHtml(String(err))}</div>`;\r\n }\r\n }\r\n\r\n // Server component \u2014 call it and recurse into the result.\r\n try {\r\n const result = type(props);\r\n const resolved = result?.then ? await result : result;\r\n return renderElementToHtml(resolved, ctx);\r\n } catch (err) {\r\n log.error('Error rendering component:', err);\r\n return `<div style=\"color:red\">Error rendering component: ${escapeHtml(String(err))}</div>`;\r\n }\r\n}\r\n\r\n// \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\r\n\r\n/**\r\n * Converts props into a JSON-serializable form for the data-hydrate-props\r\n * attribute. React elements inside props are serialized to a tagged object\r\n * format ({ __re: 'html'|'client', \u2026 }) that the browser's reconstructElement\r\n * function (in bundle.ts) can turn back into real React elements.\r\n *\r\n * Functions are dropped (cannot be serialized).\r\n */\r\nfunction serializePropsForHydration(\r\n props: any,\r\n registry: Map<string, string>,\r\n): any {\r\n if (!props || typeof props !== 'object') return props;\r\n const out: any = {};\r\n for (const [key, value] of Object.entries(props as Record<string, any>)) {\r\n const s = serializeValue(value, registry);\r\n if (s !== undefined) out[key] = s;\r\n }\r\n return out;\r\n}\r\n\r\nfunction serializeValue(value: any, registry: Map<string, string>): any {\r\n if (value === null || value === undefined) return value;\r\n if (typeof value === 'function') return undefined; // not serializable\r\n if (typeof value !== 'object') return value;\r\n if (Array.isArray(value))\r\n return value.map(v => serializeValue(v, registry)).filter(v => v !== undefined);\r\n if ((value as any).$$typeof)\r\n return serializeReactElement(value, registry);\r\n\r\n const out: any = {};\r\n for (const [k, v] of Object.entries(value as Record<string, any>)) {\r\n const s = serializeValue(v, registry);\r\n if (s !== undefined) out[k] = s;\r\n }\r\n return out;\r\n}\r\n\r\n/**\r\n * Serializes a React element to its wire format:\r\n * Native element \u2192 { __re: 'html', tag, props }\r\n * Client component \u2192 { __re: 'client', componentId, props }\r\n * Server component \u2192 undefined (cannot be serialized)\r\n */\r\nfunction serializeReactElement(element: any, registry: Map<string, string>): any {\r\n const { type, props } = element;\r\n\r\n if (typeof type === 'string') {\r\n return { __re: 'html', tag: type, props: serializePropsForHydration(props, registry) };\r\n }\r\n\r\n if (typeof type === 'function') {\r\n const componentCache = getComponentCache();\r\n for (const [id, filePath] of registry.entries()) {\r\n const info = componentCache.get(filePath);\r\n if (!info?.isClientComponent) continue;\r\n if (info.exportedName && type.name === info.exportedName) {\r\n return {\r\n __re: 'client',\r\n componentId: id,\r\n props: serializePropsForHydration(props, registry),\r\n };\r\n }\r\n }\r\n }\r\n\r\n return undefined; // Server component \u2014 not serializable\r\n}"],
|
|
5
|
-
"mappings": "AAyBA,OAAO,UAAU;AACjB,SAAS,eAAe,gBAAgB;AACxC,SAAS,sBAAsB;AAC/B,SAAS,WAAW;AACpB,SAAS,yBAAyB;AAClC,SAAS,kBAAkB;
|
|
4
|
+
"sourcesContent": ["/**\r\n * renderer.ts \u2014 Dev-Mode Async SSR Renderer\r\n *\r\n * Implements a recursive async renderer used in `nuke dev` to convert a React\r\n * element tree into an HTML string. It is a lighter alternative to\r\n * react-dom/server.renderToString that:\r\n *\r\n * - Supports async server components (components that return Promises).\r\n * - Emits <span data-hydrate-id=\"\u2026\"> markers for \"use client\" boundaries\r\n * instead of trying to render them server-side without their browser APIs.\r\n * - Serializes props passed to client components into the marker's\r\n * data-hydrate-props attribute so the browser can reconstruct them.\r\n *\r\n * In production (nuke build), the equivalent renderer is inlined into each\r\n * page's standalone bundle by build-common.ts (makePageAdapterSource).\r\n *\r\n * RenderContext:\r\n * registry \u2014 Map<id, filePath> of all client components for this page.\r\n * Populated by component-analyzer.ts before rendering.\r\n * hydrated \u2014 Set<id> populated during render; used to tell the browser\r\n * which components to hydrate on this specific request.\r\n * skipClientSSR \u2014 When true (HMR request), client components emit an empty\r\n * marker instead of running renderToString (faster dev reload).\r\n */\r\n\r\nimport path from 'path';\r\nimport { createElement, Fragment } from 'react';\r\nimport { renderToString } from 'react-dom/server';\r\nimport { log } from './logger';\r\nimport { getComponentCache } from './component-analyzer';\r\nimport { escapeHtml } from './utils';\r\n\r\n// \u2500\u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nexport interface RenderContext {\r\n /** id \u2192 absolute file path for every client component reachable from this page. */\r\n registry: Map<string, string>;\r\n /** Populated during render: IDs of client components actually encountered. */\r\n hydrated: Set<string>;\r\n /** When true, skip renderToString for client components (faster HMR). */\r\n skipClientSSR?: boolean;\r\n}\r\n\r\n// \u2500\u2500\u2500 Wrapper attribute 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\r\n\r\n/**\r\n * Attributes that belong on the hydration <span> wrapper rather than being\r\n * forwarded to the inner client component. Includes className, style, id,\r\n * and any data-* / aria-* attributes.\r\n */\r\nfunction isWrapperAttr(key: string): boolean {\r\n return (\r\n key === 'className' ||\r\n key === 'style' ||\r\n key === 'id' ||\r\n key.startsWith('data-') ||\r\n key.startsWith('aria-')\r\n );\r\n}\r\n\r\n/**\r\n * Splits props into two bags:\r\n * wrapperAttrs \u2014 keys destined for the <span> (className, style, id, data-*, aria-*)\r\n * componentProps \u2014 everything else, forwarded to the actual component\r\n */\r\nfunction splitWrapperAttrs(props: any): {\r\n wrapperAttrs: Record<string, any>;\r\n componentProps: Record<string, any>;\r\n} {\r\n const wrapperAttrs: Record<string, any> = {};\r\n const componentProps: Record<string, any> = {};\r\n for (const [key, value] of Object.entries((props || {}) as Record<string, any>)) {\r\n if (isWrapperAttr(key)) wrapperAttrs[key] = value;\r\n else componentProps[key] = value;\r\n }\r\n return { wrapperAttrs, componentProps };\r\n}\r\n\r\n/**\r\n * Converts a wrapper-attrs bag into an HTML attribute string (leading space\r\n * included when non-empty) suitable for direct interpolation into a tag.\r\n *\r\n * className \u2192 class\r\n * style obj \u2192 \"prop:value;\u2026\" CSS string\r\n */\r\nfunction buildWrapperAttrString(attrs: Record<string, any>): string {\r\n const parts = Object.entries(attrs)\r\n .map(([key, value]) => {\r\n if (key === 'className') key = 'class';\r\n\r\n if (key === 'style' && typeof value === 'object') {\r\n const css = Object.entries(value as Record<string, any>)\r\n .map(([k, v]) => {\r\n const prop = k.replace(/[A-Z]/g, m => `-${m.toLowerCase()}`);\r\n const safeVal = String(v).replace(/[<>\"'`\\\\]/g, '');\r\n return `${prop}:${safeVal}`;\r\n })\r\n .join(';');\r\n return `style=\"${css}\"`;\r\n }\r\n\r\n if (typeof value === 'boolean') return value ? key : '';\r\n if (value == null) return '';\r\n return `${key}=\"${escapeHtml(String(value))}\"`;\r\n })\r\n .filter(Boolean);\r\n\r\n return parts.length ? ' ' + parts.join(' ') : '';\r\n}\r\n\r\n// \u2500\u2500\u2500 Top-level 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\r\n\r\n/**\r\n * Recursively renders a React element (or primitive) to an HTML string.\r\n *\r\n * Handles:\r\n * null / undefined / boolean \u2192 ''\r\n * string / number \u2192 HTML-escaped text\r\n * array \u2192 rendered in parallel, joined\r\n * Fragment \u2192 renders children directly\r\n * HTML element string \u2192 renderHtmlElement()\r\n * function component \u2192 renderFunctionComponent()\r\n */\r\nexport async function renderElementToHtml(\r\n element: any,\r\n ctx: RenderContext,\r\n): Promise<string> {\r\n if (element === null || element === undefined || typeof element === 'boolean') return '';\r\n if (typeof element === 'string' || typeof element === 'number')\r\n return escapeHtml(String(element));\r\n\r\n if (Array.isArray(element)) {\r\n const parts = await Promise.all(element.map(e => renderElementToHtml(e, ctx)));\r\n return parts.join('');\r\n }\r\n\r\n if (!element.type) return '';\r\n\r\n const { type, props } = element;\r\n\r\n if (type === Fragment) return renderElementToHtml(props.children, ctx);\r\n if (typeof type === 'string') return renderHtmlElement(type, props, ctx);\r\n if (typeof type === 'function') return renderFunctionComponent(type, props, ctx);\r\n\r\n return '';\r\n}\r\n\r\n// \u2500\u2500\u2500 HTML element 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\r\n\r\n/**\r\n * Renders a native HTML element (e.g. `<div className=\"foo\">`).\r\n *\r\n * Attribute conversion:\r\n * className \u2192 class\r\n * htmlFor \u2192 for\r\n * style \u2192 converted from camelCase object to CSS string\r\n * boolean \u2192 omitted when false, rendered as name-only attribute when true\r\n * dangerouslySetInnerHTML \u2192 inner HTML set verbatim (no escaping)\r\n *\r\n * Void elements (img, br, input, etc.) are self-closed.\r\n */\r\nasync function renderHtmlElement(\r\n type: string,\r\n props: any,\r\n ctx: RenderContext,\r\n): Promise<string> {\r\n const { children, ...attributes } = (props || {}) as Record<string, any>;\r\n\r\n const attrs = Object.entries(attributes as Record<string, any>)\r\n .map(([key, value]) => {\r\n // React prop name \u2192 HTML attribute name.\r\n if (key === 'className') key = 'class';\r\n if (key === 'htmlFor') key = 'for';\r\n if (key === 'dangerouslySetInnerHTML') return ''; // handled separately below\r\n\r\n if (typeof value === 'boolean') return value ? key : '';\r\n\r\n // camelCase style object \u2192 \"prop:value;\u2026\" CSS string.\r\n if (key === 'style' && typeof value === 'object') {\r\n const styleStr = Object.entries(value)\r\n .map(([k, v]) => {\r\n const prop = k.replace(/[A-Z]/g, m => `-${m.toLowerCase()}`);\r\n // Strip characters that could break out of the attribute value.\r\n const safeVal = String(v).replace(/[<>\"'`\\\\]/g, '');\r\n return `${prop}:${safeVal}`;\r\n })\r\n .join(';');\r\n return `style=\"${styleStr}\"`;\r\n }\r\n\r\n return `${key}=\"${escapeHtml(String(value))}\"`;\r\n })\r\n .filter(Boolean)\r\n .join(' ');\r\n\r\n const attrStr = attrs ? ` ${attrs}` : '';\r\n\r\n if (props?.dangerouslySetInnerHTML) {\r\n return `<${type}${attrStr}>${props.dangerouslySetInnerHTML.__html}</${type}>`;\r\n }\r\n\r\n // Void elements cannot have children.\r\n if (['img', 'br', 'hr', 'input', 'meta', 'link'].includes(type)) {\r\n return `<${type}${attrStr} />`;\r\n }\r\n\r\n const childrenHtml = children ? await renderElementToHtml(children, ctx) : '';\r\n return `<${type}${attrStr}>${childrenHtml}</${type}>`;\r\n}\r\n\r\n// \u2500\u2500\u2500 Function component 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\r\n\r\n/**\r\n * Renders a function (or class) component.\r\n *\r\n * Client boundary detection:\r\n * The component cache maps file paths to ComponentInfo. We match the\r\n * component's function name against the default export of each registered\r\n * client file to determine whether this component is a client boundary.\r\n *\r\n * If it is, we emit a hydration marker and optionally run renderToString\r\n * to produce the initial HTML inside the marker (skipped when skipClientSSR\r\n * is set, e.g. during HMR navigation).\r\n *\r\n * Class components:\r\n * Instantiated via `new type(props)` and their render() method called.\r\n *\r\n * Async components:\r\n * Awaited if the return value is a Promise (standard server component pattern).\r\n */\r\nasync function renderFunctionComponent(\r\n type: Function,\r\n props: any,\r\n ctx: RenderContext,\r\n): Promise<string> {\r\n const componentCache = getComponentCache();\r\n\r\n // Check whether this component function is a registered client component.\r\n for (const [id, filePath] of ctx.registry.entries()) {\r\n const info = componentCache.get(filePath);\r\n if (!info?.isClientComponent) continue;\r\n\r\n // Match by default export function name (cached \u2014 handles both source and\r\n // esbuild-compiled formats; see component-analyzer.getExportedDefaultName).\r\n if (!info.exportedName || type.name !== info.exportedName) continue;\r\n\r\n // This is a client boundary.\r\n try {\r\n ctx.hydrated.add(id);\r\n\r\n // Split props: wrapper attrs go on the <span>, the rest reach the component.\r\n const { wrapperAttrs, componentProps } = splitWrapperAttrs(props);\r\n const wrapperAttrStr = buildWrapperAttrString(wrapperAttrs);\r\n const serializedProps = serializePropsForHydration(componentProps, ctx.registry);\r\n log.verbose(`Client component rendered for hydration: ${id} (${path.basename(filePath)})`);\r\n\r\n // Optionally SSR the component so the initial HTML is meaningful\r\n // (improves perceived performance and avoids layout shift).\r\n const html = ctx.skipClientSSR\r\n ? ''\r\n : renderToString(createElement(type as React.ComponentType<any>, componentProps));\r\n\r\n return `<span data-hydrate-id=\"${id}\"${wrapperAttrStr} data-hydrate-props=\"${escapeHtml(\r\n JSON.stringify(serializedProps),\r\n )}\">${html}</span>`;\r\n } catch (err) {\r\n log.error('Error rendering client component:', err);\r\n return `<div style=\"color:red\">Error rendering client component: ${escapeHtml(String(err))}</div>`;\r\n }\r\n }\r\n\r\n // Server component \u2014 call it and recurse into the result.\r\n try {\r\n const result = type(props);\r\n const resolved = result?.then ? await result : result;\r\n return renderElementToHtml(resolved, ctx);\r\n } catch (err) {\r\n log.error('Error rendering component:', err);\r\n return `<div style=\"color:red\">Error rendering component: ${escapeHtml(String(err))}</div>`;\r\n }\r\n}\r\n\r\n// \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\r\n\r\n/**\r\n * Converts props into a JSON-serializable form for the data-hydrate-props\r\n * attribute. React elements inside props are serialized to a tagged object\r\n * format ({ __re: 'html'|'client', \u2026 }) that the browser's reconstructElement\r\n * function (in bundle.ts) can turn back into real React elements.\r\n *\r\n * Functions are dropped (cannot be serialized).\r\n */\r\nfunction serializePropsForHydration(\r\n props: any,\r\n registry: Map<string, string>,\r\n): any {\r\n if (!props || typeof props !== 'object') return props;\r\n const out: any = {};\r\n for (const [key, value] of Object.entries(props as Record<string, any>)) {\r\n const s = serializeValue(value, registry);\r\n if (s !== undefined) out[key] = s;\r\n }\r\n return out;\r\n}\r\n\r\nfunction serializeValue(value: any, registry: Map<string, string>): any {\r\n if (value === null || value === undefined) return value;\r\n if (typeof value === 'function') return undefined; // not serializable\r\n if (typeof value !== 'object') return value;\r\n if (Array.isArray(value))\r\n return value.map(v => serializeValue(v, registry)).filter(v => v !== undefined);\r\n if ((value as any).$$typeof)\r\n return serializeReactElement(value, registry);\r\n\r\n const out: any = {};\r\n for (const [k, v] of Object.entries(value as Record<string, any>)) {\r\n const s = serializeValue(v, registry);\r\n if (s !== undefined) out[k] = s;\r\n }\r\n return out;\r\n}\r\n\r\n/**\r\n * Serializes a React element to its wire format:\r\n * Native element \u2192 { __re: 'html', tag, props }\r\n * Client component \u2192 { __re: 'client', componentId, props }\r\n * Server component \u2192 undefined (cannot be serialized)\r\n */\r\nfunction serializeReactElement(element: any, registry: Map<string, string>): any {\r\n const { type, props } = element;\r\n\r\n if (typeof type === 'string') {\r\n return { __re: 'html', tag: type, props: serializePropsForHydration(props, registry) };\r\n }\r\n\r\n if (typeof type === 'function') {\r\n const componentCache = getComponentCache();\r\n for (const [id, filePath] of registry.entries()) {\r\n const info = componentCache.get(filePath);\r\n if (!info?.isClientComponent) continue;\r\n if (info.exportedName && type.name === info.exportedName) {\r\n return {\r\n __re: 'client',\r\n componentId: id,\r\n props: serializePropsForHydration(props, registry),\r\n };\r\n }\r\n }\r\n }\r\n\r\n return undefined; // Server component \u2014 not serializable\r\n}"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nukejs",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.10",
|
|
4
4
|
"description": "A minimal, opinionated full-stack React framework on Node.js that server-renders everything and hydrates only interactive parts.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|