nukejs 0.0.4 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,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\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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",
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 buildCombinedBundle,\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 buildCombinedBundle(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/bundle.d.ts CHANGED
@@ -23,14 +23,24 @@
23
23
  * - The handler fetches the target URL as HTML, diffs the #app container,
24
24
  * unmounts the old React roots, and re-hydrates the new ones.
25
25
  * - HMR navigations add ?__hmr=1 so the server skips client-SSR (faster).
26
+ *
27
+ * Head tag management:
28
+ * - The SSR renderer wraps every useHtml()-generated <meta>, <link>, <style>,
29
+ * and <script> tag in <!--n-head-->…<!--/n-head--> sentinel comments.
30
+ * - On each navigation the client diffs the live sentinel block against the
31
+ * incoming one by fingerprint, adding new tags and removing gone ones.
32
+ * Tags shared between pages (e.g. a layout stylesheet) are left untouched
33
+ * so there is no removal/re-insertion flash.
34
+ * - New tags are always inserted before <!--/n-head--> so they stay inside
35
+ * the tracked block and remain visible to the diff on subsequent navigations.
26
36
  */
27
37
  /**
28
38
  * Patches history.pushState and history.replaceState to fire a custom
29
39
  * 'locationchange' event on window. Also listens to 'popstate' for
30
40
  * back/forward navigation.
31
41
  *
32
- * This must be called after initRuntime sets up the navigation listener so
33
- * there's no race between the event firing and the listener being registered.
42
+ * Called after initRuntime sets up the navigation listener so there is no
43
+ * race between the event firing and the listener being registered.
34
44
  */
35
45
  export declare function setupLocationChangeMonitor(): void;
36
46
  type ClientDebugLevel = 'silent' | 'error' | 'info' | 'verbose';
package/dist/bundle.js CHANGED
@@ -77,8 +77,8 @@ async function loadModules(ids, log, bust = "") {
77
77
  return mods;
78
78
  }
79
79
  const activeRoots = [];
80
- async function mountNodes(mods, log, isNavigation) {
81
- const { hydrateRoot, createRoot } = await import("react-dom/client");
80
+ async function mountNodes(mods, log) {
81
+ const { hydrateRoot } = await import("react-dom/client");
82
82
  const React = await import("react");
83
83
  const nodes = document.querySelectorAll("[data-hydrate-id]");
84
84
  log.verbose("Found", nodes.length, "hydration point(s)");
@@ -98,8 +98,7 @@ async function mountNodes(mods, log, isNavigation) {
98
98
  }
99
99
  try {
100
100
  const element = React.default.createElement(Comp, await reconstructProps(rawProps, mods));
101
- const root = isNavigation ? createRoot(node) : hydrateRoot(node, element);
102
- if (isNavigation) root.render(element);
101
+ const root = hydrateRoot(node, element);
103
102
  activeRoots.push(root);
104
103
  log.verbose("\u2713 Mounted:", id);
105
104
  } catch (err) {
@@ -107,13 +106,54 @@ async function mountNodes(mods, log, isNavigation) {
107
106
  }
108
107
  }
109
108
  }
109
+ function headBlock(head) {
110
+ const nodes = [];
111
+ let closeComment = null;
112
+ let inside = false;
113
+ for (const child of Array.from(head.childNodes)) {
114
+ if (child.nodeType === Node.COMMENT_NODE) {
115
+ const text = child.data.trim();
116
+ if (text === "n-head") {
117
+ inside = true;
118
+ continue;
119
+ }
120
+ if (text === "/n-head") {
121
+ closeComment = child;
122
+ inside = false;
123
+ continue;
124
+ }
125
+ }
126
+ if (inside && child.nodeType === Node.ELEMENT_NODE)
127
+ nodes.push(child);
128
+ }
129
+ return { nodes, closeComment };
130
+ }
131
+ function fingerprint(el) {
132
+ return el.tagName + "|" + Array.from(el.attributes).sort((a, b) => a.name.localeCompare(b.name)).map((a) => `${a.name}=${a.value}`).join("&");
133
+ }
134
+ function syncHeadTags(doc) {
135
+ const live = headBlock(document.head);
136
+ const next = headBlock(doc.head);
137
+ const liveMap = /* @__PURE__ */ new Map();
138
+ for (const el of live.nodes) liveMap.set(fingerprint(el), el);
139
+ const nextMap = /* @__PURE__ */ new Map();
140
+ for (const el of next.nodes) nextMap.set(fingerprint(el), el);
141
+ let anchor = live.closeComment;
142
+ if (!anchor) {
143
+ document.head.appendChild(document.createComment("n-head"));
144
+ anchor = document.createComment("/n-head");
145
+ document.head.appendChild(anchor);
146
+ }
147
+ for (const [fp, el] of nextMap)
148
+ if (!liveMap.has(fp)) document.head.insertBefore(el, anchor);
149
+ for (const [fp, el] of liveMap)
150
+ if (!nextMap.has(fp)) el.remove();
151
+ }
110
152
  function syncAttrs(live, next) {
111
- for (const { name, value } of Array.from(next.attributes)) {
153
+ for (const { name, value } of Array.from(next.attributes))
112
154
  live.setAttribute(name, value);
113
- }
114
- for (const { name } of Array.from(live.attributes)) {
155
+ for (const { name } of Array.from(live.attributes))
115
156
  if (!next.hasAttribute(name)) live.removeAttribute(name);
116
- }
117
157
  }
118
158
  function setupNavigation(log) {
119
159
  window.addEventListener("locationchange", async ({ detail: { href, hmr } }) => {
@@ -129,19 +169,20 @@ function setupNavigation(log) {
129
169
  const newApp = doc.getElementById("app");
130
170
  const currApp = document.getElementById("app");
131
171
  if (!newApp || !currApp) return;
132
- activeRoots.splice(0).forEach((r) => r.unmount());
172
+ syncHeadTags(doc);
173
+ syncAttrs(document.documentElement, doc.documentElement);
174
+ syncAttrs(document.body, doc.body);
133
175
  currApp.innerHTML = newApp.innerHTML;
176
+ const newTitle = doc.querySelector("title");
177
+ if (newTitle) document.title = newTitle.textContent ?? "";
134
178
  const newDataEl = doc.getElementById("__n_data");
135
179
  const currDataEl = document.getElementById("__n_data");
136
180
  if (newDataEl && currDataEl) currDataEl.textContent = newDataEl.textContent;
137
- const newTitle = doc.querySelector("title");
138
- if (newTitle) document.title = newTitle.textContent ?? "";
139
- syncAttrs(document.documentElement, doc.documentElement);
140
- syncAttrs(document.body, doc.body);
181
+ activeRoots.splice(0).forEach((r) => r.unmount());
141
182
  const navData = JSON.parse(currDataEl?.textContent ?? "{}");
142
183
  log.info("\u{1F504} Route \u2192", href, "\u2014 mounting", navData.hydrateIds?.length ?? 0, "component(s)");
143
184
  const mods = await loadModules(navData.allIds ?? [], log, String(Date.now()));
144
- await mountNodes(mods, log, true);
185
+ await mountNodes(mods, log);
145
186
  window.scrollTo(0, 0);
146
187
  log.info("\u{1F389} Navigation complete:", href);
147
188
  } catch (err) {
@@ -155,7 +196,7 @@ async function initRuntime(data) {
155
196
  log.info("\u{1F680} Partial hydration:", data.hydrateIds.length, "root component(s)");
156
197
  setupNavigation(log);
157
198
  const mods = await loadModules(data.allIds, log);
158
- await mountNodes(mods, log, false);
199
+ await mountNodes(mods, log);
159
200
  log.info("\u{1F389} Done!");
160
201
  setupLocationChangeMonitor();
161
202
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/bundle.ts"],
4
- "sourcesContent": ["/**\r\n * bundle.ts \u2014 NukeJS Client Runtime\r\n *\r\n * This file is compiled by esbuild into /__n.js and served to every page.\r\n * It provides:\r\n *\r\n * initRuntime(data) \u2014 called once per page load to hydrate\r\n * \"use client\" components and wire up SPA nav\r\n * setupLocationChangeMonitor() \u2014 patches history.pushState/replaceState so\r\n * SPA navigation fires a 'locationchange' event\r\n *\r\n * Hydration model (partial hydration):\r\n * - The server renders the full page to HTML, wrapping each client component\r\n * in a <span data-hydrate-id=\"cc_\u2026\" data-hydrate-props=\"\u2026\"> marker.\r\n * - initRuntime loads the matching JS bundle for each marker and calls\r\n * hydrateRoot() on it, letting React take over just that subtree.\r\n * - Props serialized by the server may include nested React elements\r\n * (serialized as { __re: 'html'|'client', \u2026 }), which are reconstructed\r\n * back into React.createElement calls before mounting.\r\n *\r\n * SPA navigation:\r\n * - Link clicks / programmatic navigation dispatch a 'locationchange' event.\r\n * - The handler fetches the target URL as HTML, diffs the #app container,\r\n * unmounts the old React roots, and re-hydrates the new ones.\r\n * - HMR navigations add ?__hmr=1 so the server skips client-SSR (faster).\r\n */\r\n\r\n// \u2500\u2500\u2500 History patch \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 * Patches history.pushState and history.replaceState to fire a custom\r\n * 'locationchange' event on window. Also listens to 'popstate' for\r\n * back/forward navigation.\r\n *\r\n * This must be called after initRuntime sets up the navigation listener so\r\n * there's no race between the event firing and the listener being registered.\r\n */\r\nexport function setupLocationChangeMonitor(): void {\r\n const originalPushState = window.history.pushState.bind(window.history);\r\n const originalReplaceState = window.history.replaceState.bind(window.history);\r\n\r\n const dispatch = (href?: any) =>\r\n window.dispatchEvent(new CustomEvent('locationchange', { detail: { href } }));\r\n\r\n window.history.pushState = function (...args) {\r\n originalPushState(...args);\r\n dispatch(args[2]); // args[2] is the URL\r\n };\r\n\r\n window.history.replaceState = function (...args) {\r\n originalReplaceState(...args);\r\n dispatch(args[2]);\r\n };\r\n\r\n // Back/forward navigation via the browser's native UI.\r\n window.addEventListener('popstate', () => dispatch(window.location.pathname));\r\n}\r\n\r\n// \u2500\u2500\u2500 Logger \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 ClientDebugLevel = 'silent' | 'error' | 'info' | 'verbose';\r\n\r\n/**\r\n * Returns a thin logger whose methods are no-ops unless `level` allows them.\r\n * The server embeds the active debug level in the __n_data JSON blob so the\r\n * client respects the same setting as the server.\r\n */\r\nfunction makeLogger(level: ClientDebugLevel) {\r\n return {\r\n verbose: (...a: any[]) => { if (level === 'verbose') console.log(...a); },\r\n info: (...a: any[]) => { if (level === 'verbose' || level === 'info') console.log(...a); },\r\n warn: (...a: any[]) => { if (level === 'verbose' || level === 'info') console.warn(...a); },\r\n error: (...a: any[]) => { if (level !== 'silent') console.error(...a); },\r\n };\r\n}\r\n\r\n// \u2500\u2500\u2500 Serialized node 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\r\n\r\n/** The wire format for React elements embedded in hydration props. */\r\ntype SerializedNode =\r\n | null\r\n | undefined\r\n | string\r\n | number\r\n | boolean\r\n | SerializedNode[]\r\n | { __re: 'html'; tag: string; props: Record<string, any> } // native DOM element\r\n | { __re: 'client'; componentId: string; props: Record<string, any> } // client component\r\n | Record<string, any>; // plain object\r\n\r\ntype ModuleMap = Map<string, any>; // componentId \u2192 default export\r\n\r\n// \u2500\u2500\u2500 Prop reconstruction \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 turns the server's serialized node tree back into real React\r\n * elements so they can be passed as props to hydrated components.\r\n *\r\n * The server serializes JSX passed as props (e.g. `<Button icon={<Icon />}>`)\r\n * into a JSON-safe format. This function reverses that process.\r\n */\r\nasync function reconstructElement(node: SerializedNode, mods: ModuleMap): Promise<any> {\r\n if (node === null || node === undefined) return node;\r\n if (typeof node !== 'object') return node; // primitive \u2014 pass through\r\n\r\n if (Array.isArray(node)) {\r\n const items = await Promise.all(node.map(n => reconstructElement(n, mods)));\r\n // Add index-based keys to any React elements in the array so React doesn't\r\n // warn about \"Each child in a list should have a unique key prop\".\r\n const React = await import('react');\r\n return items.map((el, i) =>\r\n el && typeof el === 'object' && el.$$typeof\r\n ? React.default.cloneElement(el, { key: el.key ?? i })\r\n : el,\r\n );\r\n }\r\n\r\n // Client component \u2014 look up the loaded module by ID.\r\n if ((node as any).__re === 'client') {\r\n const n = node as { __re: 'client'; componentId: string; props: Record<string, any> };\r\n const Comp = mods.get(n.componentId);\r\n if (!Comp) return null;\r\n const React = await import('react');\r\n return React.default.createElement(Comp, await reconstructProps(n.props, mods));\r\n }\r\n\r\n // Native HTML element (e.g. <div>, <span>).\r\n if ((node as any).__re === 'html') {\r\n const n = node as { __re: 'html'; tag: string; props: Record<string, any> };\r\n const React = await import('react');\r\n return React.default.createElement(n.tag, await reconstructProps(n.props, mods));\r\n }\r\n\r\n // Plain object \u2014 reconstruct each value.\r\n return node;\r\n}\r\n\r\n/** Reconstructs every value in a props object, handling nested serialized nodes. */\r\nasync function reconstructProps(\r\n props: Record<string, any> | null | undefined,\r\n mods: ModuleMap,\r\n): Promise<Record<string, any>> {\r\n if (!props || typeof props !== 'object' || Array.isArray(props))\r\n return reconstructElement(props as any, mods);\r\n\r\n const out: Record<string, any> = {};\r\n for (const [k, v] of Object.entries(props))\r\n out[k] = await reconstructElement(v, mods);\r\n return out;\r\n}\r\n\r\n// \u2500\u2500\u2500 Module loading \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 * Dynamically imports each client component bundle from /__client-component/.\r\n * All fetches are issued in parallel; failures are logged but do not abort\r\n * the rest of the hydration pass.\r\n *\r\n * @param bust Optional cache-busting suffix appended as `?t=<bust>`.\r\n * Used during HMR navigation to bypass the module cache.\r\n */\r\nasync function loadModules(\r\n ids: string[],\r\n log: ReturnType<typeof makeLogger>,\r\n bust = '',\r\n): Promise<ModuleMap> {\r\n const mods: ModuleMap = new Map();\r\n await Promise.all(\r\n ids.map(async (id) => {\r\n try {\r\n const url = `/__client-component/${id}.js` + (bust ? `?t=${bust}` : '');\r\n const m = await import(url);\r\n mods.set(id, m.default);\r\n log.verbose('\u2713 Loaded:', id);\r\n } catch (err) {\r\n log.error('\u2717 Load failed:', id, err);\r\n }\r\n }),\r\n );\r\n return mods;\r\n}\r\n\r\n// \u2500\u2500\u2500 Root mounting \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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/** All active React roots \u2014 tracked so they can be unmounted before navigation. */\r\ntype ReactRoot = { unmount(): void };\r\nconst activeRoots: ReactRoot[] = [];\r\n\r\n/**\r\n * Finds every `[data-hydrate-id]` span in the document and either:\r\n * - hydrateRoot() \u2014 on initial page load (server HTML already present)\r\n * - createRoot() \u2014 after SPA navigation (innerHTML was set by us)\r\n *\r\n * Nested markers (a client component inside another client component) are\r\n * skipped here because the parent's React tree will handle its children.\r\n */\r\nasync function mountNodes(\r\n mods: ModuleMap,\r\n log: ReturnType<typeof makeLogger>,\r\n isNavigation: boolean,\r\n): Promise<void> {\r\n const { hydrateRoot, createRoot } = await import('react-dom/client');\r\n const React = await import('react');\r\n\r\n const nodes = document.querySelectorAll<HTMLElement>('[data-hydrate-id]');\r\n log.verbose('Found', nodes.length, 'hydration point(s)');\r\n\r\n for (const node of nodes) {\r\n // Skip nested markers \u2014 the outer component owns its children.\r\n if (node.parentElement?.closest('[data-hydrate-id]')) continue;\r\n\r\n const id = node.getAttribute('data-hydrate-id')!;\r\n const Comp = mods.get(id);\r\n if (!Comp) { log.warn('No module for', id); continue; }\r\n\r\n // Deserialize props from the data attribute (JSON set by the server).\r\n let rawProps: Record<string, any> = {};\r\n try {\r\n rawProps = JSON.parse(node.getAttribute('data-hydrate-props') || '{}');\r\n } catch (e) {\r\n log.error('Props parse error for', id, e);\r\n }\r\n\r\n try {\r\n const element = React.default.createElement(Comp, await reconstructProps(rawProps, mods));\r\n\r\n // hydrateRoot reconciles React's virtual DOM against the server-rendered\r\n // HTML without fully re-rendering. createRoot replaces the content\r\n // entirely \u2014 safe after navigation because we set innerHTML ourselves.\r\n const root = isNavigation ? createRoot(node) : hydrateRoot(node, element);\r\n if (isNavigation) (root as any).render(element);\r\n activeRoots.push(root);\r\n log.verbose('\u2713 Mounted:', id);\r\n } catch (err) {\r\n log.error('\u2717 Mount failed:', id, err);\r\n }\r\n }\r\n}\r\n\r\n// \u2500\u2500\u2500 SPA navigation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 * Listens for 'locationchange' events (fired by setupLocationChangeMonitor\r\n * or by Link clicks) and performs a soft navigation:\r\n *\r\n * 1. Fetch the target URL as HTML (adds ?__hmr=1 during HMR updates so the\r\n * server skips client-side SSR for a faster response).\r\n * 2. Parse the response with DOMParser.\r\n * 3. Replace #app innerHTML and __n_data.\r\n * 4. Unmount old React roots, then re-hydrate new ones.\r\n * 5. Scroll to top.\r\n *\r\n * Falls back to a full page reload if anything goes wrong.\r\n */\r\n/**\r\n * Syncs attributes from a parsed element onto the live document element.\r\n * Adds/updates attributes present in `next`, and removes any that were set\r\n * on `live` but are absent in `next` (so stale bodyAttrs/htmlAttrs are cleared).\r\n */\r\nfunction syncAttrs(live: Element, next: Element): void {\r\n // Apply / update attributes from the new document.\r\n for (const { name, value } of Array.from(next.attributes)) {\r\n live.setAttribute(name, value);\r\n }\r\n // Remove attributes that no longer exist in the new document.\r\n for (const { name } of Array.from(live.attributes)) {\r\n if (!next.hasAttribute(name)) live.removeAttribute(name);\r\n }\r\n}\r\n\r\nfunction setupNavigation(log: ReturnType<typeof makeLogger>): void {\r\n window.addEventListener('locationchange', async ({ detail: { href, hmr } }: any) => {\r\n try {\r\n // Append ?__hmr=1 for HMR-triggered reloads so SSR skips the slower\r\n // client-component renderToString path.\r\n const fetchUrl = hmr\r\n ? href + (href.includes('?') ? '&' : '?') + '__hmr=1'\r\n : href;\r\n\r\n const response = await fetch(fetchUrl, { headers: { Accept: 'text/html' } });\r\n if (!response.ok) {\r\n log.error('Navigation fetch failed:', response.status);\r\n return;\r\n }\r\n\r\n const parser = new DOMParser();\r\n const doc = parser.parseFromString(await response.text(), 'text/html');\r\n const newApp = doc.getElementById('app');\r\n const currApp = document.getElementById('app');\r\n if (!newApp || !currApp) return;\r\n\r\n // Tear down existing React trees before mutating the DOM \u2014 avoids React\r\n // warnings about unmounting from a detached node.\r\n activeRoots.splice(0).forEach(r => r.unmount());\r\n\r\n // Swap content in-place (avoids a full document.write / navigation).\r\n currApp.innerHTML = newApp.innerHTML;\r\n\r\n // Update the runtime data blob so subsequent navigations use the new page's\r\n // client component IDs.\r\n const newDataEl = doc.getElementById('__n_data');\r\n const currDataEl = document.getElementById('__n_data');\r\n if (newDataEl && currDataEl) currDataEl.textContent = newDataEl.textContent;\r\n\r\n // Update <title>.\r\n const newTitle = doc.querySelector('title');\r\n if (newTitle) document.title = newTitle.textContent ?? '';\r\n\r\n // Sync <html> attributes (e.g. lang, class, style from useHtml({ htmlAttrs })).\r\n syncAttrs(document.documentElement, doc.documentElement);\r\n\r\n // Sync <body> attributes (e.g. style, class from useHtml({ bodyAttrs })).\r\n syncAttrs(document.body, doc.body);\r\n\r\n const navData = JSON.parse(currDataEl?.textContent ?? '{}') as RuntimeData;\r\n log.info('\uD83D\uDD04 Route \u2192', href, '\u2014 mounting', navData.hydrateIds?.length ?? 0, 'component(s)');\r\n\r\n // Load bundles with a cache-buster timestamp so stale modules are evicted.\r\n const mods = await loadModules(navData.allIds ?? [], log, String(Date.now()));\r\n await mountNodes(mods, log, true);\r\n\r\n window.scrollTo(0, 0);\r\n log.info('\uD83C\uDF89 Navigation complete:', href);\r\n } catch (err) {\r\n log.error('Navigation error, falling back to full reload:', err);\r\n window.location.href = href;\r\n }\r\n });\r\n}\r\n\r\n// \u2500\u2500\u2500 Public API \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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/** Shape of the JSON blob embedded as #__n_data in every SSR page. */\r\nexport interface RuntimeData {\r\n /** IDs of client components actually rendered on this page (subset of allIds). */\r\n hydrateIds: string[];\r\n /** All client component IDs reachable from this page, including layouts.\r\n * Pre-loaded so SPA navigations to related pages feel instant. */\r\n allIds: string[];\r\n url: string;\r\n params: Record<string, any>;\r\n debug: ClientDebugLevel;\r\n}\r\n\r\n/**\r\n * Bootstraps the NukeJS client runtime.\r\n *\r\n * Called once per page load from the inline <script type=\"module\"> injected\r\n * by the SSR renderer:\r\n *\r\n * ```js\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 * ```\r\n *\r\n * Order of operations:\r\n * 1. Create the logger at the configured debug level.\r\n * 2. Wire up SPA navigation listener.\r\n * 3. Load all client component bundles in parallel.\r\n * 4. Hydrate every [data-hydrate-id] node.\r\n * 5. Patch history.pushState/replaceState so Link clicks trigger navigation.\r\n */\r\nexport async function initRuntime(data: RuntimeData): Promise<void> {\r\n const log = makeLogger(data.debug ?? 'silent');\r\n\r\n log.info('\uD83D\uDE80 Partial hydration:', data.hydrateIds.length, 'root component(s)');\r\n\r\n // Set up navigation first so any 'locationchange' fired during hydration\r\n // is captured.\r\n setupNavigation(log);\r\n\r\n // Load all component bundles (not just hydrateIds) so SPA navigations can\r\n // mount components for other pages without an extra network round-trip.\r\n const mods = await loadModules(data.allIds, log);\r\n await mountNodes(mods, log, false);\r\n\r\n log.info('\uD83C\uDF89 Done!');\r\n\r\n // Patch history last so pushState calls during hydration (e.g. redirect\r\n // side-effects) don't trigger a navigation before roots are ready.\r\n setupLocationChangeMonitor();\r\n}"],
5
- "mappings": "AAqCO,SAAS,6BAAmC;AACjD,QAAM,oBAAoB,OAAO,QAAQ,UAAU,KAAK,OAAO,OAAO;AACtE,QAAM,uBAAuB,OAAO,QAAQ,aAAa,KAAK,OAAO,OAAO;AAE5E,QAAM,WAAW,CAAC,SAChB,OAAO,cAAc,IAAI,YAAY,kBAAkB,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;AAE9E,SAAO,QAAQ,YAAY,YAAa,MAAM;AAC5C,sBAAkB,GAAG,IAAI;AACzB,aAAS,KAAK,CAAC,CAAC;AAAA,EAClB;AAEA,SAAO,QAAQ,eAAe,YAAa,MAAM;AAC/C,yBAAqB,GAAG,IAAI;AAC5B,aAAS,KAAK,CAAC,CAAC;AAAA,EAClB;AAGA,SAAO,iBAAiB,YAAY,MAAM,SAAS,OAAO,SAAS,QAAQ,CAAC;AAC9E;AAWA,SAAS,WAAW,OAAyB;AAC3C,SAAO;AAAA,IACL,SAAS,IAAI,MAAa;AAAE,UAAI,UAAU,UAAW,SAAQ,IAAI,GAAG,CAAC;AAAA,IAAG;AAAA,IACxE,MAAM,IAAI,MAAa;AAAE,UAAI,UAAU,aAAa,UAAU,OAAQ,SAAQ,IAAI,GAAG,CAAC;AAAA,IAAG;AAAA,IACzF,MAAM,IAAI,MAAa;AAAE,UAAI,UAAU,aAAa,UAAU,OAAQ,SAAQ,KAAK,GAAG,CAAC;AAAA,IAAG;AAAA,IAC1F,OAAO,IAAI,MAAa;AAAE,UAAI,UAAU,SAAU,SAAQ,MAAM,GAAG,CAAC;AAAA,IAAG;AAAA,EACzE;AACF;AA2BA,eAAe,mBAAmB,MAAsB,MAA+B;AACrF,MAAI,SAAS,QAAQ,SAAS,OAAW,QAAO;AAChD,MAAI,OAAO,SAAS,SAAU,QAAO;AAErC,MAAI,MAAM,QAAQ,IAAI,GAAG;AACvB,UAAM,QAAQ,MAAM,QAAQ,IAAI,KAAK,IAAI,OAAK,mBAAmB,GAAG,IAAI,CAAC,CAAC;AAG1E,UAAM,QAAQ,MAAM,OAAO,OAAO;AAClC,WAAO,MAAM;AAAA,MAAI,CAAC,IAAI,MACpB,MAAM,OAAO,OAAO,YAAY,GAAG,WAC/B,MAAM,QAAQ,aAAa,IAAI,EAAE,KAAK,GAAG,OAAO,EAAE,CAAC,IACnD;AAAA,IACN;AAAA,EACF;AAGA,MAAK,KAAa,SAAS,UAAU;AACnC,UAAM,IAAI;AACV,UAAM,OAAO,KAAK,IAAI,EAAE,WAAW;AACnC,QAAI,CAAC,KAAM,QAAO;AAClB,UAAM,QAAQ,MAAM,OAAO,OAAO;AAClC,WAAO,MAAM,QAAQ,cAAc,MAAM,MAAM,iBAAiB,EAAE,OAAO,IAAI,CAAC;AAAA,EAChF;AAGA,MAAK,KAAa,SAAS,QAAQ;AACjC,UAAM,IAAI;AACV,UAAM,QAAQ,MAAM,OAAO,OAAO;AAClC,WAAO,MAAM,QAAQ,cAAc,EAAE,KAAK,MAAM,iBAAiB,EAAE,OAAO,IAAI,CAAC;AAAA,EACjF;AAGA,SAAO;AACT;AAGA,eAAe,iBACb,OACA,MAC8B;AAC9B,MAAI,CAAC,SAAS,OAAO,UAAU,YAAY,MAAM,QAAQ,KAAK;AAC5D,WAAO,mBAAmB,OAAc,IAAI;AAE9C,QAAM,MAA2B,CAAC;AAClC,aAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,KAAK;AACvC,QAAI,CAAC,IAAI,MAAM,mBAAmB,GAAG,IAAI;AAC3C,SAAO;AACT;AAYA,eAAe,YACb,KACA,KACA,OAAO,IACa;AACpB,QAAM,OAAkB,oBAAI,IAAI;AAChC,QAAM,QAAQ;AAAA,IACZ,IAAI,IAAI,OAAO,OAAO;AACpB,UAAI;AACF,cAAM,MAAM,uBAAuB,EAAE,SAAS,OAAO,MAAM,IAAI,KAAK;AACpE,cAAM,IAAI,MAAM,OAAO;AACvB,aAAK,IAAI,IAAI,EAAE,OAAO;AACtB,YAAI,QAAQ,kBAAa,EAAE;AAAA,MAC7B,SAAS,KAAK;AACZ,YAAI,MAAM,uBAAkB,IAAI,GAAG;AAAA,MACrC;AAAA,IACF,CAAC;AAAA,EACH;AACA,SAAO;AACT;AAMA,MAAM,cAA2B,CAAC;AAUlC,eAAe,WACb,MACA,KACA,cACe;AACf,QAAM,EAAE,aAAa,WAAW,IAAI,MAAM,OAAO,kBAAkB;AACnE,QAAM,QAAQ,MAAM,OAAO,OAAO;AAElC,QAAM,QAAQ,SAAS,iBAA8B,mBAAmB;AACxE,MAAI,QAAQ,SAAS,MAAM,QAAQ,oBAAoB;AAEvD,aAAW,QAAQ,OAAO;AAExB,QAAI,KAAK,eAAe,QAAQ,mBAAmB,EAAG;AAEtD,UAAM,KAAK,KAAK,aAAa,iBAAiB;AAC9C,UAAM,OAAO,KAAK,IAAI,EAAE;AACxB,QAAI,CAAC,MAAM;AAAE,UAAI,KAAK,iBAAiB,EAAE;AAAG;AAAA,IAAU;AAGtD,QAAI,WAAgC,CAAC;AACrC,QAAI;AACF,iBAAW,KAAK,MAAM,KAAK,aAAa,oBAAoB,KAAK,IAAI;AAAA,IACvE,SAAS,GAAG;AACV,UAAI,MAAM,yBAAyB,IAAI,CAAC;AAAA,IAC1C;AAEA,QAAI;AACF,YAAM,UAAU,MAAM,QAAQ,cAAc,MAAM,MAAM,iBAAiB,UAAU,IAAI,CAAC;AAKxF,YAAM,OAAO,eAAe,WAAW,IAAI,IAAI,YAAY,MAAM,OAAO;AACxE,UAAI,aAAc,CAAC,KAAa,OAAO,OAAO;AAC9C,kBAAY,KAAK,IAAI;AACrB,UAAI,QAAQ,mBAAc,EAAE;AAAA,IAC9B,SAAS,KAAK;AACZ,UAAI,MAAM,wBAAmB,IAAI,GAAG;AAAA,IACtC;AAAA,EACF;AACF;AAsBA,SAAS,UAAU,MAAe,MAAqB;AAErD,aAAW,EAAE,MAAM,MAAM,KAAK,MAAM,KAAK,KAAK,UAAU,GAAG;AACzD,SAAK,aAAa,MAAM,KAAK;AAAA,EAC/B;AAEA,aAAW,EAAE,KAAK,KAAK,MAAM,KAAK,KAAK,UAAU,GAAG;AAClD,QAAI,CAAC,KAAK,aAAa,IAAI,EAAG,MAAK,gBAAgB,IAAI;AAAA,EACzD;AACF;AAEA,SAAS,gBAAgB,KAA0C;AACjE,SAAO,iBAAiB,kBAAkB,OAAO,EAAE,QAAQ,EAAE,MAAM,IAAI,EAAE,MAAW;AAClF,QAAI;AAGF,YAAM,WAAW,MACb,QAAQ,KAAK,SAAS,GAAG,IAAI,MAAM,OAAO,YAC1C;AAEJ,YAAM,WAAW,MAAM,MAAM,UAAU,EAAE,SAAS,EAAE,QAAQ,YAAY,EAAE,CAAC;AAC3E,UAAI,CAAC,SAAS,IAAI;AAChB,YAAI,MAAM,4BAA4B,SAAS,MAAM;AACrD;AAAA,MACF;AAEA,YAAM,SAAS,IAAI,UAAU;AAC7B,YAAM,MAAM,OAAO,gBAAgB,MAAM,SAAS,KAAK,GAAG,WAAW;AACrE,YAAM,SAAS,IAAI,eAAe,KAAK;AACvC,YAAM,UAAU,SAAS,eAAe,KAAK;AAC7C,UAAI,CAAC,UAAU,CAAC,QAAS;AAIzB,kBAAY,OAAO,CAAC,EAAE,QAAQ,OAAK,EAAE,QAAQ,CAAC;AAG9C,cAAQ,YAAY,OAAO;AAI3B,YAAM,YAAY,IAAI,eAAe,UAAU;AAC/C,YAAM,aAAa,SAAS,eAAe,UAAU;AACrD,UAAI,aAAa,WAAY,YAAW,cAAc,UAAU;AAGhE,YAAM,WAAW,IAAI,cAAc,OAAO;AAC1C,UAAI,SAAU,UAAS,QAAQ,SAAS,eAAe;AAGvD,gBAAU,SAAS,iBAAiB,IAAI,eAAe;AAGvD,gBAAU,SAAS,MAAM,IAAI,IAAI;AAEjC,YAAM,UAAU,KAAK,MAAM,YAAY,eAAe,IAAI;AAC1D,UAAI,KAAK,0BAAc,MAAM,mBAAc,QAAQ,YAAY,UAAU,GAAG,cAAc;AAG1F,YAAM,OAAO,MAAM,YAAY,QAAQ,UAAU,CAAC,GAAG,KAAK,OAAO,KAAK,IAAI,CAAC,CAAC;AAC5E,YAAM,WAAW,MAAM,KAAK,IAAI;AAEhC,aAAO,SAAS,GAAG,CAAC;AACpB,UAAI,KAAK,kCAA2B,IAAI;AAAA,IAC1C,SAAS,KAAK;AACZ,UAAI,MAAM,kDAAkD,GAAG;AAC/D,aAAO,SAAS,OAAO;AAAA,IACzB;AAAA,EACF,CAAC;AACH;AAmCA,eAAsB,YAAY,MAAkC;AAClE,QAAM,MAAM,WAAW,KAAK,SAAS,QAAQ;AAE7C,MAAI,KAAK,gCAAyB,KAAK,WAAW,QAAQ,mBAAmB;AAI7E,kBAAgB,GAAG;AAInB,QAAM,OAAO,MAAM,YAAY,KAAK,QAAQ,GAAG;AAC/C,QAAM,WAAW,MAAM,KAAK,KAAK;AAEjC,MAAI,KAAK,iBAAU;AAInB,6BAA2B;AAC7B;",
4
+ "sourcesContent": ["/**\r\n * bundle.ts \u2014 NukeJS Client Runtime\r\n *\r\n * This file is compiled by esbuild into /__n.js and served to every page.\r\n * It provides:\r\n *\r\n * initRuntime(data) \u2014 called once per page load to hydrate\r\n * \"use client\" components and wire up SPA nav\r\n * setupLocationChangeMonitor() \u2014 patches history.pushState/replaceState so\r\n * SPA navigation fires a 'locationchange' event\r\n *\r\n * Hydration model (partial hydration):\r\n * - The server renders the full page to HTML, wrapping each client component\r\n * in a <span data-hydrate-id=\"cc_\u2026\" data-hydrate-props=\"\u2026\"> marker.\r\n * - initRuntime loads the matching JS bundle for each marker and calls\r\n * hydrateRoot() on it, letting React take over just that subtree.\r\n * - Props serialized by the server may include nested React elements\r\n * (serialized as { __re: 'html'|'client', \u2026 }), which are reconstructed\r\n * back into React.createElement calls before mounting.\r\n *\r\n * SPA navigation:\r\n * - Link clicks / programmatic navigation dispatch a 'locationchange' event.\r\n * - The handler fetches the target URL as HTML, diffs the #app container,\r\n * unmounts the old React roots, and re-hydrates the new ones.\r\n * - HMR navigations add ?__hmr=1 so the server skips client-SSR (faster).\r\n *\r\n * Head tag management:\r\n * - The SSR renderer wraps every useHtml()-generated <meta>, <link>, <style>,\r\n * and <script> tag in <!--n-head-->\u2026<!--/n-head--> sentinel comments.\r\n * - On each navigation the client diffs the live sentinel block against the\r\n * incoming one by fingerprint, adding new tags and removing gone ones.\r\n * Tags shared between pages (e.g. a layout stylesheet) are left untouched\r\n * so there is no removal/re-insertion flash.\r\n * - New tags are always inserted before <!--/n-head--> so they stay inside\r\n * the tracked block and remain visible to the diff on subsequent navigations.\r\n */\r\n\r\n// \u2500\u2500\u2500 History patch \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 * Patches history.pushState and history.replaceState to fire a custom\r\n * 'locationchange' event on window. Also listens to 'popstate' for\r\n * back/forward navigation.\r\n *\r\n * Called after initRuntime sets up the navigation listener so there is no\r\n * race between the event firing and the listener being registered.\r\n */\r\nexport function setupLocationChangeMonitor(): void {\r\n const originalPushState = window.history.pushState.bind(window.history);\r\n const originalReplaceState = window.history.replaceState.bind(window.history);\r\n\r\n const dispatch = (href?: any) =>\r\n window.dispatchEvent(new CustomEvent('locationchange', { detail: { href } }));\r\n\r\n window.history.pushState = function (...args) {\r\n originalPushState(...args);\r\n dispatch(args[2]); // args[2] is the URL\r\n };\r\n\r\n window.history.replaceState = function (...args) {\r\n originalReplaceState(...args);\r\n dispatch(args[2]);\r\n };\r\n\r\n // Back/forward navigation via the browser's native UI.\r\n window.addEventListener('popstate', () => dispatch(window.location.pathname));\r\n}\r\n\r\n// \u2500\u2500\u2500 Logger \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 ClientDebugLevel = 'silent' | 'error' | 'info' | 'verbose';\r\n\r\n/**\r\n * Returns a thin logger whose methods are no-ops unless `level` allows them.\r\n * The server embeds the active debug level in the __n_data JSON blob so the\r\n * client respects the same setting as the server.\r\n */\r\nfunction makeLogger(level: ClientDebugLevel) {\r\n return {\r\n verbose: (...a: any[]) => { if (level === 'verbose') console.log(...a); },\r\n info: (...a: any[]) => { if (level === 'verbose' || level === 'info') console.log(...a); },\r\n warn: (...a: any[]) => { if (level === 'verbose' || level === 'info') console.warn(...a); },\r\n error: (...a: any[]) => { if (level !== 'silent') console.error(...a); },\r\n };\r\n}\r\n\r\n// \u2500\u2500\u2500 Serialized node 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\r\n\r\n/** The wire format for React elements embedded in hydration props. */\r\ntype SerializedNode =\r\n | null\r\n | undefined\r\n | string\r\n | number\r\n | boolean\r\n | SerializedNode[]\r\n | { __re: 'html'; tag: string; props: Record<string, any> }\r\n | { __re: 'client'; componentId: string; props: Record<string, any> }\r\n | Record<string, any>;\r\n\r\ntype ModuleMap = Map<string, any>; // componentId \u2192 default export\r\n\r\n// \u2500\u2500\u2500 Prop reconstruction \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 turns the server's serialized node tree back into real React\r\n * elements so they can be passed as props to hydrated components.\r\n *\r\n * The server serializes JSX passed as props (e.g. `<Button icon={<Icon />}>`)\r\n * into a JSON-safe format. This function reverses that process.\r\n */\r\nasync function reconstructElement(node: SerializedNode, mods: ModuleMap): Promise<any> {\r\n if (node === null || node === undefined) return node;\r\n if (typeof node !== 'object') return node; // primitive \u2014 pass through\r\n\r\n if (Array.isArray(node)) {\r\n const items = await Promise.all(node.map(n => reconstructElement(n, mods)));\r\n // Add index-based keys to React elements in the array to avoid the\r\n // \"Each child in a list should have a unique key prop\" warning.\r\n const React = await import('react');\r\n return items.map((el, i) =>\r\n el && typeof el === 'object' && el.$$typeof\r\n ? React.default.cloneElement(el, { key: el.key ?? i })\r\n : el,\r\n );\r\n }\r\n\r\n // Client component \u2014 look up the loaded module by ID.\r\n if ((node as any).__re === 'client') {\r\n const n = node as { __re: 'client'; componentId: string; props: Record<string, any> };\r\n const Comp = mods.get(n.componentId);\r\n if (!Comp) return null;\r\n const React = await import('react');\r\n return React.default.createElement(Comp, await reconstructProps(n.props, mods));\r\n }\r\n\r\n // Native HTML element (e.g. <div>, <span>).\r\n if ((node as any).__re === 'html') {\r\n const n = node as { __re: 'html'; tag: string; props: Record<string, any> };\r\n const React = await import('react');\r\n return React.default.createElement(n.tag, await reconstructProps(n.props, mods));\r\n }\r\n\r\n // Plain object \u2014 pass through as-is.\r\n return node;\r\n}\r\n\r\n/** Reconstructs every value in a props object, handling nested serialized nodes. */\r\nasync function reconstructProps(\r\n props: Record<string, any> | null | undefined,\r\n mods: ModuleMap,\r\n): Promise<Record<string, any>> {\r\n if (!props || typeof props !== 'object' || Array.isArray(props))\r\n return reconstructElement(props as any, mods);\r\n\r\n const out: Record<string, any> = {};\r\n for (const [k, v] of Object.entries(props))\r\n out[k] = await reconstructElement(v, mods);\r\n return out;\r\n}\r\n\r\n// \u2500\u2500\u2500 Module loading \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 * Dynamically imports each client component bundle from /__client-component/.\r\n * All fetches are issued in parallel; failures are logged but do not abort\r\n * the rest of the hydration pass.\r\n *\r\n * @param bust Optional cache-busting suffix appended as `?t=<bust>`.\r\n * Used during HMR navigation to bypass the module cache.\r\n */\r\nasync function loadModules(\r\n ids: string[],\r\n log: ReturnType<typeof makeLogger>,\r\n bust = '',\r\n): Promise<ModuleMap> {\r\n const mods: ModuleMap = new Map();\r\n await Promise.all(\r\n ids.map(async (id) => {\r\n try {\r\n const url = `/__client-component/${id}.js` + (bust ? `?t=${bust}` : '');\r\n const m = await import(url);\r\n mods.set(id, m.default);\r\n log.verbose('\u2713 Loaded:', id);\r\n } catch (err) {\r\n log.error('\u2717 Load failed:', id, err);\r\n }\r\n }),\r\n );\r\n return mods;\r\n}\r\n\r\n// \u2500\u2500\u2500 Root mounting \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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/** All active React roots \u2014 tracked so they can be unmounted before navigation. */\r\ntype ReactRoot = { unmount(): void };\r\nconst activeRoots: ReactRoot[] = [];\r\n\r\n/**\r\n * Finds every `[data-hydrate-id]` span in the document and calls hydrateRoot()\r\n * on it. hydrateRoot reconciles React's virtual DOM against the existing server\r\n * HTML without discarding it, which avoids a visible flash on both initial load\r\n * and SPA navigation (where we set innerHTML to fresh SSR output before calling\r\n * mountNodes).\r\n *\r\n * Nested markers are skipped \u2014 the parent's React tree owns its children.\r\n */\r\nasync function mountNodes(\r\n mods: ModuleMap,\r\n log: ReturnType<typeof makeLogger>,\r\n): Promise<void> {\r\n const { hydrateRoot } = await import('react-dom/client');\r\n const React = await import('react');\r\n\r\n const nodes = document.querySelectorAll<HTMLElement>('[data-hydrate-id]');\r\n log.verbose('Found', nodes.length, 'hydration point(s)');\r\n\r\n for (const node of nodes) {\r\n // Skip nested markers \u2014 the outer component owns its children.\r\n if (node.parentElement?.closest('[data-hydrate-id]')) continue;\r\n\r\n const id = node.getAttribute('data-hydrate-id')!;\r\n const Comp = mods.get(id);\r\n if (!Comp) { log.warn('No module for', id); continue; }\r\n\r\n let rawProps: Record<string, any> = {};\r\n try {\r\n rawProps = JSON.parse(node.getAttribute('data-hydrate-props') || '{}');\r\n } catch (e) {\r\n log.error('Props parse error for', id, e);\r\n }\r\n\r\n try {\r\n const element = React.default.createElement(Comp, await reconstructProps(rawProps, mods));\r\n const root = hydrateRoot(node, element);\r\n activeRoots.push(root);\r\n log.verbose('\u2713 Mounted:', id);\r\n } catch (err) {\r\n log.error('\u2717 Mount failed:', id, err);\r\n }\r\n }\r\n}\r\n\r\n// \u2500\u2500\u2500 Head tag sync \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 a <head> element and returns every Element node that lives between\r\n * the <!--n-head--> and <!--/n-head--> sentinel comments, plus the closing\r\n * comment node itself (used as the insertion anchor).\r\n *\r\n * The SSR renderer emits these sentinels around every useHtml()-generated tag\r\n * so the client can manage exactly that set without touching permanent tags\r\n * (charset, viewport, importmap, runtime <script>).\r\n */\r\nfunction headBlock(head: HTMLHeadElement): { nodes: Element[]; closeComment: Comment | null } {\r\n const nodes: Element[] = [];\r\n let closeComment: Comment | null = null;\r\n let inside = false;\r\n\r\n for (const child of Array.from(head.childNodes)) {\r\n if (child.nodeType === Node.COMMENT_NODE) {\r\n const text = (child as Comment).data.trim();\r\n if (text === 'n-head') { inside = true; continue; }\r\n if (text === '/n-head') { closeComment = child as Comment; inside = false; continue; }\r\n }\r\n if (inside && child.nodeType === Node.ELEMENT_NODE)\r\n nodes.push(child as Element);\r\n }\r\n\r\n return { nodes, closeComment };\r\n}\r\n\r\n/** Stable key for an Element: tag name + sorted attribute list (name=value pairs). */\r\nfunction fingerprint(el: Element): string {\r\n return el.tagName + '|' + Array.from(el.attributes)\r\n .sort((a, b) => a.name.localeCompare(b.name))\r\n .map(a => `${a.name}=${a.value}`)\r\n .join('&');\r\n}\r\n\r\n/**\r\n * Diffs the live <!--n-head--> block against the incoming document's block and\r\n * applies the minimal set of DOM mutations:\r\n *\r\n * - Tags present in `next` but not in `live` \u2192 inserted before <!--/n-head-->\r\n * so they remain inside the tracked block on future navigations.\r\n * - Tags present in `live` but not in `next` \u2192 removed.\r\n * - Tags present in both \u2192 left untouched (no removal/re-insertion flash).\r\n *\r\n * If the live head has no sentinel block yet (e.g. initial page had no useHtml\r\n * tags), both sentinel comments are created on the fly.\r\n */\r\nfunction syncHeadTags(doc: Document): void {\r\n const live = headBlock(document.head);\r\n const next = headBlock(doc.head);\r\n\r\n const liveMap = new Map<string, Element>();\r\n for (const el of live.nodes) liveMap.set(fingerprint(el), el);\r\n\r\n const nextMap = new Map<string, Element>();\r\n for (const el of next.nodes) nextMap.set(fingerprint(el), el);\r\n\r\n // Ensure we have an anchor to insert before.\r\n let anchor = live.closeComment;\r\n if (!anchor) {\r\n document.head.appendChild(document.createComment('n-head'));\r\n anchor = document.createComment('/n-head');\r\n document.head.appendChild(anchor);\r\n }\r\n\r\n for (const [fp, el] of nextMap)\r\n if (!liveMap.has(fp)) document.head.insertBefore(el, anchor);\r\n\r\n for (const [fp, el] of liveMap)\r\n if (!nextMap.has(fp)) el.remove();\r\n}\r\n\r\n// \u2500\u2500\u2500 SPA navigation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 * Syncs attributes from a parsed element onto the live document element.\r\n * Adds/updates attributes present in `next` and removes any that were set\r\n * on `live` but are absent in `next` (clears stale htmlAttrs/bodyAttrs).\r\n */\r\nfunction syncAttrs(live: Element, next: Element): void {\r\n for (const { name, value } of Array.from(next.attributes))\r\n live.setAttribute(name, value);\r\n for (const { name } of Array.from(live.attributes))\r\n if (!next.hasAttribute(name)) live.removeAttribute(name);\r\n}\r\n\r\n/**\r\n * Listens for 'locationchange' events and performs a soft navigation:\r\n *\r\n * 1. Fetch the target URL as HTML (?__hmr=1 skips client-SSR for HMR speed).\r\n * 2. Parse the response with DOMParser.\r\n * 3. Apply all visual DOM changes first (head tags, html/body attrs, #app\r\n * innerHTML, title, __n_data) so the new content is painted before React\r\n * cleanup effects run \u2014 prevents a useHtml restore from briefly undoing\r\n * the new document state.\r\n * 4. Unmount old React roots (runs cleanup effects against the already-updated DOM).\r\n * 5. Re-hydrate new client component markers.\r\n * 6. Scroll to top.\r\n *\r\n * Falls back to a full page reload if anything goes wrong.\r\n */\r\nfunction setupNavigation(log: ReturnType<typeof makeLogger>): void {\r\n window.addEventListener('locationchange', async ({ detail: { href, hmr } }: any) => {\r\n try {\r\n const fetchUrl = hmr\r\n ? href + (href.includes('?') ? '&' : '?') + '__hmr=1'\r\n : href;\r\n\r\n const response = await fetch(fetchUrl, { headers: { Accept: 'text/html' } });\r\n if (!response.ok) {\r\n log.error('Navigation fetch failed:', response.status);\r\n return;\r\n }\r\n\r\n const parser = new DOMParser();\r\n const doc = parser.parseFromString(await response.text(), 'text/html');\r\n const newApp = doc.getElementById('app');\r\n const currApp = document.getElementById('app');\r\n if (!newApp || !currApp) return;\r\n\r\n // \u2500\u2500 Visual update \u2014 all DOM mutations before React teardown \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n // Styles must be in place before new content appears to avoid an unstyled\r\n // flash. Unmounting runs useEffect cleanups (including useHtml restores)\r\n // which would temporarily revert document state if done first.\r\n\r\n // 1. Head tags \u2014 diff-based sync preserves shared layout tags untouched.\r\n syncHeadTags(doc);\r\n\r\n // 2. <html> and <body> attributes (lang, class, style, etc.).\r\n syncAttrs(document.documentElement, doc.documentElement);\r\n syncAttrs(document.body, doc.body);\r\n\r\n // 3. Page content.\r\n currApp.innerHTML = newApp.innerHTML;\r\n\r\n // 4. <title>.\r\n const newTitle = doc.querySelector('title');\r\n if (newTitle) document.title = newTitle.textContent ?? '';\r\n\r\n // 5. Runtime data blob \u2014 must come after innerHTML swap so the new\r\n // __n_data element is part of the live document.\r\n const newDataEl = doc.getElementById('__n_data');\r\n const currDataEl = document.getElementById('__n_data');\r\n if (newDataEl && currDataEl) currDataEl.textContent = newDataEl.textContent;\r\n\r\n // \u2500\u2500 React teardown \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 // Unmount after the visual update. Cleanup effects now run against an\r\n // already-updated document, so there is nothing left to visually undo.\r\n activeRoots.splice(0).forEach(r => r.unmount());\r\n\r\n // \u2500\u2500 Re-hydration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 const navData = JSON.parse(currDataEl?.textContent ?? '{}') as RuntimeData;\r\n log.info('\uD83D\uDD04 Route \u2192', href, '\u2014 mounting', navData.hydrateIds?.length ?? 0, 'component(s)');\r\n\r\n const mods = await loadModules(navData.allIds ?? [], log, String(Date.now()));\r\n await mountNodes(mods, log);\r\n\r\n window.scrollTo(0, 0);\r\n log.info('\uD83C\uDF89 Navigation complete:', href);\r\n } catch (err) {\r\n log.error('Navigation error, falling back to full reload:', err);\r\n window.location.href = href;\r\n }\r\n });\r\n}\r\n\r\n// \u2500\u2500\u2500 Public API \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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/** Shape of the JSON blob embedded as #__n_data in every SSR page. */\r\nexport interface RuntimeData {\r\n /** IDs of client components actually rendered on this page (subset of allIds). */\r\n hydrateIds: string[];\r\n /** All client component IDs reachable from this page, including layouts.\r\n * Pre-loaded so SPA navigations to related pages feel instant. */\r\n allIds: string[];\r\n url: string;\r\n params: Record<string, any>;\r\n debug: ClientDebugLevel;\r\n}\r\n\r\n/**\r\n * Bootstraps the NukeJS client runtime.\r\n *\r\n * Called once per page load from the inline <script type=\"module\"> injected\r\n * by the SSR renderer:\r\n *\r\n * ```js\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 * ```\r\n *\r\n * Order of operations:\r\n * 1. Create the logger at the configured debug level.\r\n * 2. Wire up SPA navigation listener.\r\n * 3. Load all client component bundles in parallel.\r\n * 4. Hydrate every [data-hydrate-id] node.\r\n * 5. Patch history.pushState/replaceState so Link clicks trigger navigation.\r\n */\r\nexport async function initRuntime(data: RuntimeData): Promise<void> {\r\n const log = makeLogger(data.debug ?? 'silent');\r\n\r\n log.info('\uD83D\uDE80 Partial hydration:', data.hydrateIds.length, 'root component(s)');\r\n\r\n // Set up navigation first so any 'locationchange' fired during hydration\r\n // is captured (e.g. a redirect side-effect inside a component).\r\n setupNavigation(log);\r\n\r\n // Load all component bundles (not just hydrateIds) so SPA navigations to\r\n // related pages can mount their components without an extra network round-trip.\r\n const mods = await loadModules(data.allIds, log);\r\n await mountNodes(mods, log);\r\n\r\n log.info('\uD83C\uDF89 Done!');\r\n\r\n // Patch history last so pushState calls during hydration don't trigger a\r\n // navigation before roots are ready.\r\n setupLocationChangeMonitor();\r\n}"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -32,7 +32,7 @@ function extractImports(filePath) {
32
32
  const content = fs.readFileSync(filePath, "utf-8");
33
33
  const dir = path.dirname(filePath);
34
34
  const imports = [];
35
- const importRegex = /import\s+(?:(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)\s+from\s+)?['"]([^'"]+)['"]/g;
35
+ const importRegex = /(?:import|export)\s+(?:(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)\s+from\s+)?['"]([^'"]+)['"]/g;
36
36
  let match;
37
37
  while ((match = importRegex.exec(content)) !== null) {
38
38
  const importPath = match[1];
@@ -51,16 +51,31 @@ function extractImports(filePath) {
51
51
  }
52
52
  if (!importPath.startsWith(".") && !importPath.startsWith("/")) continue;
53
53
  let resolved = path.resolve(dir, importPath);
54
- if (!fs.existsSync(resolved)) {
55
- for (const ext of [".tsx", ".ts", ".jsx", ".js"]) {
54
+ const EXTS = [".tsx", ".ts", ".jsx", ".js"];
55
+ const isFile = (p) => fs.existsSync(p) && fs.statSync(p).isFile();
56
+ if (!isFile(resolved)) {
57
+ let found = false;
58
+ for (const ext of EXTS) {
56
59
  const candidate = resolved + ext;
57
- if (fs.existsSync(candidate)) {
60
+ if (isFile(candidate)) {
58
61
  resolved = candidate;
62
+ found = true;
59
63
  break;
60
64
  }
61
65
  }
66
+ if (!found && fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
67
+ for (const ext of EXTS) {
68
+ const candidate = path.join(resolved, `index${ext}`);
69
+ if (isFile(candidate)) {
70
+ resolved = candidate;
71
+ found = true;
72
+ break;
73
+ }
74
+ }
75
+ }
76
+ if (!found) continue;
62
77
  }
63
- if (fs.existsSync(resolved)) imports.push(resolved);
78
+ imports.push(resolved);
64
79
  }
65
80
  return imports;
66
81
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/component-analyzer.ts"],
4
- "sourcesContent": ["/**\r\n * component-analyzer.ts \u2014 Static Import Analyzer & Client Component Registry\r\n *\r\n * This module solves a core problem in NukeJS's partial hydration model:\r\n * the server needs to know *at render time* which components in a page's\r\n * import tree are \"use client\" boundaries so it can:\r\n *\r\n * 1. Emit <span data-hydrate-id=\"\u2026\"> markers instead of rendering them.\r\n * 2. Inject the matching bundle URLs into the page's runtime data blob.\r\n * 3. Serialize the props passed to those components so the browser can\r\n * reconstruct them after loading the bundle.\r\n *\r\n * How it works:\r\n * - analyzeComponent() checks whether a file starts with \"use client\"\r\n * and assigns a stable content-hash ID if it does.\r\n * - extractImports() parses `import \u2026 from '\u2026'` statements with a\r\n * regex and resolves relative/absolute paths.\r\n * - findClientComponentsInTree() recursively walks the import graph, stopping\r\n * at client boundaries (they own their subtree).\r\n *\r\n * Results are memoised in `componentCache` (process-lifetime) so repeated SSR\r\n * renders don't re-read and re-hash files they've already seen.\r\n *\r\n * ID scheme:\r\n * The ID for a client component is `cc_` + the first 8 hex chars of the MD5\r\n * hash of its path relative to pagesDir. This is stable across restarts and\r\n * matches what the browser will request from /__client-component/<id>.js.\r\n */\r\n\r\nimport path from 'path';\r\nimport fs from 'fs';\r\nimport crypto from 'crypto';\r\nimport { fileURLToPath } from 'url';\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 ComponentInfo {\r\n filePath: string;\r\n /** True when the file's first non-comment line is \"use client\". */\r\n isClientComponent: boolean;\r\n /** Stable hash-based ID, present only for client components. */\r\n clientComponentId?: string;\r\n}\r\n\r\n// \u2500\u2500\u2500 In-process cache \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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// Memoises analyze results for the lifetime of the dev server process.\r\n// In production builds the analysis runs once per build, so no cache is needed.\r\nconst componentCache = new Map<string, ComponentInfo>();\r\n\r\n// \u2500\u2500\u2500 Client boundary 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\u2500\r\n\r\n/**\r\n * Returns true when a file begins with a `\"use client\"` or `'use client'`\r\n * directive (ignoring blank lines and line/block comment prefixes).\r\n *\r\n * Only the first five lines are checked \u2014 the directive must appear before\r\n * any executable code.\r\n */\r\nfunction isClientComponent(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 true;\r\n break; // First substantive line is not \"use client\"\r\n }\r\n return false;\r\n}\r\n\r\n/**\r\n * Generates a deterministic, short ID for a client component.\r\n * The path is made relative to pagesDir before hashing so the ID is\r\n * portable across machines (absolute paths differ per developer).\r\n */\r\nfunction getClientComponentId(filePath: string, pagesDir: string): string {\r\n const hash = crypto\r\n .createHash('md5')\r\n .update(path.relative(pagesDir, filePath))\r\n .digest('hex')\r\n .substring(0, 8);\r\n return `cc_${hash}`;\r\n}\r\n\r\n// \u2500\u2500\u2500 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\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Analyses a component file and returns cached results on subsequent calls.\r\n *\r\n * @param filePath Absolute path to the source file.\r\n * @param pagesDir Absolute path to the pages root (used for ID generation).\r\n */\r\nexport function analyzeComponent(filePath: string, pagesDir: string): ComponentInfo {\r\n if (componentCache.has(filePath)) return componentCache.get(filePath)!;\r\n\r\n const isClient = isClientComponent(filePath);\r\n const info: ComponentInfo = {\r\n filePath,\r\n isClientComponent: isClient,\r\n clientComponentId: isClient ? getClientComponentId(filePath, pagesDir) : undefined,\r\n };\r\n\r\n componentCache.set(filePath, info);\r\n return info;\r\n}\r\n\r\n// \u2500\u2500\u2500 Import extraction \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 `import \u2026 from '\u2026'` statements in a file and returns a list of\r\n * resolved absolute paths for all *local* imports (relative or absolute paths).\r\n *\r\n * Non-local specifiers (npm packages) are skipped, except `nukejs` itself \u2014\r\n * which is resolved to our own index file so built-in \"use client\" components\r\n * like `<Link>` are included in the client component discovery walk.\r\n *\r\n * Extensions are tried in priority order if the specifier has none.\r\n */\r\nfunction extractImports(filePath: string): string[] {\r\n const content = fs.readFileSync(filePath, 'utf-8');\r\n const dir = path.dirname(filePath);\r\n const imports: string[] = [];\r\n\r\n const importRegex =\r\n /import\\s+(?:(?:\\{[^}]*\\}|\\*\\s+as\\s+\\w+|\\w+)\\s+from\\s+)?['\"]([^'\"]+)['\"]/g;\r\n let match: RegExpExecArray | null;\r\n\r\n while ((match = importRegex.exec(content)) !== null) {\r\n const importPath = match[1];\r\n\r\n // Special case: resolve the 'nukejs' package import to our own source so\r\n // built-in \"use client\" exports (Link, useRouter, etc.) are discovered.\r\n if (importPath === 'nukejs') {\r\n const selfDir = path.dirname(fileURLToPath(import.meta.url));\r\n for (const candidate of [\r\n path.join(selfDir, 'index.ts'),\r\n path.join(selfDir, 'index.js'),\r\n ]) {\r\n if (fs.existsSync(candidate)) { imports.push(candidate); break; }\r\n }\r\n continue;\r\n }\r\n\r\n // Skip npm packages and other non-local specifiers.\r\n if (!importPath.startsWith('.') && !importPath.startsWith('/')) continue;\r\n\r\n // Resolve to an absolute path and add common extensions if needed.\r\n let resolved = path.resolve(dir, importPath);\r\n if (!fs.existsSync(resolved)) {\r\n for (const ext of ['.tsx', '.ts', '.jsx', '.js']) {\r\n const candidate = resolved + ext;\r\n if (fs.existsSync(candidate)) { resolved = candidate; break; }\r\n }\r\n }\r\n if (fs.existsSync(resolved)) imports.push(resolved);\r\n }\r\n\r\n return imports;\r\n}\r\n\r\n// \u2500\u2500\u2500 Tree walk \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 walks the import graph from `filePath`, collecting every\r\n * \"use client\" file encountered.\r\n *\r\n * The walk stops at client boundaries: a \"use client\" file is collected and\r\n * its own imports are NOT walked (the client runtime handles their subtree).\r\n *\r\n * The `visited` set prevents infinite loops from circular imports.\r\n *\r\n * @returns Map<id, absoluteFilePath> for every client component reachable\r\n * from `filePath` (including `filePath` itself if it's a client).\r\n */\r\nexport function findClientComponentsInTree(\r\n filePath: string,\r\n pagesDir: string,\r\n visited = new Set<string>(),\r\n): Map<string, string> {\r\n const clientComponents = new Map<string, string>();\r\n if (visited.has(filePath)) return clientComponents;\r\n visited.add(filePath);\r\n\r\n const info = analyzeComponent(filePath, pagesDir);\r\n\r\n // This file is a client boundary \u2014 record it and stop descending.\r\n if (info.isClientComponent && info.clientComponentId) {\r\n clientComponents.set(info.clientComponentId, filePath);\r\n return clientComponents;\r\n }\r\n\r\n // Server component \u2014 recurse into its imports.\r\n for (const importPath of extractImports(filePath)) {\r\n for (const [id, p] of findClientComponentsInTree(importPath, pagesDir, visited)) {\r\n clientComponents.set(id, p);\r\n }\r\n }\r\n\r\n return clientComponents;\r\n}\r\n\r\n// \u2500\u2500\u2500 Cache access \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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/**\r\n * Looks up the absolute file path for a client component by its ID.\r\n * O(1) reverse lookup \u2014 avoids the O(n) linear scan in bundler.ts.\r\n *\r\n * Returns undefined when the ID is not in the cache (page not yet visited\r\n * in dev, or stale ID after a file change).\r\n */\r\nexport function getComponentById(id: string): string | undefined {\r\n for (const [filePath, info] of componentCache) {\r\n if (info.clientComponentId === id) return filePath;\r\n }\r\n return undefined;\r\n}\r\n\r\n/** Returns the live component cache (used by bundler.ts for ID\u2192path lookup). */\r\nexport function getComponentCache(): Map<string, ComponentInfo> {\r\n return componentCache;\r\n}\r\n\r\n/**\r\n * Removes a single file's analysis entry from the cache.\r\n * Call this whenever a source file changes in dev mode so the next render\r\n * re-analyses the file (picks up added/removed \"use client\" directives and\r\n * changed import graphs).\r\n */\r\nexport function invalidateComponentCache(filePath: string): void {\r\n componentCache.delete(filePath);\r\n}"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\r\n * component-analyzer.ts \u2014 Static Import Analyzer & Client Component Registry\r\n *\r\n * This module solves a core problem in NukeJS's partial hydration model:\r\n * the server needs to know *at render time* which components in a page's\r\n * import tree are \"use client\" boundaries so it can:\r\n *\r\n * 1. Emit <span data-hydrate-id=\"\u2026\"> markers instead of rendering them.\r\n * 2. Inject the matching bundle URLs into the page's runtime data blob.\r\n * 3. Serialize the props passed to those components so the browser can\r\n * reconstruct them after loading the bundle.\r\n *\r\n * How it works:\r\n * - analyzeComponent() checks whether a file starts with \"use client\"\r\n * and assigns a stable content-hash ID if it does.\r\n * - extractImports() parses `import \u2026 from '\u2026'` statements with a\r\n * regex and resolves relative/absolute paths.\r\n * - findClientComponentsInTree() recursively walks the import graph, stopping\r\n * at client boundaries (they own their subtree).\r\n *\r\n * Results are memoised in `componentCache` (process-lifetime) so repeated SSR\r\n * renders don't re-read and re-hash files they've already seen.\r\n *\r\n * ID scheme:\r\n * The ID for a client component is `cc_` + the first 8 hex chars of the MD5\r\n * hash of its path relative to pagesDir. This is stable across restarts and\r\n * matches what the browser will request from /__client-component/<id>.js.\r\n */\r\n\r\nimport path from 'path';\r\nimport fs from 'fs';\r\nimport crypto from 'crypto';\r\nimport { fileURLToPath } from 'url';\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 ComponentInfo {\r\n filePath: string;\r\n /** True when the file's first non-comment line is \"use client\". */\r\n isClientComponent: boolean;\r\n /** Stable hash-based ID, present only for client components. */\r\n clientComponentId?: string;\r\n}\r\n\r\n// \u2500\u2500\u2500 In-process cache \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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// Memoises analyze results for the lifetime of the dev server process.\r\n// In production builds the analysis runs once per build, so no cache is needed.\r\nconst componentCache = new Map<string, ComponentInfo>();\r\n\r\n// \u2500\u2500\u2500 Client boundary 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\u2500\r\n\r\n/**\r\n * Returns true when a file begins with a `\"use client\"` or `'use client'`\r\n * directive (ignoring blank lines and line/block comment prefixes).\r\n *\r\n * Only the first five lines are checked \u2014 the directive must appear before\r\n * any executable code.\r\n */\r\nfunction isClientComponent(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 true;\r\n break; // First substantive line is not \"use client\"\r\n }\r\n return false;\r\n}\r\n\r\n/**\r\n * Generates a deterministic, short ID for a client component.\r\n * The path is made relative to pagesDir before hashing so the ID is\r\n * portable across machines (absolute paths differ per developer).\r\n */\r\nfunction getClientComponentId(filePath: string, pagesDir: string): string {\r\n const hash = crypto\r\n .createHash('md5')\r\n .update(path.relative(pagesDir, filePath))\r\n .digest('hex')\r\n .substring(0, 8);\r\n return `cc_${hash}`;\r\n}\r\n\r\n// \u2500\u2500\u2500 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\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Analyses a component file and returns cached results on subsequent calls.\r\n *\r\n * @param filePath Absolute path to the source file.\r\n * @param pagesDir Absolute path to the pages root (used for ID generation).\r\n */\r\nexport function analyzeComponent(filePath: string, pagesDir: string): ComponentInfo {\r\n if (componentCache.has(filePath)) return componentCache.get(filePath)!;\r\n\r\n const isClient = isClientComponent(filePath);\r\n const info: ComponentInfo = {\r\n filePath,\r\n isClientComponent: isClient,\r\n clientComponentId: isClient ? getClientComponentId(filePath, pagesDir) : undefined,\r\n };\r\n\r\n componentCache.set(filePath, info);\r\n return info;\r\n}\r\n\r\n// \u2500\u2500\u2500 Import extraction \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 `import \u2026 from '\u2026'` and `export \u2026 from '\u2026'` statements in a file and returns a list of\r\n * resolved absolute paths for all *local* imports (relative or absolute paths).\r\n *\r\n * Non-local specifiers (npm packages) are skipped, except `nukejs` itself \u2014\r\n * which is resolved to our own index file so built-in \"use client\" components\r\n * like `<Link>` are included in the client component discovery walk.\r\n *\r\n * Extensions are tried in priority order if the specifier has none.\r\n */\r\nfunction extractImports(filePath: string): string[] {\r\n const content = fs.readFileSync(filePath, 'utf-8');\r\n const dir = path.dirname(filePath);\r\n const imports: string[] = [];\r\n\r\n const importRegex =\r\n /(?:import|export)\\s+(?:(?:\\{[^}]*\\}|\\*\\s+as\\s+\\w+|\\w+)\\s+from\\s+)?['\"]([^'\"]+)['\"]/g;\r\n let match: RegExpExecArray | null;\r\n\r\n while ((match = importRegex.exec(content)) !== null) {\r\n const importPath = match[1];\r\n\r\n // Special case: resolve the 'nukejs' package import to our own source so\r\n // built-in \"use client\" exports (Link, useRouter, etc.) are discovered.\r\n if (importPath === 'nukejs') {\r\n const selfDir = path.dirname(fileURLToPath(import.meta.url));\r\n for (const candidate of [\r\n path.join(selfDir, 'index.ts'),\r\n path.join(selfDir, 'index.js'),\r\n ]) {\r\n if (fs.existsSync(candidate)) { imports.push(candidate); break; }\r\n }\r\n continue;\r\n }\r\n\r\n // Skip npm packages and other non-local specifiers.\r\n if (!importPath.startsWith('.') && !importPath.startsWith('/')) continue;\r\n\r\n // Resolve to an absolute path and add common extensions if needed.\r\n let resolved = path.resolve(dir, importPath);\r\n const EXTS = ['.tsx', '.ts', '.jsx', '.js'] as const;\r\n\r\n const isFile = (p: string) =>\r\n fs.existsSync(p) && fs.statSync(p).isFile();\r\n\r\n if (!isFile(resolved)) {\r\n let found = false;\r\n\r\n // 1. Try appending an extension (e.g. './Button' \u2192 './Button.tsx')\r\n for (const ext of EXTS) {\r\n const candidate = resolved + ext;\r\n if (isFile(candidate)) { resolved = candidate; found = true; break; }\r\n }\r\n\r\n // 2. If resolved is a directory, look for an index file inside it\r\n // (e.g. './components' \u2192 './components/index.tsx')\r\n if (!found && fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {\r\n for (const ext of EXTS) {\r\n const candidate = path.join(resolved, `index${ext}`);\r\n if (isFile(candidate)) { resolved = candidate; found = true; break; }\r\n }\r\n }\r\n\r\n if (!found) continue; // unresolvable import \u2014 skip silently\r\n }\r\n\r\n imports.push(resolved);\r\n }\r\n\r\n return imports;\r\n}\r\n\r\n// \u2500\u2500\u2500 Tree walk \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 walks the import graph from `filePath`, collecting every\r\n * \"use client\" file encountered.\r\n *\r\n * The walk stops at client boundaries: a \"use client\" file is collected and\r\n * its own imports are NOT walked (the client runtime handles their subtree).\r\n *\r\n * The `visited` set prevents infinite loops from circular imports.\r\n *\r\n * @returns Map<id, absoluteFilePath> for every client component reachable\r\n * from `filePath` (including `filePath` itself if it's a client).\r\n */\r\nexport function findClientComponentsInTree(\r\n filePath: string,\r\n pagesDir: string,\r\n visited = new Set<string>(),\r\n): Map<string, string> {\r\n const clientComponents = new Map<string, string>();\r\n if (visited.has(filePath)) return clientComponents;\r\n visited.add(filePath);\r\n\r\n const info = analyzeComponent(filePath, pagesDir);\r\n\r\n // This file is a client boundary \u2014 record it and stop descending.\r\n if (info.isClientComponent && info.clientComponentId) {\r\n clientComponents.set(info.clientComponentId, filePath);\r\n return clientComponents;\r\n }\r\n\r\n // Server component \u2014 recurse into its imports.\r\n for (const importPath of extractImports(filePath)) {\r\n for (const [id, p] of findClientComponentsInTree(importPath, pagesDir, visited)) {\r\n clientComponents.set(id, p);\r\n }\r\n }\r\n\r\n return clientComponents;\r\n}\r\n\r\n// \u2500\u2500\u2500 Cache access \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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/**\r\n * Looks up the absolute file path for a client component by its ID.\r\n * O(1) reverse lookup \u2014 avoids the O(n) linear scan in bundler.ts.\r\n *\r\n * Returns undefined when the ID is not in the cache (page not yet visited\r\n * in dev, or stale ID after a file change).\r\n */\r\nexport function getComponentById(id: string): string | undefined {\r\n for (const [filePath, info] of componentCache) {\r\n if (info.clientComponentId === id) return filePath;\r\n }\r\n return undefined;\r\n}\r\n\r\n/** Returns the live component cache (used by bundler.ts for ID\u2192path lookup). */\r\nexport function getComponentCache(): Map<string, ComponentInfo> {\r\n return componentCache;\r\n}\r\n\r\n/**\r\n * Removes a single file's analysis entry from the cache.\r\n * Call this whenever a source file changes in dev mode so the next render\r\n * re-analyses the file (picks up added/removed \"use client\" directives and\r\n * changed import graphs).\r\n */\r\nexport function invalidateComponentCache(filePath: string): void {\r\n componentCache.delete(filePath);\r\n}"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/dist/router.d.ts CHANGED
@@ -20,8 +20,12 @@
20
20
  * checking whether the file exists.
21
21
  */
22
22
  /**
23
- * Recursively collects all .ts/.tsx files in `dir`, returning paths relative to
24
- * `baseDir` without the file extension.
23
+ * Recursively collects all routable .ts/.tsx files in `dir`, returning paths
24
+ * relative to `baseDir` without the file extension.
25
+ *
26
+ * layout.tsx files are excluded — they wrap pages but are never routes
27
+ * themselves. This mirrors the filter in collectServerPages() so dev-mode
28
+ * route matching behaves identically to the production build.
25
29
  *
26
30
  * Example output: ['index', 'users/index', 'users/[id]', 'blog/[...slug]']
27
31
  */
@@ -62,6 +66,7 @@ export interface RouteMatch {
62
66
  * Steps:
63
67
  * 1. Reject '..' or '.' path segments (path traversal guard).
64
68
  * 2. Try an exact file match (e.g. /about → baseDir/about.tsx).
69
+ * layout.tsx is explicitly excluded from exact matching.
65
70
  * 3. Sort all discovered routes by specificity (most specific first).
66
71
  * 4. Return the first dynamic route that matches.
67
72
  *
package/dist/router.js CHANGED
@@ -8,6 +8,8 @@ function findAllRoutes(dir, baseDir = dir) {
8
8
  if (entry.isDirectory()) {
9
9
  routes.push(...findAllRoutes(fullPath, baseDir));
10
10
  } else if (entry.name.endsWith(".tsx") || entry.name.endsWith(".ts")) {
11
+ const stem = entry.name.replace(/\.(tsx|ts)$/, "");
12
+ if (stem === "layout") continue;
11
13
  routes.push(path.relative(baseDir, fullPath).replace(/\.(tsx|ts)$/, ""));
12
14
  }
13
15
  }
@@ -63,8 +65,9 @@ function matchRoute(urlPath, baseDir, extension = ".tsx") {
63
65
  if (rawSegments.some((s) => s === ".." || s === ".")) return null;
64
66
  const segments = rawSegments.length === 0 ? ["index"] : rawSegments;
65
67
  const exactPath = path.join(baseDir, ...segments) + extension;
68
+ const exactStem = path.basename(exactPath, extension);
66
69
  if (!isWithinBase(baseDir, exactPath)) return null;
67
- if (fs.existsSync(exactPath)) {
70
+ if (exactStem !== "layout" && fs.existsSync(exactPath)) {
68
71
  return { filePath: exactPath, params: {}, routePattern: segments.join("/") };
69
72
  }
70
73
  const sortedRoutes = findAllRoutes(baseDir).sort(