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.
- package/dist/build-common.d.ts +13 -10
- package/dist/build-common.js +19 -25
- package/dist/build-common.js.map +2 -2
- package/dist/build-node.js +3 -6
- package/dist/build-node.js.map +2 -2
- package/dist/build-vercel.js +2 -4
- package/dist/build-vercel.js.map +2 -2
- package/dist/bundle.d.ts +12 -2
- package/dist/bundle.js +56 -15
- package/dist/bundle.js.map +2 -2
- package/dist/component-analyzer.js +20 -5
- package/dist/component-analyzer.js.map +2 -2
- package/dist/router.d.ts +7 -2
- package/dist/router.js +4 -1
- package/dist/router.js.map +2 -2
- package/dist/ssr.d.ts +10 -4
- package/dist/ssr.js +11 -6
- package/dist/ssr.js.map +2 -2
- package/package.json +1 -1
package/dist/router.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/router.ts"],
|
|
4
|
-
"sourcesContent": ["/**\r\n * router.ts \u2014 File-System Based URL Router\r\n *\r\n * Maps incoming URL paths to handler files using Next.js-compatible conventions:\r\n *\r\n * server/users/index.ts \u2192 /users\r\n * server/users/[id].ts \u2192 /users/:id (dynamic segment)\r\n * server/blog/[...slug].ts \u2192 /blog/* (catch-all)\r\n * server/files/[[...path]].ts \u2192 /files or /files/* (optional catch-all)\r\n *\r\n * Route specificity (higher = wins over lower):\r\n * static segment +4 (e.g. 'about')\r\n * dynamic segment +3 (e.g. '[id]')\r\n * catch-all +2 (e.g. '[...slug]')\r\n * optional catch-all +1 (e.g. '[[...path]]')\r\n *\r\n * Path traversal protection:\r\n * matchRoute() rejects URL segments that contain '..' or '.' and verifies\r\n * that the resolved file path stays inside the base directory before\r\n * checking whether the file exists.\r\n */\r\n\r\nimport path from 'path';\r\nimport fs from 'fs';\r\n\r\n// \u2500\u2500\u2500 Route file discovery \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Recursively collects all .ts/.tsx files in `dir`, returning paths relative to\r\n * `baseDir` without the file extension.\r\n *\r\n * Example output: ['index', 'users/index', 'users/[id]', 'blog/[...slug]']\r\n */\r\nexport function findAllRoutes(dir: string, baseDir: string = dir): string[] {\r\n if (!fs.existsSync(dir)) return [];\r\n\r\n const routes: string[] = [];\r\n for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {\r\n const fullPath = path.join(dir, entry.name);\r\n if (entry.isDirectory()) {\r\n routes.push(...findAllRoutes(fullPath, baseDir));\r\n } else if (entry.name.endsWith('.tsx') || entry.name.endsWith('.ts')) {\r\n routes.push(path.relative(baseDir, fullPath).replace(/\\.(tsx|ts)$/, ''));\r\n }\r\n }\r\n return routes;\r\n}\r\n\r\n// \u2500\u2500\u2500 Dynamic segment matching \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Attempts to match `urlSegments` against a route that may contain dynamic\r\n * segments ([param]), catch-alls ([...slug]), and optional catch-alls ([[...path]]).\r\n *\r\n * Returns the captured params on success, or null if the route does not match.\r\n *\r\n * Param value types:\r\n * [param] \u2192 string\r\n * [...slug] \u2192 string[] (at least one segment required)\r\n * [[...path]] \u2192 string[] (zero or more segments)\r\n */\r\nexport function matchDynamicRoute(\r\n urlSegments: string[],\r\n routePath: string,\r\n): { params: Record<string, string | string[]> } | null {\r\n const routeSegments = routePath.split(path.sep);\r\n\r\n // 'index' at the end of a route path means the route handles the parent directory URL.\r\n if (routeSegments.at(-1) === 'index') routeSegments.pop();\r\n\r\n const params: Record<string, string | string[]> = {};\r\n let ri = 0; // route segment index\r\n let ui = 0; // URL segment index\r\n\r\n while (ri < routeSegments.length) {\r\n const seg = routeSegments[ri];\r\n\r\n // [[...name]] \u2014 optional catch-all: consumes zero or more remaining URL segments.\r\n const optCatchAll = seg.match(/^\\[\\[\\.\\.\\.(.+)\\]\\]$/);\r\n if (optCatchAll) {\r\n params[optCatchAll[1]] = urlSegments.slice(ui);\r\n return { params };\r\n }\r\n\r\n // [...name] \u2014 required catch-all: must consume at least one URL segment.\r\n const catchAll = seg.match(/^\\[\\.\\.\\.(.+)\\]$/);\r\n if (catchAll) {\r\n const remaining = urlSegments.slice(ui);\r\n if (!remaining.length) return null;\r\n params[catchAll[1]] = remaining;\r\n return { params };\r\n }\r\n\r\n // [name] \u2014 single dynamic segment: consumes exactly one URL segment.\r\n const dynamic = seg.match(/^\\[(.+)\\]$/);\r\n if (dynamic) {\r\n if (ui >= urlSegments.length) return null;\r\n params[dynamic[1]] = urlSegments[ui++];\r\n ri++;\r\n continue;\r\n }\r\n\r\n // Static segment \u2014 must match exactly.\r\n if (ui >= urlSegments.length || seg !== urlSegments[ui]) return null;\r\n ui++; ri++;\r\n }\r\n\r\n // All route segments consumed \u2014 URL must be fully consumed too.\r\n return ui < urlSegments.length ? null : { params };\r\n}\r\n\r\n// \u2500\u2500\u2500 Specificity scoring \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 * Computes a specificity score for a route path.\r\n * Used to sort candidate routes so more specific routes shadow catch-alls.\r\n *\r\n * Higher score = more specific:\r\n * static segment 4\r\n * [dynamic] 3\r\n * [...catchAll] 2\r\n * [[...optCatchAll]] 1\r\n */\r\nexport function getRouteSpecificity(routePath: string): number {\r\n return routePath.split(path.sep).reduce((score, seg) => {\r\n if (seg.match(/^\\[\\[\\.\\.\\.(.+)\\]\\]$/)) return score + 1;\r\n if (seg.match(/^\\[\\.\\.\\.(.+)\\]$/)) return score + 2;\r\n if (seg.match(/^\\[(.+)\\]$/)) return score + 3;\r\n return score + 4; // static segment\r\n }, 0);\r\n}\r\n\r\n// \u2500\u2500\u2500 Route match result \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 RouteMatch {\r\n filePath: string;\r\n params: Record<string, string | string[]>;\r\n routePattern: string;\r\n}\r\n\r\n// \u2500\u2500\u2500 Path traversal guard \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 only when `filePath` is a descendant of `baseDir`.\r\n * Used to prevent URL path traversal attacks (e.g. /../../etc/passwd).\r\n */\r\nfunction isWithinBase(baseDir: string, filePath: string): boolean {\r\n const rel = path.relative(baseDir, filePath);\r\n return Boolean(rel) && !rel.startsWith('..') && !path.isAbsolute(rel);\r\n}\r\n\r\n// \u2500\u2500\u2500 Route matching \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Resolves a URL path to a route file inside `baseDir`.\r\n *\r\n * Steps:\r\n * 1. Reject '..' or '.' path segments (path traversal guard).\r\n * 2. Try an exact file match (e.g. /about \u2192 baseDir/about.tsx).\r\n * 3. Sort all discovered routes by specificity (most specific first).\r\n * 4. Return the first dynamic route that matches.\r\n *\r\n * @param urlPath The URL path to match (e.g. '/users/42').\r\n * @param baseDir Absolute path to the directory containing route files.\r\n * @param extension File extension to look for ('.tsx' or '.ts').\r\n */\r\nexport function matchRoute(\r\n urlPath: string,\r\n baseDir: string,\r\n extension = '.tsx',\r\n): RouteMatch | null {\r\n // Split the URL path into segments, rejecting any that attempt path traversal.\r\n const rawSegments = urlPath === '/' ? [] : urlPath.slice(1).split('/');\r\n if (rawSegments.some(s => s === '..' || s === '.')) return null;\r\n\r\n // For the root URL, look for an index file.\r\n const segments = rawSegments.length === 0 ? ['index'] : rawSegments;\r\n\r\n // 1. Exact match: /about \u2192 about.tsx\r\n const exactPath = path.join(baseDir, ...segments) + extension;\r\n if (!isWithinBase(baseDir, exactPath)) return null;\r\n if (fs.existsSync(exactPath)) {\r\n return { filePath: exactPath, params: {}, routePattern: segments.join('/') };\r\n }\r\n\r\n // 2. Dynamic match \u2014 try routes sorted by specificity so '[id]' wins over '[...all]'.\r\n const sortedRoutes = findAllRoutes(baseDir).sort(\r\n (a, b) => getRouteSpecificity(b) - getRouteSpecificity(a),\r\n );\r\n\r\n for (const route of sortedRoutes) {\r\n const match = matchDynamicRoute(segments, route);\r\n if (!match) continue;\r\n const filePath = path.join(baseDir, route) + extension;\r\n if (!isWithinBase(baseDir, filePath)) continue;\r\n if (fs.existsSync(filePath)) {\r\n return { filePath, params: match.params, routePattern: route };\r\n }\r\n }\r\n\r\n return null;\r\n}\r\n\r\n// \u2500\u2500\u2500 Layout discovery \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Returns every layout.tsx file that wraps a given route file, in\r\n * outermost-first order (root layout first, nearest layout last).\r\n *\r\n * Layout chain example for app/pages/blog/[slug]/page.tsx:\r\n * app/pages/layout.tsx \u2190 root layout\r\n * app/pages/blog/layout.tsx \u2190 blog section layout\r\n *\r\n * The outermost-first order matches how wrapWithLayouts() nests them:\r\n * the last layout in the array is the innermost wrapper.\r\n */\r\nexport function findLayoutsForRoute(routeFilePath: string, pagesDir: string): string[] {\r\n const layouts: string[] = [];\r\n\r\n // Root layout wraps everything.\r\n const rootLayout = path.join(pagesDir, 'layout.tsx');\r\n if (fs.existsSync(rootLayout)) layouts.push(rootLayout);\r\n\r\n // Walk the directory hierarchy from pagesDir to the file's parent.\r\n const relativePath = path.relative(pagesDir, path.dirname(routeFilePath));\r\n if (!relativePath || relativePath === '.') return layouts;\r\n\r\n const segments = relativePath.split(path.sep).filter(s => s !== '.');\r\n for (let i = 1; i <= segments.length; i++) {\r\n const layoutPath = path.join(pagesDir, ...segments.slice(0, i), 'layout.tsx');\r\n if (fs.existsSync(layoutPath)) layouts.push(layoutPath);\r\n }\r\n\r\n return layouts;\r\n}\r\n"],
|
|
5
|
-
"mappings": "AAsBA,OAAO,UAAU;AACjB,OAAO,QAAU;
|
|
4
|
+
"sourcesContent": ["/**\r\n * router.ts \u2014 File-System Based URL Router\r\n *\r\n * Maps incoming URL paths to handler files using Next.js-compatible conventions:\r\n *\r\n * server/users/index.ts \u2192 /users\r\n * server/users/[id].ts \u2192 /users/:id (dynamic segment)\r\n * server/blog/[...slug].ts \u2192 /blog/* (catch-all)\r\n * server/files/[[...path]].ts \u2192 /files or /files/* (optional catch-all)\r\n *\r\n * Route specificity (higher = wins over lower):\r\n * static segment +4 (e.g. 'about')\r\n * dynamic segment +3 (e.g. '[id]')\r\n * catch-all +2 (e.g. '[...slug]')\r\n * optional catch-all +1 (e.g. '[[...path]]')\r\n *\r\n * Path traversal protection:\r\n * matchRoute() rejects URL segments that contain '..' or '.' and verifies\r\n * that the resolved file path stays inside the base directory before\r\n * checking whether the file exists.\r\n */\r\n\r\nimport path from 'path';\r\nimport fs from 'fs';\r\n\r\n// \u2500\u2500\u2500 Route file discovery \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Recursively collects all routable .ts/.tsx files in `dir`, returning paths\r\n * relative to `baseDir` without the file extension.\r\n *\r\n * layout.tsx files are excluded \u2014 they wrap pages but are never routes\r\n * themselves. This mirrors the filter in collectServerPages() so dev-mode\r\n * route matching behaves identically to the production build.\r\n *\r\n * Example output: ['index', 'users/index', 'users/[id]', 'blog/[...slug]']\r\n */\r\nexport function findAllRoutes(dir: string, baseDir: string = dir): string[] {\r\n if (!fs.existsSync(dir)) return [];\r\n\r\n const routes: string[] = [];\r\n for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {\r\n const fullPath = path.join(dir, entry.name);\r\n if (entry.isDirectory()) {\r\n routes.push(...findAllRoutes(fullPath, baseDir));\r\n } else if (entry.name.endsWith('.tsx') || entry.name.endsWith('.ts')) {\r\n const stem = entry.name.replace(/\\.(tsx|ts)$/, '');\r\n if (stem === 'layout') continue;\r\n routes.push(path.relative(baseDir, fullPath).replace(/\\.(tsx|ts)$/, ''));\r\n }\r\n }\r\n return routes;\r\n}\r\n\r\n// \u2500\u2500\u2500 Dynamic segment matching \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Attempts to match `urlSegments` against a route that may contain dynamic\r\n * segments ([param]), catch-alls ([...slug]), and optional catch-alls ([[...path]]).\r\n *\r\n * Returns the captured params on success, or null if the route does not match.\r\n *\r\n * Param value types:\r\n * [param] \u2192 string\r\n * [...slug] \u2192 string[] (at least one segment required)\r\n * [[...path]] \u2192 string[] (zero or more segments)\r\n */\r\nexport function matchDynamicRoute(\r\n urlSegments: string[],\r\n routePath: string,\r\n): { params: Record<string, string | string[]> } | null {\r\n const routeSegments = routePath.split(path.sep);\r\n\r\n // 'index' at the end of a route path means the route handles the parent directory URL.\r\n if (routeSegments.at(-1) === 'index') routeSegments.pop();\r\n\r\n const params: Record<string, string | string[]> = {};\r\n let ri = 0; // route segment index\r\n let ui = 0; // URL segment index\r\n\r\n while (ri < routeSegments.length) {\r\n const seg = routeSegments[ri];\r\n\r\n // [[...name]] \u2014 optional catch-all: consumes zero or more remaining URL segments.\r\n const optCatchAll = seg.match(/^\\[\\[\\.\\.\\.(.+)\\]\\]$/);\r\n if (optCatchAll) {\r\n params[optCatchAll[1]] = urlSegments.slice(ui);\r\n return { params };\r\n }\r\n\r\n // [...name] \u2014 required catch-all: must consume at least one URL segment.\r\n const catchAll = seg.match(/^\\[\\.\\.\\.(.+)\\]$/);\r\n if (catchAll) {\r\n const remaining = urlSegments.slice(ui);\r\n if (!remaining.length) return null;\r\n params[catchAll[1]] = remaining;\r\n return { params };\r\n }\r\n\r\n // [name] \u2014 single dynamic segment: consumes exactly one URL segment.\r\n const dynamic = seg.match(/^\\[(.+)\\]$/);\r\n if (dynamic) {\r\n if (ui >= urlSegments.length) return null;\r\n params[dynamic[1]] = urlSegments[ui++];\r\n ri++;\r\n continue;\r\n }\r\n\r\n // Static segment \u2014 must match exactly.\r\n if (ui >= urlSegments.length || seg !== urlSegments[ui]) return null;\r\n ui++; ri++;\r\n }\r\n\r\n // All route segments consumed \u2014 URL must be fully consumed too.\r\n return ui < urlSegments.length ? null : { params };\r\n}\r\n\r\n// \u2500\u2500\u2500 Specificity scoring \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 * Computes a specificity score for a route path.\r\n * Used to sort candidate routes so more specific routes shadow catch-alls.\r\n *\r\n * Higher score = more specific:\r\n * static segment 4\r\n * [dynamic] 3\r\n * [...catchAll] 2\r\n * [[...optCatchAll]] 1\r\n */\r\nexport function getRouteSpecificity(routePath: string): number {\r\n return routePath.split(path.sep).reduce((score, seg) => {\r\n if (seg.match(/^\\[\\[\\.\\.\\.(.+)\\]\\]$/)) return score + 1;\r\n if (seg.match(/^\\[\\.\\.\\.(.+)\\]$/)) return score + 2;\r\n if (seg.match(/^\\[(.+)\\]$/)) return score + 3;\r\n return score + 4; // static segment\r\n }, 0);\r\n}\r\n\r\n// \u2500\u2500\u2500 Route match result \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 RouteMatch {\r\n filePath: string;\r\n params: Record<string, string | string[]>;\r\n routePattern: string;\r\n}\r\n\r\n// \u2500\u2500\u2500 Path traversal guard \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 only when `filePath` is a descendant of `baseDir`.\r\n * Used to prevent URL path traversal attacks (e.g. /../../etc/passwd).\r\n */\r\nfunction isWithinBase(baseDir: string, filePath: string): boolean {\r\n const rel = path.relative(baseDir, filePath);\r\n return Boolean(rel) && !rel.startsWith('..') && !path.isAbsolute(rel);\r\n}\r\n\r\n// \u2500\u2500\u2500 Route matching \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Resolves a URL path to a route file inside `baseDir`.\r\n *\r\n * Steps:\r\n * 1. Reject '..' or '.' path segments (path traversal guard).\r\n * 2. Try an exact file match (e.g. /about \u2192 baseDir/about.tsx).\r\n * layout.tsx is explicitly excluded from exact matching.\r\n * 3. Sort all discovered routes by specificity (most specific first).\r\n * 4. Return the first dynamic route that matches.\r\n *\r\n * @param urlPath The URL path to match (e.g. '/users/42').\r\n * @param baseDir Absolute path to the directory containing route files.\r\n * @param extension File extension to look for ('.tsx' or '.ts').\r\n */\r\nexport function matchRoute(\r\n urlPath: string,\r\n baseDir: string,\r\n extension = '.tsx',\r\n): RouteMatch | null {\r\n // Split the URL path into segments, rejecting any that attempt path traversal.\r\n const rawSegments = urlPath === '/' ? [] : urlPath.slice(1).split('/');\r\n if (rawSegments.some(s => s === '..' || s === '.')) return null;\r\n\r\n // For the root URL, look for an index file.\r\n const segments = rawSegments.length === 0 ? ['index'] : rawSegments;\r\n\r\n // 1. Exact match: /about \u2192 about.tsx \u2014 never resolve to a layout file.\r\n const exactPath = path.join(baseDir, ...segments) + extension;\r\n const exactStem = path.basename(exactPath, extension);\r\n if (!isWithinBase(baseDir, exactPath)) return null;\r\n if (exactStem !== 'layout' && fs.existsSync(exactPath)) {\r\n return { filePath: exactPath, params: {}, routePattern: segments.join('/') };\r\n }\r\n\r\n // 2. Dynamic match \u2014 try routes sorted by specificity so '[id]' wins over '[...all]'.\r\n const sortedRoutes = findAllRoutes(baseDir).sort(\r\n (a, b) => getRouteSpecificity(b) - getRouteSpecificity(a),\r\n );\r\n\r\n for (const route of sortedRoutes) {\r\n const match = matchDynamicRoute(segments, route);\r\n if (!match) continue;\r\n const filePath = path.join(baseDir, route) + extension;\r\n if (!isWithinBase(baseDir, filePath)) continue;\r\n if (fs.existsSync(filePath)) {\r\n return { filePath, params: match.params, routePattern: route };\r\n }\r\n }\r\n\r\n return null;\r\n}\r\n\r\n// \u2500\u2500\u2500 Layout discovery \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Returns every layout.tsx file that wraps a given route file, in\r\n * outermost-first order (root layout first, nearest layout last).\r\n *\r\n * Layout chain example for app/pages/blog/[slug]/page.tsx:\r\n * app/pages/layout.tsx \u2190 root layout\r\n * app/pages/blog/layout.tsx \u2190 blog section layout\r\n *\r\n * The outermost-first order matches how wrapWithLayouts() nests them:\r\n * the last layout in the array is the innermost wrapper.\r\n */\r\nexport function findLayoutsForRoute(routeFilePath: string, pagesDir: string): string[] {\r\n const layouts: string[] = [];\r\n\r\n // Root layout wraps everything.\r\n const rootLayout = path.join(pagesDir, 'layout.tsx');\r\n if (fs.existsSync(rootLayout)) layouts.push(rootLayout);\r\n\r\n // Walk the directory hierarchy from pagesDir to the file's parent.\r\n const relativePath = path.relative(pagesDir, path.dirname(routeFilePath));\r\n if (!relativePath || relativePath === '.') return layouts;\r\n\r\n const segments = relativePath.split(path.sep).filter(s => s !== '.');\r\n for (let i = 1; i <= segments.length; i++) {\r\n const layoutPath = path.join(pagesDir, ...segments.slice(0, i), 'layout.tsx');\r\n if (fs.existsSync(layoutPath)) layouts.push(layoutPath);\r\n }\r\n\r\n return layouts;\r\n}"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/ssr.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* 1. Match the incoming URL to a page file in app/pages using the file-system
|
|
7
7
|
* router.
|
|
8
8
|
* 2. Discover the layout chain (layout.tsx files from root to the page dir).
|
|
9
|
-
* 3. Dynamically import the page and layout modules (always fresh via
|
|
9
|
+
* 3. Dynamically import the page and layout modules (always fresh via tsImport).
|
|
10
10
|
* 4. Walk the import tree to discover all "use client" components.
|
|
11
11
|
* 5. Render the wrapped element tree with the async renderer.
|
|
12
12
|
* 6. Flush the html-store (title, meta, link, script, style tags).
|
|
@@ -23,9 +23,15 @@
|
|
|
23
23
|
* HMR fast path:
|
|
24
24
|
* When the request URL contains `__hmr=1` (added by the HMR client during
|
|
25
25
|
* soft navigation), the renderer skips client-component renderToString
|
|
26
|
-
* (ctx.skipClientSSR = true). This
|
|
27
|
-
*
|
|
28
|
-
*
|
|
26
|
+
* (ctx.skipClientSSR = true). This speeds up HMR reloads because the
|
|
27
|
+
* client already has the DOM in place and only needs fresh server markup.
|
|
28
|
+
*
|
|
29
|
+
* Head tag sentinels:
|
|
30
|
+
* Every useHtml()-generated <meta>, <link>, <style>, and <script> tag is
|
|
31
|
+
* wrapped in <!--n-head-->…<!--/n-head--> comment sentinels. The client
|
|
32
|
+
* runtime uses these to diff and sync head tags on SPA navigation without
|
|
33
|
+
* touching permanent tags (charset, viewport, importmap, runtime script).
|
|
34
|
+
* Pages with no useHtml head tags emit no sentinels.
|
|
29
35
|
*/
|
|
30
36
|
import type { ServerResponse } from 'http';
|
|
31
37
|
/**
|
package/dist/ssr.js
CHANGED
|
@@ -73,6 +73,16 @@ function renderStyleTag(tag) {
|
|
|
73
73
|
const media = tag.media ? ` media="${escapeAttr(tag.media)}"` : "";
|
|
74
74
|
return ` <style${media}>${tag.content ?? ""}</style>`;
|
|
75
75
|
}
|
|
76
|
+
function renderManagedHeadTags(store) {
|
|
77
|
+
const tags = [
|
|
78
|
+
...store.meta.map(renderMetaTag),
|
|
79
|
+
...store.link.map(renderLinkTag),
|
|
80
|
+
...store.style.map(renderStyleTag),
|
|
81
|
+
...store.script.map(renderScriptTag)
|
|
82
|
+
];
|
|
83
|
+
if (tags.length === 0) return [];
|
|
84
|
+
return [" <!--n-head-->", ...tags, " <!--/n-head-->"];
|
|
85
|
+
}
|
|
76
86
|
async function serverSideRender(url, res, pagesDir, isDev = false) {
|
|
77
87
|
const skipClientSSR = url.includes("__hmr=1");
|
|
78
88
|
const cleanUrl = url.split("?")[0];
|
|
@@ -114,16 +124,11 @@ async function serverSideRender(url, res, pagesDir, isDev = false) {
|
|
|
114
124
|
' <meta charset="utf-8" />',
|
|
115
125
|
' <meta name="viewport" content="width=device-width, initial-scale=1" />',
|
|
116
126
|
` <title>${escapeAttr(pageTitle)}</title>`,
|
|
117
|
-
...store
|
|
118
|
-
...store.link.map(renderLinkTag),
|
|
119
|
-
...store.style.map(renderStyleTag),
|
|
120
|
-
...store.script.map(renderScriptTag)
|
|
127
|
+
...renderManagedHeadTags(store)
|
|
121
128
|
];
|
|
122
129
|
const runtimeData = JSON.stringify({
|
|
123
130
|
hydrateIds: [...ctx.hydrated],
|
|
124
|
-
// only components actually rendered
|
|
125
131
|
allIds: [...registry.keys()],
|
|
126
|
-
// all reachable — pre-load for SPA nav
|
|
127
132
|
url,
|
|
128
133
|
params,
|
|
129
134
|
debug: toClientDebugLevel(getDebugLevel())
|
package/dist/ssr.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/ssr.ts"],
|
|
4
|
-
"sourcesContent": ["/**\r\n * ssr.ts \u2014 Server-Side Rendering Pipeline (Dev Mode)\r\n *\r\n * Handles the full page render cycle for `nuke dev`:\r\n *\r\n * 1. Match the incoming URL to a page file in app/pages using the file-system\r\n * router.\r\n * 2. Discover the layout chain (layout.tsx files from root to the page dir).\r\n * 3. Dynamically import the page and layout modules (always fresh via ?t=Date.now()).\r\n * 4. Walk the import tree to discover all \"use client\" components.\r\n * 5. Render the wrapped element tree with the async renderer.\r\n * 6. Flush the html-store (title, meta, link, script, style tags).\r\n * 7. Assemble and send the full HTML document including:\r\n * - The rendered app HTML\r\n * - An importmap pointing react/nukejs to the bundled versions\r\n * - A __n_data JSON blob with hydration IDs and runtime config\r\n * - An inline bootstrap <script> that calls initRuntime()\r\n * - (dev only) A /__hmr.js script for Hot Module Replacement\r\n *\r\n * In production, a pre-built standalone handler (generated by build-common.ts)\r\n * handles each page without dynamic imports or file-system access.\r\n *\r\n * HMR fast path:\r\n * When the request URL contains `__hmr=1` (added by the HMR client during\r\n * soft navigation), the renderer skips client-component renderToString\r\n * (ctx.skipClientSSR = true). This significantly speeds up HMR reloads\r\n * because the client already has the DOM in place and only needs the new\r\n * server-rendered markup.\r\n */\r\n\r\nimport path from 'path';\r\nimport { createElement } from 'react';\r\nimport { pathToFileURL } from 'url';\r\nimport { tsImport } from 'tsx/esm/api';\r\nimport type { ServerResponse } from 'http';\r\nimport { log, getDebugLevel, type DebugLevel } from './logger';\r\nimport { matchRoute, findLayoutsForRoute } from './router';\r\nimport { findClientComponentsInTree } from './component-analyzer';\r\nimport { renderElementToHtml, type RenderContext } from './renderer';\r\nimport {\r\n runWithHtmlStore,\r\n resolveTitle,\r\n type HtmlStore,\r\n type HtmlAttrs,\r\n type BodyAttrs,\r\n type MetaTag,\r\n type LinkTag,\r\n type ScriptTag,\r\n type StyleTag,\r\n} from './html-store';\r\n\r\n// \u2500\u2500\u2500 Layout wrapping \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Wraps a page element in its layout chain, outermost-first.\r\n * Each layout receives `{ children: innerElement }` as props.\r\n *\r\n * Cache-busting (?t=Date.now()) forces Node to re-import the module on every\r\n * request in dev, so editing a layout is reflected immediately.\r\n */\r\nasync function wrapWithLayouts(pageElement: any, layoutPaths: string[]): Promise<any> {\r\n let element = pageElement;\r\n // Iterate in reverse so the outermost layout wraps last (becomes the root).\r\n for (let i = layoutPaths.length - 1; i >= 0; i--) {\r\n const { default: LayoutComponent } = await tsImport(\r\n pathToFileURL(layoutPaths[i]).href,\r\n { parentURL: import.meta.url },\r\n );\r\n element = createElement(LayoutComponent, { children: element });\r\n }\r\n return element;\r\n}\r\n\r\n// \u2500\u2500\u2500 Debug level conversion \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Converts the server-side DebugLevel to the string format expected by the\r\n * browser's makeLogger() function in bundle.ts.\r\n */\r\nfunction toClientDebugLevel(level: DebugLevel): string {\r\n if (level === true) return 'verbose';\r\n if (level === 'info') return 'info';\r\n if (level === 'error') return 'error';\r\n return 'silent';\r\n}\r\n\r\n// \u2500\u2500\u2500 HTML attribute serialization \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nfunction escapeAttr(str: string): string {\r\n return str.replace(/&/g, '&').replace(/\"/g, '"');\r\n}\r\n\r\n/** Serializes an attribute map to `key=\"value\" \u2026`, omitting undefined/false values. */\r\nfunction renderAttrs(attrs: Record<string, string | boolean | undefined>): string {\r\n return Object.entries(attrs)\r\n .filter(([, v]) => v !== undefined && v !== false)\r\n .map(([k, v]) => v === true ? k : `${k}=\"${escapeAttr(String(v))}\"`)\r\n .join(' ');\r\n}\r\n\r\n/** Returns `<tag attrs>` or `<tag>` depending on whether attrs are non-empty. */\r\nfunction openTag(tag: string, attrs: Record<string, string | undefined>): string {\r\n const str = renderAttrs(attrs);\r\n return str ? `<${tag} ${str}>` : `<${tag}>`;\r\n}\r\n\r\n// \u2500\u2500\u2500 Head tag renderers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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/** httpEquiv \u2192 http-equiv for proper HTML attribute serialization. */\r\nfunction metaKey(k: string): string {\r\n return k === 'httpEquiv' ? 'http-equiv' : k;\r\n}\r\n\r\n/** hrefLang \u2192 hreflang, crossOrigin \u2192 crossorigin. */\r\nfunction linkKey(k: string): string {\r\n if (k === 'hrefLang') return 'hreflang';\r\n if (k === 'crossOrigin') return 'crossorigin';\r\n return k;\r\n}\r\n\r\nfunction renderMetaTag(tag: MetaTag): string {\r\n const attrs: Record<string, string | undefined> = {};\r\n for (const [k, v] of Object.entries(tag)) if (v !== undefined) attrs[metaKey(k)] = v;\r\n return ` <meta ${renderAttrs(attrs)} />`;\r\n}\r\n\r\nfunction renderLinkTag(tag: LinkTag): string {\r\n const attrs: Record<string, string | undefined> = {};\r\n for (const [k, v] of Object.entries(tag)) if (v !== undefined) attrs[linkKey(k)] = v;\r\n return ` <link ${renderAttrs(attrs)} />`;\r\n}\r\n\r\nfunction renderScriptTag(tag: ScriptTag): string {\r\n const attrs: Record<string, string | boolean | undefined> = {\r\n src: tag.src,\r\n type: tag.type,\r\n crossorigin: tag.crossOrigin,\r\n integrity: tag.integrity,\r\n defer: tag.defer,\r\n async: tag.async,\r\n nomodule: tag.noModule,\r\n };\r\n const attrStr = renderAttrs(attrs);\r\n const open = attrStr ? `<script ${attrStr}>` : '<script>';\r\n return ` ${open}${tag.src ? '' : (tag.content ?? '')}</script>`;\r\n}\r\n\r\nfunction renderStyleTag(tag: StyleTag): string {\r\n const media = tag.media ? ` media=\"${escapeAttr(tag.media)}\"` : '';\r\n return ` <style${media}>${tag.content ?? ''}</style>`;\r\n}\r\n\r\n// \u2500\u2500\u2500 Main SSR handler \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Renders a page for the given URL and writes the full HTML response.\r\n *\r\n * @param url The raw request URL (may include query string).\r\n * @param res Node ServerResponse to write to.\r\n * @param pagesDir Absolute path to the app/pages directory.\r\n * @param isDev When true, injects the HMR client script into the page.\r\n */\r\nexport async function serverSideRender(\r\n url: string,\r\n res: ServerResponse,\r\n pagesDir: string,\r\n isDev = false,\r\n): Promise<void> {\r\n // Strip __hmr=1 flag from the URL before routing; set skipClientSSR accordingly.\r\n const skipClientSSR = url.includes('__hmr=1');\r\n const cleanUrl = url.split('?')[0];\r\n\r\n // \u2500\u2500 Route resolution \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 routeMatch = matchRoute(cleanUrl, pagesDir);\r\n if (!routeMatch) {\r\n log.verbose(`No route found for: ${url}`);\r\n res.statusCode = 404;\r\n res.end('Page not found');\r\n return;\r\n }\r\n\r\n const { filePath, params, routePattern } = routeMatch;\r\n log.verbose(`SSR ${cleanUrl} -> ${path.relative(process.cwd(), filePath)}`);\r\n\r\n // \u2500\u2500 Module import \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 // ?t=Date.now() bypasses Node's module cache so edits are reflected immediately.\r\n const layoutPaths = findLayoutsForRoute(filePath, pagesDir);\r\n // tsImport creates a fresh isolated module namespace per call \u2014 all static\r\n // imports within the page (layouts, server components, utilities) are loaded\r\n // fresh from disk, bypassing Node's ESM module cache entirely.\r\n const { default: PageComponent } = await tsImport(\r\n pathToFileURL(filePath).href,\r\n { parentURL: import.meta.url },\r\n );\r\n const wrappedElement = await wrapWithLayouts(\r\n createElement(PageComponent, params),\r\n layoutPaths,\r\n );\r\n\r\n // \u2500\u2500 Client component discovery \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n // Walk the import tree for both the page and its layout chain to collect every\r\n // \"use client\" component that might be rendered.\r\n const registry = new Map<string, string>();\r\n for (const [id, p] of findClientComponentsInTree(filePath, pagesDir))\r\n registry.set(id, p);\r\n for (const layoutPath of layoutPaths)\r\n for (const [id, p] of findClientComponentsInTree(layoutPath, pagesDir))\r\n registry.set(id, p);\r\n\r\n log.verbose(\r\n `Page ${routePattern}: found ${registry.size} client component(s)`,\r\n `[${[...registry.keys()].join(', ')}]`,\r\n );\r\n\r\n // \u2500\u2500 Rendering \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 ctx: RenderContext = { registry, hydrated: new Set(), skipClientSSR };\r\n\r\n let appHtml = '';\r\n const store: HtmlStore = await runWithHtmlStore(async () => {\r\n appHtml = await renderElementToHtml(wrappedElement, ctx);\r\n });\r\n\r\n // \u2500\u2500 Head assembly \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 pageTitle = resolveTitle(store.titleOps, 'SSR App');\r\n\r\n const headLines: string[] = [\r\n ' <meta charset=\"utf-8\" />',\r\n ' <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />',\r\n ` <title>${escapeAttr(pageTitle)}</title>`,\r\n ...store.meta.map(renderMetaTag),\r\n ...store.link.map(renderLinkTag),\r\n ...store.style.map(renderStyleTag),\r\n ...store.script.map(renderScriptTag),\r\n ];\r\n\r\n // \u2500\u2500 Runtime data blob \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 // Escape `</script>` so the embedded JSON cannot break out of the script tag.\r\n // JSON parsers accept Unicode escapes transparently.\r\n const runtimeData = JSON.stringify({\r\n hydrateIds: [...ctx.hydrated], // only components actually rendered\r\n allIds: [...registry.keys()], // all reachable \u2014 pre-load for SPA nav\r\n url,\r\n params,\r\n debug: toClientDebugLevel(getDebugLevel()),\r\n })\r\n .replace(/</g, '\\\\u003c')\r\n .replace(/>/g, '\\\\u003e')\r\n .replace(/&/g, '\\\\u0026');\r\n\r\n // \u2500\u2500 Full document \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 html = `<!DOCTYPE html>\r\n${openTag('html', store.htmlAttrs)}\r\n<head>\r\n${headLines.join('\\n')}\r\n</head>\r\n${openTag('body', store.bodyAttrs)}\r\n <div id=\"app\">${appHtml}</div>\r\n\r\n <script id=\"__n_data\" type=\"application/json\">${runtimeData}</script>\r\n\r\n <script type=\"importmap\">\r\n{\r\n \"imports\": {\r\n \"react\": \"/__react.js\",\r\n \"react-dom/client\": \"/__react.js\",\r\n \"react/jsx-runtime\": \"/__react.js\",\r\n \"nukejs\": \"/__n.js\"\r\n }\r\n}\r\n </script>\r\n\r\n <script type=\"module\">\r\n await import('react');\r\n const { initRuntime } = await import('nukejs');\r\n const data = JSON.parse(document.getElementById('__n_data').textContent);\r\n initRuntime(data);\r\n </script>\r\n\r\n ${isDev ? '<script type=\"module\" src=\"/__hmr.js\"></script>' : ''}\r\n</body>\r\n</html>`;\r\n\r\n res.setHeader('Content-Type', 'text/html');\r\n res.end(html);\r\n}"],
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["/**\r\n * ssr.ts \u2014 Server-Side Rendering Pipeline (Dev Mode)\r\n *\r\n * Handles the full page render cycle for `nuke dev`:\r\n *\r\n * 1. Match the incoming URL to a page file in app/pages using the file-system\r\n * router.\r\n * 2. Discover the layout chain (layout.tsx files from root to the page dir).\r\n * 3. Dynamically import the page and layout modules (always fresh via tsImport).\r\n * 4. Walk the import tree to discover all \"use client\" components.\r\n * 5. Render the wrapped element tree with the async renderer.\r\n * 6. Flush the html-store (title, meta, link, script, style tags).\r\n * 7. Assemble and send the full HTML document including:\r\n * - The rendered app HTML\r\n * - An importmap pointing react/nukejs to the bundled versions\r\n * - A __n_data JSON blob with hydration IDs and runtime config\r\n * - An inline bootstrap <script> that calls initRuntime()\r\n * - (dev only) A /__hmr.js script for Hot Module Replacement\r\n *\r\n * In production, a pre-built standalone handler (generated by build-common.ts)\r\n * handles each page without dynamic imports or file-system access.\r\n *\r\n * HMR fast path:\r\n * When the request URL contains `__hmr=1` (added by the HMR client during\r\n * soft navigation), the renderer skips client-component renderToString\r\n * (ctx.skipClientSSR = true). This speeds up HMR reloads because the\r\n * client already has the DOM in place and only needs fresh server markup.\r\n *\r\n * Head tag sentinels:\r\n * Every useHtml()-generated <meta>, <link>, <style>, and <script> tag is\r\n * wrapped in <!--n-head-->\u2026<!--/n-head--> comment sentinels. The client\r\n * runtime uses these to diff and sync head tags on SPA navigation without\r\n * touching permanent tags (charset, viewport, importmap, runtime script).\r\n * Pages with no useHtml head tags emit no sentinels.\r\n */\r\n\r\nimport path from 'path';\r\nimport { createElement } from 'react';\r\nimport { pathToFileURL } from 'url';\r\nimport { tsImport } from 'tsx/esm/api';\r\nimport type { ServerResponse } from 'http';\r\nimport { log, getDebugLevel, type DebugLevel } from './logger';\r\nimport { matchRoute, findLayoutsForRoute } from './router';\r\nimport { findClientComponentsInTree } from './component-analyzer';\r\nimport { renderElementToHtml, type RenderContext } from './renderer';\r\nimport {\r\n runWithHtmlStore,\r\n resolveTitle,\r\n type HtmlStore,\r\n type HtmlAttrs,\r\n type BodyAttrs,\r\n type MetaTag,\r\n type LinkTag,\r\n type ScriptTag,\r\n type StyleTag,\r\n} from './html-store';\r\n\r\n// \u2500\u2500\u2500 Layout wrapping \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Wraps a page element in its layout chain, outermost-first.\r\n * Each layout receives `{ children: innerElement }` as props.\r\n *\r\n * tsImport creates a fresh isolated module namespace per call so edits to\r\n * layouts are reflected immediately without a server restart.\r\n */\r\nasync function wrapWithLayouts(pageElement: any, layoutPaths: string[]): Promise<any> {\r\n let element = pageElement;\r\n // Iterate in reverse so the outermost layout wraps last (becomes the root).\r\n for (let i = layoutPaths.length - 1; i >= 0; i--) {\r\n const { default: LayoutComponent } = await tsImport(\r\n pathToFileURL(layoutPaths[i]).href,\r\n { parentURL: import.meta.url },\r\n );\r\n element = createElement(LayoutComponent, { children: element });\r\n }\r\n return element;\r\n}\r\n\r\n// \u2500\u2500\u2500 Debug level conversion \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Converts the server-side DebugLevel to the string format expected by the\r\n * browser's makeLogger() function in bundle.ts.\r\n */\r\nfunction toClientDebugLevel(level: DebugLevel): string {\r\n if (level === true) return 'verbose';\r\n if (level === 'info') return 'info';\r\n if (level === 'error') return 'error';\r\n return 'silent';\r\n}\r\n\r\n// \u2500\u2500\u2500 HTML serialization 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\r\n\r\nfunction escapeAttr(str: string): string {\r\n return str.replace(/&/g, '&').replace(/\"/g, '"');\r\n}\r\n\r\n/** Serializes an attribute map to `key=\"value\" \u2026`, omitting undefined/false values. */\r\nfunction renderAttrs(attrs: Record<string, string | boolean | undefined>): string {\r\n return Object.entries(attrs)\r\n .filter(([, v]) => v !== undefined && v !== false)\r\n .map(([k, v]) => v === true ? k : `${k}=\"${escapeAttr(String(v))}\"`)\r\n .join(' ');\r\n}\r\n\r\n/** Returns `<tag attrs>` or `<tag>` depending on whether attrs are non-empty. */\r\nfunction openTag(tag: string, attrs: Record<string, string | undefined>): string {\r\n const str = renderAttrs(attrs);\r\n return str ? `<${tag} ${str}>` : `<${tag}>`;\r\n}\r\n\r\n// \u2500\u2500\u2500 Head tag renderers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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\nfunction metaKey(k: string): string {\r\n return k === 'httpEquiv' ? 'http-equiv' : k;\r\n}\r\n\r\nfunction linkKey(k: string): string {\r\n if (k === 'hrefLang') return 'hreflang';\r\n if (k === 'crossOrigin') return 'crossorigin';\r\n return k;\r\n}\r\n\r\nfunction renderMetaTag(tag: MetaTag): string {\r\n const attrs: Record<string, string | undefined> = {};\r\n for (const [k, v] of Object.entries(tag)) if (v !== undefined) attrs[metaKey(k)] = v;\r\n return ` <meta ${renderAttrs(attrs)} />`;\r\n}\r\n\r\nfunction renderLinkTag(tag: LinkTag): string {\r\n const attrs: Record<string, string | undefined> = {};\r\n for (const [k, v] of Object.entries(tag)) if (v !== undefined) attrs[linkKey(k)] = v;\r\n return ` <link ${renderAttrs(attrs)} />`;\r\n}\r\n\r\nfunction renderScriptTag(tag: ScriptTag): string {\r\n const attrs: Record<string, string | boolean | undefined> = {\r\n src: tag.src,\r\n type: tag.type,\r\n crossorigin: tag.crossOrigin,\r\n integrity: tag.integrity,\r\n defer: tag.defer,\r\n async: tag.async,\r\n nomodule: tag.noModule,\r\n };\r\n const attrStr = renderAttrs(attrs);\r\n const open = attrStr ? `<script ${attrStr}>` : '<script>';\r\n return ` ${open}${tag.src ? '' : (tag.content ?? '')}</script>`;\r\n}\r\n\r\nfunction renderStyleTag(tag: StyleTag): string {\r\n const media = tag.media ? ` media=\"${escapeAttr(tag.media)}\"` : '';\r\n return ` <style${media}>${tag.content ?? ''}</style>`;\r\n}\r\n\r\n/**\r\n * Renders all useHtml()-sourced head tags wrapped in <!--n-head--> sentinels.\r\n * Returns an empty array when none of the stores have any tags (no sentinels\r\n * emitted for pages that don't call useHtml).\r\n */\r\nfunction renderManagedHeadTags(store: HtmlStore): string[] {\r\n const tags = [\r\n ...store.meta.map(renderMetaTag),\r\n ...store.link.map(renderLinkTag),\r\n ...store.style.map(renderStyleTag),\r\n ...store.script.map(renderScriptTag),\r\n ];\r\n if (tags.length === 0) return [];\r\n return [' <!--n-head-->', ...tags, ' <!--/n-head-->'];\r\n}\r\n\r\n// \u2500\u2500\u2500 Main SSR handler \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Renders a page for the given URL and writes the full HTML response.\r\n *\r\n * @param url The raw request URL (may include query string).\r\n * @param res Node ServerResponse to write to.\r\n * @param pagesDir Absolute path to the app/pages directory.\r\n * @param isDev When true, injects the HMR client script into the page.\r\n */\r\nexport async function serverSideRender(\r\n url: string,\r\n res: ServerResponse,\r\n pagesDir: string,\r\n isDev = false,\r\n): Promise<void> {\r\n const skipClientSSR = url.includes('__hmr=1');\r\n const cleanUrl = url.split('?')[0];\r\n\r\n // \u2500\u2500 Route resolution \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 routeMatch = matchRoute(cleanUrl, pagesDir);\r\n if (!routeMatch) {\r\n log.verbose(`No route found for: ${url}`);\r\n res.statusCode = 404;\r\n res.end('Page not found');\r\n return;\r\n }\r\n\r\n const { filePath, params, routePattern } = routeMatch;\r\n log.verbose(`SSR ${cleanUrl} -> ${path.relative(process.cwd(), filePath)}`);\r\n\r\n // \u2500\u2500 Module import \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 // tsImport bypasses Node's ESM module cache entirely so edits are reflected\r\n // immediately on every request in dev.\r\n const layoutPaths = findLayoutsForRoute(filePath, pagesDir);\r\n const { default: PageComponent } = await tsImport(\r\n pathToFileURL(filePath).href,\r\n { parentURL: import.meta.url },\r\n );\r\n const wrappedElement = await wrapWithLayouts(\r\n createElement(PageComponent, params),\r\n layoutPaths,\r\n );\r\n\r\n // \u2500\u2500 Client component discovery \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n // Walk the import tree for both the page and its layout chain.\r\n const registry = new Map<string, string>();\r\n for (const [id, p] of findClientComponentsInTree(filePath, pagesDir))\r\n registry.set(id, p);\r\n for (const layoutPath of layoutPaths)\r\n for (const [id, p] of findClientComponentsInTree(layoutPath, pagesDir))\r\n registry.set(id, p);\r\n\r\n log.verbose(\r\n `Page ${routePattern}: found ${registry.size} client component(s)`,\r\n `[${[...registry.keys()].join(', ')}]`,\r\n );\r\n\r\n // \u2500\u2500 Rendering \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 ctx: RenderContext = { registry, hydrated: new Set(), skipClientSSR };\r\n\r\n let appHtml = '';\r\n const store: HtmlStore = await runWithHtmlStore(async () => {\r\n appHtml = await renderElementToHtml(wrappedElement, ctx);\r\n });\r\n\r\n // \u2500\u2500 Head assembly \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 pageTitle = resolveTitle(store.titleOps, 'SSR App');\r\n\r\n const headLines: string[] = [\r\n ' <meta charset=\"utf-8\" />',\r\n ' <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />',\r\n ` <title>${escapeAttr(pageTitle)}</title>`,\r\n ...renderManagedHeadTags(store),\r\n ];\r\n\r\n // \u2500\u2500 Runtime data blob \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 // Escape </script> sequences so the JSON cannot break out of the script tag.\r\n const runtimeData = JSON.stringify({\r\n hydrateIds: [...ctx.hydrated],\r\n allIds: [...registry.keys()],\r\n url,\r\n params,\r\n debug: toClientDebugLevel(getDebugLevel()),\r\n })\r\n .replace(/</g, '\\\\u003c')\r\n .replace(/>/g, '\\\\u003e')\r\n .replace(/&/g, '\\\\u0026');\r\n\r\n // \u2500\u2500 Full document \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 html = `<!DOCTYPE html>\r\n${openTag('html', store.htmlAttrs)}\r\n<head>\r\n${headLines.join('\\n')}\r\n</head>\r\n${openTag('body', store.bodyAttrs)}\r\n <div id=\"app\">${appHtml}</div>\r\n\r\n <script id=\"__n_data\" type=\"application/json\">${runtimeData}</script>\r\n\r\n <script type=\"importmap\">\r\n{\r\n \"imports\": {\r\n \"react\": \"/__react.js\",\r\n \"react-dom/client\": \"/__react.js\",\r\n \"react/jsx-runtime\": \"/__react.js\",\r\n \"nukejs\": \"/__n.js\"\r\n }\r\n}\r\n </script>\r\n\r\n <script type=\"module\">\r\n await import('react');\r\n const { initRuntime } = await import('nukejs');\r\n const data = JSON.parse(document.getElementById('__n_data').textContent);\r\n initRuntime(data);\r\n </script>\r\n\r\n ${isDev ? '<script type=\"module\" src=\"/__hmr.js\"></script>' : ''}\r\n</body>\r\n</html>`;\r\n\r\n res.setHeader('Content-Type', 'text/html');\r\n res.end(html);\r\n}"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nukejs",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "A minimal, opinionated full-stack React framework on Node.js that server-renders everything and hydrates only interactive parts.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|