nukejs 0.0.2 → 0.0.4
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 +0 -1
- package/dist/build-common.js.map +2 -2
- package/dist/build-vercel.js +231 -20
- package/dist/build-vercel.js.map +2 -2
- package/dist/bundler.js +35 -33
- package/dist/bundler.js.map +2 -2
- package/dist/middleware.js +26 -13
- package/dist/middleware.js.map +2 -2
- package/package.json +1 -2
package/dist/build-common.js
CHANGED
|
@@ -632,7 +632,6 @@ async function bundleClientComponents(globalRegistry, pagesDir, staticDir) {
|
|
|
632
632
|
prerendered.set(id, renderToString(createElement(Component, {})));
|
|
633
633
|
console.log(` prerendered ${id}`);
|
|
634
634
|
} catch (e) {
|
|
635
|
-
console.warn(` [SSR prerender failed for ${id}]`, e);
|
|
636
635
|
prerendered.set(id, "");
|
|
637
636
|
} finally {
|
|
638
637
|
if (fs.existsSync(ssrTmp)) fs.unlinkSync(ssrTmp);
|
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\r\n *\r\n * Shared build logic used by both build-vercel.ts and build-node.ts.\r\n *\r\n * Exports:\r\n * \u2014 utility helpers : walkFiles, analyzeFile, isServerComponent,\r\n * findPageLayouts, extractDefaultExportName\r\n * \u2014 collection : collectServerPages, collectGlobalClientRegistry\r\n * \u2014 template codegen : makeApiAdapterSource, makePageAdapterSource\r\n * \u2014 bundle operations : bundleApiHandler, bundlePageHandler,\r\n * bundleClientComponents, buildReactBundle, buildNukeBundle\r\n */\r\n\r\nimport fs from 'fs';\r\nimport path from 'path';\r\nimport crypto from '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 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 srcRegex: string;\r\n paramNames: string[];\r\n /** Path used as function namespace, 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\n/** A server page together with its fully bundled ESM text, ready to emit. */\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, a function path, and a specificity score.\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 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) { paramNames.push(optCatchAll[1]); regexParts.push('(.*)'); specificity += 1; continue; }\r\n const catchAll = seg.match(/^\\[\\.\\.\\.(.+)\\]$/);\r\n if (catchAll) { paramNames.push(catchAll[1]); regexParts.push('(.+)'); specificity += 10; continue; }\r\n const dynamic = seg.match(/^\\[(.+)\\]$/);\r\n if (dynamic) { paramNames.push(dynamic[1]); regexParts.push('([^/]+)'); specificity += 100; continue; }\r\n regexParts.push(seg.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&'));\r\n specificity += 1000;\r\n }\r\n\r\n const srcRegex = segments.length === 0\r\n ? '^/$'\r\n : '^/' + regexParts.join('/') + '$';\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, 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 \u2014\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\nexport function extractDefaultExportName(filePath: string): string | null {\r\n const content = fs.readFileSync(filePath, 'utf-8');\r\n const match = content.match(/export\\s+default\\s+(?:function\\s+)?(\\w+)/);\r\n return match?.[1] ?? 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 by specificity\r\n * (most-specific first so more-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 const base = path.basename(relPath, path.extname(relPath));\r\n if (base === '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. Deduplication is automatic because the\r\n * Map key is the stable content-hash ID produced by component-analyzer.ts.\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>(); // id \u2192 absFilePath\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 }\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 }\r\n }\r\n return registry;\r\n}\r\n\r\n// \u2500\u2500\u2500 Per-page registry 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 * Builds the per-page client component registry (page + its layout chain) and\r\n * returns both the id\u2192path map and the name\u2192id map needed by bundlePageHandler.\r\n *\r\n * Extracted here to eliminate the identical loop duplicated across\r\n * build-node.ts and build-vercel.ts.\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 }\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 }\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\n * Callers (build-node, build-vercel) only need to write the bundled text to\r\n * their respective output destinations \u2014 the format-specific logic stays local.\r\n *\r\n * Returns an empty array when there are no server pages.\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 // Pass 1 \u2014 bundle all client components to static files.\r\n const globalClientRegistry = collectGlobalClientRegistry(serverPages, pagesDir);\r\n const prerenderedHtml = await bundleClientComponents(globalClientRegistry, pagesDir, staticDir);\r\n const prerenderedHtmlRecord = Object.fromEntries(prerenderedHtml);\r\n\r\n // Pass 2 \u2014 bundle each server-component page.\r\n const builtPages: BuiltPage[] = [];\r\n\r\n for (const page of serverPages) {\r\n const { funcPath, absPath } = page;\r\n console.log(` building ${fs.existsSync(absPath) ? absPath : absPath} \u2192 ${funcPath} [page]`);\r\n\r\n const layoutPaths = findPageLayouts(absPath, pagesDir);\r\n const { registry, clientComponentNames } = buildPerPageRegistry(absPath, layoutPaths, pagesDir);\r\n\r\n const bundleText = await bundlePageHandler({\r\n absPath,\r\n pagesDir,\r\n clientComponentNames,\r\n allClientIds: [...registry.keys()],\r\n layoutPaths,\r\n prerenderedHtml: prerenderedHtmlRecord,\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\n * @param handlerFilename Basename of the handler file relative to the adapter\r\n * (e.g. 'users.ts'). Must be in the same directory.\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`.trimStart();\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}\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 that handles server + client\r\n * components without react-dom/server.\r\n * \u2022 Client components are identified via the pre-computed CLIENT_COMPONENTS\r\n * map (no fs.readFileSync at runtime).\r\n * \u2022 Emits the same full HTML document structure as ssr.ts including the\r\n * __n_data blob, importmap, and initRuntime bootstrap.\r\n */\r\nexport function makePageAdapterSource(opts: PageAdapterOptions): string {\r\n const {\r\n pageImport,\r\n layoutImports,\r\n clientComponentNames,\r\n allClientIds,\r\n layoutArrayItems,\r\n prerenderedHtml,\r\n } = opts;\r\n\r\n return `\r\nimport type { IncomingMessage, ServerResponse } from 'http';\r\nimport * as __page__ from ${pageImport};\r\n${layoutImports}\r\n\r\n// \u2500\u2500\u2500 Pre-built client component registry \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n// Computed at BUILD TIME from the import tree. Source files are not deployed,\r\n// so we must not read them with fs.readFileSync at runtime.\r\n// Key: default-export function name \u2192 Value: stable content-hash ID\r\nconst CLIENT_COMPONENTS: Record<string, string> = ${JSON.stringify(clientComponentNames)};\r\n\r\n// All client component IDs reachable from this page (page + layouts).\r\n// Sent to initRuntime so the browser pre-loads all bundles for SPA navigation.\r\nconst ALL_CLIENT_IDS: string[] = ${JSON.stringify(allClientIds)};\r\n\r\n// Pre-rendered HTML for each client component, produced at BUILD TIME by\r\n// renderToString with default props. Used directly in the span wrapper so\r\n// the server response contains real markup and React hydration never sees a\r\n// mismatch. No react-dom/server is needed at runtime.\r\nconst PRERENDERED_HTML: Record<string, string> = ${JSON.stringify(prerenderedHtml)};\r\n\r\n// \u2500\u2500\u2500 html-store (inlined \u2014 no external refs) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n// Uses the same globalThis Symbol key as html-store.ts so any useHtml() call\r\n// (however imported) writes into the same store that runWithHtmlStore reads.\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\nfunction __getStore(): HtmlStore | null { return (globalThis as any)[__STORE_KEY__] ?? null; }\r\nfunction __setStore(s: HtmlStore | null): void { (globalThis as any)[__STORE_KEY__] = s; }\r\nfunction __emptyStore(): HtmlStore {\r\n return { titleOps: [], htmlAttrs: {}, bodyAttrs: {}, meta: [], link: [], script: [], style: [] };\r\n}\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 metaKey(k: string): string { return k === 'httpEquiv' ? 'http-equiv' : k; }\r\nfunction linkKey(k: string): string {\r\n if (k === 'hrefLang') return 'hreflang';\r\n if (k === 'crossOrigin') return 'crossorigin';\r\n return k;\r\n}\r\nfunction renderMetaTag(tag: Record<string, string | undefined>): string {\r\n const attrs: Record<string, string | undefined> = {};\r\n for (const [k, v] of Object.entries(tag)) if (v !== undefined) attrs[metaKey(k)] = v;\r\n return \\` <meta \\${renderAttrs(attrs as any)} />\\`;\r\n}\r\nfunction renderLinkTag(tag: Record<string, string | undefined>): string {\r\n const attrs: Record<string, string | undefined> = {};\r\n for (const [k, v] of Object.entries(tag)) if (v !== undefined) attrs[linkKey(k)] = v;\r\n return \\` <link \\${renderAttrs(attrs as any)} />\\`;\r\n}\r\nfunction renderScriptTag(tag: any): string {\r\n const attrs: Record<string, any> = {\r\n 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 };\r\n const s = renderAttrs(attrs);\r\n const open = s ? \\`<script \\${s}>\\` : '<script>';\r\n return \\` \\${open}\\${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 Void element set \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 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// Converts React element trees in props to a JSON-safe format that\r\n// bundle.ts / initRuntime can reconstruct on the client.\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)) {\r\n return value.map(serializeProps).filter((v: any) => v !== undefined);\r\n }\r\n if ((value as any).$$typeof) {\r\n const { type, props: elProps } = value as any;\r\n if (typeof type === 'string') {\r\n return { __re: 'html', tag: type, props: serializeProps(elProps) };\r\n }\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(elProps) };\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\n// \u2500\u2500\u2500 Async recursive renderer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n// Handles: null/undefined/boolean \u2192 '', strings/numbers \u2192 escaped text, arrays,\r\n// Fragment, void/non-void HTML elements, class components, sync + async functions.\r\n// Client components \u2192 <span data-hydrate-id=\"\u2026\"> markers for browser hydration.\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)) {\r\n return (await Promise.all(node.map(n => renderNode(n, hydrated)))).join('');\r\n }\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')) {\r\n return renderNode(props?.children ?? null, hydrated);\r\n }\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 // Render with actual props so children/content appear in the SSR HTML.\r\n // Fall back to the build-time pre-rendered HTML if the component throws\r\n // (e.g. it references browser-only APIs during render).\r\n let ssrHtml: string;\r\n try {\r\n const result = await (type as Function)(props || {});\r\n ssrHtml = await renderNode(result, new Set());\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 const result = instance ? instance.render() : await (type as Function)(props || {});\r\n return renderNode(result, 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 }\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> = {};\r\n parsed.searchParams.forEach((v, k) => { params[k] = v; });\r\n const url = req.url || '/';\r\n\r\n const hydrated = new Set<string>();\r\n const pageElement = { type: __page__.default, props: params as any, key: null, ref: null };\r\n const wrapped = wrapWithLayouts(pageElement);\r\n\r\n let appHtml = '';\r\n const store = await runWithHtmlStore(async () => {\r\n appHtml = await renderNode(wrapped, hydrated);\r\n });\r\n\r\n const pageTitle = resolveTitle(store.titleOps, 'SSR App');\r\n const headLines: string[] = [\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.map(renderMetaTag),\r\n ...store.link.map(renderLinkTag),\r\n ...store.style.map(renderStyleTag),\r\n ...store.script.map(renderScriptTag),\r\n ];\r\n\r\n const runtimeData = JSON.stringify({\r\n hydrateIds: [...hydrated],\r\n allIds: ALL_CLIENT_IDS,\r\n url,\r\n params,\r\n 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\": \"/__react.js\",\r\n \"react-dom/client\": \"/__react.js\",\r\n \"react/jsx-runtime\": \"/__react.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</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`.trimStart();\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 *\r\n * Writes a temporary adapter next to `absPath`, bundles them together with\r\n * esbuild (node_modules kept external), then removes the temp file.\r\n *\r\n * @returns The bundled ESM text ready to write to disk.\r\n */\r\nexport async function bundleApiHandler(absPath: string): Promise<string> {\r\n const adapterName = `_api_adapter_${crypto.randomBytes(4).toString('hex')}.ts`;\r\n const adapterPath = path.join(path.dirname(absPath), adapterName);\r\n\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}\r\n\r\n/**\r\n * Bundles a server-component page into a single self-contained ESM string.\r\n *\r\n * Writes a temporary adapter next to `absPath` (so relative imports inside\r\n * the component resolve from the correct base directory), bundles it with\r\n * esbuild (React and all npm deps inlined, only Node built-ins stay external),\r\n * then removes the temp file.\r\n *\r\n * @returns The bundled ESM text ready to write to disk.\r\n */\r\nexport async function bundlePageHandler(opts: PageBundleOptions): Promise<string> {\r\n const { absPath, clientComponentNames, allClientIds, layoutPaths, prerenderedHtml } = opts;\r\n\r\n // The adapter is written next to the page file, so make every layout path\r\n // relative to that same directory. Absolute Windows paths like \"C:\\...\" in\r\n // import statements are not valid ESM URL schemes and throw\r\n // ERR_UNSUPPORTED_ESM_URL_SCHEME. Relative paths work on all platforms.\r\n const adapterName = `_page_adapter_${crypto.randomBytes(4).toString('hex')}.ts`;\r\n const adapterDir = path.dirname(absPath);\r\n const adapterPath = path.join(adapterDir, adapterName);\r\n\r\n const layoutImports = layoutPaths\r\n .map((lp, i) => {\r\n const rel = path.relative(adapterDir, lp).replace(/\\\\/g, '/');\r\n const importPath = rel.startsWith('.') ? rel : './' + rel;\r\n return `import __layout_${i}__ from ${JSON.stringify(importPath)};`;\r\n })\r\n .join('\\n');\r\n const layoutArrayItems = layoutPaths\r\n .map((_, i) => `__layout_${i}__`)\r\n .join(', ');\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,\r\n prerenderedHtml,\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 external: [\r\n // Node built-ins only \u2014 all npm packages (react, nukejs, \u2026) are inlined\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 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`.\r\n *\r\n * Mirrors bundleClientComponent() in bundler.ts:\r\n * \u2022 browser ESM, JSX automatic\r\n * \u2022 react / react-dom/client / react/jsx-runtime kept external so the\r\n * importmap can resolve them to the already-loaded /__react.js bundle.\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>(); // id \u2192 pre-rendered HTML\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 // 1. 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 // 2. 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}_${crypto.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 (e) {\r\n console.warn(` [SSR prerender failed for ${id}]`, e);\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 full React + ReactDOM browser bundle to `<staticDir>/__react.js`.\r\n * Exports every public hook and helper so client component bundles can import\r\n * them via the importmap without bundling React a second time.\r\n */\r\nexport async function buildReactBundle(staticDir: string): Promise<void> {\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\n\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 },\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 fs.writeFileSync(path.join(staticDir, '__react.js'), result.outputFiles[0].text);\r\n console.log(' built __react.js');\r\n}\r\n\r\n/**\r\n * Builds the nukejs client runtime to `<staticDir>/__n.js`.\r\n * React and react-dom/client are kept external (resolved via importmap).\r\n */\r\nexport async function buildNukeBundle(staticDir: string): Promise<void> {\r\n const nukeDir = path.dirname(fileURLToPath(import.meta.url));\r\n const result = await build({\r\n entryPoints: [path.join(nukeDir, 'bundle.js')],\r\n bundle: true,\r\n write: false,\r\n format: 'esm',\r\n minify: true,\r\n external: ['react', 'react-dom/client'],\r\n });\r\n fs.writeFileSync(path.join(staticDir, '__n.js'), result.outputFiles[0].text);\r\n console.log(' built __n.js');\r\n}\r\n\r\n// \u2500\u2500\u2500 Public static 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\r\n\r\n/**\r\n * Recursively copies every file from `app/public/` into `destDir`,\r\n * preserving the directory structure.\r\n *\r\n * Called by both build-vercel.ts (dest = .vercel/output/static/) and\r\n * build-node.ts (dest = dist/static/) so that:\r\n *\r\n * app/public/favicon.ico \u2192 <destDir>/favicon.ico\r\n * app/public/images/logo.png \u2192 <destDir>/images/logo.png\r\n *\r\n * On Vercel, the Build Output API v3 serves everything in .vercel/output/static/\r\n * directly \u2014 no route entry needed, same as __react.js and __n.js.\r\n *\r\n * On Node, the serverEntry template serves files from dist/static/ with the\r\n * same MIME-type logic as the dev middleware.\r\n *\r\n * Skips silently when the public directory does not exist so projects without\r\n * one don't need any special configuration.\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\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 srcPath = path.join(src, entry.name);\r\n const destPath = path.join(dest, entry.name);\r\n if (entry.isDirectory()) {\r\n walk(srcPath, destPath);\r\n } else {\r\n fs.copyFileSync(srcPath, destPath);\r\n count++;\r\n }\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 }\r\n}"],
|
|
5
|
-
"mappings": "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AU,YAAY,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mDAMZ,KAAK,UAAU,eAAe,CAAC;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;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,yDA0LzB,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;A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",
|
|
4
|
+
"sourcesContent": ["/**\r\n * build-common.ts\r\n *\r\n * Shared build logic used by both build-vercel.ts and build-node.ts.\r\n *\r\n * Exports:\r\n * \u2014 utility helpers : walkFiles, analyzeFile, isServerComponent,\r\n * findPageLayouts, extractDefaultExportName\r\n * \u2014 collection : collectServerPages, collectGlobalClientRegistry\r\n * \u2014 template codegen : makeApiAdapterSource, makePageAdapterSource\r\n * \u2014 bundle operations : bundleApiHandler, bundlePageHandler,\r\n * bundleClientComponents, buildReactBundle, buildNukeBundle\r\n */\r\n\r\nimport fs from 'fs';\r\nimport path from 'path';\r\nimport crypto from '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 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 srcRegex: string;\r\n paramNames: string[];\r\n /** Path used as function namespace, 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\n/** A server page together with its fully bundled ESM text, ready to emit. */\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, a function path, and a specificity score.\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 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) { paramNames.push(optCatchAll[1]); regexParts.push('(.*)'); specificity += 1; continue; }\r\n const catchAll = seg.match(/^\\[\\.\\.\\.(.+)\\]$/);\r\n if (catchAll) { paramNames.push(catchAll[1]); regexParts.push('(.+)'); specificity += 10; continue; }\r\n const dynamic = seg.match(/^\\[(.+)\\]$/);\r\n if (dynamic) { paramNames.push(dynamic[1]); regexParts.push('([^/]+)'); specificity += 100; continue; }\r\n regexParts.push(seg.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&'));\r\n specificity += 1000;\r\n }\r\n\r\n const srcRegex = segments.length === 0\r\n ? '^/$'\r\n : '^/' + regexParts.join('/') + '$';\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, 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 \u2014\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\nexport function extractDefaultExportName(filePath: string): string | null {\r\n const content = fs.readFileSync(filePath, 'utf-8');\r\n const match = content.match(/export\\s+default\\s+(?:function\\s+)?(\\w+)/);\r\n return match?.[1] ?? 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 by specificity\r\n * (most-specific first so more-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 const base = path.basename(relPath, path.extname(relPath));\r\n if (base === '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. Deduplication is automatic because the\r\n * Map key is the stable content-hash ID produced by component-analyzer.ts.\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>(); // id \u2192 absFilePath\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 }\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 }\r\n }\r\n return registry;\r\n}\r\n\r\n// \u2500\u2500\u2500 Per-page registry 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 * Builds the per-page client component registry (page + its layout chain) and\r\n * returns both the id\u2192path map and the name\u2192id map needed by bundlePageHandler.\r\n *\r\n * Extracted here to eliminate the identical loop duplicated across\r\n * build-node.ts and build-vercel.ts.\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 }\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 }\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\n * Callers (build-node, build-vercel) only need to write the bundled text to\r\n * their respective output destinations \u2014 the format-specific logic stays local.\r\n *\r\n * Returns an empty array when there are no server pages.\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 // Pass 1 \u2014 bundle all client components to static files.\r\n const globalClientRegistry = collectGlobalClientRegistry(serverPages, pagesDir);\r\n const prerenderedHtml = await bundleClientComponents(globalClientRegistry, pagesDir, staticDir);\r\n const prerenderedHtmlRecord = Object.fromEntries(prerenderedHtml);\r\n\r\n // Pass 2 \u2014 bundle each server-component page.\r\n const builtPages: BuiltPage[] = [];\r\n\r\n for (const page of serverPages) {\r\n const { funcPath, absPath } = page;\r\n console.log(` building ${fs.existsSync(absPath) ? absPath : absPath} \u2192 ${funcPath} [page]`);\r\n\r\n const layoutPaths = findPageLayouts(absPath, pagesDir);\r\n const { registry, clientComponentNames } = buildPerPageRegistry(absPath, layoutPaths, pagesDir);\r\n\r\n const bundleText = await bundlePageHandler({\r\n absPath,\r\n pagesDir,\r\n clientComponentNames,\r\n allClientIds: [...registry.keys()],\r\n layoutPaths,\r\n prerenderedHtml: prerenderedHtmlRecord,\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\n * @param handlerFilename Basename of the handler file relative to the adapter\r\n * (e.g. 'users.ts'). Must be in the same directory.\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`.trimStart();\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}\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 that handles server + client\r\n * components without react-dom/server.\r\n * \u2022 Client components are identified via the pre-computed CLIENT_COMPONENTS\r\n * map (no fs.readFileSync at runtime).\r\n * \u2022 Emits the same full HTML document structure as ssr.ts including the\r\n * __n_data blob, importmap, and initRuntime bootstrap.\r\n */\r\nexport function makePageAdapterSource(opts: PageAdapterOptions): string {\r\n const {\r\n pageImport,\r\n layoutImports,\r\n clientComponentNames,\r\n allClientIds,\r\n layoutArrayItems,\r\n prerenderedHtml,\r\n } = opts;\r\n\r\n return `\r\nimport type { IncomingMessage, ServerResponse } from 'http';\r\nimport * as __page__ from ${pageImport};\r\n${layoutImports}\r\n\r\n// \u2500\u2500\u2500 Pre-built client component registry \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n// Computed at BUILD TIME from the import tree. Source files are not deployed,\r\n// so we must not read them with fs.readFileSync at runtime.\r\n// Key: default-export function name \u2192 Value: stable content-hash ID\r\nconst CLIENT_COMPONENTS: Record<string, string> = ${JSON.stringify(clientComponentNames)};\r\n\r\n// All client component IDs reachable from this page (page + layouts).\r\n// Sent to initRuntime so the browser pre-loads all bundles for SPA navigation.\r\nconst ALL_CLIENT_IDS: string[] = ${JSON.stringify(allClientIds)};\r\n\r\n// Pre-rendered HTML for each client component, produced at BUILD TIME by\r\n// renderToString with default props. Used directly in the span wrapper so\r\n// the server response contains real markup and React hydration never sees a\r\n// mismatch. No react-dom/server is needed at runtime.\r\nconst PRERENDERED_HTML: Record<string, string> = ${JSON.stringify(prerenderedHtml)};\r\n\r\n// \u2500\u2500\u2500 html-store (inlined \u2014 no external refs) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n// Uses the same globalThis Symbol key as html-store.ts so any useHtml() call\r\n// (however imported) writes into the same store that runWithHtmlStore reads.\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\nfunction __getStore(): HtmlStore | null { return (globalThis as any)[__STORE_KEY__] ?? null; }\r\nfunction __setStore(s: HtmlStore | null): void { (globalThis as any)[__STORE_KEY__] = s; }\r\nfunction __emptyStore(): HtmlStore {\r\n return { titleOps: [], htmlAttrs: {}, bodyAttrs: {}, meta: [], link: [], script: [], style: [] };\r\n}\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 metaKey(k: string): string { return k === 'httpEquiv' ? 'http-equiv' : k; }\r\nfunction linkKey(k: string): string {\r\n if (k === 'hrefLang') return 'hreflang';\r\n if (k === 'crossOrigin') return 'crossorigin';\r\n return k;\r\n}\r\nfunction renderMetaTag(tag: Record<string, string | undefined>): string {\r\n const attrs: Record<string, string | undefined> = {};\r\n for (const [k, v] of Object.entries(tag)) if (v !== undefined) attrs[metaKey(k)] = v;\r\n return \\` <meta \\${renderAttrs(attrs as any)} />\\`;\r\n}\r\nfunction renderLinkTag(tag: Record<string, string | undefined>): string {\r\n const attrs: Record<string, string | undefined> = {};\r\n for (const [k, v] of Object.entries(tag)) if (v !== undefined) attrs[linkKey(k)] = v;\r\n return \\` <link \\${renderAttrs(attrs as any)} />\\`;\r\n}\r\nfunction renderScriptTag(tag: any): string {\r\n const attrs: Record<string, any> = {\r\n 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 };\r\n const s = renderAttrs(attrs);\r\n const open = s ? \\`<script \\${s}>\\` : '<script>';\r\n return \\` \\${open}\\${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 Void element set \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 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// Converts React element trees in props to a JSON-safe format that\r\n// bundle.ts / initRuntime can reconstruct on the client.\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)) {\r\n return value.map(serializeProps).filter((v: any) => v !== undefined);\r\n }\r\n if ((value as any).$$typeof) {\r\n const { type, props: elProps } = value as any;\r\n if (typeof type === 'string') {\r\n return { __re: 'html', tag: type, props: serializeProps(elProps) };\r\n }\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(elProps) };\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\n// \u2500\u2500\u2500 Async recursive renderer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n// Handles: null/undefined/boolean \u2192 '', strings/numbers \u2192 escaped text, arrays,\r\n// Fragment, void/non-void HTML elements, class components, sync + async functions.\r\n// Client components \u2192 <span data-hydrate-id=\"\u2026\"> markers for browser hydration.\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)) {\r\n return (await Promise.all(node.map(n => renderNode(n, hydrated)))).join('');\r\n }\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')) {\r\n return renderNode(props?.children ?? null, hydrated);\r\n }\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 // Render with actual props so children/content appear in the SSR HTML.\r\n // Fall back to the build-time pre-rendered HTML if the component throws\r\n // (e.g. it references browser-only APIs during render).\r\n let ssrHtml: string;\r\n try {\r\n const result = await (type as Function)(props || {});\r\n ssrHtml = await renderNode(result, new Set());\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 const result = instance ? instance.render() : await (type as Function)(props || {});\r\n return renderNode(result, 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 }\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> = {};\r\n parsed.searchParams.forEach((v, k) => { params[k] = v; });\r\n const url = req.url || '/';\r\n\r\n const hydrated = new Set<string>();\r\n const pageElement = { type: __page__.default, props: params as any, key: null, ref: null };\r\n const wrapped = wrapWithLayouts(pageElement);\r\n\r\n let appHtml = '';\r\n const store = await runWithHtmlStore(async () => {\r\n appHtml = await renderNode(wrapped, hydrated);\r\n });\r\n\r\n const pageTitle = resolveTitle(store.titleOps, 'SSR App');\r\n const headLines: string[] = [\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.map(renderMetaTag),\r\n ...store.link.map(renderLinkTag),\r\n ...store.style.map(renderStyleTag),\r\n ...store.script.map(renderScriptTag),\r\n ];\r\n\r\n const runtimeData = JSON.stringify({\r\n hydrateIds: [...hydrated],\r\n allIds: ALL_CLIENT_IDS,\r\n url,\r\n params,\r\n 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\": \"/__react.js\",\r\n \"react-dom/client\": \"/__react.js\",\r\n \"react/jsx-runtime\": \"/__react.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</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`.trimStart();\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 *\r\n * Writes a temporary adapter next to `absPath`, bundles them together with\r\n * esbuild (node_modules kept external), then removes the temp file.\r\n *\r\n * @returns The bundled ESM text ready to write to disk.\r\n */\r\nexport async function bundleApiHandler(absPath: string): Promise<string> {\r\n const adapterName = `_api_adapter_${crypto.randomBytes(4).toString('hex')}.ts`;\r\n const adapterPath = path.join(path.dirname(absPath), adapterName);\r\n\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}\r\n\r\n/**\r\n * Bundles a server-component page into a single self-contained ESM string.\r\n *\r\n * Writes a temporary adapter next to `absPath` (so relative imports inside\r\n * the component resolve from the correct base directory), bundles it with\r\n * esbuild (React and all npm deps inlined, only Node built-ins stay external),\r\n * then removes the temp file.\r\n *\r\n * @returns The bundled ESM text ready to write to disk.\r\n */\r\nexport async function bundlePageHandler(opts: PageBundleOptions): Promise<string> {\r\n const { absPath, clientComponentNames, allClientIds, layoutPaths, prerenderedHtml } = opts;\r\n\r\n // The adapter is written next to the page file, so make every layout path\r\n // relative to that same directory. Absolute Windows paths like \"C:\\...\" in\r\n // import statements are not valid ESM URL schemes and throw\r\n // ERR_UNSUPPORTED_ESM_URL_SCHEME. Relative paths work on all platforms.\r\n const adapterName = `_page_adapter_${crypto.randomBytes(4).toString('hex')}.ts`;\r\n const adapterDir = path.dirname(absPath);\r\n const adapterPath = path.join(adapterDir, adapterName);\r\n\r\n const layoutImports = layoutPaths\r\n .map((lp, i) => {\r\n const rel = path.relative(adapterDir, lp).replace(/\\\\/g, '/');\r\n const importPath = rel.startsWith('.') ? rel : './' + rel;\r\n return `import __layout_${i}__ from ${JSON.stringify(importPath)};`;\r\n })\r\n .join('\\n');\r\n const layoutArrayItems = layoutPaths\r\n .map((_, i) => `__layout_${i}__`)\r\n .join(', ');\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,\r\n prerenderedHtml,\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 external: [\r\n // Node built-ins only \u2014 all npm packages (react, nukejs, \u2026) are inlined\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 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`.\r\n *\r\n * Mirrors bundleClientComponent() in bundler.ts:\r\n * \u2022 browser ESM, JSX automatic\r\n * \u2022 react / react-dom/client / react/jsx-runtime kept external so the\r\n * importmap can resolve them to the already-loaded /__react.js bundle.\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>(); // id \u2192 pre-rendered HTML\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 // 1. 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 // 2. 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}_${crypto.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 (e) {\r\n //console.warn(` [SSR prerender failed for ${id}]`, e);\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 full React + ReactDOM browser bundle to `<staticDir>/__react.js`.\r\n * Exports every public hook and helper so client component bundles can import\r\n * them via the importmap without bundling React a second time.\r\n */\r\nexport async function buildReactBundle(staticDir: string): Promise<void> {\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\n\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 },\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 fs.writeFileSync(path.join(staticDir, '__react.js'), result.outputFiles[0].text);\r\n console.log(' built __react.js');\r\n}\r\n\r\n/**\r\n * Builds the nukejs client runtime to `<staticDir>/__n.js`.\r\n * React and react-dom/client are kept external (resolved via importmap).\r\n */\r\nexport async function buildNukeBundle(staticDir: string): Promise<void> {\r\n const nukeDir = path.dirname(fileURLToPath(import.meta.url));\r\n const result = await build({\r\n entryPoints: [path.join(nukeDir, 'bundle.js')],\r\n bundle: true,\r\n write: false,\r\n format: 'esm',\r\n minify: true,\r\n external: ['react', 'react-dom/client'],\r\n });\r\n fs.writeFileSync(path.join(staticDir, '__n.js'), result.outputFiles[0].text);\r\n console.log(' built __n.js');\r\n}\r\n\r\n// \u2500\u2500\u2500 Public static 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\r\n\r\n/**\r\n * Recursively copies every file from `app/public/` into `destDir`,\r\n * preserving the directory structure.\r\n *\r\n * Called by both build-vercel.ts (dest = .vercel/output/static/) and\r\n * build-node.ts (dest = dist/static/) so that:\r\n *\r\n * app/public/favicon.ico \u2192 <destDir>/favicon.ico\r\n * app/public/images/logo.png \u2192 <destDir>/images/logo.png\r\n *\r\n * On Vercel, the Build Output API v3 serves everything in .vercel/output/static/\r\n * directly \u2014 no route entry needed, same as __react.js and __n.js.\r\n *\r\n * On Node, the serverEntry template serves files from dist/static/ with the\r\n * same MIME-type logic as the dev middleware.\r\n *\r\n * Skips silently when the public directory does not exist so projects without\r\n * one don't need any special configuration.\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\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 srcPath = path.join(src, entry.name);\r\n const destPath = path.join(dest, entry.name);\r\n if (entry.isDirectory()) {\r\n walk(srcPath, destPath);\r\n } else {\r\n fs.copyFileSync(srcPath, destPath);\r\n count++;\r\n }\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 }\r\n}"],
|
|
5
|
+
"mappings": "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AU,YAAY,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mDAMZ,KAAK,UAAU,eAAe,CAAC;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;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,yDA0LzB,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;A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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/build-vercel.js
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
|
+
import crypto from "crypto";
|
|
4
|
+
import { build } from "esbuild";
|
|
3
5
|
import { loadConfig } from "./config.js";
|
|
4
6
|
import {
|
|
5
|
-
analyzeFile,
|
|
6
7
|
walkFiles,
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
analyzeFile,
|
|
9
|
+
collectServerPages,
|
|
10
|
+
collectGlobalClientRegistry,
|
|
11
|
+
bundleClientComponents,
|
|
12
|
+
findPageLayouts,
|
|
13
|
+
buildPerPageRegistry,
|
|
14
|
+
makePageAdapterSource,
|
|
9
15
|
buildReactBundle,
|
|
10
16
|
buildNukeBundle,
|
|
11
17
|
copyPublicFiles
|
|
@@ -19,8 +25,8 @@ const config = await loadConfig();
|
|
|
19
25
|
const SERVER_DIR = path.resolve(config.serverDir);
|
|
20
26
|
const PAGES_DIR = path.resolve("./app/pages");
|
|
21
27
|
const PUBLIC_DIR = path.resolve("./app/public");
|
|
22
|
-
function emitVercelFunction(
|
|
23
|
-
const funcDir = path.join(FUNCTIONS_DIR,
|
|
28
|
+
function emitVercelFunction(name, bundleText) {
|
|
29
|
+
const funcDir = path.join(FUNCTIONS_DIR, name + ".func");
|
|
24
30
|
fs.mkdirSync(funcDir, { recursive: true });
|
|
25
31
|
fs.writeFileSync(path.join(funcDir, "index.mjs"), bundleText);
|
|
26
32
|
fs.writeFileSync(
|
|
@@ -28,26 +34,230 @@ function emitVercelFunction(funcPath, bundleText) {
|
|
|
28
34
|
JSON.stringify({ runtime: "nodejs20.x", handler: "index.mjs", launcherType: "Nodejs" }, null, 2)
|
|
29
35
|
);
|
|
30
36
|
}
|
|
31
|
-
function
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
37
|
+
function makeApiDispatcherSource(routes) {
|
|
38
|
+
const imports = routes.map((r, i) => `import * as __api_${i}__ from ${JSON.stringify(r.absPath)};`).join("\n");
|
|
39
|
+
const routeEntries = routes.map(
|
|
40
|
+
(r, i) => ` { regex: ${JSON.stringify(r.srcRegex)}, params: ${JSON.stringify(r.paramNames)}, mod: __api_${i}__ },`
|
|
41
|
+
).join("\n");
|
|
42
|
+
return `import type { IncomingMessage, ServerResponse } from 'http';
|
|
43
|
+
${imports}
|
|
44
|
+
|
|
45
|
+
function enhance(res: ServerResponse) {
|
|
46
|
+
(res as any).json = function(data: any, status = 200) {
|
|
47
|
+
this.statusCode = status;
|
|
48
|
+
this.setHeader('Content-Type', 'application/json');
|
|
49
|
+
this.end(JSON.stringify(data));
|
|
50
|
+
};
|
|
51
|
+
(res as any).status = function(code: number) { this.statusCode = code; return this; };
|
|
52
|
+
return res;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function parseBody(req: IncomingMessage): Promise<any> {
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
let body = '';
|
|
58
|
+
req.on('data', (chunk: any) => { body += chunk.toString(); });
|
|
59
|
+
req.on('end', () => {
|
|
60
|
+
try {
|
|
61
|
+
resolve(
|
|
62
|
+
body && req.headers['content-type']?.includes('application/json')
|
|
63
|
+
? JSON.parse(body)
|
|
64
|
+
: body,
|
|
65
|
+
);
|
|
66
|
+
} catch (e) { reject(e); }
|
|
67
|
+
});
|
|
68
|
+
req.on('error', reject);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const ROUTES = [
|
|
73
|
+
${routeEntries}
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
export default async function handler(req: IncomingMessage, res: ServerResponse) {
|
|
77
|
+
const url = new URL(req.url || '/', 'http://localhost');
|
|
78
|
+
const pathname = url.pathname;
|
|
79
|
+
|
|
80
|
+
for (const route of ROUTES) {
|
|
81
|
+
const m = pathname.match(new RegExp(route.regex));
|
|
82
|
+
if (!m) continue;
|
|
83
|
+
|
|
84
|
+
const method = (req.method || 'GET').toUpperCase();
|
|
85
|
+
const apiRes = enhance(res);
|
|
86
|
+
const apiReq = req as any;
|
|
87
|
+
|
|
88
|
+
apiReq.body = await parseBody(req);
|
|
89
|
+
apiReq.query = Object.fromEntries(url.searchParams);
|
|
90
|
+
apiReq.params = {};
|
|
91
|
+
route.params.forEach((name: string, i: number) => { apiReq.params[name] = m[i + 1]; });
|
|
92
|
+
|
|
93
|
+
const fn = (route.mod as any)[method] ?? (route.mod as any)['default'];
|
|
94
|
+
if (typeof fn !== 'function') {
|
|
95
|
+
(apiRes as any).json({ error: \`Method \${method} not allowed\` }, 405);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
await fn(apiReq, apiRes);
|
|
99
|
+
return;
|
|
35
100
|
}
|
|
36
|
-
|
|
101
|
+
|
|
102
|
+
res.statusCode = 404;
|
|
103
|
+
res.setHeader('Content-Type', 'application/json');
|
|
104
|
+
res.end(JSON.stringify({ error: 'Not Found' }));
|
|
105
|
+
}
|
|
106
|
+
`;
|
|
107
|
+
}
|
|
108
|
+
function makePagesDispatcherSource(routes) {
|
|
109
|
+
const imports = routes.map((r, i) => `import __page_${i}__ from ${JSON.stringify(r.adapterPath)};`).join("\n");
|
|
110
|
+
const routeEntries = routes.map(
|
|
111
|
+
(r, i) => ` { regex: ${JSON.stringify(r.srcRegex)}, params: ${JSON.stringify(r.paramNames)}, handler: __page_${i}__ },`
|
|
112
|
+
).join("\n");
|
|
113
|
+
return `import type { IncomingMessage, ServerResponse } from 'http';
|
|
114
|
+
${imports}
|
|
115
|
+
|
|
116
|
+
const ROUTES: Array<{
|
|
117
|
+
regex: string;
|
|
118
|
+
params: string[];
|
|
119
|
+
handler: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
120
|
+
}> = [
|
|
121
|
+
${routeEntries}
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
export default async function handler(req: IncomingMessage, res: ServerResponse) {
|
|
125
|
+
const url = new URL(req.url || '/', 'http://localhost');
|
|
126
|
+
const pathname = url.pathname;
|
|
127
|
+
|
|
128
|
+
for (const route of ROUTES) {
|
|
129
|
+
const m = pathname.match(new RegExp(route.regex));
|
|
130
|
+
if (!m) continue;
|
|
131
|
+
|
|
132
|
+
// Inject dynamic params as query-string values so page handlers can read
|
|
133
|
+
// them via new URL(req.url).searchParams \u2014 the same way they always have.
|
|
134
|
+
route.params.forEach((name, i) => url.searchParams.set(name, m[i + 1]));
|
|
135
|
+
req.url = pathname + (url.search || '');
|
|
136
|
+
|
|
137
|
+
return route.handler(req, res);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
res.statusCode = 404;
|
|
141
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
142
|
+
res.end('Not Found');
|
|
143
|
+
}
|
|
144
|
+
`;
|
|
37
145
|
}
|
|
146
|
+
const vercelRoutes = [];
|
|
38
147
|
const apiFiles = walkFiles(SERVER_DIR);
|
|
39
148
|
if (apiFiles.length === 0) console.warn(`\u26A0 No server files found in ${SERVER_DIR}`);
|
|
40
149
|
const apiRoutes = apiFiles.map((relPath) => ({ ...analyzeFile(relPath, "api"), absPath: path.join(SERVER_DIR, relPath) })).sort((a, b) => b.specificity - a.specificity);
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
150
|
+
if (apiRoutes.length > 0) {
|
|
151
|
+
const dispatcherSource = makeApiDispatcherSource(apiRoutes);
|
|
152
|
+
const dispatcherPath = path.join(SERVER_DIR, `_api_dispatcher_${crypto.randomBytes(4).toString("hex")}.ts`);
|
|
153
|
+
fs.writeFileSync(dispatcherPath, dispatcherSource);
|
|
154
|
+
try {
|
|
155
|
+
const result = await build({
|
|
156
|
+
entryPoints: [dispatcherPath],
|
|
157
|
+
bundle: true,
|
|
158
|
+
format: "esm",
|
|
159
|
+
platform: "node",
|
|
160
|
+
target: "node20",
|
|
161
|
+
packages: "external",
|
|
162
|
+
write: false
|
|
163
|
+
});
|
|
164
|
+
emitVercelFunction("api", result.outputFiles[0].text);
|
|
165
|
+
console.log(` built API dispatcher \u2192 api.func (${apiRoutes.length} route(s))`);
|
|
166
|
+
} finally {
|
|
167
|
+
fs.unlinkSync(dispatcherPath);
|
|
168
|
+
}
|
|
169
|
+
for (const { srcRegex } of apiRoutes) {
|
|
170
|
+
vercelRoutes.push({ src: srcRegex, dest: "/api" });
|
|
171
|
+
}
|
|
46
172
|
}
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
173
|
+
const serverPages = collectServerPages(PAGES_DIR);
|
|
174
|
+
if (serverPages.length > 0) {
|
|
175
|
+
const globalClientRegistry = collectGlobalClientRegistry(serverPages, PAGES_DIR);
|
|
176
|
+
const prerenderedHtml = await bundleClientComponents(globalClientRegistry, PAGES_DIR, STATIC_DIR);
|
|
177
|
+
const prerenderedHtmlRecord = Object.fromEntries(prerenderedHtml);
|
|
178
|
+
const tempAdapterPaths = [];
|
|
179
|
+
for (const page of serverPages) {
|
|
180
|
+
const { absPath } = page;
|
|
181
|
+
const adapterDir = path.dirname(absPath);
|
|
182
|
+
const adapterPath = path.join(adapterDir, `_page_adapter_${crypto.randomBytes(4).toString("hex")}.ts`);
|
|
183
|
+
const layoutPaths = findPageLayouts(absPath, PAGES_DIR);
|
|
184
|
+
const { registry, clientComponentNames } = buildPerPageRegistry(absPath, layoutPaths, PAGES_DIR);
|
|
185
|
+
const layoutImports = layoutPaths.map((lp, i) => {
|
|
186
|
+
const rel = path.relative(adapterDir, lp).replace(/\\/g, "/");
|
|
187
|
+
return `import __layout_${i}__ from ${JSON.stringify(rel.startsWith(".") ? rel : "./" + rel)};`;
|
|
188
|
+
}).join("\n");
|
|
189
|
+
fs.writeFileSync(
|
|
190
|
+
adapterPath,
|
|
191
|
+
makePageAdapterSource({
|
|
192
|
+
pageImport: JSON.stringify("./" + path.basename(absPath)),
|
|
193
|
+
layoutImports,
|
|
194
|
+
clientComponentNames,
|
|
195
|
+
allClientIds: [...registry.keys()],
|
|
196
|
+
layoutArrayItems: layoutPaths.map((_, i) => `__layout_${i}__`).join(", "),
|
|
197
|
+
prerenderedHtml: prerenderedHtmlRecord
|
|
198
|
+
})
|
|
199
|
+
);
|
|
200
|
+
tempAdapterPaths.push(adapterPath);
|
|
201
|
+
console.log(` prepared ${path.relative(PAGES_DIR, absPath)} \u2192 ${page.funcPath} [page]`);
|
|
202
|
+
}
|
|
203
|
+
const dispatcherRoutes = serverPages.map((page, i) => ({
|
|
204
|
+
adapterPath: tempAdapterPaths[i],
|
|
205
|
+
srcRegex: page.srcRegex,
|
|
206
|
+
paramNames: page.paramNames
|
|
207
|
+
}));
|
|
208
|
+
const dispatcherPath = path.join(PAGES_DIR, `_pages_dispatcher_${crypto.randomBytes(4).toString("hex")}.ts`);
|
|
209
|
+
fs.writeFileSync(dispatcherPath, makePagesDispatcherSource(dispatcherRoutes));
|
|
210
|
+
try {
|
|
211
|
+
const result = await build({
|
|
212
|
+
entryPoints: [dispatcherPath],
|
|
213
|
+
bundle: true,
|
|
214
|
+
format: "esm",
|
|
215
|
+
platform: "node",
|
|
216
|
+
target: "node20",
|
|
217
|
+
jsx: "automatic",
|
|
218
|
+
external: [
|
|
219
|
+
"node:*",
|
|
220
|
+
"http",
|
|
221
|
+
"https",
|
|
222
|
+
"fs",
|
|
223
|
+
"path",
|
|
224
|
+
"url",
|
|
225
|
+
"crypto",
|
|
226
|
+
"stream",
|
|
227
|
+
"buffer",
|
|
228
|
+
"events",
|
|
229
|
+
"util",
|
|
230
|
+
"os",
|
|
231
|
+
"net",
|
|
232
|
+
"tls",
|
|
233
|
+
"child_process",
|
|
234
|
+
"worker_threads",
|
|
235
|
+
"cluster",
|
|
236
|
+
"dgram",
|
|
237
|
+
"dns",
|
|
238
|
+
"readline",
|
|
239
|
+
"zlib",
|
|
240
|
+
"assert",
|
|
241
|
+
"module",
|
|
242
|
+
"perf_hooks",
|
|
243
|
+
"string_decoder",
|
|
244
|
+
"timers",
|
|
245
|
+
"async_hooks",
|
|
246
|
+
"v8",
|
|
247
|
+
"vm"
|
|
248
|
+
],
|
|
249
|
+
define: { "process.env.NODE_ENV": '"production"' },
|
|
250
|
+
write: false
|
|
251
|
+
});
|
|
252
|
+
emitVercelFunction("pages", result.outputFiles[0].text);
|
|
253
|
+
console.log(` built Pages dispatcher \u2192 pages.func (${serverPages.length} page(s))`);
|
|
254
|
+
} finally {
|
|
255
|
+
fs.unlinkSync(dispatcherPath);
|
|
256
|
+
for (const p of tempAdapterPaths) if (fs.existsSync(p)) fs.unlinkSync(p);
|
|
257
|
+
}
|
|
258
|
+
for (const { srcRegex } of serverPages) {
|
|
259
|
+
vercelRoutes.push({ src: srcRegex, dest: "/pages" });
|
|
260
|
+
}
|
|
51
261
|
}
|
|
52
262
|
fs.writeFileSync(
|
|
53
263
|
path.join(OUTPUT_DIR, "config.json"),
|
|
@@ -60,6 +270,7 @@ fs.writeFileSync(
|
|
|
60
270
|
await buildReactBundle(STATIC_DIR);
|
|
61
271
|
await buildNukeBundle(STATIC_DIR);
|
|
62
272
|
copyPublicFiles(PUBLIC_DIR, STATIC_DIR);
|
|
273
|
+
const fnCount = (apiRoutes.length > 0 ? 1 : 0) + (serverPages.length > 0 ? 1 : 0);
|
|
63
274
|
console.log(`
|
|
64
|
-
\u2713 Vercel build complete \u2014 ${
|
|
275
|
+
\u2713 Vercel build complete \u2014 ${fnCount} function(s) \u2192 .vercel/output`);
|
|
65
276
|
//# sourceMappingURL=build-vercel.js.map
|
package/dist/build-vercel.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/build-vercel.ts"],
|
|
4
|
-
"sourcesContent": ["import fs from 'fs';\r\nimport path from 'path';\r\n\r\nimport { loadConfig } from './config';\r\nimport {\r\n analyzeFile,\r\n walkFiles,\r\n buildPages,\r\n bundleApiHandler,\r\n buildReactBundle,\r\n buildNukeBundle,\r\n copyPublicFiles,\r\n} from './build-common';\r\n\r\n// \u2500\u2500\u2500 Output directories \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 OUTPUT_DIR = path.resolve('.vercel/output');\r\nconst FUNCTIONS_DIR = path.join(OUTPUT_DIR, 'functions');\r\nconst STATIC_DIR = path.join(OUTPUT_DIR, 'static');\r\n\r\nfs.mkdirSync(FUNCTIONS_DIR, { recursive: true });\r\nfs.mkdirSync(STATIC_DIR, { recursive: true });\r\n\r\n// \u2500\u2500\u2500 Config \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 config = await loadConfig();\r\nconst SERVER_DIR = path.resolve(config.serverDir);\r\nconst PAGES_DIR = path.resolve('./app/pages');\r\nconst PUBLIC_DIR = path.resolve('./app/public');\r\n\r\n// \u2500\u2500\u2500 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\u2500\u2500\u2500\u2500\u2500\r\n\r\n/** Writes a bundled handler into a Vercel .func directory. */\r\nfunction emitVercelFunction(funcPath: string, bundleText: string): void {\r\n const funcDir = path.join(FUNCTIONS_DIR, funcPath.slice(1) + '.func');\r\n fs.mkdirSync(funcDir, { recursive: true });\r\n fs.writeFileSync(path.join(funcDir, 'index.mjs'), bundleText);\r\n fs.writeFileSync(\r\n path.join(funcDir, '.vc-config.json'),\r\n JSON.stringify({ runtime: 'nodejs20.x', handler: 'index.mjs', launcherType: 'Nodejs' }, null, 2),\r\n );\r\n}\r\n\r\ntype VercelRoute = { src: string; dest: string };\r\n\r\nfunction makeVercelRoute(srcRegex: string, paramNames: string[], funcPath: string): VercelRoute {\r\n let dest = funcPath;\r\n if (paramNames.length > 0) {\r\n dest += '?' + paramNames.map((name, i) => `${name}=$${i + 1}`).join('&');\r\n }\r\n return { src: srcRegex, dest };\r\n}\r\n\r\n// \u2500\u2500\u2500 API routes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 apiFiles = walkFiles(SERVER_DIR);\r\nif (apiFiles.length === 0) console.warn(`\u26A0 No server files found in ${SERVER_DIR}`);\r\n\r\nconst apiRoutes = apiFiles\r\n .map(relPath => ({ ...analyzeFile(relPath, 'api'), absPath: path.join(SERVER_DIR, relPath) }))\r\n .sort((a, b) => b.specificity - a.specificity);\r\n\r\nconst vercelRoutes: VercelRoute[] = [];\r\n\r\nfor (const { srcRegex, paramNames, funcPath, absPath } of apiRoutes) {\r\n console.log(` building ${path.relative(SERVER_DIR, absPath)} \u2192 ${funcPath}`);\r\n emitVercelFunction(funcPath, await bundleApiHandler(absPath));\r\n vercelRoutes.push(makeVercelRoute(srcRegex, paramNames, funcPath));\r\n}\r\n\r\n// \u2500\u2500\u2500 Page routes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 builtPages = await buildPages(PAGES_DIR, STATIC_DIR);\r\n\r\nfor (const { srcRegex, paramNames, funcPath, bundleText } of builtPages) {\r\n emitVercelFunction(funcPath, bundleText);\r\n vercelRoutes.push(makeVercelRoute(srcRegex, paramNames, funcPath));\r\n}\r\n\r\n// \u2500\u2500\u2500 Vercel config \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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\nfs.writeFileSync(\r\n path.join(OUTPUT_DIR, 'config.json'),\r\n JSON.stringify({ version: 3, routes: vercelRoutes }, null, 2),\r\n);\r\n\r\nfs.writeFileSync(\r\n path.resolve('vercel.json'),\r\n JSON.stringify({ runtime: 'nodejs20.x' }, null, 2),\r\n);\r\n\r\n// \u2500\u2500\u2500 Static assets \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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\nawait buildReactBundle(STATIC_DIR);\r\nawait buildNukeBundle(STATIC_DIR);\r\ncopyPublicFiles(PUBLIC_DIR, STATIC_DIR);\r\n\r\nconsole.log(`\\n\u2713 Vercel build complete \u2014 ${vercelRoutes.length} function(s) \u2192 .vercel/output`);"],
|
|
5
|
-
"mappings": "AAAA,OAAO,QAAQ;AACf,OAAO,UAAU;
|
|
4
|
+
"sourcesContent": ["import fs from 'fs';\r\nimport path from 'path';\r\nimport crypto from 'crypto';\r\nimport { build } from 'esbuild';\r\n\r\nimport { loadConfig } from './config';\r\nimport {\r\n walkFiles,\r\n analyzeFile,\r\n collectServerPages,\r\n collectGlobalClientRegistry,\r\n bundleClientComponents,\r\n findPageLayouts,\r\n buildPerPageRegistry,\r\n makePageAdapterSource,\r\n buildReactBundle,\r\n buildNukeBundle,\r\n copyPublicFiles,\r\n} from './build-common';\r\n\r\n// \u2500\u2500\u2500 Output directories \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 OUTPUT_DIR = path.resolve('.vercel/output');\r\nconst FUNCTIONS_DIR = path.join(OUTPUT_DIR, 'functions');\r\nconst STATIC_DIR = path.join(OUTPUT_DIR, 'static');\r\n\r\nfs.mkdirSync(FUNCTIONS_DIR, { recursive: true });\r\nfs.mkdirSync(STATIC_DIR, { recursive: true });\r\n\r\n// \u2500\u2500\u2500 Config \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 config = await loadConfig();\r\nconst SERVER_DIR = path.resolve(config.serverDir);\r\nconst PAGES_DIR = path.resolve('./app/pages');\r\nconst PUBLIC_DIR = path.resolve('./app/public');\r\n\r\n// \u2500\u2500\u2500 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\u2500\u2500\u2500\u2500\u2500\r\n\r\ntype VercelRoute = { src: string; dest: string };\r\n\r\n/** Writes a bundled dispatcher into a Vercel .func directory. */\r\nfunction emitVercelFunction(name: string, bundleText: string): void {\r\n const funcDir = path.join(FUNCTIONS_DIR, name + '.func');\r\n fs.mkdirSync(funcDir, { recursive: true });\r\n fs.writeFileSync(path.join(funcDir, 'index.mjs'), bundleText);\r\n fs.writeFileSync(\r\n path.join(funcDir, '.vc-config.json'),\r\n JSON.stringify({ runtime: 'nodejs20.x', handler: 'index.mjs', launcherType: 'Nodejs' }, null, 2),\r\n );\r\n}\r\n\r\n// \u2500\u2500\u2500 API dispatcher source \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 * Generates a single dispatcher that imports every API route module directly,\r\n * matches the incoming URL against each route's regex, injects captured params,\r\n * and calls the right HTTP-method export (GET, POST, \u2026) or default export.\r\n *\r\n * enhance / parseBody helpers are included once rather than once per route.\r\n */\r\nfunction makeApiDispatcherSource(\r\n routes: Array<{ absPath: string; srcRegex: string; paramNames: string[] }>,\r\n): string {\r\n const imports = routes\r\n .map((r, i) => `import * as __api_${i}__ from ${JSON.stringify(r.absPath)};`)\r\n .join('\\n');\r\n\r\n const routeEntries = routes\r\n .map((r, i) =>\r\n ` { regex: ${JSON.stringify(r.srcRegex)}, params: ${JSON.stringify(r.paramNames)}, mod: __api_${i}__ },`,\r\n )\r\n .join('\\n');\r\n\r\n return `\\\r\nimport type { IncomingMessage, ServerResponse } from 'http';\r\n${imports}\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: any) => { body += chunk.toString(); });\r\n req.on('end', () => {\r\n try {\r\n resolve(\r\n body && req.headers['content-type']?.includes('application/json')\r\n ? JSON.parse(body)\r\n : body,\r\n );\r\n } catch (e) { reject(e); }\r\n });\r\n req.on('error', reject);\r\n });\r\n}\r\n\r\nconst ROUTES = [\r\n${routeEntries}\r\n];\r\n\r\nexport default async function handler(req: IncomingMessage, res: ServerResponse) {\r\n const url = new URL(req.url || '/', 'http://localhost');\r\n const pathname = url.pathname;\r\n\r\n for (const route of ROUTES) {\r\n const m = pathname.match(new RegExp(route.regex));\r\n if (!m) continue;\r\n\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(url.searchParams);\r\n apiReq.params = {};\r\n route.params.forEach((name: string, i: number) => { apiReq.params[name] = m[i + 1]; });\r\n\r\n const fn = (route.mod as any)[method] ?? (route.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 return;\r\n }\r\n\r\n res.statusCode = 404;\r\n res.setHeader('Content-Type', 'application/json');\r\n res.end(JSON.stringify({ error: 'Not Found' }));\r\n}\r\n`;\r\n}\r\n\r\n// \u2500\u2500\u2500 Pages dispatcher source \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 * Generates a dispatcher that imports each page's pre-generated adapter by its\r\n * temp file path, matches the incoming URL, injects captured dynamic params as\r\n * query-string values (page handlers read params from req.url searchParams),\r\n * then delegates to the matching handler.\r\n */\r\nfunction makePagesDispatcherSource(\r\n routes: Array<{ adapterPath: string; srcRegex: string; paramNames: string[] }>,\r\n): string {\r\n const imports = routes\r\n .map((r, i) => `import __page_${i}__ from ${JSON.stringify(r.adapterPath)};`)\r\n .join('\\n');\r\n\r\n const routeEntries = routes\r\n .map((r, i) =>\r\n ` { regex: ${JSON.stringify(r.srcRegex)}, params: ${JSON.stringify(r.paramNames)}, handler: __page_${i}__ },`,\r\n )\r\n .join('\\n');\r\n\r\n return `\\\r\nimport type { IncomingMessage, ServerResponse } from 'http';\r\n${imports}\r\n\r\nconst ROUTES: Array<{\r\n regex: string;\r\n params: string[];\r\n handler: (req: IncomingMessage, res: ServerResponse) => Promise<void>;\r\n}> = [\r\n${routeEntries}\r\n];\r\n\r\nexport default async function handler(req: IncomingMessage, res: ServerResponse) {\r\n const url = new URL(req.url || '/', 'http://localhost');\r\n const pathname = url.pathname;\r\n\r\n for (const route of ROUTES) {\r\n const m = pathname.match(new RegExp(route.regex));\r\n if (!m) continue;\r\n\r\n // Inject dynamic params as query-string values so page handlers can read\r\n // them via new URL(req.url).searchParams \u2014 the same way they always have.\r\n route.params.forEach((name, i) => url.searchParams.set(name, m[i + 1]));\r\n req.url = pathname + (url.search || '');\r\n\r\n return route.handler(req, res);\r\n }\r\n\r\n res.statusCode = 404;\r\n res.setHeader('Content-Type', 'text/plain; charset=utf-8');\r\n res.end('Not Found');\r\n}\r\n`;\r\n}\r\n\r\n// \u2500\u2500\u2500 Build API function \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 vercelRoutes: VercelRoute[] = [];\r\n\r\nconst apiFiles = walkFiles(SERVER_DIR);\r\nif (apiFiles.length === 0) console.warn(`\u26A0 No server files found in ${SERVER_DIR}`);\r\n\r\nconst apiRoutes = apiFiles\r\n .map(relPath => ({ ...analyzeFile(relPath, 'api'), absPath: path.join(SERVER_DIR, relPath) }))\r\n .sort((a, b) => b.specificity - a.specificity);\r\n\r\nif (apiRoutes.length > 0) {\r\n const dispatcherSource = makeApiDispatcherSource(apiRoutes);\r\n const dispatcherPath = path.join(SERVER_DIR, `_api_dispatcher_${crypto.randomBytes(4).toString('hex')}.ts`);\r\n fs.writeFileSync(dispatcherPath, dispatcherSource);\r\n\r\n try {\r\n const result = await build({\r\n entryPoints: [dispatcherPath],\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 emitVercelFunction('api', result.outputFiles[0].text);\r\n console.log(` built API dispatcher \u2192 api.func (${apiRoutes.length} route(s))`);\r\n } finally {\r\n fs.unlinkSync(dispatcherPath);\r\n }\r\n\r\n // API routes are listed first \u2014 they win on any URL collision with pages.\r\n for (const { srcRegex } of apiRoutes) {\r\n vercelRoutes.push({ src: srcRegex, dest: '/api' });\r\n }\r\n}\r\n\r\n// \u2500\u2500\u2500 Build Pages function \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 serverPages = collectServerPages(PAGES_DIR);\r\n\r\nif (serverPages.length > 0) {\r\n // Pass 1 \u2014 bundle all client components to static files.\r\n const globalClientRegistry = collectGlobalClientRegistry(serverPages, PAGES_DIR);\r\n const prerenderedHtml = await bundleClientComponents(globalClientRegistry, PAGES_DIR, STATIC_DIR);\r\n const prerenderedHtmlRecord = Object.fromEntries(prerenderedHtml);\r\n\r\n // Pass 2 \u2014 write one temp adapter per page next to its source file (so\r\n // relative imports inside the component resolve correctly), then\r\n // bundle everything in one esbuild pass via the dispatcher.\r\n const tempAdapterPaths: string[] = [];\r\n\r\n for (const page of serverPages) {\r\n const { absPath } = page;\r\n const adapterDir = path.dirname(absPath);\r\n const adapterPath = path.join(adapterDir, `_page_adapter_${crypto.randomBytes(4).toString('hex')}.ts`);\r\n\r\n const layoutPaths = findPageLayouts(absPath, PAGES_DIR);\r\n const { registry, clientComponentNames } = buildPerPageRegistry(absPath, layoutPaths, PAGES_DIR);\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(\r\n adapterPath,\r\n makePageAdapterSource({\r\n pageImport: JSON.stringify('./' + path.basename(absPath)),\r\n layoutImports,\r\n clientComponentNames,\r\n allClientIds: [...registry.keys()],\r\n layoutArrayItems: layoutPaths.map((_, i) => `__layout_${i}__`).join(', '),\r\n prerenderedHtml: prerenderedHtmlRecord,\r\n }),\r\n );\r\n\r\n tempAdapterPaths.push(adapterPath);\r\n console.log(` prepared ${path.relative(PAGES_DIR, absPath)} \u2192 ${page.funcPath} [page]`);\r\n }\r\n\r\n // Write the dispatcher and let esbuild bundle all adapters in one pass.\r\n const dispatcherRoutes = serverPages.map((page, i) => ({\r\n adapterPath: tempAdapterPaths[i],\r\n srcRegex: page.srcRegex,\r\n paramNames: page.paramNames,\r\n }));\r\n\r\n const dispatcherPath = path.join(PAGES_DIR, `_pages_dispatcher_${crypto.randomBytes(4).toString('hex')}.ts`);\r\n fs.writeFileSync(dispatcherPath, makePagesDispatcherSource(dispatcherRoutes));\r\n\r\n try {\r\n const result = await build({\r\n entryPoints: [dispatcherPath],\r\n bundle: true,\r\n format: 'esm',\r\n platform: 'node',\r\n target: 'node20',\r\n jsx: 'automatic',\r\n external: [\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 define: { 'process.env.NODE_ENV': '\"production\"' },\r\n write: false,\r\n });\r\n emitVercelFunction('pages', result.outputFiles[0].text);\r\n console.log(` built Pages dispatcher \u2192 pages.func (${serverPages.length} page(s))`);\r\n } finally {\r\n fs.unlinkSync(dispatcherPath);\r\n for (const p of tempAdapterPaths) if (fs.existsSync(p)) fs.unlinkSync(p);\r\n }\r\n\r\n for (const { srcRegex } of serverPages) {\r\n vercelRoutes.push({ src: srcRegex, dest: '/pages' });\r\n }\r\n}\r\n\r\n// \u2500\u2500\u2500 Vercel config \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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\nfs.writeFileSync(\r\n path.join(OUTPUT_DIR, 'config.json'),\r\n JSON.stringify({ version: 3, routes: vercelRoutes }, null, 2),\r\n);\r\n\r\nfs.writeFileSync(\r\n path.resolve('vercel.json'),\r\n JSON.stringify({ runtime: 'nodejs20.x' }, null, 2),\r\n);\r\n\r\n// \u2500\u2500\u2500 Static assets \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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\nawait buildReactBundle(STATIC_DIR);\r\nawait buildNukeBundle(STATIC_DIR);\r\ncopyPublicFiles(PUBLIC_DIR, STATIC_DIR);\r\n\r\nconst fnCount = (apiRoutes.length > 0 ? 1 : 0) + (serverPages.length > 0 ? 1 : 0);\r\nconsole.log(`\\n\u2713 Vercel build complete \u2014 ${fnCount} function(s) \u2192 .vercel/output`);"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/bundler.js
CHANGED
|
@@ -3,6 +3,8 @@ import { fileURLToPath } from "url";
|
|
|
3
3
|
import { build } from "esbuild";
|
|
4
4
|
import { log } from "./logger.js";
|
|
5
5
|
import { getComponentById } from "./component-analyzer.js";
|
|
6
|
+
let reactBundlePromise = null;
|
|
7
|
+
let nukeBundlePromise = null;
|
|
6
8
|
async function bundleClientComponent(filePath) {
|
|
7
9
|
const result = await build({
|
|
8
10
|
entryPoints: [filePath],
|
|
@@ -30,11 +32,10 @@ async function serveClientComponentBundle(componentId, res) {
|
|
|
30
32
|
}
|
|
31
33
|
async function serveReactBundle(res) {
|
|
32
34
|
log.verbose("Bundling React runtime");
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
contents: `
|
|
35
|
+
if (!reactBundlePromise) {
|
|
36
|
+
reactBundlePromise = build({
|
|
37
|
+
stdin: {
|
|
38
|
+
contents: `
|
|
38
39
|
import React, {
|
|
39
40
|
useState, useEffect, useContext, useReducer, useCallback, useMemo,
|
|
40
41
|
useRef, useImperativeHandle, useLayoutEffect, useDebugValue,
|
|
@@ -55,39 +56,40 @@ async function serveReactBundle(res) {
|
|
|
55
56
|
};
|
|
56
57
|
export default React;
|
|
57
58
|
`,
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
});
|
|
59
|
+
loader: "ts"
|
|
60
|
+
},
|
|
61
|
+
bundle: true,
|
|
62
|
+
write: false,
|
|
63
|
+
treeShaking: true,
|
|
64
|
+
minify: false,
|
|
65
|
+
format: "esm",
|
|
66
|
+
jsx: "automatic",
|
|
67
|
+
alias: {
|
|
68
|
+
react: path.dirname(fileURLToPath(import.meta.resolve("react/package.json"))),
|
|
69
|
+
"react-dom": path.dirname(fileURLToPath(import.meta.resolve("react-dom/package.json")))
|
|
70
|
+
},
|
|
71
|
+
define: { "process.env.NODE_ENV": '"development"' }
|
|
72
|
+
}).then((r) => r.outputFiles[0].text);
|
|
73
|
+
}
|
|
74
74
|
res.setHeader("Content-Type", "application/javascript");
|
|
75
|
-
res.end(
|
|
75
|
+
res.end(await reactBundlePromise);
|
|
76
76
|
}
|
|
77
77
|
async function serveNukeBundle(res) {
|
|
78
78
|
log.verbose("Bundling nuke runtime");
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
79
|
+
if (!nukeBundlePromise) {
|
|
80
|
+
const dir = path.dirname(fileURLToPath(import.meta.url));
|
|
81
|
+
const entry = path.join(dir, `bundle.${dir.endsWith("dist") ? "js" : "ts"}`);
|
|
82
|
+
nukeBundlePromise = build({
|
|
83
|
+
entryPoints: [entry],
|
|
84
|
+
write: false,
|
|
85
|
+
format: "esm",
|
|
86
|
+
minify: true,
|
|
87
|
+
bundle: true,
|
|
88
|
+
external: ["react", "react-dom/client"]
|
|
89
|
+
}).then((r) => r.outputFiles[0].text);
|
|
90
|
+
}
|
|
89
91
|
res.setHeader("Content-Type", "application/javascript");
|
|
90
|
-
res.end(
|
|
92
|
+
res.end(await nukeBundlePromise);
|
|
91
93
|
}
|
|
92
94
|
export {
|
|
93
95
|
bundleClientComponent,
|
package/dist/bundler.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/bundler.ts"],
|
|
4
|
-
"sourcesContent": ["/**\r\n * bundler.ts \u2014 Dev-Mode On-Demand Bundler\r\n *\r\n * Handles the three internal JS routes served only in `nuke dev`:\r\n *\r\n * /__react.js \u2014 Full React + ReactDOM browser bundle.\r\n * All hooks, jsx-runtime, hydrateRoot, createRoot.\r\n * Served once and cached by the browser.\r\n *\r\n * /__n.js \u2014 NukeJS client runtime (bundle.ts compiled to ESM).\r\n * Provides initRuntime and SPA navigation.\r\n *\r\n * /__client-component/<id> \u2014 Individual \"use client\" component bundles.\r\n * Built on-demand the first time they're requested.\r\n * Re-built on every request in dev (no disk cache).\r\n *\r\n * In production (`nuke build`), equivalent bundles are written to dist/static/\r\n * by build-common.ts instead of being served dynamically.\r\n */\r\n\r\nimport path from 'path';\r\nimport { fileURLToPath } from 'url';\r\nimport { build } from 'esbuild';\r\nimport type { ServerResponse } from 'http';\r\nimport { log } from './logger';\r\nimport { getComponentById } from './component-analyzer';\r\n\r\n// \u2500\u2500\u2500 Client component bundle \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 a single \"use client\" file for the browser.\r\n *\r\n * React and react-dom/client are kept external so the importmap can resolve\r\n * them to the already-loaded /__react.js bundle (avoids shipping React twice).\r\n *\r\n * @param filePath Absolute path to the source file.\r\n * @returns ESM string ready to serve as application/javascript.\r\n */\r\nexport async function bundleClientComponent(filePath: string): Promise<string> {\r\n const result = await build({\r\n entryPoints: [filePath],\r\n bundle:
|
|
5
|
-
"mappings": "AAoBA,OAAO,UAAU;AACjB,SAAS,qBAAqB;AAC9B,SAAS,aAAa;AAEtB,SAAS,WAAW;AACpB,SAAS,wBAAwB;
|
|
4
|
+
"sourcesContent": ["/**\r\n * bundler.ts \u2014 Dev-Mode On-Demand Bundler\r\n *\r\n * Handles the three internal JS routes served only in `nuke dev`:\r\n *\r\n * /__react.js \u2014 Full React + ReactDOM browser bundle.\r\n * All hooks, jsx-runtime, hydrateRoot, createRoot.\r\n * Served once and cached by the browser.\r\n *\r\n * /__n.js \u2014 NukeJS client runtime (bundle.ts compiled to ESM).\r\n * Provides initRuntime and SPA navigation.\r\n *\r\n * /__client-component/<id> \u2014 Individual \"use client\" component bundles.\r\n * Built on-demand the first time they're requested.\r\n * Re-built on every request in dev (no disk cache).\r\n *\r\n * In production (`nuke build`), equivalent bundles are written to dist/static/\r\n * by build-common.ts instead of being served dynamically.\r\n */\r\n\r\nimport path from 'path';\r\nimport { fileURLToPath } from 'url';\r\nimport { build } from 'esbuild';\r\nimport type { ServerResponse } from 'http';\r\nimport { log } from './logger';\r\nimport { getComponentById } from './component-analyzer';\r\n\r\n// \u2500\u2500\u2500 Bundle caches \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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// Cache Promises (not just results) so concurrent requests share one build\r\n// instead of each spawning their own esbuild process.\r\nlet reactBundlePromise: Promise<string> | null = null;\r\nlet nukeBundlePromise: Promise<string> | null = null;\r\n\r\n// \u2500\u2500\u2500 Client component bundle \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 a single \"use client\" file for the browser.\r\n *\r\n * React and react-dom/client are kept external so the importmap can resolve\r\n * them to the already-loaded /__react.js bundle (avoids shipping React twice).\r\n *\r\n * @param filePath Absolute path to the source file.\r\n * @returns ESM string ready to serve as application/javascript.\r\n */\r\nexport async function bundleClientComponent(filePath: string): Promise<string> {\r\n const result = await build({\r\n entryPoints: [filePath],\r\n bundle: true,\r\n format: 'esm',\r\n platform: 'browser',\r\n write: false,\r\n jsx: 'automatic',\r\n // Keep React external \u2014 resolved by the importmap to /__react.js\r\n external: ['react', 'react-dom/client', 'react/jsx-runtime'],\r\n });\r\n return result.outputFiles[0].text;\r\n}\r\n\r\n/**\r\n * Looks up a client component by its content-hash ID (e.g. `cc_a1b2c3d4`),\r\n * bundles it on-demand, and writes the result to the HTTP response.\r\n *\r\n * The ID\u2192path mapping comes from the component analyzer cache, which is\r\n * populated during SSR as pages and their layouts are rendered.\r\n */\r\nexport async function serveClientComponentBundle(\r\n componentId: string,\r\n res: ServerResponse,\r\n): Promise<void> {\r\n const filePath = getComponentById(componentId);\r\n if (filePath) {\r\n log.verbose(`Bundling client component: ${componentId} (${path.basename(filePath)})`);\r\n res.setHeader('Content-Type', 'application/javascript');\r\n res.end(await bundleClientComponent(filePath));\r\n return;\r\n }\r\n\r\n // ID not found \u2014 either the page hasn't been visited yet (cache is empty)\r\n // or the ID is stale.\r\n log.error(`Client component not found: ${componentId}`);\r\n res.statusCode = 404;\r\n res.end('Client component not found');\r\n}\r\n\r\n// \u2500\u2500\u2500 React bundle \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 and serves the unified React browser bundle to /__react.js.\r\n *\r\n * Exports every public React API so client components can import from 'react'\r\n * or 'react-dom/client' and have them resolve via the importmap to this single\r\n * pre-loaded bundle \u2014 no duplicate copies of React in the browser.\r\n *\r\n * esbuild aliases point at the project's installed React version so the bundle\r\n * always matches what the server is actually running.\r\n */\r\nexport async function serveReactBundle(res: ServerResponse): Promise<void> {\r\n log.verbose('Bundling React runtime');\r\n\r\n if (!reactBundlePromise) {\r\n reactBundlePromise = build({\r\n stdin: {\r\n contents: `\r\n import 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\n import { jsx, jsxs } from 'react/jsx-runtime';\r\n import { hydrateRoot, createRoot } from 'react-dom/client';\r\n\r\n export {\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\n export default React;\r\n `,\r\n loader: 'ts',\r\n },\r\n bundle: true,\r\n write: false,\r\n treeShaking: true,\r\n minify: false,\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': '\"development\"' },\r\n }).then(r => r.outputFiles[0].text);\r\n }\r\n\r\n res.setHeader('Content-Type', 'application/javascript');\r\n res.end(await reactBundlePromise);\r\n}\r\n\r\n// \u2500\u2500\u2500 NukeJS runtime bundle \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 and serves the NukeJS client runtime to /__n.js.\r\n *\r\n * The entry point is bundle.ts (or bundle.js in production dist/).\r\n * React is kept external so it resolves via the importmap to /__react.js.\r\n *\r\n * Minified because this script is loaded on every page request.\r\n */\r\nexport async function serveNukeBundle(res: ServerResponse): Promise<void> {\r\n log.verbose('Bundling nuke runtime');\r\n\r\n if (!nukeBundlePromise) {\r\n const dir = path.dirname(fileURLToPath(import.meta.url));\r\n const entry = path.join(dir, `bundle.${dir.endsWith('dist') ? 'js' : 'ts'}`);\r\n nukeBundlePromise = build({\r\n entryPoints: [entry],\r\n write: false,\r\n format: 'esm',\r\n minify: true,\r\n bundle: true,\r\n external: ['react', 'react-dom/client'],\r\n }).then(r => r.outputFiles[0].text);\r\n }\r\n\r\n res.setHeader('Content-Type', 'application/javascript');\r\n res.end(await nukeBundlePromise);\r\n}"],
|
|
5
|
+
"mappings": "AAoBA,OAAO,UAAU;AACjB,SAAS,qBAAqB;AAC9B,SAAS,aAAa;AAEtB,SAAS,WAAW;AACpB,SAAS,wBAAwB;AAMjC,IAAI,qBAA6C;AACjD,IAAI,oBAA4C;AAahD,eAAsB,sBAAsB,UAAmC;AAC7E,QAAM,SAAS,MAAM,MAAM;AAAA,IACzB,aAAa,CAAC,QAAQ;AAAA,IACtB,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,OAAO;AAAA,IACP,KAAK;AAAA;AAAA,IAEL,UAAU,CAAC,SAAS,oBAAoB,mBAAmB;AAAA,EAC7D,CAAC;AACD,SAAO,OAAO,YAAY,CAAC,EAAE;AAC/B;AASA,eAAsB,2BACpB,aACA,KACe;AACf,QAAM,WAAW,iBAAiB,WAAW;AAC7C,MAAI,UAAU;AACZ,QAAI,QAAQ,8BAA8B,WAAW,KAAK,KAAK,SAAS,QAAQ,CAAC,GAAG;AACpF,QAAI,UAAU,gBAAgB,wBAAwB;AACtD,QAAI,IAAI,MAAM,sBAAsB,QAAQ,CAAC;AAC7C;AAAA,EACF;AAIA,MAAI,MAAM,+BAA+B,WAAW,EAAE;AACtD,MAAI,aAAa;AACjB,MAAI,IAAI,4BAA4B;AACtC;AAcA,eAAsB,iBAAiB,KAAoC;AACzE,MAAI,QAAQ,wBAAwB;AAEpC,MAAI,CAAC,oBAAoB;AACvB,yBAAqB,MAAM;AAAA,MACzB,OAAO;AAAA,QACL,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAqBV,QAAQ;AAAA,MACV;AAAA,MACA,QAAQ;AAAA,MACR,OAAO;AAAA,MACP,aAAa;AAAA,MACb,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,KAAK;AAAA,MACL,OAAO;AAAA,QACL,OAAO,KAAK,QAAQ,cAAc,YAAY,QAAQ,oBAAoB,CAAC,CAAC;AAAA,QAC5E,aAAa,KAAK,QAAQ,cAAc,YAAY,QAAQ,wBAAwB,CAAC,CAAC;AAAA,MACxF;AAAA,MACA,QAAQ,EAAE,wBAAwB,gBAAgB;AAAA,IACpD,CAAC,EAAE,KAAK,OAAK,EAAE,YAAY,CAAC,EAAE,IAAI;AAAA,EACpC;AAEA,MAAI,UAAU,gBAAgB,wBAAwB;AACtD,MAAI,IAAI,MAAM,kBAAkB;AAClC;AAYA,eAAsB,gBAAgB,KAAoC;AACxE,MAAI,QAAQ,uBAAuB;AAEnC,MAAI,CAAC,mBAAmB;AACtB,UAAM,MAAM,KAAK,QAAQ,cAAc,YAAY,GAAG,CAAC;AACvD,UAAM,QAAQ,KAAK,KAAK,KAAK,UAAU,IAAI,SAAS,MAAM,IAAI,OAAO,IAAI,EAAE;AAC3E,wBAAoB,MAAM;AAAA,MACxB,aAAa,CAAC,KAAK;AAAA,MACnB,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,UAAU,CAAC,SAAS,kBAAkB;AAAA,IACxC,CAAC,EAAE,KAAK,OAAK,EAAE,YAAY,CAAC,EAAE,IAAI;AAAA,EACpC;AAEA,MAAI,UAAU,gBAAgB,wBAAwB;AACtD,MAAI,IAAI,MAAM,iBAAiB;AACjC;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/middleware.js
CHANGED
|
@@ -4,6 +4,7 @@ import fs from "fs";
|
|
|
4
4
|
import { fileURLToPath } from "url";
|
|
5
5
|
import { hmrClients } from "./hmr.js";
|
|
6
6
|
import { getMimeType } from "./utils.js";
|
|
7
|
+
let hmrBundlePromise = null;
|
|
7
8
|
const PUBLIC_DIR = path.resolve("./app/public");
|
|
8
9
|
async function middleware(req, res) {
|
|
9
10
|
const rawUrl = req.url ?? "/";
|
|
@@ -25,23 +26,35 @@ async function middleware(req, res) {
|
|
|
25
26
|
}
|
|
26
27
|
}
|
|
27
28
|
if (rawUrl === "/__hmr.js") {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
29
|
+
if (!hmrBundlePromise) {
|
|
30
|
+
const dir = path.dirname(fileURLToPath(import.meta.url));
|
|
31
|
+
const entry = path.join(dir, `hmr-bundle.${dir.endsWith("dist") ? "js" : "ts"}`);
|
|
32
|
+
hmrBundlePromise = build({
|
|
33
|
+
entryPoints: [entry],
|
|
34
|
+
write: false,
|
|
35
|
+
format: "esm",
|
|
36
|
+
minify: true,
|
|
37
|
+
bundle: true,
|
|
38
|
+
external: ["react", "react-dom/client", "react/jsx-runtime"]
|
|
39
|
+
}).then((r) => r.outputFiles[0].text);
|
|
40
|
+
}
|
|
40
41
|
res.setHeader("Content-Type", "application/javascript");
|
|
41
|
-
res.end(
|
|
42
|
+
res.end(await hmrBundlePromise);
|
|
42
43
|
return;
|
|
43
44
|
}
|
|
44
45
|
if (rawUrl === "/__hmr") {
|
|
46
|
+
const MAX_SSE_PER_IP = 5;
|
|
47
|
+
const remoteAddr = req.socket?.remoteAddress;
|
|
48
|
+
if (remoteAddr) {
|
|
49
|
+
const fromSameIp = [...hmrClients].filter(
|
|
50
|
+
(c) => c.socket?.remoteAddress === remoteAddr
|
|
51
|
+
);
|
|
52
|
+
if (fromSameIp.length >= MAX_SSE_PER_IP) {
|
|
53
|
+
const oldest = fromSameIp[0];
|
|
54
|
+
oldest.destroy();
|
|
55
|
+
hmrClients.delete(oldest);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
45
58
|
res.setHeader("Content-Type", "text/event-stream");
|
|
46
59
|
res.setHeader("Cache-Control", "no-cache");
|
|
47
60
|
res.setHeader("Connection", "keep-alive");
|
package/dist/middleware.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/middleware.ts"],
|
|
4
|
-
"sourcesContent": ["/**\r\n * middleware.ts \u2014 Built-In NukeJS Middleware\r\n *\r\n * This is the internal middleware loaded before any user-defined middleware.\r\n * It handles three responsibilities:\r\n *\r\n * 1. Static public files (app/public/**)\r\n * Any file placed in app/public/ is served at its path relative to\r\n * that directory. E.g. app/public/favicon.ico \u2192 GET /favicon.ico.\r\n * The correct Content-Type is set automatically. Path traversal attempts\r\n * are rejected with 400.\r\n *\r\n * 2. HMR client script (/__hmr.js)\r\n * Builds and serves hmr-bundle.ts on demand. Injected into every dev\r\n * page as <script type=\"module\" src=\"/__hmr.js\">.\r\n *\r\n * 3. HMR SSE stream (/__hmr)\r\n * Long-lived Server-Sent Events connection used by the browser to receive\r\n * reload/replace/restart events when source files change.\r\n */\r\n\r\nimport { build } from 'esbuild';\r\nimport type { IncomingMessage, ServerResponse } from 'http';\r\nimport path from 'path';\r\nimport fs from 'fs';\r\nimport { fileURLToPath } from 'url';\r\nimport { hmrClients } from './hmr';\r\nimport { getMimeType } from './utils';\r\n\r\n// Absolute path to the static public directory.\r\n// Files here are served at their path relative to this directory.\r\nconst PUBLIC_DIR = path.resolve('./app/public');\r\n\r\nexport default async function middleware(\r\n req: IncomingMessage,\r\n res: ServerResponse,\r\n): Promise<void> {\r\n\r\n // \u2500\u2500 Static public files \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 // Checked first so /favicon.ico, /main.css, etc. are never accidentally\r\n // routed to the SSR or API layers.\r\n const rawUrl = req.url ?? '/';\r\n const pathname = rawUrl.split('?')[0]; // strip query string\r\n\r\n if (fs.existsSync(PUBLIC_DIR)) {\r\n // path.join handles the leading '/' in pathname naturally and normalises\r\n // any '..' segments, making it safe to use directly with a startsWith guard.\r\n // Using path.join (not path.resolve) ensures an absolute second argument\r\n // cannot silently escape PUBLIC_DIR the way path.resolve would allow.\r\n const candidate = path.join(PUBLIC_DIR, pathname);\r\n\r\n // Path traversal guard: the resolved path must be inside PUBLIC_DIR.\r\n // We normalise PUBLIC_DIR with a trailing separator so that a directory\r\n // whose name is a prefix of another cannot pass (e.g. /public2 vs /public).\r\n const publicBase = PUBLIC_DIR.endsWith(path.sep) ? PUBLIC_DIR : PUBLIC_DIR + path.sep;\r\n const safe = candidate.startsWith(publicBase) || candidate === PUBLIC_DIR;\r\n\r\n if (!safe) {\r\n res.statusCode = 400;\r\n res.end('Bad request');\r\n return;\r\n }\r\n\r\n // Serve the file if it exists at any depth inside PUBLIC_DIR.\r\n // Directories are intentionally skipped (no directory listings).\r\n if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {\r\n const ext = path.extname(candidate);\r\n res.setHeader('Content-Type', getMimeType(ext));\r\n res.end(fs.readFileSync(candidate));\r\n return;\r\n }\r\n }\r\n\r\n // \u2500\u2500 HMR client script \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 // Builds hmr-bundle.ts on demand so the browser always gets the latest version.\r\n if (rawUrl === '/__hmr.js') {\r\n const dir = path.dirname(fileURLToPath(import.meta.url));\r\n
|
|
5
|
-
"mappings": "AAqBA,SAAS,aAAa;AAEtB,OAAO,UAAU;AACjB,OAAO,QAAQ;AACf,SAAS,qBAAqB;AAC9B,SAAS,kBAAkB;AAC3B,SAAS,mBAAmB;
|
|
4
|
+
"sourcesContent": ["/**\r\n * middleware.ts \u2014 Built-In NukeJS Middleware\r\n *\r\n * This is the internal middleware loaded before any user-defined middleware.\r\n * It handles three responsibilities:\r\n *\r\n * 1. Static public files (app/public/**)\r\n * Any file placed in app/public/ is served at its path relative to\r\n * that directory. E.g. app/public/favicon.ico \u2192 GET /favicon.ico.\r\n * The correct Content-Type is set automatically. Path traversal attempts\r\n * are rejected with 400.\r\n *\r\n * 2. HMR client script (/__hmr.js)\r\n * Builds and serves hmr-bundle.ts on demand. Injected into every dev\r\n * page as <script type=\"module\" src=\"/__hmr.js\">.\r\n *\r\n * 3. HMR SSE stream (/__hmr)\r\n * Long-lived Server-Sent Events connection used by the browser to receive\r\n * reload/replace/restart events when source files change.\r\n */\r\n\r\nimport { build } from 'esbuild';\r\nimport type { IncomingMessage, ServerResponse } from 'http';\r\nimport path from 'path';\r\nimport fs from 'fs';\r\nimport { fileURLToPath } from 'url';\r\nimport { hmrClients } from './hmr';\r\nimport { getMimeType } from './utils';\r\n\r\n// Cache the compiled HMR bundle Promise so esbuild only runs once per server lifetime.\r\n// Caching the Promise (not just the result) prevents a race condition where multiple\r\n// concurrent requests all see null and each kick off their own build.\r\nlet hmrBundlePromise: Promise<string> | null = null;\r\n\r\n// Absolute path to the static public directory.\r\n// Files here are served at their path relative to this directory.\r\nconst PUBLIC_DIR = path.resolve('./app/public');\r\n\r\nexport default async function middleware(\r\n req: IncomingMessage,\r\n res: ServerResponse,\r\n): Promise<void> {\r\n\r\n // \u2500\u2500 Static public files \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 // Checked first so /favicon.ico, /main.css, etc. are never accidentally\r\n // routed to the SSR or API layers.\r\n const rawUrl = req.url ?? '/';\r\n const pathname = rawUrl.split('?')[0]; // strip query string\r\n\r\n if (fs.existsSync(PUBLIC_DIR)) {\r\n // path.join handles the leading '/' in pathname naturally and normalises\r\n // any '..' segments, making it safe to use directly with a startsWith guard.\r\n // Using path.join (not path.resolve) ensures an absolute second argument\r\n // cannot silently escape PUBLIC_DIR the way path.resolve would allow.\r\n const candidate = path.join(PUBLIC_DIR, pathname);\r\n\r\n // Path traversal guard: the resolved path must be inside PUBLIC_DIR.\r\n // We normalise PUBLIC_DIR with a trailing separator so that a directory\r\n // whose name is a prefix of another cannot pass (e.g. /public2 vs /public).\r\n const publicBase = PUBLIC_DIR.endsWith(path.sep) ? PUBLIC_DIR : PUBLIC_DIR + path.sep;\r\n const safe = candidate.startsWith(publicBase) || candidate === PUBLIC_DIR;\r\n\r\n if (!safe) {\r\n res.statusCode = 400;\r\n res.end('Bad request');\r\n return;\r\n }\r\n\r\n // Serve the file if it exists at any depth inside PUBLIC_DIR.\r\n // Directories are intentionally skipped (no directory listings).\r\n if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {\r\n const ext = path.extname(candidate);\r\n res.setHeader('Content-Type', getMimeType(ext));\r\n res.end(fs.readFileSync(candidate));\r\n return;\r\n }\r\n }\r\n\r\n // \u2500\u2500 HMR client script \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 // Builds hmr-bundle.ts on demand so the browser always gets the latest version.\r\n if (rawUrl === '/__hmr.js') {\r\n if (!hmrBundlePromise) {\r\n const dir = path.dirname(fileURLToPath(import.meta.url));\r\n const entry = path.join(dir, `hmr-bundle.${dir.endsWith('dist') ? 'js' : 'ts'}`);\r\n hmrBundlePromise = build({\r\n entryPoints: [entry],\r\n write: false,\r\n format: 'esm',\r\n minify: true,\r\n bundle: true,\r\n external: ['react', 'react-dom/client', 'react/jsx-runtime'],\r\n }).then(r => r.outputFiles[0].text);\r\n }\r\n\r\n res.setHeader('Content-Type', 'application/javascript');\r\n res.end(await hmrBundlePromise);\r\n return;\r\n }\r\n\r\n // \u2500\u2500 HMR SSE stream \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 // Long-lived connection tracked in hmrClients so hmr.ts can broadcast events\r\n // to all connected browsers when a file changes.\r\n if (rawUrl === '/__hmr') {\r\n // Each full-page reload opens a new SSE connection before the old one has\r\n // fully closed. After ~6 reloads the browser's connection limit is exhausted\r\n // and it can't make any new requests \u2014 the server appears to hang.\r\n // Fix: allow up to 5 SSE connections per IP; when a 6th arrives, destroy\r\n // the oldest one from that IP to free a slot before accepting the new one.\r\n const MAX_SSE_PER_IP = 5;\r\n const remoteAddr = req.socket?.remoteAddress;\r\n if (remoteAddr) {\r\n const fromSameIp = [...hmrClients].filter(\r\n c => (c as any).socket?.remoteAddress === remoteAddr\r\n );\r\n if (fromSameIp.length >= MAX_SSE_PER_IP) {\r\n // Drop the oldest (first in insertion order).\r\n const oldest = fromSameIp[0];\r\n oldest.destroy();\r\n hmrClients.delete(oldest);\r\n }\r\n }\r\n\r\n res.setHeader('Content-Type', 'text/event-stream');\r\n res.setHeader('Cache-Control', 'no-cache');\r\n res.setHeader('Connection', 'keep-alive');\r\n res.setHeader('Access-Control-Allow-Origin', '*');\r\n res.flushHeaders();\r\n\r\n res.write('data: {\"type\":\"connected\"}\\n\\n');\r\n\r\n hmrClients.add(res);\r\n req.on('close', () => hmrClients.delete(res));\r\n return;\r\n }\r\n}"],
|
|
5
|
+
"mappings": "AAqBA,SAAS,aAAa;AAEtB,OAAO,UAAU;AACjB,OAAO,QAAQ;AACf,SAAS,qBAAqB;AAC9B,SAAS,kBAAkB;AAC3B,SAAS,mBAAmB;AAK5B,IAAI,mBAA2C;AAI/C,MAAM,aAAa,KAAK,QAAQ,cAAc;AAE9C,eAAO,WACL,KACA,KACe;AAKf,QAAM,SAAS,IAAI,OAAO;AAC1B,QAAM,WAAW,OAAO,MAAM,GAAG,EAAE,CAAC;AAEpC,MAAI,GAAG,WAAW,UAAU,GAAG;AAK7B,UAAM,YAAY,KAAK,KAAK,YAAY,QAAQ;AAKhD,UAAM,aAAa,WAAW,SAAS,KAAK,GAAG,IAAI,aAAa,aAAa,KAAK;AAClF,UAAM,OAAO,UAAU,WAAW,UAAU,KAAK,cAAc;AAE/D,QAAI,CAAC,MAAM;AACT,UAAI,aAAa;AACjB,UAAI,IAAI,aAAa;AACrB;AAAA,IACF;AAIA,QAAI,GAAG,WAAW,SAAS,KAAK,GAAG,SAAS,SAAS,EAAE,OAAO,GAAG;AAC/D,YAAM,MAAM,KAAK,QAAQ,SAAS;AAClC,UAAI,UAAU,gBAAgB,YAAY,GAAG,CAAC;AAC9C,UAAI,IAAI,GAAG,aAAa,SAAS,CAAC;AAClC;AAAA,IACF;AAAA,EACF;AAIA,MAAI,WAAW,aAAa;AAC1B,QAAI,CAAC,kBAAkB;AACrB,YAAM,MAAQ,KAAK,QAAQ,cAAc,YAAY,GAAG,CAAC;AACzD,YAAM,QAAQ,KAAK,KAAK,KAAK,cAAc,IAAI,SAAS,MAAM,IAAI,OAAO,IAAI,EAAE;AAC/E,yBAAmB,MAAM;AAAA,QACvB,aAAa,CAAC,KAAK;AAAA,QACnB,OAAa;AAAA,QACb,QAAa;AAAA,QACb,QAAa;AAAA,QACb,QAAa;AAAA,QACb,UAAa,CAAC,SAAS,oBAAoB,mBAAmB;AAAA,MAChE,CAAC,EAAE,KAAK,OAAK,EAAE,YAAY,CAAC,EAAE,IAAI;AAAA,IACpC;AAEA,QAAI,UAAU,gBAAgB,wBAAwB;AACtD,QAAI,IAAI,MAAM,gBAAgB;AAC9B;AAAA,EACF;AAKA,MAAI,WAAW,UAAU;AAMvB,UAAM,iBAAiB;AACvB,UAAM,aAAa,IAAI,QAAQ;AAC/B,QAAI,YAAY;AACd,YAAM,aAAa,CAAC,GAAG,UAAU,EAAE;AAAA,QACjC,OAAM,EAAU,QAAQ,kBAAkB;AAAA,MAC5C;AACA,UAAI,WAAW,UAAU,gBAAgB;AAEvC,cAAM,SAAS,WAAW,CAAC;AAC3B,eAAO,QAAQ;AACf,mBAAW,OAAO,MAAM;AAAA,MAC1B;AAAA,IACF;AAEA,QAAI,UAAU,gBAAgB,mBAAmB;AACjD,QAAI,UAAU,iBAAiB,UAAU;AACzC,QAAI,UAAU,cAAc,YAAY;AACxC,QAAI,UAAU,+BAA+B,GAAG;AAChD,QAAI,aAAa;AAEjB,QAAI,MAAM,gCAAgC;AAE1C,eAAW,IAAI,GAAG;AAClB,QAAI,GAAG,SAAS,MAAM,WAAW,OAAO,GAAG,CAAC;AAC5C;AAAA,EACF;AACF;",
|
|
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.4",
|
|
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",
|
|
@@ -21,7 +21,6 @@
|
|
|
21
21
|
"build": "npx tsx src/builder.ts",
|
|
22
22
|
"dev": "cd playground && node ../bin/index.mjs dev",
|
|
23
23
|
"pack": "npm run build && npm pack",
|
|
24
|
-
"publish": "npm run build && npm publish",
|
|
25
24
|
"test": "npx jest"
|
|
26
25
|
},
|
|
27
26
|
"repository": {
|