nukejs 0.0.7 → 0.0.9
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/README.md +2 -1
- package/dist/app.d.ts +3 -2
- package/dist/app.js +3 -13
- package/dist/app.js.map +2 -2
- package/dist/build-node.d.ts +1 -1
- package/dist/build-node.js +6 -17
- package/dist/build-node.js.map +2 -2
- package/dist/build-vercel.js +1 -1
- package/dist/build-vercel.js.map +2 -2
- package/dist/http-server.d.ts +2 -9
- package/dist/http-server.js +9 -2
- package/dist/http-server.js.map +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
[](https://nukejs.com)
|
|
2
2
|
|
|
3
|
-
# NukeJS
|
|
3
|
+
# NukeJS  [](https://nukejs.com) [<img src="https://developer.stackblitz.com/img/open_in_stackblitz.svg" height="20">](https://stackblitz.com/edit/nuke?file=app/pages/index.tsx)
|
|
4
|
+
|
|
4
5
|
|
|
5
6
|
A **minimal**, opinionated full-stack React framework on Node.js that server-renders everything and hydrates only interactive parts.
|
|
6
7
|
|
package/dist/app.d.ts
CHANGED
|
@@ -5,12 +5,13 @@
|
|
|
5
5
|
* 1. Loads your nuke.config.ts (or uses sensible defaults)
|
|
6
6
|
* 2. Discovers API route prefixes from your server directory
|
|
7
7
|
* 3. Starts an HTTP server that handles:
|
|
8
|
+
* app/public/** — static files (highest priority, via middleware)
|
|
8
9
|
* /__hmr_ping — heartbeat for HMR reconnect polling
|
|
9
10
|
* /__react.js — bundled React + ReactDOM (resolved via importmap)
|
|
10
11
|
* /__n.js — NukeJS client runtime bundle
|
|
11
12
|
* /__client-component/* — on-demand "use client" component bundles
|
|
12
|
-
*
|
|
13
|
-
* /** — SSR pages from app/pages
|
|
13
|
+
* server/** — API route handlers from serverDir
|
|
14
|
+
* /** — SSR pages from app/pages (lowest priority)
|
|
14
15
|
* 4. Watches for file changes and broadcasts HMR events to connected browsers
|
|
15
16
|
*
|
|
16
17
|
* In production (ENVIRONMENT=production), HMR and all file watching are skipped.
|
package/dist/app.js
CHANGED
|
@@ -26,17 +26,7 @@ log.info(` - Debug level: ${String(getDebugLevel())}`);
|
|
|
26
26
|
log.info(` - Dev mode: ${String(isDev)}`);
|
|
27
27
|
if (isDev) watchDir(path.resolve("./app"), "App");
|
|
28
28
|
const apiPrefixes = discoverApiPrefixes(SERVER_DIR);
|
|
29
|
-
const handleApiRoute = createApiHandler({ apiPrefixes, port: PORT });
|
|
30
|
-
if (isDev && existsSync(SERVER_DIR)) {
|
|
31
|
-
watch(SERVER_DIR, { recursive: true }, (_event, filename) => {
|
|
32
|
-
if (!filename) return;
|
|
33
|
-
const ext = path.extname(filename);
|
|
34
|
-
if (ext !== ".ts" && ext !== ".tsx") return;
|
|
35
|
-
const fresh = discoverApiPrefixes(SERVER_DIR);
|
|
36
|
-
apiPrefixes.splice(0, apiPrefixes.length, ...fresh);
|
|
37
|
-
log.info("[Server] Routes updated (" + fresh.length + " prefix" + (fresh.length === 1 ? "" : "es") + ")");
|
|
38
|
-
});
|
|
39
|
-
}
|
|
29
|
+
const handleApiRoute = createApiHandler({ apiPrefixes, port: PORT, isDev });
|
|
40
30
|
log.info(`API prefixes discovered: ${apiPrefixes.length === 0 ? "none" : ""}`);
|
|
41
31
|
apiPrefixes.forEach((p) => {
|
|
42
32
|
log.info(` - ${p.prefix || "/"} -> ${path.relative(process.cwd(), p.directory)}`);
|
|
@@ -62,8 +52,6 @@ const server = http.createServer(async (req, res) => {
|
|
|
62
52
|
const middlewareHandled = await runMiddleware(req, res);
|
|
63
53
|
if (middlewareHandled) return;
|
|
64
54
|
const url = req.url || "/";
|
|
65
|
-
if (matchApiPrefix(url, apiPrefixes))
|
|
66
|
-
return await handleApiRoute(url, req, res);
|
|
67
55
|
if (url === "/__hmr_ping") {
|
|
68
56
|
res.setHeader("Content-Type", "text/plain");
|
|
69
57
|
res.end("ok");
|
|
@@ -78,6 +66,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
78
66
|
url.slice(20).split("?")[0].replace(".js", ""),
|
|
79
67
|
res
|
|
80
68
|
);
|
|
69
|
+
if (matchApiPrefix(url, apiPrefixes))
|
|
70
|
+
return await handleApiRoute(url, req, res);
|
|
81
71
|
return await serverSideRender(url, res, PAGES_DIR, isDev);
|
|
82
72
|
} catch (error) {
|
|
83
73
|
log.error("Server error:", error);
|
package/dist/app.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/app.ts"],
|
|
4
|
-
"sourcesContent": ["/**\r\n * app.ts \u2014 NukeJS Dev Server Entry Point\r\n *\r\n * This is the runtime that powers `nuke dev`. It:\r\n * 1. Loads your nuke.config.ts (or uses sensible defaults)\r\n * 2. Discovers API route prefixes from your server directory\r\n * 3. Starts an HTTP server that handles:\r\n * /__hmr_ping \u2014 heartbeat for HMR reconnect polling\r\n * /__react.js \u2014 bundled React + ReactDOM (resolved via importmap)\r\n * /__n.js \u2014 NukeJS client runtime bundle\r\n * /__client-component/* \u2014 on-demand \"use client\" component bundles\r\n * /api/** \u2014 API route handlers from serverDir\r\n * /** \u2014 SSR pages from app/pages\r\n * 4. Watches for file changes and broadcasts HMR events to connected browsers\r\n *\r\n * In production (ENVIRONMENT=production), HMR and all file watching are skipped.\r\n */\r\n\r\nimport http from 'http';\r\nimport path from 'path';\r\nimport { existsSync, watch } from 'fs';\r\n\r\nimport { ansi, c, log, setDebugLevel, getDebugLevel } from './logger';\r\nimport { loadConfig } from './config';\r\nimport { discoverApiPrefixes, matchApiPrefix, createApiHandler } from './http-server';\r\nimport { loadMiddleware, runMiddleware } from './middleware-loader';\r\nimport { serveReactBundle, serveNukeBundle, serveClientComponentBundle } from './bundler';\r\nimport { serverSideRender } from './ssr';\r\nimport { watchDir, broadcastRestart } from './hmr';\r\n\r\n// \u2500\u2500\u2500 Environment \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 isDev = process.env.ENVIRONMENT !== 'production';\r\n\r\n// React must live on globalThis so dynamically-imported page modules can share\r\n// the same React instance without each bundling their own copy.\r\nif (isDev) {\r\n const React = await import('react');\r\n (global as any).React = React;\r\n}\r\n\r\n// \u2500\u2500\u2500 Config & paths \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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\nsetDebugLevel(config.debug ?? false);\r\n\r\nconst PAGES_DIR = path.resolve('./app/pages');\r\nconst SERVER_DIR = path.resolve(config.serverDir);\r\nconst PORT = config.port;\r\n\r\nlog.info('Configuration loaded:');\r\nlog.info(` - Pages directory: ${PAGES_DIR}`);\r\nlog.info(` - Server directory: ${SERVER_DIR}`);\r\nlog.info(` - Port: ${PORT}`);\r\nlog.info(` - Debug level: ${String(getDebugLevel())}`);\r\nlog.info(` - Dev mode: ${String(isDev)}`);\r\n\r\n// \u2500\u2500\u2500 API route discovery \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n// Start watching the app directory for HMR.\r\nif (isDev) watchDir(path.resolve('./app'), 'App');\r\n\r\n// apiPrefixes is a live, mutable array. In dev, we splice it in-place whenever\r\n// the server directory changes so handlers always see the latest routes without\r\n// a full restart.\r\nconst apiPrefixes = discoverApiPrefixes(SERVER_DIR);\r\nconst handleApiRoute = createApiHandler({ apiPrefixes, port: PORT });\r\n\r\nif (isDev && existsSync(SERVER_DIR)) {\r\n watch(SERVER_DIR, { recursive: true }, (_event, filename) => {\r\n if (!filename) return;\r\n\r\n // Only react to TypeScript source changes, not compiled output or assets.\r\n const ext = path.extname(filename);\r\n if (ext !== '.ts' && ext !== '.tsx') return;\r\n\r\n const fresh = discoverApiPrefixes(SERVER_DIR);\r\n apiPrefixes.splice(0, apiPrefixes.length, ...fresh);\r\n log.info('[Server] Routes updated (' + fresh.length + ' prefix' + (fresh.length === 1 ? '' : 'es') + ')');\r\n });\r\n}\r\n\r\nlog.info(`API prefixes discovered: ${apiPrefixes.length === 0 ? 'none' : ''}`);\r\napiPrefixes.forEach(p => {\r\n log.info(` - ${p.prefix || '/'} -> ${path.relative(process.cwd(), p.directory)}`);\r\n});\r\n\r\n// \u2500\u2500\u2500 Full-restart file watchers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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// Some changes can't be hot-patched: middleware exports change the request\r\n// pipeline, and nuke.config.ts may change the port or serverDir. On change we\r\n// broadcast a 'restart' SSE event so browsers reconnect automatically, then\r\n// exit with code 75 \u2014 the CLI watches for this to respawn the process.\r\nif (isDev) {\r\n const RESTART_EXIT_CODE = 75;\r\n const restartFiles = [\r\n path.resolve('./middleware.ts'),\r\n path.resolve('./nuke.config.ts'),\r\n ];\r\n\r\n for (const filePath of restartFiles) {\r\n if (!existsSync(filePath)) continue;\r\n watch(filePath, async () => {\r\n log.info(`[Server] ${path.basename(filePath)} changed \u2014 restarting...`);\r\n await broadcastRestart();\r\n process.exit(RESTART_EXIT_CODE);\r\n });\r\n }\r\n}\r\n\r\n// \u2500\u2500\u2500 Middleware \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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// Loads built-in middleware (HMR SSE/JS endpoints) and the user-supplied\r\n// middleware.ts from the project root (if it exists).\r\nawait loadMiddleware();\r\n\r\n// \u2500\u2500\u2500 Request handler \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nconst server = http.createServer(async (req, res) => {\r\n try {\r\n // Middleware runs first. If it calls res.end() the request is fully\r\n // handled and we bail out immediately.\r\n const middlewareHandled = await runMiddleware(req, res);\r\n if (middlewareHandled) return;\r\n\r\n const url = req.url || '/';\r\n\r\n // API routes \u2014 prefixes discovered from serverDir take priority over pages.\r\n if (matchApiPrefix(url, apiPrefixes))\r\n return await handleApiRoute(url, req, res);\r\n\r\n // \u2500\u2500 Internal NukeJS routes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n // Heartbeat polled by the HMR client to know when the server is back up.\r\n if (url === '/__hmr_ping') {\r\n res.setHeader('Content-Type', 'text/plain');\r\n res.end('ok');\r\n return;\r\n }\r\n\r\n // Unified React bundle (react + react-dom/client + react/jsx-runtime).\r\n // Resolved by the importmap injected into every SSR page, so client\r\n // components never bundle React themselves.\r\n if (url === '/__react.js')\r\n return await serveReactBundle(res);\r\n\r\n // NukeJS browser runtime: initRuntime, SPA navigation, partial hydration.\r\n if (url === '/__n.js')\r\n return await serveNukeBundle(res);\r\n\r\n // On-demand bundles for individual \"use client\" components.\r\n // Strip the prefix, the .js extension, and any query string (cache buster).\r\n if (url.startsWith('/__client-component/'))\r\n return await serveClientComponentBundle(\r\n url.slice(20).split('?')[0].replace('.js', ''),\r\n res,\r\n );\r\n\r\n // \u2500\u2500 Page SSR \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 // No API prefix matched \u2014 render a page from app/pages.\r\n return await serverSideRender(url, res, PAGES_DIR, isDev);\r\n\r\n } catch (error) {\r\n log.error('Server error:', error);\r\n res.statusCode = 500;\r\n res.end('Internal server error');\r\n }\r\n});\r\n\r\n// \u2500\u2500\u2500 Port binding \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 * Tries to listen on `port`. If the port is already in use (EADDRINUSE),\r\n * increments and tries the next port until one is free.\r\n *\r\n * Returns the port that was actually bound.\r\n */\r\nfunction tryListen(port: number): Promise<number> {\r\n return new Promise((resolve, reject) => {\r\n server.once('error', (err: NodeJS.ErrnoException) => {\r\n if (err.code === 'EADDRINUSE') resolve(tryListen(port + 1));\r\n else reject(err);\r\n });\r\n server.listen(port, () => resolve(port));\r\n });\r\n}\r\n\r\n// \u2500\u2500\u2500 Startup banner \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 the \u2622\uFE0F NukeJS startup box to stdout.\r\n * Uses box-drawing characters and ANSI colour codes for a clean terminal UI.\r\n */\r\nfunction printStartupBanner(port: number, isDev: boolean): void {\r\n const url = `http://localhost:${port}`;\r\n const level = getDebugLevel();\r\n const debugStr = String(level);\r\n const innerWidth = 42;\r\n const line = '\u2500'.repeat(innerWidth);\r\n\r\n /** Right-pads `text` to `width` columns, ignoring invisible ANSI sequences. */\r\n const pad = (text: string, width: number) => {\r\n const visibleLen = text.replace(/\\x1b\\[[0-9;]*m/g, '').length;\r\n return text + ' '.repeat(Math.max(0, width - visibleLen));\r\n };\r\n\r\n const row = (content: string, w = 2) =>\r\n `${ansi.gray}\u2502${ansi.reset} ${pad(content, innerWidth - w)} ${ansi.gray}\u2502${ansi.reset}`;\r\n const label = (key: string, val: string) =>\r\n row(`${c('gray', key)} ${val}`);\r\n\r\n console.log('');\r\n console.log(`${ansi.gray}\u250C${line}\u2510${ansi.reset}`);\r\n console.log(row(` ${c('red', '\u2622\uFE0F nukejs ', true)}`, 1));\r\n console.log(`${ansi.gray}\u251C${line}\u2524${ansi.reset}`);\r\n console.log(label(' Local ', c('cyan', url, true)));\r\n console.log(`${ansi.gray}\u251C${line}\u2524${ansi.reset}`);\r\n console.log(label(' Pages ', c('white', path.relative(process.cwd(), PAGES_DIR))));\r\n console.log(label(' Server ', c('white', path.relative(process.cwd(), SERVER_DIR))));\r\n console.log(label(' Dev ', isDev ? c('green', 'yes') : c('gray', 'no')));\r\n console.log(label(' Debug ', level === false\r\n ? c('gray', 'off')\r\n : level === true\r\n ? c('green', 'verbose')\r\n : c('yellow', debugStr)));\r\n console.log(`${ansi.gray}\u2514${line}\u2518${ansi.reset}`);\r\n console.log('');\r\n}\r\n\r\nconst actualPort = await tryListen(PORT);\r\nprintStartupBanner(actualPort, isDev);"],
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["/**\r\n * app.ts \u2014 NukeJS Dev Server Entry Point\r\n *\r\n * This is the runtime that powers `nuke dev`. It:\r\n * 1. Loads your nuke.config.ts (or uses sensible defaults)\r\n * 2. Discovers API route prefixes from your server directory\r\n * 3. Starts an HTTP server that handles:\r\n * app/public/** \u2014 static files (highest priority, via middleware)\r\n * /__hmr_ping \u2014 heartbeat for HMR reconnect polling\r\n * /__react.js \u2014 bundled React + ReactDOM (resolved via importmap)\r\n * /__n.js \u2014 NukeJS client runtime bundle\r\n * /__client-component/* \u2014 on-demand \"use client\" component bundles\r\n * server/** \u2014 API route handlers from serverDir\r\n * /** \u2014 SSR pages from app/pages (lowest priority)\r\n * 4. Watches for file changes and broadcasts HMR events to connected browsers\r\n *\r\n * In production (ENVIRONMENT=production), HMR and all file watching are skipped.\r\n */\r\n\r\nimport http from 'http';\r\nimport path from 'path';\r\nimport { existsSync, watch } from 'fs';\r\n\r\nimport { ansi, c, log, setDebugLevel, getDebugLevel } from './logger';\r\nimport { loadConfig } from './config';\r\nimport { discoverApiPrefixes, matchApiPrefix, createApiHandler } from './http-server';\r\nimport { loadMiddleware, runMiddleware } from './middleware-loader';\r\nimport { serveReactBundle, serveNukeBundle, serveClientComponentBundle } from './bundler';\r\nimport { serverSideRender } from './ssr';\r\nimport { watchDir, broadcastRestart } from './hmr';\r\n\r\n// \u2500\u2500\u2500 Environment \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 isDev = process.env.ENVIRONMENT !== 'production';\r\n\r\n// React must live on globalThis so dynamically-imported page modules can share\r\n// the same React instance without each bundling their own copy.\r\nif (isDev) {\r\n const React = await import('react');\r\n (global as any).React = React;\r\n}\r\n\r\n// \u2500\u2500\u2500 Config & paths \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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\nsetDebugLevel(config.debug ?? false);\r\n\r\nconst PAGES_DIR = path.resolve('./app/pages');\r\nconst SERVER_DIR = path.resolve(config.serverDir);\r\nconst PORT = config.port;\r\n\r\nlog.info('Configuration loaded:');\r\nlog.info(` - Pages directory: ${PAGES_DIR}`);\r\nlog.info(` - Server directory: ${SERVER_DIR}`);\r\nlog.info(` - Port: ${PORT}`);\r\nlog.info(` - Debug level: ${String(getDebugLevel())}`);\r\nlog.info(` - Dev mode: ${String(isDev)}`);\r\n\r\n// \u2500\u2500\u2500 API route discovery \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n// Start watching the app directory for HMR.\r\nif (isDev) watchDir(path.resolve('./app'), 'App');\r\n\r\n// apiPrefixes is a live, mutable array. In dev, we splice it in-place whenever\r\n// the server directory changes so handlers always see the latest routes without\r\n// a full restart.\r\nconst apiPrefixes = discoverApiPrefixes(SERVER_DIR);\r\nconst handleApiRoute = createApiHandler({ apiPrefixes, port: PORT, isDev });\r\n\r\n\r\nlog.info(`API prefixes discovered: ${apiPrefixes.length === 0 ? 'none' : ''}`);\r\napiPrefixes.forEach(p => {\r\n log.info(` - ${p.prefix || '/'} -> ${path.relative(process.cwd(), p.directory)}`);\r\n});\r\n\r\n// \u2500\u2500\u2500 Full-restart file watchers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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// Some changes can't be hot-patched: middleware exports change the request\r\n// pipeline, and nuke.config.ts may change the port or serverDir. On change we\r\n// broadcast a 'restart' SSE event so browsers reconnect automatically, then\r\n// exit with code 75 \u2014 the CLI watches for this to respawn the process.\r\nif (isDev) {\r\n const RESTART_EXIT_CODE = 75;\r\n const restartFiles = [\r\n path.resolve('./middleware.ts'),\r\n path.resolve('./nuke.config.ts'),\r\n ];\r\n\r\n for (const filePath of restartFiles) {\r\n if (!existsSync(filePath)) continue;\r\n watch(filePath, async () => {\r\n log.info(`[Server] ${path.basename(filePath)} changed \u2014 restarting...`);\r\n await broadcastRestart();\r\n process.exit(RESTART_EXIT_CODE);\r\n });\r\n }\r\n}\r\n\r\n// \u2500\u2500\u2500 Middleware \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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// Loads built-in middleware (HMR SSE/JS endpoints) and the user-supplied\r\n// middleware.ts from the project root (if it exists).\r\nawait loadMiddleware();\r\n\r\n// \u2500\u2500\u2500 Request handler \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nconst server = http.createServer(async (req, res) => {\r\n try {\r\n // Middleware runs first. If it calls res.end() the request is fully\r\n // handled and we bail out immediately.\r\n const middlewareHandled = await runMiddleware(req, res);\r\n if (middlewareHandled) return;\r\n\r\n const url = req.url || '/';\r\n\r\n // \u2500\u2500 Internal NukeJS routes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n // Framework files are checked before server routes so a user route can\r\n // never accidentally shadow /__n.js, /__react.js, etc.\r\n\r\n // Heartbeat polled by the HMR client to know when the server is back up.\r\n if (url === '/__hmr_ping') {\r\n res.setHeader('Content-Type', 'text/plain');\r\n res.end('ok');\r\n return;\r\n }\r\n\r\n // Unified React bundle (react + react-dom/client + react/jsx-runtime).\r\n // Resolved by the importmap injected into every SSR page, so client\r\n // components never bundle React themselves.\r\n if (url === '/__react.js')\r\n return await serveReactBundle(res);\r\n\r\n // NukeJS browser runtime: initRuntime, SPA navigation, partial hydration.\r\n if (url === '/__n.js')\r\n return await serveNukeBundle(res);\r\n\r\n // On-demand bundles for individual \"use client\" components.\r\n // Strip the prefix, the .js extension, and any query string (cache buster).\r\n if (url.startsWith('/__client-component/'))\r\n return await serveClientComponentBundle(\r\n url.slice(20).split('?')[0].replace('.js', ''),\r\n res,\r\n );\r\n\r\n // \u2500\u2500 Server routes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n // API routes from serverDir \u2014 checked after framework files, before pages.\r\n if (matchApiPrefix(url, apiPrefixes))\r\n return await handleApiRoute(url, req, res);\r\n\r\n // \u2500\u2500 Page SSR \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 // Nothing above matched \u2014 render a page from app/pages.\r\n return await serverSideRender(url, res, PAGES_DIR, isDev);\r\n\r\n } catch (error) {\r\n log.error('Server error:', error);\r\n res.statusCode = 500;\r\n res.end('Internal server error');\r\n }\r\n});\r\n\r\n// \u2500\u2500\u2500 Port binding \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 * Tries to listen on `port`. If the port is already in use (EADDRINUSE),\r\n * increments and tries the next port until one is free.\r\n *\r\n * Returns the port that was actually bound.\r\n */\r\nfunction tryListen(port: number): Promise<number> {\r\n return new Promise((resolve, reject) => {\r\n server.once('error', (err: NodeJS.ErrnoException) => {\r\n if (err.code === 'EADDRINUSE') resolve(tryListen(port + 1));\r\n else reject(err);\r\n });\r\n server.listen(port, () => resolve(port));\r\n });\r\n}\r\n\r\n// \u2500\u2500\u2500 Startup banner \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 the \u2622\uFE0F NukeJS startup box to stdout.\r\n * Uses box-drawing characters and ANSI colour codes for a clean terminal UI.\r\n */\r\nfunction printStartupBanner(port: number, isDev: boolean): void {\r\n const url = `http://localhost:${port}`;\r\n const level = getDebugLevel();\r\n const debugStr = String(level);\r\n const innerWidth = 42;\r\n const line = '\u2500'.repeat(innerWidth);\r\n\r\n /** Right-pads `text` to `width` columns, ignoring invisible ANSI sequences. */\r\n const pad = (text: string, width: number) => {\r\n const visibleLen = text.replace(/\\x1b\\[[0-9;]*m/g, '').length;\r\n return text + ' '.repeat(Math.max(0, width - visibleLen));\r\n };\r\n\r\n const row = (content: string, w = 2) =>\r\n `${ansi.gray}\u2502${ansi.reset} ${pad(content, innerWidth - w)} ${ansi.gray}\u2502${ansi.reset}`;\r\n const label = (key: string, val: string) =>\r\n row(`${c('gray', key)} ${val}`);\r\n\r\n console.log('');\r\n console.log(`${ansi.gray}\u250C${line}\u2510${ansi.reset}`);\r\n console.log(row(` ${c('red', '\u2622\uFE0F nukejs ', true)}`, 1));\r\n console.log(`${ansi.gray}\u251C${line}\u2524${ansi.reset}`);\r\n console.log(label(' Local ', c('cyan', url, true)));\r\n console.log(`${ansi.gray}\u251C${line}\u2524${ansi.reset}`);\r\n console.log(label(' Pages ', c('white', path.relative(process.cwd(), PAGES_DIR))));\r\n console.log(label(' Server ', c('white', path.relative(process.cwd(), SERVER_DIR))));\r\n console.log(label(' Dev ', isDev ? c('green', 'yes') : c('gray', 'no')));\r\n console.log(label(' Debug ', level === false\r\n ? c('gray', 'off')\r\n : level === true\r\n ? c('green', 'verbose')\r\n : c('yellow', debugStr)));\r\n console.log(`${ansi.gray}\u2514${line}\u2518${ansi.reset}`);\r\n console.log('');\r\n}\r\n\r\nconst actualPort = await tryListen(PORT);\r\nprintStartupBanner(actualPort, isDev);"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["isDev"]
|
|
7
7
|
}
|
package/dist/build-node.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Output layout:
|
|
8
8
|
* dist/
|
|
9
|
-
* index.mjs ← HTTP server entry point
|
|
9
|
+
* index.mjs ← HTTP server entry point (routing: static → framework → api → pages)
|
|
10
10
|
* manifest.json ← Route → handler mapping
|
|
11
11
|
* api/<route>.mjs ← Bundled API handlers
|
|
12
12
|
* pages/<route>.mjs ← Bundled page handlers
|
package/dist/build-node.js
CHANGED
|
@@ -98,22 +98,10 @@ const server = http.createServer(async (req, res) => {
|
|
|
98
98
|
const url = req.url || '/';
|
|
99
99
|
const clean = url.split('?')[0];
|
|
100
100
|
|
|
101
|
-
//
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
res.setHeader('Content-Type', MIME_MAP[path.extname(filePath)] ?? 'application/javascript');
|
|
106
|
-
res.end(fs.readFileSync(filePath));
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
res.statusCode = 404;
|
|
110
|
-
res.end('Not found');
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Public static files from app/public/.
|
|
115
|
-
// path.join normalises '..' segments before the startsWith guard,
|
|
116
|
-
// preventing directory traversal.
|
|
101
|
+
// 1. Static files \u2014 app/public/ files are copied into STATIC_DIR last at
|
|
102
|
+
// build time (after framework bundles), so they take priority over
|
|
103
|
+
// framework files on name collision. path.join normalises '..' segments
|
|
104
|
+
// before the startsWith guard, preventing directory traversal.
|
|
117
105
|
{
|
|
118
106
|
const candidate = path.join(STATIC_DIR, clean);
|
|
119
107
|
const staticBase = STATIC_DIR.endsWith(path.sep) ? STATIC_DIR : STATIC_DIR + path.sep;
|
|
@@ -129,7 +117,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
129
117
|
}
|
|
130
118
|
}
|
|
131
119
|
|
|
132
|
-
// Route dispatch
|
|
120
|
+
// 2. Route dispatch \u2014 API routes appear before page routes in the manifest
|
|
121
|
+
// (built in build-node.ts), so they are matched first.
|
|
133
122
|
for (const { regex, paramNames, catchAllNames, handler } of compiled) {
|
|
134
123
|
const m = clean.match(regex);
|
|
135
124
|
if (!m) continue;
|
package/dist/build-node.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/build-node.ts"],
|
|
4
|
-
"sourcesContent": ["/**\r\n * build-node.ts \u2014 Node.js Production Build\r\n *\r\n * Produces a self-contained dist/ directory that runs with:\r\n * node dist/index.mjs\r\n *\r\n * Output layout:\r\n * dist/\r\n * index.mjs \u2190 HTTP server entry point\r\n * manifest.json \u2190 Route \u2192 handler mapping\r\n * api/<route>.mjs \u2190 Bundled API handlers\r\n * pages/<route>.mjs \u2190 Bundled page handlers\r\n * static/ \u2190 __n.js, client components, public files\r\n */\r\n\r\nimport fs from 'fs';\r\nimport path from 'path';\r\n\r\nimport { loadConfig } from './config';\r\nimport {\r\n analyzeFile,\r\n walkFiles,\r\n buildPages,\r\n bundleApiHandler,\r\n buildCombinedBundle,\r\n copyPublicFiles,\r\n type AnalyzedRoute,\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 OUT_DIR = path.resolve('dist');\r\nconst API_DIR = path.join(OUT_DIR, 'api');\r\nconst PAGES_DIR_ = path.join(OUT_DIR, 'pages');\r\nconst STATIC_DIR = path.join(OUT_DIR, 'static');\r\n\r\nfor (const dir of [API_DIR, PAGES_DIR_, STATIC_DIR])\r\n fs.mkdirSync(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 Route manifest \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\ninterface ManifestEntry {\r\n /** Regex string matching the URL path, e.g. '^/users/([^/]+)$' */\r\n srcRegex: string;\r\n /** Names of captured groups in srcRegex order */\r\n paramNames: string[];\r\n /** Subset of paramNames whose runtime values are string[] (catch-all params) */\r\n catchAllNames: string[];\r\n /** Path to the bundled handler relative to dist/, e.g. 'api/users/[id].mjs' */\r\n handler: string;\r\n type: 'api' | 'page';\r\n}\r\n\r\nconst manifest: ManifestEntry[] = [];\r\n\r\n// \u2500\u2500\u2500 Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/** Converts a funcPath like '/api/users/[id]' to a filename 'users/[id].mjs'. */\r\nfunction funcPathToFilename(funcPath: string, prefix: 'api' | 'page'): string {\r\n return funcPath.replace(new RegExp(`^\\\\/${prefix}\\\\/`), '') + '.mjs';\r\n}\r\n\r\n// \u2500\u2500\u2500 API routes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nconst apiFiles = walkFiles(SERVER_DIR);\r\nif (apiFiles.length === 0) console.warn(`\u26A0 No server files found in ${SERVER_DIR}`);\r\n\r\nconst apiRoutes = apiFiles\r\n .map(relPath => ({ ...analyzeFile(relPath, 'api'), absPath: path.join(SERVER_DIR, relPath) }))\r\n .sort((a, b) => b.specificity - a.specificity);\r\n\r\nfor (const { srcRegex, paramNames, catchAllNames, funcPath, absPath } of apiRoutes) {\r\n console.log(` building ${path.relative(SERVER_DIR, absPath)} \u2192 ${funcPath}`);\r\n\r\n const filename = funcPathToFilename(funcPath, 'api');\r\n const outPath = path.join(API_DIR, filename);\r\n fs.mkdirSync(path.dirname(outPath), { recursive: true });\r\n fs.writeFileSync(outPath, await bundleApiHandler(absPath));\r\n\r\n manifest.push({ srcRegex, paramNames, catchAllNames, handler: path.join('api', filename), type: 'api' });\r\n}\r\n\r\n// \u2500\u2500\u2500 Page routes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nconst builtPages = await buildPages(PAGES_DIR, STATIC_DIR);\r\n\r\nfor (const { srcRegex, paramNames, catchAllNames, funcPath, bundleText } of builtPages) {\r\n const filename = funcPathToFilename(funcPath, 'page');\r\n const outPath = path.join(PAGES_DIR_, filename);\r\n fs.mkdirSync(path.dirname(outPath), { recursive: true });\r\n fs.writeFileSync(outPath, bundleText);\r\n\r\n manifest.push({ srcRegex, paramNames, catchAllNames, handler: path.join('pages', filename), type: 'page' });\r\n}\r\n\r\n// \u2500\u2500\u2500 Manifest \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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// Entries are already sorted most-specific first (both loops sort before\r\n// iterating), so the runtime can match top-to-bottom.\r\nfs.writeFileSync(\r\n path.join(OUT_DIR, 'manifest.json'),\r\n JSON.stringify({ routes: manifest }, 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\n// \u2500\u2500\u2500 Server entry \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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// A thin HTTP server that reads manifest.json at startup and dispatches\r\n// incoming requests to the correct pre-built handler module.\r\n\r\nconst MIME_MAP_ENTRIES = `\r\n '.html': 'text/html; charset=utf-8',\r\n '.htm': 'text/html; charset=utf-8',\r\n '.css': 'text/css; charset=utf-8',\r\n '.js': 'application/javascript; charset=utf-8',\r\n '.mjs': 'application/javascript; charset=utf-8',\r\n '.cjs': 'application/javascript; charset=utf-8',\r\n '.map': 'application/json; charset=utf-8',\r\n '.json': 'application/json; charset=utf-8',\r\n '.xml': 'application/xml; charset=utf-8',\r\n '.txt': 'text/plain; charset=utf-8',\r\n '.csv': 'text/csv; charset=utf-8',\r\n '.png': 'image/png',\r\n '.jpg': 'image/jpeg',\r\n '.jpeg': 'image/jpeg',\r\n '.gif': 'image/gif',\r\n '.webp': 'image/webp',\r\n '.avif': 'image/avif',\r\n '.svg': 'image/svg+xml',\r\n '.ico': 'image/x-icon',\r\n '.bmp': 'image/bmp',\r\n '.woff': 'font/woff',\r\n '.woff2':'font/woff2',\r\n '.ttf': 'font/ttf',\r\n '.otf': 'font/otf',\r\n '.mp4': 'video/mp4',\r\n '.webm': 'video/webm',\r\n '.mp3': 'audio/mpeg',\r\n '.wav': 'audio/wav',\r\n '.ogg': 'audio/ogg',\r\n '.pdf': 'application/pdf',\r\n '.wasm': 'application/wasm',\r\n`.trim();\r\n\r\nconst serverEntry = `\\\r\nimport http from 'http';\r\nimport path from 'path';\r\nimport fs from 'fs';\r\nimport { fileURLToPath, pathToFileURL } from 'url';\r\n\r\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\r\n\r\nconst { routes } = JSON.parse(fs.readFileSync(path.join(__dirname, 'manifest.json'), 'utf-8'));\r\nconst compiled = routes.map(r => ({ ...r, regex: new RegExp(r.srcRegex) }));\r\n\r\nconst STATIC_DIR = path.join(__dirname, 'static');\r\nconst MIME_MAP = { ${MIME_MAP_ENTRIES} };\r\n\r\nconst server = http.createServer(async (req, res) => {\r\n const url = req.url || '/';\r\n const clean = url.split('?')[0];\r\n\r\n // Internal assets: /__n.js and /__client-component/*\r\n if (clean === '/__n.js' || clean.startsWith('/__client-component/')) {\r\n const filePath = path.join(STATIC_DIR, clean);\r\n if (fs.existsSync(filePath)) {\r\n res.setHeader('Content-Type', MIME_MAP[path.extname(filePath)] ?? 'application/javascript');\r\n res.end(fs.readFileSync(filePath));\r\n return;\r\n }\r\n res.statusCode = 404;\r\n res.end('Not found');\r\n return;\r\n }\r\n\r\n // Public static files from app/public/.\r\n // path.join normalises '..' segments before the startsWith guard,\r\n // preventing directory traversal.\r\n {\r\n const candidate = path.join(STATIC_DIR, clean);\r\n const staticBase = STATIC_DIR.endsWith(path.sep) ? STATIC_DIR : STATIC_DIR + path.sep;\r\n if (\r\n candidate.startsWith(staticBase) &&\r\n candidate !== STATIC_DIR &&\r\n fs.existsSync(candidate) &&\r\n fs.statSync(candidate).isFile()\r\n ) {\r\n res.setHeader('Content-Type', MIME_MAP[path.extname(candidate)] ?? 'application/octet-stream');\r\n res.end(fs.readFileSync(candidate));\r\n return;\r\n }\r\n }\r\n\r\n // Route dispatch.\r\n for (const { regex, paramNames, catchAllNames, handler } of compiled) {\r\n const m = clean.match(regex);\r\n if (!m) continue;\r\n\r\n const catchAllSet = new Set(catchAllNames);\r\n const qs = new URLSearchParams(Object.fromEntries(new URL(url, 'http://localhost').searchParams));\r\n paramNames.forEach((name, i) => {\r\n const raw = m[i + 1] ?? '';\r\n if (catchAllSet.has(name)) {\r\n raw.split('/').filter(Boolean).forEach(seg => qs.append(name, seg));\r\n } else {\r\n qs.set(name, raw);\r\n }\r\n });\r\n req.url = clean + (qs.toString() ? '?' + qs.toString() : '');\r\n\r\n const mod = await import(pathToFileURL(path.join(__dirname, handler)).href);\r\n await mod.default(req, res);\r\n return;\r\n }\r\n\r\n res.statusCode = 404;\r\n res.setHeader('Content-Type', 'text/plain');\r\n res.end('Not found');\r\n});\r\n\r\nconst PORT = Number(process.env.PORT ?? 3000);\r\nserver.listen(PORT, () => console.log('nukejs built server listening on http://localhost:' + PORT));\r\n`;\r\n\r\nfs.writeFileSync(path.join(OUT_DIR, 'index.mjs'), serverEntry);\r\n\r\nconsole.log(`\\n\u2713 Node build complete \u2014 ${manifest.length} route(s) \u2192 dist/`);\r\nconsole.log(' run with: node dist/index.mjs');\r\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["/**\r\n * build-node.ts \u2014 Node.js Production Build\r\n *\r\n * Produces a self-contained dist/ directory that runs with:\r\n * node dist/index.mjs\r\n *\r\n * Output layout:\r\n * dist/\r\n * index.mjs \u2190 HTTP server entry point (routing: static \u2192 framework \u2192 api \u2192 pages)\r\n * manifest.json \u2190 Route \u2192 handler mapping\r\n * api/<route>.mjs \u2190 Bundled API handlers\r\n * pages/<route>.mjs \u2190 Bundled page handlers\r\n * static/ \u2190 __n.js, client components, public files\r\n */\r\n\r\nimport fs from 'fs';\r\nimport path from 'path';\r\n\r\nimport { loadConfig } from './config';\r\nimport {\r\n analyzeFile,\r\n walkFiles,\r\n buildPages,\r\n bundleApiHandler,\r\n buildCombinedBundle,\r\n copyPublicFiles,\r\n type AnalyzedRoute,\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 OUT_DIR = path.resolve('dist');\r\nconst API_DIR = path.join(OUT_DIR, 'api');\r\nconst PAGES_DIR_ = path.join(OUT_DIR, 'pages');\r\nconst STATIC_DIR = path.join(OUT_DIR, 'static');\r\n\r\nfor (const dir of [API_DIR, PAGES_DIR_, STATIC_DIR])\r\n fs.mkdirSync(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 Route manifest \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\ninterface ManifestEntry {\r\n /** Regex string matching the URL path, e.g. '^/users/([^/]+)$' */\r\n srcRegex: string;\r\n /** Names of captured groups in srcRegex order */\r\n paramNames: string[];\r\n /** Subset of paramNames whose runtime values are string[] (catch-all params) */\r\n catchAllNames: string[];\r\n /** Path to the bundled handler relative to dist/, e.g. 'api/users/[id].mjs' */\r\n handler: string;\r\n type: 'api' | 'page';\r\n}\r\n\r\nconst manifest: ManifestEntry[] = [];\r\n\r\n// \u2500\u2500\u2500 Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/** Converts a funcPath like '/api/users/[id]' to a filename 'users/[id].mjs'. */\r\nfunction funcPathToFilename(funcPath: string, prefix: 'api' | 'page'): string {\r\n return funcPath.replace(new RegExp(`^\\\\/${prefix}\\\\/`), '') + '.mjs';\r\n}\r\n\r\n// \u2500\u2500\u2500 API routes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nconst apiFiles = walkFiles(SERVER_DIR);\r\nif (apiFiles.length === 0) console.warn(`\u26A0 No server files found in ${SERVER_DIR}`);\r\n\r\nconst apiRoutes = apiFiles\r\n .map(relPath => ({ ...analyzeFile(relPath, 'api'), absPath: path.join(SERVER_DIR, relPath) }))\r\n .sort((a, b) => b.specificity - a.specificity);\r\n\r\nfor (const { srcRegex, paramNames, catchAllNames, funcPath, absPath } of apiRoutes) {\r\n console.log(` building ${path.relative(SERVER_DIR, absPath)} \u2192 ${funcPath}`);\r\n\r\n const filename = funcPathToFilename(funcPath, 'api');\r\n const outPath = path.join(API_DIR, filename);\r\n fs.mkdirSync(path.dirname(outPath), { recursive: true });\r\n fs.writeFileSync(outPath, await bundleApiHandler(absPath));\r\n\r\n manifest.push({ srcRegex, paramNames, catchAllNames, handler: path.join('api', filename), type: 'api' });\r\n}\r\n\r\n// \u2500\u2500\u2500 Page routes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nconst builtPages = await buildPages(PAGES_DIR, STATIC_DIR);\r\n\r\nfor (const { srcRegex, paramNames, catchAllNames, funcPath, bundleText } of builtPages) {\r\n const filename = funcPathToFilename(funcPath, 'page');\r\n const outPath = path.join(PAGES_DIR_, filename);\r\n fs.mkdirSync(path.dirname(outPath), { recursive: true });\r\n fs.writeFileSync(outPath, bundleText);\r\n\r\n manifest.push({ srcRegex, paramNames, catchAllNames, handler: path.join('pages', filename), type: 'page' });\r\n}\r\n\r\n// \u2500\u2500\u2500 Manifest \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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// Entries are already sorted most-specific first (both loops sort before\r\n// iterating), so the runtime can match top-to-bottom.\r\nfs.writeFileSync(\r\n path.join(OUT_DIR, 'manifest.json'),\r\n JSON.stringify({ routes: manifest }, 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\n// \u2500\u2500\u2500 Server entry \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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// A thin HTTP server that reads manifest.json at startup and dispatches\r\n// incoming requests to the correct pre-built handler module.\r\n\r\nconst MIME_MAP_ENTRIES = `\r\n '.html': 'text/html; charset=utf-8',\r\n '.htm': 'text/html; charset=utf-8',\r\n '.css': 'text/css; charset=utf-8',\r\n '.js': 'application/javascript; charset=utf-8',\r\n '.mjs': 'application/javascript; charset=utf-8',\r\n '.cjs': 'application/javascript; charset=utf-8',\r\n '.map': 'application/json; charset=utf-8',\r\n '.json': 'application/json; charset=utf-8',\r\n '.xml': 'application/xml; charset=utf-8',\r\n '.txt': 'text/plain; charset=utf-8',\r\n '.csv': 'text/csv; charset=utf-8',\r\n '.png': 'image/png',\r\n '.jpg': 'image/jpeg',\r\n '.jpeg': 'image/jpeg',\r\n '.gif': 'image/gif',\r\n '.webp': 'image/webp',\r\n '.avif': 'image/avif',\r\n '.svg': 'image/svg+xml',\r\n '.ico': 'image/x-icon',\r\n '.bmp': 'image/bmp',\r\n '.woff': 'font/woff',\r\n '.woff2':'font/woff2',\r\n '.ttf': 'font/ttf',\r\n '.otf': 'font/otf',\r\n '.mp4': 'video/mp4',\r\n '.webm': 'video/webm',\r\n '.mp3': 'audio/mpeg',\r\n '.wav': 'audio/wav',\r\n '.ogg': 'audio/ogg',\r\n '.pdf': 'application/pdf',\r\n '.wasm': 'application/wasm',\r\n`.trim();\r\n\r\nconst serverEntry = `\\\r\nimport http from 'http';\r\nimport path from 'path';\r\nimport fs from 'fs';\r\nimport { fileURLToPath, pathToFileURL } from 'url';\r\n\r\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\r\n\r\nconst { routes } = JSON.parse(fs.readFileSync(path.join(__dirname, 'manifest.json'), 'utf-8'));\r\nconst compiled = routes.map(r => ({ ...r, regex: new RegExp(r.srcRegex) }));\r\n\r\nconst STATIC_DIR = path.join(__dirname, 'static');\r\nconst MIME_MAP = { ${MIME_MAP_ENTRIES} };\r\n\r\nconst server = http.createServer(async (req, res) => {\r\n const url = req.url || '/';\r\n const clean = url.split('?')[0];\r\n\r\n // 1. Static files \u2014 app/public/ files are copied into STATIC_DIR last at\r\n // build time (after framework bundles), so they take priority over\r\n // framework files on name collision. path.join normalises '..' segments\r\n // before the startsWith guard, preventing directory traversal.\r\n {\r\n const candidate = path.join(STATIC_DIR, clean);\r\n const staticBase = STATIC_DIR.endsWith(path.sep) ? STATIC_DIR : STATIC_DIR + path.sep;\r\n if (\r\n candidate.startsWith(staticBase) &&\r\n candidate !== STATIC_DIR &&\r\n fs.existsSync(candidate) &&\r\n fs.statSync(candidate).isFile()\r\n ) {\r\n res.setHeader('Content-Type', MIME_MAP[path.extname(candidate)] ?? 'application/octet-stream');\r\n res.end(fs.readFileSync(candidate));\r\n return;\r\n }\r\n }\r\n\r\n // 2. Route dispatch \u2014 API routes appear before page routes in the manifest\r\n // (built in build-node.ts), so they are matched first.\r\n for (const { regex, paramNames, catchAllNames, handler } of compiled) {\r\n const m = clean.match(regex);\r\n if (!m) continue;\r\n\r\n const catchAllSet = new Set(catchAllNames);\r\n const qs = new URLSearchParams(Object.fromEntries(new URL(url, 'http://localhost').searchParams));\r\n paramNames.forEach((name, i) => {\r\n const raw = m[i + 1] ?? '';\r\n if (catchAllSet.has(name)) {\r\n raw.split('/').filter(Boolean).forEach(seg => qs.append(name, seg));\r\n } else {\r\n qs.set(name, raw);\r\n }\r\n });\r\n req.url = clean + (qs.toString() ? '?' + qs.toString() : '');\r\n\r\n const mod = await import(pathToFileURL(path.join(__dirname, handler)).href);\r\n await mod.default(req, res);\r\n return;\r\n }\r\n\r\n res.statusCode = 404;\r\n res.setHeader('Content-Type', 'text/plain');\r\n res.end('Not found');\r\n});\r\n\r\nconst PORT = Number(process.env.PORT ?? 3000);\r\nserver.listen(PORT, () => console.log('nukejs built server listening on http://localhost:' + PORT));\r\n`;\r\n\r\nfs.writeFileSync(path.join(OUT_DIR, 'index.mjs'), serverEntry);\r\n\r\nconsole.log(`\\n\u2713 Node build complete \u2014 ${manifest.length} route(s) \u2192 dist/`);\r\nconsole.log(' run with: node dist/index.mjs');"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/build-vercel.js
CHANGED
|
@@ -273,7 +273,7 @@ if (serverPages.length > 0) {
|
|
|
273
273
|
}
|
|
274
274
|
fs.writeFileSync(
|
|
275
275
|
path.join(OUTPUT_DIR, "config.json"),
|
|
276
|
-
JSON.stringify({ version: 3, routes: vercelRoutes }, null, 2)
|
|
276
|
+
JSON.stringify({ version: 3, routes: [{ handle: "filesystem" }, ...vercelRoutes] }, null, 2)
|
|
277
277
|
);
|
|
278
278
|
fs.writeFileSync(
|
|
279
279
|
path.resolve("vercel.json"),
|
package/dist/build-vercel.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/build-vercel.ts"],
|
|
4
|
-
"sourcesContent": ["/**\r\n * build-vercel.ts \u2014 Vercel Production Build\r\n *\r\n * Produces a .vercel/output/ directory conforming to the Vercel Build Output\r\n * API v3. Two serverless functions are emitted:\r\n *\r\n * api.func/ \u2190 single dispatcher bundling all API route handlers\r\n * pages.func/ \u2190 single dispatcher bundling all SSR page handlers\r\n *\r\n * Static assets (React runtime, client components, public files) go to\r\n * .vercel/output/static/ and are served by Vercel's CDN directly.\r\n *\r\n * Notes on bundling strategy:\r\n * - npm packages are FULLY BUNDLED (no node_modules at Vercel runtime).\r\n * - Node built-ins are kept external (available in the nodejs20.x runtime).\r\n * - A createRequire banner lets CJS packages (mongoose, etc.) resolve Node\r\n * built-ins correctly inside the ESM output bundle.\r\n */\r\n\r\nimport fs from 'fs';\r\nimport path from 'path';\r\nimport { randomBytes } from 'node: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\nfor (const dir of [FUNCTIONS_DIR, STATIC_DIR])\r\n fs.mkdirSync(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 Shared esbuild 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\r\n\r\n/**\r\n * Node built-ins that should never be bundled.\r\n * npm packages are intentionally absent \u2014 they must be bundled because\r\n * Vercel serverless functions have no node_modules at runtime.\r\n */\r\nconst NODE_BUILTINS = [\r\n 'node:*',\r\n 'http', 'https', 'fs', 'path', 'url', 'crypto', 'stream', 'buffer',\r\n 'events', 'util', 'os', 'net', 'tls', 'child_process', 'worker_threads',\r\n 'cluster', 'dgram', 'dns', 'readline', 'zlib', 'assert', 'module',\r\n 'perf_hooks', 'string_decoder', 'timers', 'async_hooks', 'v8', 'vm',\r\n];\r\n\r\n/**\r\n * Banner injected at the top of every Vercel function bundle.\r\n *\r\n * Why it's needed: esbuild bundles CJS packages (mongoose, etc.) into ESM\r\n * output and replaces their require() calls with a __require2 shim. That\r\n * shim cannot resolve Node built-ins on its own inside an ESM module scope.\r\n * Injecting a real require (backed by createRequire) fixes the shim so that\r\n * dynamic require('crypto'), require('stream'), etc. work correctly.\r\n */\r\nconst CJS_COMPAT_BANNER = {\r\n js: `import { createRequire } from 'module';\\nconst require = createRequire(import.meta.url);`,\r\n};\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 TypeScript dispatcher that imports every API route module,\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\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 TypeScript dispatcher that imports each page's pre-generated\r\n * adapter, matches the incoming URL, encodes captured dynamic params as\r\n * query-string values (catch-all params use repeated keys), then delegates\r\n * to the matching handler.\r\n */\r\nfunction makePagesDispatcherSource(\r\n routes: Array<{\r\n adapterPath: string;\r\n srcRegex: string;\r\n paramNames: string[];\r\n catchAllNames: string[];\r\n }>,\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)}, catchAll: ${JSON.stringify(r.catchAllNames)}, 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 catchAll: 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 const catchAllSet = new Set(route.catchAll);\r\n route.params.forEach((name, i) => {\r\n const raw = m[i + 1] ?? '';\r\n if (catchAllSet.has(name)) {\r\n // Encode catch-all as repeated keys so the handler can getAll() \u2192 string[]\r\n raw.split('/').filter(Boolean).forEach(seg => url.searchParams.append(name, seg));\r\n } else {\r\n url.searchParams.set(name, raw);\r\n }\r\n });\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 dispatcherPath = path.join(SERVER_DIR, `_api_dispatcher_${randomBytes(4).toString('hex')}.ts`);\r\n fs.writeFileSync(dispatcherPath, makeApiDispatcherSource(apiRoutes));\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 banner: CJS_COMPAT_BANNER,\r\n external: NODE_BUILTINS,\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// \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 globalRegistry = collectGlobalClientRegistry(serverPages, PAGES_DIR);\r\n const prerenderedHtml = await bundleClientComponents(globalRegistry, PAGES_DIR, STATIC_DIR);\r\n const prerenderedRecord = 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 resolve correctly), then bundle everything in\r\n // one esbuild pass via the dispatcher.\r\n const tempAdapterPaths: string[] = [];\r\n\r\n for (const page of serverPages) {\r\n const adapterDir = path.dirname(page.absPath);\r\n const adapterPath = path.join(adapterDir, `_page_adapter_${randomBytes(4).toString('hex')}.ts`);\r\n\r\n const layoutPaths = findPageLayouts(page.absPath, PAGES_DIR);\r\n const { registry, clientComponentNames } = buildPerPageRegistry(page.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(page.absPath)),\r\n layoutImports,\r\n clientComponentNames,\r\n allClientIds: [...registry.keys()],\r\n layoutArrayItems: layoutPaths.map((_, i) => `__layout_${i}__`).join(', '),\r\n prerenderedHtml: prerenderedRecord,\r\n catchAllNames: page.catchAllNames,\r\n }),\r\n );\r\n\r\n tempAdapterPaths.push(adapterPath);\r\n console.log(` prepared ${path.relative(PAGES_DIR, page.absPath)} \u2192 ${page.funcPath} [page]`);\r\n }\r\n\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 catchAllNames: page.catchAllNames,\r\n }));\r\n\r\n const dispatcherPath = path.join(PAGES_DIR, `_pages_dispatcher_${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 banner: CJS_COMPAT_BANNER,\r\n external: NODE_BUILTINS,\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// \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\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`);\r\n"],
|
|
5
|
-
"mappings": "AAmBA,OAAO,QAAU;AACjB,OAAO,UAAU;AACjB,SAAS,mBAAmB;AAC5B,SAAS,aAAmB;AAE5B,SAAS,kBAAkB;AAC3B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAIP,MAAM,aAAgB,KAAK,QAAQ,gBAAgB;AACnD,MAAM,gBAAgB,KAAK,KAAK,YAAY,WAAW;AACvD,MAAM,aAAgB,KAAK,KAAK,YAAY,QAAQ;AAEpD,WAAW,OAAO,CAAC,eAAe,UAAU;AAC1C,KAAG,UAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AAIvC,MAAM,SAAa,MAAM,WAAW;AACpC,MAAM,aAAa,KAAK,QAAQ,OAAO,SAAS;AAChD,MAAM,YAAa,KAAK,QAAQ,aAAa;AAC7C,MAAM,aAAa,KAAK,QAAQ,cAAc;AAS9C,MAAM,gBAAgB;AAAA,EACpB;AAAA,EACA;AAAA,EAAQ;AAAA,EAAS;AAAA,EAAM;AAAA,EAAQ;AAAA,EAAO;AAAA,EAAU;AAAA,EAAU;AAAA,EAC1D;AAAA,EAAU;AAAA,EAAQ;AAAA,EAAM;AAAA,EAAO;AAAA,EAAO;AAAA,EAAiB;AAAA,EACvD;AAAA,EAAW;AAAA,EAAS;AAAA,EAAO;AAAA,EAAY;AAAA,EAAQ;AAAA,EAAU;AAAA,EACzD;AAAA,EAAc;AAAA,EAAkB;AAAA,EAAU;AAAA,EAAe;AAAA,EAAM;AACjE;AAWA,MAAM,oBAAoB;AAAA,EACxB,IAAI;AAAA;AACN;AAOA,SAAS,mBAAmB,MAAc,YAA0B;AAClE,QAAM,UAAU,KAAK,KAAK,eAAe,GAAG,IAAI,OAAO;AACvD,KAAG,UAAU,SAAS,EAAE,WAAW,KAAK,CAAC;AACzC,KAAG,cAAc,KAAK,KAAK,SAAS,WAAW,GAAG,UAAU;AAC5D,KAAG;AAAA,IACD,KAAK,KAAK,SAAS,iBAAiB;AAAA,IACpC,KAAK,UAAU,EAAE,SAAS,cAAc,SAAS,aAAa,cAAc,SAAS,GAAG,MAAM,CAAC;AAAA,EACjG;AACF;AASA,SAAS,wBACP,QACQ;AACR,QAAM,UAAU,OACb,IAAI,CAAC,GAAG,MAAM,qBAAqB,CAAC,WAAW,KAAK,UAAU,EAAE,OAAO,CAAC,GAAG,EAC3E,KAAK,IAAI;AAEZ,QAAM,eAAe,OAClB;AAAA,IAAI,CAAC,GAAG,MACP,cAAc,KAAK,UAAU,EAAE,QAAQ,CAAC,aAAa,KAAK,UAAU,EAAE,UAAU,CAAC,gBAAgB,CAAC;AAAA,EACpG,EACC,KAAK,IAAI;AAEZ,SAAO;AAAA,EAEP,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA8BP,YAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAkCd;AAUA,SAAS,0BACP,QAMQ;AACR,QAAM,UAAU,OACb,IAAI,CAAC,GAAG,MAAM,iBAAiB,CAAC,WAAW,KAAK,UAAU,EAAE,WAAW,CAAC,GAAG,EAC3E,KAAK,IAAI;AAEZ,QAAM,eAAe,OAClB;AAAA,IAAI,CAAC,GAAG,MACP,cAAc,KAAK,UAAU,EAAE,QAAQ,CAAC,aAAa,KAAK,UAAU,EAAE,UAAU,CAAC,eAAe,KAAK,UAAU,EAAE,aAAa,CAAC,qBAAqB,CAAC;AAAA,EACvJ,EACC,KAAK,IAAI;AAEZ,SAAO;AAAA,EAEP,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQP,YAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA+Bd;AAIA,MAAM,eAA8B,CAAC;AAErC,MAAM,WAAW,UAAU,UAAU;AACrC,IAAI,SAAS,WAAW,EAAG,SAAQ,KAAK,oCAA+B,UAAU,EAAE;AAEnF,MAAM,YAAY,SACf,IAAI,cAAY,EAAE,GAAG,YAAY,SAAS,KAAK,GAAG,SAAS,KAAK,KAAK,YAAY,OAAO,EAAE,EAAE,EAC5F,KAAK,CAAC,GAAG,MAAM,EAAE,cAAc,EAAE,WAAW;AAE/C,IAAI,UAAU,SAAS,GAAG;AACxB,QAAM,iBAAiB,KAAK,KAAK,YAAY,mBAAmB,YAAY,CAAC,EAAE,SAAS,KAAK,CAAC,KAAK;AACnG,KAAG,cAAc,gBAAgB,wBAAwB,SAAS,CAAC;AAEnE,MAAI;AACF,UAAM,SAAS,MAAM,MAAM;AAAA,MACzB,aAAa,CAAC,cAAc;AAAA,MAC5B,QAAa;AAAA,MACb,QAAa;AAAA,MACb,UAAa;AAAA,MACb,QAAa;AAAA,MACb,QAAa;AAAA,MACb,UAAa;AAAA,MACb,OAAa;AAAA,IACf,CAAC;AACD,uBAAmB,OAAO,OAAO,YAAY,CAAC,EAAE,IAAI;AACpD,YAAQ,IAAI,gDAA2C,UAAU,MAAM,YAAY;AAAA,EACrF,UAAE;AACA,OAAG,WAAW,cAAc;AAAA,EAC9B;
|
|
4
|
+
"sourcesContent": ["/**\r\n * build-vercel.ts \u2014 Vercel Production Build\r\n *\r\n * Produces a .vercel/output/ directory conforming to the Vercel Build Output\r\n * API v3. Two serverless functions are emitted:\r\n *\r\n * api.func/ \u2190 single dispatcher bundling all API route handlers\r\n * pages.func/ \u2190 single dispatcher bundling all SSR page handlers\r\n *\r\n * Static assets (React runtime, client components, public files) go to\r\n * .vercel/output/static/ and are served by Vercel's CDN directly.\r\n *\r\n * Notes on bundling strategy:\r\n * - npm packages are FULLY BUNDLED (no node_modules at Vercel runtime).\r\n * - Node built-ins are kept external (available in the nodejs20.x runtime).\r\n * - A createRequire banner lets CJS packages (mongoose, etc.) resolve Node\r\n * built-ins correctly inside the ESM output bundle.\r\n */\r\n\r\nimport fs from 'fs';\r\nimport path from 'path';\r\nimport { randomBytes } from 'node: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\nfor (const dir of [FUNCTIONS_DIR, STATIC_DIR])\r\n fs.mkdirSync(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 Shared esbuild 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\r\n\r\n/**\r\n * Node built-ins that should never be bundled.\r\n * npm packages are intentionally absent \u2014 they must be bundled because\r\n * Vercel serverless functions have no node_modules at runtime.\r\n */\r\nconst NODE_BUILTINS = [\r\n 'node:*',\r\n 'http', 'https', 'fs', 'path', 'url', 'crypto', 'stream', 'buffer',\r\n 'events', 'util', 'os', 'net', 'tls', 'child_process', 'worker_threads',\r\n 'cluster', 'dgram', 'dns', 'readline', 'zlib', 'assert', 'module',\r\n 'perf_hooks', 'string_decoder', 'timers', 'async_hooks', 'v8', 'vm',\r\n];\r\n\r\n/**\r\n * Banner injected at the top of every Vercel function bundle.\r\n *\r\n * Why it's needed: esbuild bundles CJS packages (mongoose, etc.) into ESM\r\n * output and replaces their require() calls with a __require2 shim. That\r\n * shim cannot resolve Node built-ins on its own inside an ESM module scope.\r\n * Injecting a real require (backed by createRequire) fixes the shim so that\r\n * dynamic require('crypto'), require('stream'), etc. work correctly.\r\n */\r\nconst CJS_COMPAT_BANNER = {\r\n js: `import { createRequire } from 'module';\\nconst require = createRequire(import.meta.url);`,\r\n};\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 } | { handle: 'filesystem' };\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 TypeScript dispatcher that imports every API route module,\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\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 TypeScript dispatcher that imports each page's pre-generated\r\n * adapter, matches the incoming URL, encodes captured dynamic params as\r\n * query-string values (catch-all params use repeated keys), then delegates\r\n * to the matching handler.\r\n */\r\nfunction makePagesDispatcherSource(\r\n routes: Array<{\r\n adapterPath: string;\r\n srcRegex: string;\r\n paramNames: string[];\r\n catchAllNames: string[];\r\n }>,\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)}, catchAll: ${JSON.stringify(r.catchAllNames)}, 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 catchAll: 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 const catchAllSet = new Set(route.catchAll);\r\n route.params.forEach((name, i) => {\r\n const raw = m[i + 1] ?? '';\r\n if (catchAllSet.has(name)) {\r\n // Encode catch-all as repeated keys so the handler can getAll() \u2192 string[]\r\n raw.split('/').filter(Boolean).forEach(seg => url.searchParams.append(name, seg));\r\n } else {\r\n url.searchParams.set(name, raw);\r\n }\r\n });\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 dispatcherPath = path.join(SERVER_DIR, `_api_dispatcher_${randomBytes(4).toString('hex')}.ts`);\r\n fs.writeFileSync(dispatcherPath, makeApiDispatcherSource(apiRoutes));\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 banner: CJS_COMPAT_BANNER,\r\n external: NODE_BUILTINS,\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 before pages in config.json so they win on any\r\n // URL collision. Static files in .vercel/output/static/ (app/public +\r\n // framework bundles) are served by Vercel's CDN before any route is checked.\r\n for (const { srcRegex } of apiRoutes)\r\n vercelRoutes.push({ src: srcRegex, dest: '/api' });\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 globalRegistry = collectGlobalClientRegistry(serverPages, PAGES_DIR);\r\n const prerenderedHtml = await bundleClientComponents(globalRegistry, PAGES_DIR, STATIC_DIR);\r\n const prerenderedRecord = 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 resolve correctly), then bundle everything in\r\n // one esbuild pass via the dispatcher.\r\n const tempAdapterPaths: string[] = [];\r\n\r\n for (const page of serverPages) {\r\n const adapterDir = path.dirname(page.absPath);\r\n const adapterPath = path.join(adapterDir, `_page_adapter_${randomBytes(4).toString('hex')}.ts`);\r\n\r\n const layoutPaths = findPageLayouts(page.absPath, PAGES_DIR);\r\n const { registry, clientComponentNames } = buildPerPageRegistry(page.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(page.absPath)),\r\n layoutImports,\r\n clientComponentNames,\r\n allClientIds: [...registry.keys()],\r\n layoutArrayItems: layoutPaths.map((_, i) => `__layout_${i}__`).join(', '),\r\n prerenderedHtml: prerenderedRecord,\r\n catchAllNames: page.catchAllNames,\r\n }),\r\n );\r\n\r\n tempAdapterPaths.push(adapterPath);\r\n console.log(` prepared ${path.relative(PAGES_DIR, page.absPath)} \u2192 ${page.funcPath} [page]`);\r\n }\r\n\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 catchAllNames: page.catchAllNames,\r\n }));\r\n\r\n const dispatcherPath = path.join(PAGES_DIR, `_pages_dispatcher_${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 banner: CJS_COMPAT_BANNER,\r\n external: NODE_BUILTINS,\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// \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\n// `{ handle: 'filesystem' }` instructs Vercel's routing layer to check\r\n// .vercel/output/static/ BEFORE evaluating any of our dynamic route rules.\r\n// Without this, an optional catch-all like [[page]].tsx would intercept\r\n// /__n.js, /__react.js, and app/public/* before the CDN can serve them.\r\nfs.writeFileSync(\r\n path.join(OUTPUT_DIR, 'config.json'),\r\n JSON.stringify({ version: 3, routes: [{ handle: 'filesystem' }, ...vercelRoutes] }, null, 2),\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/http-server.d.ts
CHANGED
|
@@ -86,14 +86,7 @@ export declare function matchApiPrefix(url: string, apiPrefixes: ApiPrefixInfo[]
|
|
|
86
86
|
interface ApiHandlerOptions {
|
|
87
87
|
apiPrefixes: ApiPrefixInfo[];
|
|
88
88
|
port: number;
|
|
89
|
+
isDev: boolean;
|
|
89
90
|
}
|
|
90
|
-
|
|
91
|
-
* Creates the main API request dispatcher. The returned function:
|
|
92
|
-
*
|
|
93
|
-
* 1. Finds the matching prefix for the URL.
|
|
94
|
-
* 2. Resolves the exact handler file (direct file path, index, or dynamic route).
|
|
95
|
-
* 3. Dynamically imports the module (always fresh in dev thanks to file: URLs).
|
|
96
|
-
* 4. Calls the method-specific export (GET, POST, …) or `default`.
|
|
97
|
-
*/
|
|
98
|
-
export declare function createApiHandler({ apiPrefixes, port }: ApiHandlerOptions): (url: string, req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
91
|
+
export declare function createApiHandler({ apiPrefixes, port, isDev }: ApiHandlerOptions): (url: string, req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
99
92
|
export {};
|
package/dist/http-server.js
CHANGED
|
@@ -102,7 +102,14 @@ function matchApiPrefix(url, apiPrefixes) {
|
|
|
102
102
|
}
|
|
103
103
|
return null;
|
|
104
104
|
}
|
|
105
|
-
function
|
|
105
|
+
async function importFreshInDev(filePath) {
|
|
106
|
+
const { tsImport } = await import("tsx/esm/api");
|
|
107
|
+
return await tsImport(
|
|
108
|
+
pathToFileURL(filePath).href,
|
|
109
|
+
{ parentURL: import.meta.url }
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
function createApiHandler({ apiPrefixes, port, isDev }) {
|
|
106
113
|
return async function handleApiRoute(url, req, res) {
|
|
107
114
|
const apiRes = enhanceResponse(res);
|
|
108
115
|
const apiMatch = matchApiPrefix(url, apiPrefixes);
|
|
@@ -142,7 +149,7 @@ function createApiHandler({ apiPrefixes, port }) {
|
|
|
142
149
|
apiReq.body = await parseBody(req);
|
|
143
150
|
apiReq.params = params;
|
|
144
151
|
apiReq.query = parseQuery(url, port);
|
|
145
|
-
const apiModule = await import(pathToFileURL(filePath).href);
|
|
152
|
+
const apiModule = isDev ? await importFreshInDev(filePath) : await import(pathToFileURL(filePath).href);
|
|
146
153
|
const handler = apiModule[method] ?? apiModule.default;
|
|
147
154
|
if (!handler) {
|
|
148
155
|
apiRes.json({ error: `Method ${method} not allowed` }, 405);
|
package/dist/http-server.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/http-server.ts"],
|
|
4
|
-
"sourcesContent": ["/**\r\n * http-server.ts \u2014 API Route Dispatcher\r\n *\r\n * Handles discovery and dispatch of API routes inside `serverDir`.\r\n *\r\n * Directory conventions (mirrors Next.js):\r\n * server/\r\n * users/ \u2192 prefix /users (directory)\r\n * index.ts \u2192 GET /users (method exports: GET, POST, \u2026)\r\n * [id].ts \u2192 GET /users/:id\r\n * auth.ts \u2192 prefix /auth (top-level file)\r\n * index.ts \u2192 prefix / (root handler)\r\n *\r\n * Route handler exports:\r\n * export function GET(req, res) { \u2026 }\r\n * export function POST(req, res) { \u2026 }\r\n * export default function(req, res) { \u2026 } // matches any method\r\n *\r\n * Request augmentation:\r\n * req.body \u2014 parsed JSON or raw string (10 MB limit)\r\n * req.params \u2014 dynamic route segments (e.g. { id: '42' })\r\n * req.query \u2014 URL search params\r\n *\r\n * Response augmentation:\r\n * res.json(data, status?) \u2014 JSON response shorthand\r\n * res.status(code) \u2014 sets statusCode, returns res for chaining\r\n */\r\n\r\nimport path from 'path';\r\nimport fs from 'fs';\r\nimport { pathToFileURL } from 'url';\r\nimport type { IncomingMessage, ServerResponse } from 'http';\r\nimport { log } from './logger';\r\nimport { matchRoute } from './router';\r\n\r\n// \u2500\u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/** Describes a single API prefix discovered in serverDir. */\r\nexport interface ApiPrefixInfo {\r\n /** URL prefix this entry handles (e.g. '/users', ''). */\r\n prefix: string;\r\n /** Directory to scan for route files. */\r\n directory: string;\r\n /** Set when the prefix comes from a top-level file (not a directory). */\r\n filePath?: string;\r\n}\r\n\r\n/** Node's IncomingMessage with parsed body, params, and query. */\r\nexport interface ApiRequest extends IncomingMessage {\r\n params?: Record<string, string | string[]>;\r\n query?: Record<string, string>;\r\n body?: any;\r\n}\r\n\r\n/** Node's ServerResponse with json() and status() convenience methods. */\r\nexport interface ApiResponse extends ServerResponse {\r\n json: (data: any, status?: number) => void;\r\n status: (code: number) => ApiResponse;\r\n}\r\n\r\ntype HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'OPTIONS';\r\ntype ApiHandler = (req: ApiRequest, res: ApiResponse) => void | Promise<void>;\r\n\r\ninterface ApiModule {\r\n default?: ApiHandler;\r\n GET?: ApiHandler;\r\n POST?: ApiHandler;\r\n PUT?: ApiHandler;\r\n DELETE?: ApiHandler;\r\n PATCH?: ApiHandler;\r\n OPTIONS?: ApiHandler;\r\n}\r\n\r\n// \u2500\u2500\u2500 Route discovery \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Scans `serverDir` and returns one ApiPrefixInfo per directory, top-level\r\n * file, and root index.ts. Directories are returned before same-stem files\r\n * so `/a/b` routes resolve to the directory tree before any flat file.\r\n *\r\n * Called at startup and again whenever the server directory changes (in dev).\r\n */\r\nexport function discoverApiPrefixes(serverDir: string): ApiPrefixInfo[] {\r\n if (!fs.existsSync(serverDir)) {\r\n log.warn('Server directory not found:', serverDir);\r\n return [];\r\n }\r\n\r\n const entries = fs.readdirSync(serverDir, { withFileTypes: true });\r\n const prefixes: ApiPrefixInfo[] = [];\r\n\r\n // Directories first (higher specificity than same-stem files).\r\n for (const e of entries) {\r\n if (e.isDirectory()) {\r\n prefixes.push({ prefix: `/${e.name}`, directory: path.join(serverDir, e.name) });\r\n }\r\n }\r\n\r\n // Top-level .ts/.tsx files (excluding index which is handled separately below).\r\n for (const e of entries) {\r\n if (\r\n e.isFile() &&\r\n (e.name.endsWith('.ts') || e.name.endsWith('.tsx')) &&\r\n e.name !== 'index.ts' &&\r\n e.name !== 'index.tsx'\r\n ) {\r\n const stem = e.name.replace(/\\.tsx?$/, '');\r\n prefixes.push({\r\n prefix: `/${stem}`,\r\n directory: serverDir,\r\n filePath: path.join(serverDir, e.name),\r\n });\r\n }\r\n }\r\n\r\n // index.ts/tsx at the root of serverDir handles unmatched paths (prefix '').\r\n if (fs.existsSync(path.join(serverDir, 'index.ts'))) {\r\n prefixes.push({ prefix: '', directory: serverDir });\r\n }\r\n\r\n return prefixes;\r\n}\r\n\r\n// \u2500\u2500\u2500 Body parsing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nconst MAX_BODY_BYTES = 10 * 1024 * 1024; // 10 MB\r\n\r\n/**\r\n * Buffers the request body and returns:\r\n * - Parsed JSON object if Content-Type is application/json.\r\n * - Raw string otherwise.\r\n *\r\n * Rejects with an error if the body exceeds MAX_BODY_BYTES to prevent\r\n * memory exhaustion attacks. Deletes __proto__ and constructor from parsed\r\n * JSON objects to guard against prototype pollution.\r\n */\r\nexport async function parseBody(req: IncomingMessage): Promise<any> {\r\n return new Promise((resolve, reject) => {\r\n let body = '';\r\n let bytes = 0;\r\n\r\n req.on('data', chunk => {\r\n bytes += chunk.length;\r\n if (bytes > MAX_BODY_BYTES) {\r\n req.destroy();\r\n return reject(new Error('Request body too large'));\r\n }\r\n body += chunk.toString();\r\n });\r\n\r\n req.on('end', () => {\r\n try {\r\n if (body && req.headers['content-type']?.includes('application/json')) {\r\n const parsed = JSON.parse(body);\r\n // Guard against prototype pollution via __proto__ / constructor.\r\n if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {\r\n delete parsed.__proto__;\r\n delete parsed.constructor;\r\n }\r\n resolve(parsed);\r\n } else {\r\n resolve(body);\r\n }\r\n } catch (err) {\r\n reject(err);\r\n }\r\n });\r\n\r\n req.on('error', reject);\r\n });\r\n}\r\n\r\n// \u2500\u2500\u2500 Query parsing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/** Extracts URL search params into a plain string map. */\r\nexport function parseQuery(url: string, port: number): Record<string, string> {\r\n const query: Record<string, string> = {};\r\n new URL(url, `http://localhost:${port}`)\r\n .searchParams\r\n .forEach((v, k) => { query[k] = v; });\r\n return query;\r\n}\r\n\r\n// \u2500\u2500\u2500 Response enhancement \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Adds `json()` and `status()` convenience methods to a raw ServerResponse,\r\n * mirroring the Express API surface that most API handlers expect.\r\n */\r\nexport function enhanceResponse(res: ServerResponse): ApiResponse {\r\n const apiRes = res as ApiResponse;\r\n apiRes.json = function (data, statusCode = 200) {\r\n this.statusCode = statusCode;\r\n this.setHeader('Content-Type', 'application/json');\r\n this.end(JSON.stringify(data));\r\n };\r\n apiRes.status = function (code) {\r\n this.statusCode = code;\r\n return this;\r\n };\r\n return apiRes;\r\n}\r\n\r\n/** Responds to an OPTIONS preflight with permissive CORS headers. */\r\nfunction respondOptions(res: ApiResponse): void {\r\n res.setHeader('Access-Control-Allow-Origin', '*');\r\n res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');\r\n res.setHeader('Access-Control-Allow-Headers', 'Content-Type');\r\n res.statusCode = 204;\r\n res.end();\r\n}\r\n\r\n// \u2500\u2500\u2500 Prefix matching \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Finds the first ApiPrefixInfo whose prefix is a prefix of `url`.\r\n *\r\n * The empty-string prefix ('') acts as a catch-all and only matches when no\r\n * other prefix claims the URL.\r\n *\r\n * Returns `null` when no prefix matches (request should fall through to SSR).\r\n */\r\nexport function matchApiPrefix(\r\n url: string,\r\n apiPrefixes: ApiPrefixInfo[],\r\n): { prefix: ApiPrefixInfo; apiPath: string } | null {\r\n for (const prefix of apiPrefixes) {\r\n if (prefix.prefix === '') {\r\n // Empty prefix \u2014 only match if no other prefix has claimed this URL.\r\n const claimedByOther = apiPrefixes.some(\r\n p => p.prefix !== '' && url.startsWith(p.prefix),\r\n );\r\n if (!claimedByOther) return { prefix, apiPath: url || '/' };\r\n } else if (url.startsWith(prefix.prefix)) {\r\n return { prefix, apiPath: url.slice(prefix.prefix.length) || '/' };\r\n }\r\n }\r\n return null;\r\n}\r\n\r\n// \u2500\u2500\u2500 Request handler factory \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\ninterface ApiHandlerOptions {\r\n apiPrefixes: ApiPrefixInfo[];\r\n port: number;\r\n}\r\n\r\n/**\r\n * Creates the main API request dispatcher. The returned function:\r\n *\r\n * 1. Finds the matching prefix for the URL.\r\n * 2. Resolves the exact handler file (direct file path, index, or dynamic route).\r\n * 3. Dynamically imports the module (always fresh in dev thanks to file: URLs).\r\n * 4. Calls the method-specific export (GET, POST, \u2026) or `default`.\r\n */\r\nexport function createApiHandler({ apiPrefixes, port }: ApiHandlerOptions) {\r\n return async function handleApiRoute(\r\n url: string,\r\n req: IncomingMessage,\r\n res: ServerResponse,\r\n ): Promise<void> {\r\n const apiRes = enhanceResponse(res);\r\n const apiMatch = matchApiPrefix(url, apiPrefixes);\r\n\r\n if (!apiMatch) {\r\n apiRes.json({ error: 'API endpoint not found' }, 404);\r\n return;\r\n }\r\n\r\n const { prefix, apiPath } = apiMatch;\r\n let filePath: string | null = null;\r\n let params: Record<string, string | string[]> = {};\r\n\r\n // 1. Direct file match (top-level file prefix, e.g. server/auth.ts \u2192 /auth).\r\n if (prefix.filePath) {\r\n filePath = prefix.filePath;\r\n }\r\n\r\n // 2. Root index.ts (prefix === '' and path === '/').\r\n if (!filePath && prefix.prefix === '' && apiPath === '/') {\r\n const indexPath = path.join(prefix.directory, 'index.ts');\r\n if (fs.existsSync(indexPath)) filePath = indexPath;\r\n }\r\n\r\n // 3. Dynamic route matching inside the prefix directory.\r\n if (!filePath) {\r\n const routeMatch =\r\n matchRoute(apiPath, prefix.directory, '.ts') ??\r\n matchRoute(apiPath, prefix.directory, '.tsx');\r\n if (routeMatch) { filePath = routeMatch.filePath; params = routeMatch.params; }\r\n }\r\n\r\n if (!filePath) {\r\n apiRes.json({ error: 'API endpoint not found' }, 404);\r\n return;\r\n }\r\n\r\n try {\r\n const method = (req.method || 'GET').toUpperCase() as HttpMethod;\r\n log.verbose(`API ${method} ${url} -> ${path.relative(process.cwd(), filePath)}`);\r\n\r\n // OPTIONS preflight \u2014 respond immediately with CORS headers.\r\n if (method === 'OPTIONS') { respondOptions(apiRes); return; }\r\n\r\n // Augment the request object with parsed body, params, and query.\r\n const apiReq = req as ApiRequest;\r\n apiReq.body = await parseBody(req);\r\n apiReq.params = params;\r\n apiReq.query = parseQuery(url, port);\r\n\r\n // Dynamic import is always fresh because each path includes a unique\r\n // file:// URL \u2014 Node will not serve a stale cached module.\r\n const apiModule: ApiModule = await import(pathToFileURL(filePath).href);\r\n const handler = apiModule[method] ?? apiModule.default;\r\n\r\n if (!handler) {\r\n apiRes.json({ error: `Method ${method} not allowed` }, 405);\r\n return;\r\n }\r\n\r\n await handler(apiReq, apiRes);\r\n } catch (error) {\r\n log.error('API Error:', error);\r\n apiRes.json({ error: 'Internal server error' }, 500);\r\n }\r\n };\r\n}\r\n"],
|
|
5
|
-
"mappings": "AA4BA,OAAO,UAAU;AACjB,OAAO,QAAQ;AACf,SAAS,qBAAqB;AAE9B,SAAS,WAAW;AACpB,SAAS,kBAAkB;AAiDpB,SAAS,oBAAoB,WAAoC;AACtE,MAAI,CAAC,GAAG,WAAW,SAAS,GAAG;AAC7B,QAAI,KAAK,+BAA+B,SAAS;AACjD,WAAO,CAAC;AAAA,EACV;AAEA,QAAM,
|
|
4
|
+
"sourcesContent": ["/**\r\n * http-server.ts \u2014 API Route Dispatcher\r\n *\r\n * Handles discovery and dispatch of API routes inside `serverDir`.\r\n *\r\n * Directory conventions (mirrors Next.js):\r\n * server/\r\n * users/ \u2192 prefix /users (directory)\r\n * index.ts \u2192 GET /users (method exports: GET, POST, \u2026)\r\n * [id].ts \u2192 GET /users/:id\r\n * auth.ts \u2192 prefix /auth (top-level file)\r\n * index.ts \u2192 prefix / (root handler)\r\n *\r\n * Route handler exports:\r\n * export function GET(req, res) { \u2026 }\r\n * export function POST(req, res) { \u2026 }\r\n * export default function(req, res) { \u2026 } // matches any method\r\n *\r\n * Request augmentation:\r\n * req.body \u2014 parsed JSON or raw string (10 MB limit)\r\n * req.params \u2014 dynamic route segments (e.g. { id: '42' })\r\n * req.query \u2014 URL search params\r\n *\r\n * Response augmentation:\r\n * res.json(data, status?) \u2014 JSON response shorthand\r\n * res.status(code) \u2014 sets statusCode, returns res for chaining\r\n */\r\n\r\nimport path from 'path';\r\nimport fs from 'fs';\r\nimport { pathToFileURL } from 'url';\r\nimport type { IncomingMessage, ServerResponse } from 'http';\r\nimport { log } from './logger';\r\nimport { matchRoute } from './router';\r\n\r\n// \u2500\u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/** Describes a single API prefix discovered in serverDir. */\r\nexport interface ApiPrefixInfo {\r\n /** URL prefix this entry handles (e.g. '/users', ''). */\r\n prefix: string;\r\n /** Directory to scan for route files. */\r\n directory: string;\r\n /** Set when the prefix comes from a top-level file (not a directory). */\r\n filePath?: string;\r\n}\r\n\r\n/** Node's IncomingMessage with parsed body, params, and query. */\r\nexport interface ApiRequest extends IncomingMessage {\r\n params?: Record<string, string | string[]>;\r\n query?: Record<string, string>;\r\n body?: any;\r\n}\r\n\r\n/** Node's ServerResponse with json() and status() convenience methods. */\r\nexport interface ApiResponse extends ServerResponse {\r\n json: (data: any, status?: number) => void;\r\n status: (code: number) => ApiResponse;\r\n}\r\n\r\ntype HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'OPTIONS';\r\ntype ApiHandler = (req: ApiRequest, res: ApiResponse) => void | Promise<void>;\r\n\r\ninterface ApiModule {\r\n default?: ApiHandler;\r\n GET?: ApiHandler;\r\n POST?: ApiHandler;\r\n PUT?: ApiHandler;\r\n DELETE?: ApiHandler;\r\n PATCH?: ApiHandler;\r\n OPTIONS?: ApiHandler;\r\n}\r\n\r\n// \u2500\u2500\u2500 Route discovery \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Scans `serverDir` and returns one ApiPrefixInfo per directory, top-level\r\n * file, and root index.ts. Directories are returned before same-stem files\r\n * so `/a/b` routes resolve to the directory tree before any flat file.\r\n *\r\n * Called at startup and again whenever the server directory changes (in dev).\r\n */\r\nexport function discoverApiPrefixes(serverDir: string): ApiPrefixInfo[] {\r\n if (!fs.existsSync(serverDir)) {\r\n log.warn('Server directory not found:', serverDir);\r\n return [];\r\n }\r\n\r\n const entries = fs.readdirSync(serverDir, { withFileTypes: true });\r\n const prefixes: ApiPrefixInfo[] = [];\r\n\r\n // Directories first (higher specificity than same-stem files).\r\n for (const e of entries) {\r\n if (e.isDirectory()) {\r\n prefixes.push({ prefix: `/${e.name}`, directory: path.join(serverDir, e.name) });\r\n }\r\n }\r\n\r\n // Top-level .ts/.tsx files (excluding index which is handled separately below).\r\n for (const e of entries) {\r\n if (\r\n e.isFile() &&\r\n (e.name.endsWith('.ts') || e.name.endsWith('.tsx')) &&\r\n e.name !== 'index.ts' &&\r\n e.name !== 'index.tsx'\r\n ) {\r\n const stem = e.name.replace(/\\.tsx?$/, '');\r\n prefixes.push({\r\n prefix: `/${stem}`,\r\n directory: serverDir,\r\n filePath: path.join(serverDir, e.name),\r\n });\r\n }\r\n }\r\n\r\n // index.ts/tsx at the root of serverDir handles unmatched paths (prefix '').\r\n if (fs.existsSync(path.join(serverDir, 'index.ts'))) {\r\n prefixes.push({ prefix: '', directory: serverDir });\r\n }\r\n\r\n return prefixes;\r\n}\r\n\r\n// \u2500\u2500\u2500 Body parsing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nconst MAX_BODY_BYTES = 10 * 1024 * 1024; // 10 MB\r\n\r\n/**\r\n * Buffers the request body and returns:\r\n * - Parsed JSON object if Content-Type is application/json.\r\n * - Raw string otherwise.\r\n *\r\n * Rejects with an error if the body exceeds MAX_BODY_BYTES to prevent\r\n * memory exhaustion attacks. Deletes __proto__ and constructor from parsed\r\n * JSON objects to guard against prototype pollution.\r\n */\r\nexport async function parseBody(req: IncomingMessage): Promise<any> {\r\n return new Promise((resolve, reject) => {\r\n let body = '';\r\n let bytes = 0;\r\n\r\n req.on('data', chunk => {\r\n bytes += chunk.length;\r\n if (bytes > MAX_BODY_BYTES) {\r\n req.destroy();\r\n return reject(new Error('Request body too large'));\r\n }\r\n body += chunk.toString();\r\n });\r\n\r\n req.on('end', () => {\r\n try {\r\n if (body && req.headers['content-type']?.includes('application/json')) {\r\n const parsed = JSON.parse(body);\r\n // Guard against prototype pollution via __proto__ / constructor.\r\n if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {\r\n delete parsed.__proto__;\r\n delete parsed.constructor;\r\n }\r\n resolve(parsed);\r\n } else {\r\n resolve(body);\r\n }\r\n } catch (err) {\r\n reject(err);\r\n }\r\n });\r\n\r\n req.on('error', reject);\r\n });\r\n}\r\n\r\n// \u2500\u2500\u2500 Query parsing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/** Extracts URL search params into a plain string map. */\r\nexport function parseQuery(url: string, port: number): Record<string, string> {\r\n const query: Record<string, string> = {};\r\n new URL(url, `http://localhost:${port}`)\r\n .searchParams\r\n .forEach((v, k) => { query[k] = v; });\r\n return query;\r\n}\r\n\r\n// \u2500\u2500\u2500 Response enhancement \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Adds `json()` and `status()` convenience methods to a raw ServerResponse,\r\n * mirroring the Express API surface that most API handlers expect.\r\n */\r\nexport function enhanceResponse(res: ServerResponse): ApiResponse {\r\n const apiRes = res as ApiResponse;\r\n apiRes.json = function (data, statusCode = 200) {\r\n this.statusCode = statusCode;\r\n this.setHeader('Content-Type', 'application/json');\r\n this.end(JSON.stringify(data));\r\n };\r\n apiRes.status = function (code) {\r\n this.statusCode = code;\r\n return this;\r\n };\r\n return apiRes;\r\n}\r\n\r\n/** Responds to an OPTIONS preflight with permissive CORS headers. */\r\nfunction respondOptions(res: ApiResponse): void {\r\n res.setHeader('Access-Control-Allow-Origin', '*');\r\n res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');\r\n res.setHeader('Access-Control-Allow-Headers', 'Content-Type');\r\n res.statusCode = 204;\r\n res.end();\r\n}\r\n\r\n// \u2500\u2500\u2500 Prefix matching \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Finds the first ApiPrefixInfo whose prefix is a prefix of `url`.\r\n *\r\n * The empty-string prefix ('') acts as a catch-all and only matches when no\r\n * other prefix claims the URL.\r\n *\r\n * Returns `null` when no prefix matches (request should fall through to SSR).\r\n */\r\nexport function matchApiPrefix(\r\n url: string,\r\n apiPrefixes: ApiPrefixInfo[],\r\n): { prefix: ApiPrefixInfo; apiPath: string } | null {\r\n for (const prefix of apiPrefixes) {\r\n if (prefix.prefix === '') {\r\n // Empty prefix \u2014 only match if no other prefix has claimed this URL.\r\n const claimedByOther = apiPrefixes.some(\r\n p => p.prefix !== '' && url.startsWith(p.prefix),\r\n );\r\n if (!claimedByOther) return { prefix, apiPath: url || '/' };\r\n } else if (url.startsWith(prefix.prefix)) {\r\n return { prefix, apiPath: url.slice(prefix.prefix.length) || '/' };\r\n }\r\n }\r\n return null;\r\n}\r\n\r\n// \u2500\u2500\u2500 Dev-mode fresh importer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Imports `filePath` fresh on every call using tsx's tsImport, which creates\r\n * an isolated module namespace that bypasses Node's ESM cache entirely.\r\n *\r\n * This is identical to how ssr.ts loads page and layout modules in dev mode.\r\n * tsx handles TypeScript and TSX natively, and bare specifiers (e.g.\r\n * \"@orpc/server/node\") resolve normally through the standard node_modules\r\n * chain \u2014 no bundling, no temp files, no watchers needed.\r\n */\r\nasync function importFreshInDev(filePath: string): Promise<ApiModule> {\r\n const { tsImport } = await import('tsx/esm/api');\r\n return await tsImport(\r\n pathToFileURL(filePath).href,\r\n { parentURL: import.meta.url },\r\n ) as ApiModule;\r\n}\r\n\r\n// \u2500\u2500\u2500 Request handler factory \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\ninterface ApiHandlerOptions {\r\n apiPrefixes: ApiPrefixInfo[];\r\n port: number;\r\n isDev: boolean;\r\n}\r\n\r\nexport function createApiHandler({ apiPrefixes, port, isDev }: ApiHandlerOptions) {\r\n return async function handleApiRoute(\r\n url: string,\r\n req: IncomingMessage,\r\n res: ServerResponse,\r\n ): Promise<void> {\r\n const apiRes = enhanceResponse(res);\r\n const apiMatch = matchApiPrefix(url, apiPrefixes);\r\n\r\n if (!apiMatch) {\r\n apiRes.json({ error: 'API endpoint not found' }, 404);\r\n return;\r\n }\r\n\r\n const { prefix, apiPath } = apiMatch;\r\n let filePath: string | null = null;\r\n let params: Record<string, string | string[]> = {};\r\n\r\n // 1. Direct file match (top-level file prefix, e.g. server/auth.ts \u2192 /auth).\r\n if (prefix.filePath) {\r\n filePath = prefix.filePath;\r\n }\r\n\r\n // 2. Root index.ts (prefix === '' and path === '/').\r\n if (!filePath && prefix.prefix === '' && apiPath === '/') {\r\n const indexPath = path.join(prefix.directory, 'index.ts');\r\n if (fs.existsSync(indexPath)) filePath = indexPath;\r\n }\r\n\r\n // 3. Dynamic route matching inside the prefix directory.\r\n if (!filePath) {\r\n const routeMatch =\r\n matchRoute(apiPath, prefix.directory, '.ts') ??\r\n matchRoute(apiPath, prefix.directory, '.tsx');\r\n if (routeMatch) { filePath = routeMatch.filePath; params = routeMatch.params; }\r\n }\r\n\r\n if (!filePath) {\r\n apiRes.json({ error: 'API endpoint not found' }, 404);\r\n return;\r\n }\r\n\r\n try {\r\n const method = (req.method || 'GET').toUpperCase() as HttpMethod;\r\n log.verbose(`API ${method} ${url} -> ${path.relative(process.cwd(), filePath)}`);\r\n\r\n // OPTIONS preflight \u2014 respond immediately with CORS headers.\r\n if (method === 'OPTIONS') { respondOptions(apiRes); return; }\r\n\r\n // Augment the request object with parsed body, params, and query.\r\n const apiReq = req as ApiRequest;\r\n apiReq.body = await parseBody(req);\r\n apiReq.params = params;\r\n apiReq.query = parseQuery(url, port);\r\n\r\n const apiModule: ApiModule = isDev\r\n ? await importFreshInDev(filePath)\r\n : await import(pathToFileURL(filePath).href);\r\n const handler = apiModule[method] ?? apiModule.default;\r\n\r\n if (!handler) {\r\n apiRes.json({ error: `Method ${method} not allowed` }, 405);\r\n return;\r\n }\r\n\r\n await handler(apiReq, apiRes);\r\n } catch (error) {\r\n log.error('API Error:', error);\r\n apiRes.json({ error: 'Internal server error' }, 500);\r\n }\r\n };\r\n}"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nukejs",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.9",
|
|
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",
|