nukejs 0.0.1 → 0.0.2
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/LICENSE +21 -0
- package/README.md +529 -0
- package/bin/index.mjs +126 -0
- package/dist/app.d.ts +18 -0
- package/dist/app.js +124 -0
- package/dist/app.js.map +7 -0
- package/dist/as-is/Link.d.ts +6 -0
- package/dist/as-is/Link.tsx +20 -0
- package/dist/as-is/useRouter.d.ts +7 -0
- package/dist/as-is/useRouter.ts +33 -0
- package/dist/build-common.d.ts +192 -0
- package/dist/build-common.js +737 -0
- package/dist/build-common.js.map +7 -0
- package/dist/build-node.d.ts +1 -0
- package/dist/build-node.js +170 -0
- package/dist/build-node.js.map +7 -0
- package/dist/build-vercel.d.ts +1 -0
- package/dist/build-vercel.js +65 -0
- package/dist/build-vercel.js.map +7 -0
- package/dist/builder.d.ts +1 -0
- package/dist/builder.js +97 -0
- package/dist/builder.js.map +7 -0
- package/dist/bundle.d.ts +68 -0
- package/dist/bundle.js +166 -0
- package/dist/bundle.js.map +7 -0
- package/dist/bundler.d.ts +58 -0
- package/dist/bundler.js +98 -0
- package/dist/bundler.js.map +7 -0
- package/dist/component-analyzer.d.ts +72 -0
- package/dist/component-analyzer.js +102 -0
- package/dist/component-analyzer.js.map +7 -0
- package/dist/config.d.ts +35 -0
- package/dist/config.js +30 -0
- package/dist/config.js.map +7 -0
- package/dist/hmr-bundle.d.ts +25 -0
- package/dist/hmr-bundle.js +76 -0
- package/dist/hmr-bundle.js.map +7 -0
- package/dist/hmr.d.ts +55 -0
- package/dist/hmr.js +62 -0
- package/dist/hmr.js.map +7 -0
- package/dist/html-store.d.ts +121 -0
- package/dist/html-store.js +42 -0
- package/dist/html-store.js.map +7 -0
- package/dist/http-server.d.ts +99 -0
- package/dist/http-server.js +166 -0
- package/dist/http-server.js.map +7 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +7 -0
- package/dist/logger.d.ts +58 -0
- package/dist/logger.js +53 -0
- package/dist/logger.js.map +7 -0
- package/dist/metadata.d.ts +50 -0
- package/dist/metadata.js +43 -0
- package/dist/metadata.js.map +7 -0
- package/dist/middleware-loader.d.ts +50 -0
- package/dist/middleware-loader.js +50 -0
- package/dist/middleware-loader.js.map +7 -0
- package/dist/middleware.d.ts +22 -0
- package/dist/middleware.example.d.ts +8 -0
- package/dist/middleware.example.js +58 -0
- package/dist/middleware.example.js.map +7 -0
- package/dist/middleware.js +59 -0
- package/dist/middleware.js.map +7 -0
- package/dist/renderer.d.ts +44 -0
- package/dist/renderer.js +130 -0
- package/dist/renderer.js.map +7 -0
- package/dist/router.d.ts +84 -0
- package/dist/router.js +104 -0
- package/dist/router.js.map +7 -0
- package/dist/ssr.d.ts +39 -0
- package/dist/ssr.js +168 -0
- package/dist/ssr.js.map +7 -0
- package/dist/use-html.d.ts +64 -0
- package/dist/use-html.js +125 -0
- package/dist/use-html.js.map +7 -0
- package/dist/utils.d.ts +26 -0
- package/dist/utils.js +62 -0
- package/dist/utils.js.map +7 -0
- package/package.json +64 -12
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import { pathToFileURL, fileURLToPath } from "url";
|
|
4
|
+
import { log } from "./logger.js";
|
|
5
|
+
const middlewares = [];
|
|
6
|
+
async function loadMiddlewareFromPath(middlewarePath) {
|
|
7
|
+
if (!fs.existsSync(middlewarePath)) {
|
|
8
|
+
log.verbose(`No middleware found at ${middlewarePath}, skipping`);
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
const mod = await import(pathToFileURL(middlewarePath).href);
|
|
13
|
+
if (typeof mod.default === "function") {
|
|
14
|
+
middlewares.push(mod.default);
|
|
15
|
+
log.info(`Middleware loaded from ${middlewarePath}`);
|
|
16
|
+
} else {
|
|
17
|
+
log.warn(`${middlewarePath} does not export a default function`);
|
|
18
|
+
}
|
|
19
|
+
} catch (error) {
|
|
20
|
+
log.error(`Error loading middleware from ${middlewarePath}:`, error);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async function loadMiddleware() {
|
|
24
|
+
const appDir = path.dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
const builtinPath = path.join(
|
|
26
|
+
appDir,
|
|
27
|
+
`middleware.${appDir.endsWith("dist") ? "js" : "ts"}`
|
|
28
|
+
);
|
|
29
|
+
const userPath = path.join(process.cwd(), "middleware.ts");
|
|
30
|
+
const paths = [.../* @__PURE__ */ new Set([builtinPath, userPath])];
|
|
31
|
+
for (const middlewarePath of paths) {
|
|
32
|
+
await loadMiddlewareFromPath(middlewarePath);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async function runMiddleware(req, res) {
|
|
36
|
+
if (middlewares.length === 0) return false;
|
|
37
|
+
for (const middleware of middlewares) {
|
|
38
|
+
await middleware(req, res);
|
|
39
|
+
if (res.writableEnded || res.headersSent) {
|
|
40
|
+
log.verbose("Middleware handled request, skipping further processing");
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
export {
|
|
47
|
+
loadMiddleware,
|
|
48
|
+
runMiddleware
|
|
49
|
+
};
|
|
50
|
+
//# sourceMappingURL=middleware-loader.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/middleware-loader.ts"],
|
|
4
|
+
"sourcesContent": ["/**\r\n * middleware-loader.ts \u2014 Middleware Chain Manager\r\n *\r\n * Loads and runs the NukeJS middleware stack. Two layers are supported:\r\n *\r\n * 1. Built-in middleware \u2014 shipped with the nukejs package.\r\n * Currently handles the /__hmr and /__hmr.js\r\n * routes required by the HMR client.\r\n * Located next to this file as `middleware.ts`\r\n * (or `middleware.js` in the compiled dist/).\r\n *\r\n * 2. User middleware \u2014 `middleware.ts` in the project root (cwd).\r\n * Runs after the built-in layer so it can inspect\r\n * or short-circuit every incoming request, including\r\n * API and page routes.\r\n *\r\n * Each middleware function receives (req, res) and may either:\r\n * - End the response (res.end / res.json) to short-circuit further handling.\r\n * - Return without touching res to pass control to the next layer.\r\n *\r\n * runMiddleware() returns `true` if any middleware ended the response,\r\n * allowing app.ts to skip its own routing logic.\r\n *\r\n * Restart behaviour:\r\n * When nuke.config.ts or middleware.ts change in dev, app.ts restarts the\r\n * process. The new process calls loadMiddleware() fresh so stale module\r\n * caches are not an issue.\r\n */\r\n\r\nimport path from 'path';\r\nimport fs from 'fs';\r\nimport { pathToFileURL, fileURLToPath } from 'url';\r\nimport type { IncomingMessage, ServerResponse } from 'http';\r\nimport { log } from './logger';\r\n\r\n// \u2500\u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nexport type MiddlewareFunction = (\r\n req: IncomingMessage,\r\n res: ServerResponse,\r\n) => Promise<void> | void;\r\n\r\n// \u2500\u2500\u2500 Internal state \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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/** Ordered list of loaded middleware functions. Populated by loadMiddleware(). */\r\nconst middlewares: MiddlewareFunction[] = [];\r\n\r\n// \u2500\u2500\u2500 Loader 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\r\n\r\n/**\r\n * Attempts to import a middleware file and push its default export onto the\r\n * stack. Skips silently if the file doesn't exist. Logs a warning if the\r\n * file exists but doesn't export a default function.\r\n */\r\nasync function loadMiddlewareFromPath(middlewarePath: string): Promise<void> {\r\n if (!fs.existsSync(middlewarePath)) {\r\n log.verbose(`No middleware found at ${middlewarePath}, skipping`);\r\n return;\r\n }\r\n\r\n try {\r\n const mod = await import(pathToFileURL(middlewarePath).href);\r\n if (typeof mod.default === 'function') {\r\n middlewares.push(mod.default);\r\n log.info(`Middleware loaded from ${middlewarePath}`);\r\n } else {\r\n log.warn(`${middlewarePath} does not export a default function`);\r\n }\r\n } catch (error) {\r\n log.error(`Error loading middleware from ${middlewarePath}:`, error);\r\n }\r\n}\r\n\r\n// \u2500\u2500\u2500 Public API \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Discovers and loads all middleware in priority order:\r\n * 1. Built-in (this package's own middleware.ts / middleware.js)\r\n * 2. User-supplied (cwd/middleware.ts)\r\n *\r\n * Duplicate paths (e.g. if cwd === package dir in a monorepo) are deduplicated\r\n * via a Set so the same file is never loaded twice.\r\n *\r\n * Should be called once at startup after the config is loaded.\r\n */\r\nexport async function loadMiddleware(): Promise<void> {\r\n // __dirname equivalent in ESM.\r\n const appDir = path.dirname(fileURLToPath(import.meta.url));\r\n\r\n // The built-in middleware handles /__hmr and /__hmr.js for the HMR client.\r\n const builtinPath = path.join(\r\n appDir,\r\n `middleware.${appDir.endsWith('dist') ? 'js' : 'ts'}`,\r\n );\r\n\r\n const userPath = path.join(process.cwd(), 'middleware.ts');\r\n\r\n // Deduplicate in case the two paths resolve to the same file.\r\n const paths = [...new Set([builtinPath, userPath])];\r\n\r\n for (const middlewarePath of paths) {\r\n await loadMiddlewareFromPath(middlewarePath);\r\n }\r\n}\r\n\r\n/**\r\n * Runs all loaded middleware in registration order.\r\n *\r\n * Stops and returns `true` as soon as any middleware ends or sends a response\r\n * (res.writableEnded or res.headersSent), allowing app.ts to skip routing.\r\n *\r\n * Returns `false` if no middleware handled the request.\r\n */\r\nexport async function runMiddleware(\r\n req: IncomingMessage,\r\n res: ServerResponse,\r\n): Promise<boolean> {\r\n if (middlewares.length === 0) return false;\r\n\r\n for (const middleware of middlewares) {\r\n await middleware(req, res);\r\n\r\n if (res.writableEnded || res.headersSent) {\r\n log.verbose('Middleware handled request, skipping further processing');\r\n return true;\r\n }\r\n }\r\n\r\n return false;\r\n}\r\n"],
|
|
5
|
+
"mappings": "AA6BA,OAAO,UAAU;AACjB,OAAO,QAAQ;AACf,SAAS,eAAe,qBAAqB;AAE7C,SAAS,WAAW;AAYpB,MAAM,cAAoC,CAAC;AAS3C,eAAe,uBAAuB,gBAAuC;AAC3E,MAAI,CAAC,GAAG,WAAW,cAAc,GAAG;AAClC,QAAI,QAAQ,0BAA0B,cAAc,YAAY;AAChE;AAAA,EACF;AAEA,MAAI;AACF,UAAM,MAAM,MAAM,OAAO,cAAc,cAAc,EAAE;AACvD,QAAI,OAAO,IAAI,YAAY,YAAY;AACrC,kBAAY,KAAK,IAAI,OAAO;AAC5B,UAAI,KAAK,0BAA0B,cAAc,EAAE;AAAA,IACrD,OAAO;AACL,UAAI,KAAK,GAAG,cAAc,qCAAqC;AAAA,IACjE;AAAA,EACF,SAAS,OAAO;AACd,QAAI,MAAM,iCAAiC,cAAc,KAAK,KAAK;AAAA,EACrE;AACF;AAcA,eAAsB,iBAAgC;AAEpD,QAAM,SAAS,KAAK,QAAQ,cAAc,YAAY,GAAG,CAAC;AAG1D,QAAM,cAAc,KAAK;AAAA,IACvB;AAAA,IACA,cAAc,OAAO,SAAS,MAAM,IAAI,OAAO,IAAI;AAAA,EACrD;AAEA,QAAM,WAAW,KAAK,KAAK,QAAQ,IAAI,GAAG,eAAe;AAGzD,QAAM,QAAQ,CAAC,GAAG,oBAAI,IAAI,CAAC,aAAa,QAAQ,CAAC,CAAC;AAElD,aAAW,kBAAkB,OAAO;AAClC,UAAM,uBAAuB,cAAc;AAAA,EAC7C;AACF;AAUA,eAAsB,cACpB,KACA,KACkB;AAClB,MAAI,YAAY,WAAW,EAAG,QAAO;AAErC,aAAW,cAAc,aAAa;AACpC,UAAM,WAAW,KAAK,GAAG;AAEzB,QAAI,IAAI,iBAAiB,IAAI,aAAa;AACxC,UAAI,QAAQ,yDAAyD;AACrE,aAAO;AAAA,IACT;AAAA,EACF;AAEA,SAAO;AACT;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* middleware.ts — Built-In NukeJS Middleware
|
|
3
|
+
*
|
|
4
|
+
* This is the internal middleware loaded before any user-defined middleware.
|
|
5
|
+
* It handles three responsibilities:
|
|
6
|
+
*
|
|
7
|
+
* 1. Static public files (app/public/**)
|
|
8
|
+
* Any file placed in app/public/ is served at its path relative to
|
|
9
|
+
* that directory. E.g. app/public/favicon.ico → GET /favicon.ico.
|
|
10
|
+
* The correct Content-Type is set automatically. Path traversal attempts
|
|
11
|
+
* are rejected with 400.
|
|
12
|
+
*
|
|
13
|
+
* 2. HMR client script (/__hmr.js)
|
|
14
|
+
* Builds and serves hmr-bundle.ts on demand. Injected into every dev
|
|
15
|
+
* page as <script type="module" src="/__hmr.js">.
|
|
16
|
+
*
|
|
17
|
+
* 3. HMR SSE stream (/__hmr)
|
|
18
|
+
* Long-lived Server-Sent Events connection used by the browser to receive
|
|
19
|
+
* reload/replace/restart events when source files change.
|
|
20
|
+
*/
|
|
21
|
+
import type { IncomingMessage, ServerResponse } from 'http';
|
|
22
|
+
export default function middleware(req: IncomingMessage, res: ServerResponse): Promise<void>;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* middleware.ts (place in project root)
|
|
3
|
+
*
|
|
4
|
+
* Runs before every request. Inspect/modify req, add headers, or send early
|
|
5
|
+
* responses to halt further processing.
|
|
6
|
+
*/
|
|
7
|
+
import type { IncomingMessage, ServerResponse } from 'http';
|
|
8
|
+
export default function middleware(req: IncomingMessage, res: ServerResponse): Promise<void>;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
async function middleware(req, res) {
|
|
2
|
+
console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] ${req.method} ${req.url}`);
|
|
3
|
+
if (req.url?.startsWith("/admin") && !isAuthenticated(req)) {
|
|
4
|
+
res.statusCode = 401;
|
|
5
|
+
res.setHeader("Content-Type", "text/html");
|
|
6
|
+
res.end("<h1>401 Unauthorized</h1><p>Please log in to access this page.</p>");
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
if (req.url === "/old-page") {
|
|
10
|
+
req.url = "/new-page";
|
|
11
|
+
}
|
|
12
|
+
res.setHeader("X-Powered-By", "nukejs-framework");
|
|
13
|
+
res.setHeader("X-Request-Id", generateRequestId());
|
|
14
|
+
if (req.url?.startsWith("/api/")) {
|
|
15
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
16
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
|
|
17
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
18
|
+
if (req.method === "OPTIONS") {
|
|
19
|
+
res.statusCode = 204;
|
|
20
|
+
res.end();
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const clientIp = req.socket.remoteAddress || "unknown";
|
|
25
|
+
if (isRateLimited(clientIp)) {
|
|
26
|
+
res.statusCode = 429;
|
|
27
|
+
res.setHeader("Content-Type", "application/json");
|
|
28
|
+
res.end(JSON.stringify({ error: "Too many requests" }));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (process.env.MAINTENANCE_MODE === "true" && !req.url?.startsWith("/__")) {
|
|
32
|
+
res.statusCode = 503;
|
|
33
|
+
res.setHeader("Content-Type", "text/html");
|
|
34
|
+
res.end("<h1>503 Service Unavailable</h1><p>We are currently down for maintenance.</p>");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function isAuthenticated(req) {
|
|
39
|
+
return req.headers.authorization === "Bearer valid-token";
|
|
40
|
+
}
|
|
41
|
+
function generateRequestId() {
|
|
42
|
+
return Math.random().toString(36).substring(2, 15);
|
|
43
|
+
}
|
|
44
|
+
const rateLimitMap = /* @__PURE__ */ new Map();
|
|
45
|
+
function isRateLimited(ip) {
|
|
46
|
+
const now = Date.now();
|
|
47
|
+
const limit = rateLimitMap.get(ip);
|
|
48
|
+
if (!limit || now > limit.resetAt) {
|
|
49
|
+
rateLimitMap.set(ip, { count: 1, resetAt: now + 6e4 });
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
limit.count++;
|
|
53
|
+
return limit.count > 100;
|
|
54
|
+
}
|
|
55
|
+
export {
|
|
56
|
+
middleware as default
|
|
57
|
+
};
|
|
58
|
+
//# sourceMappingURL=middleware.example.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/middleware.example.ts"],
|
|
4
|
+
"sourcesContent": ["/**\r\n * middleware.ts (place in project root)\r\n *\r\n * Runs before every request. Inspect/modify req, add headers, or send early\r\n * responses to halt further processing.\r\n */\r\n\r\nimport type { IncomingMessage, ServerResponse } from 'http';\r\n\r\nexport default async function middleware(req: IncomingMessage, res: ServerResponse): Promise<void> {\r\n console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);\r\n\r\n if (req.url?.startsWith('/admin') && !isAuthenticated(req)) {\r\n res.statusCode = 401;\r\n res.setHeader('Content-Type', 'text/html');\r\n res.end('<h1>401 Unauthorized</h1><p>Please log in to access this page.</p>');\r\n return;\r\n }\r\n\r\n if (req.url === '/old-page') {\r\n req.url = '/new-page';\r\n }\r\n\r\n res.setHeader('X-Powered-By', 'nukejs-framework');\r\n res.setHeader('X-Request-Id', generateRequestId());\r\n\r\n if (req.url?.startsWith('/api/')) {\r\n res.setHeader('Access-Control-Allow-Origin', '*');\r\n res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');\r\n res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');\r\n\r\n if (req.method === 'OPTIONS') {\r\n res.statusCode = 204;\r\n res.end();\r\n return;\r\n }\r\n }\r\n\r\n const clientIp = req.socket.remoteAddress || 'unknown';\r\n if (isRateLimited(clientIp)) {\r\n res.statusCode = 429;\r\n res.setHeader('Content-Type', 'application/json');\r\n res.end(JSON.stringify({ error: 'Too many requests' }));\r\n return;\r\n }\r\n\r\n if (process.env.MAINTENANCE_MODE === 'true' && !req.url?.startsWith('/__')) {\r\n res.statusCode = 503;\r\n res.setHeader('Content-Type', 'text/html');\r\n res.end('<h1>503 Service Unavailable</h1><p>We are currently down for maintenance.</p>');\r\n return;\r\n }\r\n}\r\n\r\nfunction isAuthenticated(req: IncomingMessage): boolean {\r\n return req.headers.authorization === 'Bearer valid-token';\r\n}\r\n\r\nfunction generateRequestId(): string {\r\n return Math.random().toString(36).substring(2, 15);\r\n}\r\n\r\nconst rateLimitMap = new Map<string, { count: number; resetAt: number }>();\r\n\r\nfunction isRateLimited(ip: string): boolean {\r\n const now = Date.now();\r\n const limit = rateLimitMap.get(ip);\r\n\r\n if (!limit || now > limit.resetAt) {\r\n rateLimitMap.set(ip, { count: 1, resetAt: now + 60000 });\r\n return false;\r\n }\r\n\r\n limit.count++;\r\n return limit.count > 100;\r\n}\r\n"],
|
|
5
|
+
"mappings": "AASA,eAAO,WAAkC,KAAsB,KAAoC;AACjG,UAAQ,IAAI,KAAI,oBAAI,KAAK,GAAE,YAAY,CAAC,KAAK,IAAI,MAAM,IAAI,IAAI,GAAG,EAAE;AAEpE,MAAI,IAAI,KAAK,WAAW,QAAQ,KAAK,CAAC,gBAAgB,GAAG,GAAG;AAC1D,QAAI,aAAa;AACjB,QAAI,UAAU,gBAAgB,WAAW;AACzC,QAAI,IAAI,oEAAoE;AAC5E;AAAA,EACF;AAEA,MAAI,IAAI,QAAQ,aAAa;AAC3B,QAAI,MAAM;AAAA,EACZ;AAEA,MAAI,UAAU,gBAAgB,kBAAkB;AAChD,MAAI,UAAU,gBAAgB,kBAAkB,CAAC;AAEjD,MAAI,IAAI,KAAK,WAAW,OAAO,GAAG;AAChC,QAAI,UAAU,+BAA+B,GAAG;AAChD,QAAI,UAAU,gCAAgC,iCAAiC;AAC/E,QAAI,UAAU,gCAAgC,6BAA6B;AAE3E,QAAI,IAAI,WAAW,WAAW;AAC5B,UAAI,aAAa;AACjB,UAAI,IAAI;AACR;AAAA,IACF;AAAA,EACF;AAEA,QAAM,WAAW,IAAI,OAAO,iBAAiB;AAC7C,MAAI,cAAc,QAAQ,GAAG;AAC3B,QAAI,aAAa;AACjB,QAAI,UAAU,gBAAgB,kBAAkB;AAChD,QAAI,IAAI,KAAK,UAAU,EAAE,OAAO,oBAAoB,CAAC,CAAC;AACtD;AAAA,EACF;AAEA,MAAI,QAAQ,IAAI,qBAAqB,UAAU,CAAC,IAAI,KAAK,WAAW,KAAK,GAAG;AAC1E,QAAI,aAAa;AACjB,QAAI,UAAU,gBAAgB,WAAW;AACzC,QAAI,IAAI,+EAA+E;AACvF;AAAA,EACF;AACF;AAEA,SAAS,gBAAgB,KAA+B;AACtD,SAAO,IAAI,QAAQ,kBAAkB;AACvC;AAEA,SAAS,oBAA4B;AACnC,SAAO,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,UAAU,GAAG,EAAE;AACnD;AAEA,MAAM,eAAe,oBAAI,IAAgD;AAEzE,SAAS,cAAc,IAAqB;AAC1C,QAAM,MAAM,KAAK,IAAI;AACrB,QAAM,QAAQ,aAAa,IAAI,EAAE;AAEjC,MAAI,CAAC,SAAS,MAAM,MAAM,SAAS;AACjC,iBAAa,IAAI,IAAI,EAAE,OAAO,GAAG,SAAS,MAAM,IAAM,CAAC;AACvD,WAAO;AAAA,EACT;AAEA,QAAM;AACN,SAAO,MAAM,QAAQ;AACvB;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { build } from "esbuild";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import { hmrClients } from "./hmr.js";
|
|
6
|
+
import { getMimeType } from "./utils.js";
|
|
7
|
+
const PUBLIC_DIR = path.resolve("./app/public");
|
|
8
|
+
async function middleware(req, res) {
|
|
9
|
+
const rawUrl = req.url ?? "/";
|
|
10
|
+
const pathname = rawUrl.split("?")[0];
|
|
11
|
+
if (fs.existsSync(PUBLIC_DIR)) {
|
|
12
|
+
const candidate = path.join(PUBLIC_DIR, pathname);
|
|
13
|
+
const publicBase = PUBLIC_DIR.endsWith(path.sep) ? PUBLIC_DIR : PUBLIC_DIR + path.sep;
|
|
14
|
+
const safe = candidate.startsWith(publicBase) || candidate === PUBLIC_DIR;
|
|
15
|
+
if (!safe) {
|
|
16
|
+
res.statusCode = 400;
|
|
17
|
+
res.end("Bad request");
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
|
|
21
|
+
const ext = path.extname(candidate);
|
|
22
|
+
res.setHeader("Content-Type", getMimeType(ext));
|
|
23
|
+
res.end(fs.readFileSync(candidate));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (rawUrl === "/__hmr.js") {
|
|
28
|
+
const dir = path.dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
const entry = path.join(dir, `hmr-bundle.${dir.endsWith("dist") ? "js" : "ts"}`);
|
|
30
|
+
const result = await build({
|
|
31
|
+
entryPoints: [entry],
|
|
32
|
+
write: false,
|
|
33
|
+
format: "esm",
|
|
34
|
+
minify: true,
|
|
35
|
+
bundle: true,
|
|
36
|
+
// React is not used in the HMR client, but excluding it prevents esbuild
|
|
37
|
+
// from inlining it if any transitive import references react.
|
|
38
|
+
external: ["react", "react-dom/client", "react/jsx-runtime"]
|
|
39
|
+
});
|
|
40
|
+
res.setHeader("Content-Type", "application/javascript");
|
|
41
|
+
res.end(result.outputFiles[0].text);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (rawUrl === "/__hmr") {
|
|
45
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
46
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
47
|
+
res.setHeader("Connection", "keep-alive");
|
|
48
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
49
|
+
res.flushHeaders();
|
|
50
|
+
res.write('data: {"type":"connected"}\n\n');
|
|
51
|
+
hmrClients.add(res);
|
|
52
|
+
req.on("close", () => hmrClients.delete(res));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export {
|
|
57
|
+
middleware as default
|
|
58
|
+
};
|
|
59
|
+
//# sourceMappingURL=middleware.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/middleware.ts"],
|
|
4
|
+
"sourcesContent": ["/**\r\n * middleware.ts \u2014 Built-In NukeJS Middleware\r\n *\r\n * This is the internal middleware loaded before any user-defined middleware.\r\n * It handles three responsibilities:\r\n *\r\n * 1. Static public files (app/public/**)\r\n * Any file placed in app/public/ is served at its path relative to\r\n * that directory. E.g. app/public/favicon.ico \u2192 GET /favicon.ico.\r\n * The correct Content-Type is set automatically. Path traversal attempts\r\n * are rejected with 400.\r\n *\r\n * 2. HMR client script (/__hmr.js)\r\n * Builds and serves hmr-bundle.ts on demand. Injected into every dev\r\n * page as <script type=\"module\" src=\"/__hmr.js\">.\r\n *\r\n * 3. HMR SSE stream (/__hmr)\r\n * Long-lived Server-Sent Events connection used by the browser to receive\r\n * reload/replace/restart events when source files change.\r\n */\r\n\r\nimport { build } from 'esbuild';\r\nimport type { IncomingMessage, ServerResponse } from 'http';\r\nimport path from 'path';\r\nimport fs from 'fs';\r\nimport { fileURLToPath } from 'url';\r\nimport { hmrClients } from './hmr';\r\nimport { getMimeType } from './utils';\r\n\r\n// Absolute path to the static public directory.\r\n// Files here are served at their path relative to this directory.\r\nconst PUBLIC_DIR = path.resolve('./app/public');\r\n\r\nexport default async function middleware(\r\n req: IncomingMessage,\r\n res: ServerResponse,\r\n): Promise<void> {\r\n\r\n // \u2500\u2500 Static public files \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n // Checked first so /favicon.ico, /main.css, etc. are never accidentally\r\n // routed to the SSR or API layers.\r\n const rawUrl = req.url ?? '/';\r\n const pathname = rawUrl.split('?')[0]; // strip query string\r\n\r\n if (fs.existsSync(PUBLIC_DIR)) {\r\n // path.join handles the leading '/' in pathname naturally and normalises\r\n // any '..' segments, making it safe to use directly with a startsWith guard.\r\n // Using path.join (not path.resolve) ensures an absolute second argument\r\n // cannot silently escape PUBLIC_DIR the way path.resolve would allow.\r\n const candidate = path.join(PUBLIC_DIR, pathname);\r\n\r\n // Path traversal guard: the resolved path must be inside PUBLIC_DIR.\r\n // We normalise PUBLIC_DIR with a trailing separator so that a directory\r\n // whose name is a prefix of another cannot pass (e.g. /public2 vs /public).\r\n const publicBase = PUBLIC_DIR.endsWith(path.sep) ? PUBLIC_DIR : PUBLIC_DIR + path.sep;\r\n const safe = candidate.startsWith(publicBase) || candidate === PUBLIC_DIR;\r\n\r\n if (!safe) {\r\n res.statusCode = 400;\r\n res.end('Bad request');\r\n return;\r\n }\r\n\r\n // Serve the file if it exists at any depth inside PUBLIC_DIR.\r\n // Directories are intentionally skipped (no directory listings).\r\n if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {\r\n const ext = path.extname(candidate);\r\n res.setHeader('Content-Type', getMimeType(ext));\r\n res.end(fs.readFileSync(candidate));\r\n return;\r\n }\r\n }\r\n\r\n // \u2500\u2500 HMR client script \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n // Builds hmr-bundle.ts on demand so the browser always gets the latest version.\r\n if (rawUrl === '/__hmr.js') {\r\n const dir = path.dirname(fileURLToPath(import.meta.url));\r\n // Resolve to .js in dist/ or .ts when running from source.\r\n const entry = path.join(dir, `hmr-bundle.${dir.endsWith('dist') ? 'js' : 'ts'}`);\r\n\r\n const result = await build({\r\n entryPoints: [entry],\r\n write: false,\r\n format: 'esm',\r\n minify: true,\r\n bundle: true,\r\n // React is not used in the HMR client, but excluding it prevents esbuild\r\n // from inlining it if any transitive import references react.\r\n external: ['react', 'react-dom/client', 'react/jsx-runtime'],\r\n });\r\n\r\n res.setHeader('Content-Type', 'application/javascript');\r\n res.end(result.outputFiles[0].text);\r\n return; // Prevent fall-through to the SSE block below.\r\n }\r\n\r\n // \u2500\u2500 HMR SSE stream \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n // Long-lived connection tracked in hmrClients so hmr.ts can broadcast events\r\n // to all connected browsers when a file changes.\r\n if (rawUrl === '/__hmr') {\r\n res.setHeader('Content-Type', 'text/event-stream');\r\n res.setHeader('Cache-Control', 'no-cache');\r\n res.setHeader('Connection', 'keep-alive');\r\n res.setHeader('Access-Control-Allow-Origin', '*');\r\n res.flushHeaders();\r\n\r\n // Send an immediate 'connected' event so the client knows the stream is live.\r\n res.write('data: {\"type\":\"connected\"}\\n\\n');\r\n\r\n hmrClients.add(res);\r\n req.on('close', () => hmrClients.delete(res));\r\n return;\r\n }\r\n}"],
|
|
5
|
+
"mappings": "AAqBA,SAAS,aAAa;AAEtB,OAAO,UAAU;AACjB,OAAO,QAAQ;AACf,SAAS,qBAAqB;AAC9B,SAAS,kBAAkB;AAC3B,SAAS,mBAAmB;AAI5B,MAAM,aAAa,KAAK,QAAQ,cAAc;AAE9C,eAAO,WACL,KACA,KACe;AAKf,QAAM,SAAS,IAAI,OAAO;AAC1B,QAAM,WAAW,OAAO,MAAM,GAAG,EAAE,CAAC;AAEpC,MAAI,GAAG,WAAW,UAAU,GAAG;AAK7B,UAAM,YAAY,KAAK,KAAK,YAAY,QAAQ;AAKhD,UAAM,aAAa,WAAW,SAAS,KAAK,GAAG,IAAI,aAAa,aAAa,KAAK;AAClF,UAAM,OAAO,UAAU,WAAW,UAAU,KAAK,cAAc;AAE/D,QAAI,CAAC,MAAM;AACT,UAAI,aAAa;AACjB,UAAI,IAAI,aAAa;AACrB;AAAA,IACF;AAIA,QAAI,GAAG,WAAW,SAAS,KAAK,GAAG,SAAS,SAAS,EAAE,OAAO,GAAG;AAC/D,YAAM,MAAM,KAAK,QAAQ,SAAS;AAClC,UAAI,UAAU,gBAAgB,YAAY,GAAG,CAAC;AAC9C,UAAI,IAAI,GAAG,aAAa,SAAS,CAAC;AAClC;AAAA,IACF;AAAA,EACF;AAIA,MAAI,WAAW,aAAa;AAC1B,UAAM,MAAQ,KAAK,QAAQ,cAAc,YAAY,GAAG,CAAC;AAEzD,UAAM,QAAQ,KAAK,KAAK,KAAK,cAAc,IAAI,SAAS,MAAM,IAAI,OAAO,IAAI,EAAE;AAE/E,UAAM,SAAS,MAAM,MAAM;AAAA,MACzB,aAAa,CAAC,KAAK;AAAA,MACnB,OAAa;AAAA,MACb,QAAa;AAAA,MACb,QAAa;AAAA,MACb,QAAa;AAAA;AAAA;AAAA,MAGb,UAAa,CAAC,SAAS,oBAAoB,mBAAmB;AAAA,IAChE,CAAC;AAED,QAAI,UAAU,gBAAgB,wBAAwB;AACtD,QAAI,IAAI,OAAO,YAAY,CAAC,EAAE,IAAI;AAClC;AAAA,EACF;AAKA,MAAI,WAAW,UAAU;AACvB,QAAI,UAAU,gBAAgB,mBAAmB;AACjD,QAAI,UAAU,iBAAiB,UAAU;AACzC,QAAI,UAAU,cAAc,YAAY;AACxC,QAAI,UAAU,+BAA+B,GAAG;AAChD,QAAI,aAAa;AAGjB,QAAI,MAAM,gCAAgC;AAE1C,eAAW,IAAI,GAAG;AAClB,QAAI,GAAG,SAAS,MAAM,WAAW,OAAO,GAAG,CAAC;AAC5C;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* renderer.ts — Dev-Mode Async SSR Renderer
|
|
3
|
+
*
|
|
4
|
+
* Implements a recursive async renderer used in `nuke dev` to convert a React
|
|
5
|
+
* element tree into an HTML string. It is a lighter alternative to
|
|
6
|
+
* react-dom/server.renderToString that:
|
|
7
|
+
*
|
|
8
|
+
* - Supports async server components (components that return Promises).
|
|
9
|
+
* - Emits <span data-hydrate-id="…"> markers for "use client" boundaries
|
|
10
|
+
* instead of trying to render them server-side without their browser APIs.
|
|
11
|
+
* - Serializes props passed to client components into the marker's
|
|
12
|
+
* data-hydrate-props attribute so the browser can reconstruct them.
|
|
13
|
+
*
|
|
14
|
+
* In production (nuke build), the equivalent renderer is inlined into each
|
|
15
|
+
* page's standalone bundle by build-common.ts (makePageAdapterSource).
|
|
16
|
+
*
|
|
17
|
+
* RenderContext:
|
|
18
|
+
* registry — Map<id, filePath> of all client components for this page.
|
|
19
|
+
* Populated by component-analyzer.ts before rendering.
|
|
20
|
+
* hydrated — Set<id> populated during render; used to tell the browser
|
|
21
|
+
* which components to hydrate on this specific request.
|
|
22
|
+
* skipClientSSR — When true (HMR request), client components emit an empty
|
|
23
|
+
* marker instead of running renderToString (faster dev reload).
|
|
24
|
+
*/
|
|
25
|
+
export interface RenderContext {
|
|
26
|
+
/** id → absolute file path for every client component reachable from this page. */
|
|
27
|
+
registry: Map<string, string>;
|
|
28
|
+
/** Populated during render: IDs of client components actually encountered. */
|
|
29
|
+
hydrated: Set<string>;
|
|
30
|
+
/** When true, skip renderToString for client components (faster HMR). */
|
|
31
|
+
skipClientSSR?: boolean;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Recursively renders a React element (or primitive) to an HTML string.
|
|
35
|
+
*
|
|
36
|
+
* Handles:
|
|
37
|
+
* null / undefined / boolean → ''
|
|
38
|
+
* string / number → HTML-escaped text
|
|
39
|
+
* array → rendered in parallel, joined
|
|
40
|
+
* Fragment → renders children directly
|
|
41
|
+
* HTML element string → renderHtmlElement()
|
|
42
|
+
* function component → renderFunctionComponent()
|
|
43
|
+
*/
|
|
44
|
+
export declare function renderElementToHtml(element: any, ctx: RenderContext): Promise<string>;
|
package/dist/renderer.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import { createElement, Fragment } from "react";
|
|
4
|
+
import { renderToString } from "react-dom/server";
|
|
5
|
+
import { log } from "./logger.js";
|
|
6
|
+
import { getComponentCache } from "./component-analyzer.js";
|
|
7
|
+
import { escapeHtml } from "./utils.js";
|
|
8
|
+
async function renderElementToHtml(element, ctx) {
|
|
9
|
+
if (element === null || element === void 0 || typeof element === "boolean") return "";
|
|
10
|
+
if (typeof element === "string" || typeof element === "number")
|
|
11
|
+
return escapeHtml(String(element));
|
|
12
|
+
if (Array.isArray(element)) {
|
|
13
|
+
const parts = await Promise.all(element.map((e) => renderElementToHtml(e, ctx)));
|
|
14
|
+
return parts.join("");
|
|
15
|
+
}
|
|
16
|
+
if (!element.type) return "";
|
|
17
|
+
const { type, props } = element;
|
|
18
|
+
if (type === Fragment) return renderElementToHtml(props.children, ctx);
|
|
19
|
+
if (typeof type === "string") return renderHtmlElement(type, props, ctx);
|
|
20
|
+
if (typeof type === "function") return renderFunctionComponent(type, props, ctx);
|
|
21
|
+
return "";
|
|
22
|
+
}
|
|
23
|
+
async function renderHtmlElement(type, props, ctx) {
|
|
24
|
+
const { children, ...attributes } = props || {};
|
|
25
|
+
const attrs = Object.entries(attributes).map(([key, value]) => {
|
|
26
|
+
if (key === "className") key = "class";
|
|
27
|
+
if (key === "htmlFor") key = "for";
|
|
28
|
+
if (key === "dangerouslySetInnerHTML") return "";
|
|
29
|
+
if (typeof value === "boolean") return value ? key : "";
|
|
30
|
+
if (key === "style" && typeof value === "object") {
|
|
31
|
+
const styleStr = Object.entries(value).map(([k, v]) => {
|
|
32
|
+
const prop = k.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
|
|
33
|
+
const safeVal = String(v).replace(/[<>"'`\\]/g, "");
|
|
34
|
+
return `${prop}:${safeVal}`;
|
|
35
|
+
}).join(";");
|
|
36
|
+
return `style="${styleStr}"`;
|
|
37
|
+
}
|
|
38
|
+
return `${key}="${escapeHtml(String(value))}"`;
|
|
39
|
+
}).filter(Boolean).join(" ");
|
|
40
|
+
const attrStr = attrs ? ` ${attrs}` : "";
|
|
41
|
+
if (props?.dangerouslySetInnerHTML) {
|
|
42
|
+
return `<${type}${attrStr}>${props.dangerouslySetInnerHTML.__html}</${type}>`;
|
|
43
|
+
}
|
|
44
|
+
if (["img", "br", "hr", "input", "meta", "link"].includes(type)) {
|
|
45
|
+
return `<${type}${attrStr} />`;
|
|
46
|
+
}
|
|
47
|
+
const childrenHtml = children ? await renderElementToHtml(children, ctx) : "";
|
|
48
|
+
return `<${type}${attrStr}>${childrenHtml}</${type}>`;
|
|
49
|
+
}
|
|
50
|
+
async function renderFunctionComponent(type, props, ctx) {
|
|
51
|
+
const componentCache = getComponentCache();
|
|
52
|
+
for (const [id, filePath] of ctx.registry.entries()) {
|
|
53
|
+
const info = componentCache.get(filePath);
|
|
54
|
+
if (!info?.isClientComponent) continue;
|
|
55
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
56
|
+
const match = content.match(/export\s+default\s+(?:function\s+)?(\w+)/);
|
|
57
|
+
if (!match?.[1] || type.name !== match[1]) continue;
|
|
58
|
+
try {
|
|
59
|
+
ctx.hydrated.add(id);
|
|
60
|
+
const serializedProps = serializePropsForHydration(props, ctx.registry);
|
|
61
|
+
log.verbose(`Client component rendered for hydration: ${id} (${path.basename(filePath)})`);
|
|
62
|
+
const html = ctx.skipClientSSR ? "" : renderToString(createElement(type, props));
|
|
63
|
+
return `<span data-hydrate-id="${id}" data-hydrate-props="${escapeHtml(
|
|
64
|
+
JSON.stringify(serializedProps)
|
|
65
|
+
)}">${html}</span>`;
|
|
66
|
+
} catch (err) {
|
|
67
|
+
log.error("Error rendering client component:", err);
|
|
68
|
+
return `<div style="color:red">Error rendering client component: ${escapeHtml(String(err))}</div>`;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
const result = type(props);
|
|
73
|
+
const resolved = result?.then ? await result : result;
|
|
74
|
+
return renderElementToHtml(resolved, ctx);
|
|
75
|
+
} catch (err) {
|
|
76
|
+
log.error("Error rendering component:", err);
|
|
77
|
+
return `<div style="color:red">Error rendering component: ${escapeHtml(String(err))}</div>`;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function serializePropsForHydration(props, registry) {
|
|
81
|
+
if (!props || typeof props !== "object") return props;
|
|
82
|
+
const out = {};
|
|
83
|
+
for (const [key, value] of Object.entries(props)) {
|
|
84
|
+
const s = serializeValue(value, registry);
|
|
85
|
+
if (s !== void 0) out[key] = s;
|
|
86
|
+
}
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
function serializeValue(value, registry) {
|
|
90
|
+
if (value === null || value === void 0) return value;
|
|
91
|
+
if (typeof value === "function") return void 0;
|
|
92
|
+
if (typeof value !== "object") return value;
|
|
93
|
+
if (Array.isArray(value))
|
|
94
|
+
return value.map((v) => serializeValue(v, registry)).filter((v) => v !== void 0);
|
|
95
|
+
if (value.$$typeof)
|
|
96
|
+
return serializeReactElement(value, registry);
|
|
97
|
+
const out = {};
|
|
98
|
+
for (const [k, v] of Object.entries(value)) {
|
|
99
|
+
const s = serializeValue(v, registry);
|
|
100
|
+
if (s !== void 0) out[k] = s;
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
function serializeReactElement(element, registry) {
|
|
105
|
+
const { type, props } = element;
|
|
106
|
+
if (typeof type === "string") {
|
|
107
|
+
return { __re: "html", tag: type, props: serializePropsForHydration(props, registry) };
|
|
108
|
+
}
|
|
109
|
+
if (typeof type === "function") {
|
|
110
|
+
const componentCache = getComponentCache();
|
|
111
|
+
for (const [id, filePath] of registry.entries()) {
|
|
112
|
+
const info = componentCache.get(filePath);
|
|
113
|
+
if (!info?.isClientComponent) continue;
|
|
114
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
115
|
+
const match = content.match(/export\s+default\s+(?:function\s+)?(\w+)/);
|
|
116
|
+
if (match?.[1] && type.name === match[1]) {
|
|
117
|
+
return {
|
|
118
|
+
__re: "client",
|
|
119
|
+
componentId: id,
|
|
120
|
+
props: serializePropsForHydration(props, registry)
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return void 0;
|
|
126
|
+
}
|
|
127
|
+
export {
|
|
128
|
+
renderElementToHtml
|
|
129
|
+
};
|
|
130
|
+
//# sourceMappingURL=renderer.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/renderer.ts"],
|
|
4
|
+
"sourcesContent": ["/**\r\n * renderer.ts \u2014 Dev-Mode Async SSR Renderer\r\n *\r\n * Implements a recursive async renderer used in `nuke dev` to convert a React\r\n * element tree into an HTML string. It is a lighter alternative to\r\n * react-dom/server.renderToString that:\r\n *\r\n * - Supports async server components (components that return Promises).\r\n * - Emits <span data-hydrate-id=\"\u2026\"> markers for \"use client\" boundaries\r\n * instead of trying to render them server-side without their browser APIs.\r\n * - Serializes props passed to client components into the marker's\r\n * data-hydrate-props attribute so the browser can reconstruct them.\r\n *\r\n * In production (nuke build), the equivalent renderer is inlined into each\r\n * page's standalone bundle by build-common.ts (makePageAdapterSource).\r\n *\r\n * RenderContext:\r\n * registry \u2014 Map<id, filePath> of all client components for this page.\r\n * Populated by component-analyzer.ts before rendering.\r\n * hydrated \u2014 Set<id> populated during render; used to tell the browser\r\n * which components to hydrate on this specific request.\r\n * skipClientSSR \u2014 When true (HMR request), client components emit an empty\r\n * marker instead of running renderToString (faster dev reload).\r\n */\r\n\r\nimport path from 'path';\r\nimport fs from 'fs';\r\nimport { createElement, Fragment } from 'react';\r\nimport { renderToString } from 'react-dom/server';\r\nimport { log } from './logger';\r\nimport { getComponentCache } from './component-analyzer';\r\nimport { escapeHtml } from './utils';\r\n\r\n// \u2500\u2500\u2500 Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\nexport interface RenderContext {\r\n /** id \u2192 absolute file path for every client component reachable from this page. */\r\n registry: Map<string, string>;\r\n /** Populated during render: IDs of client components actually encountered. */\r\n hydrated: Set<string>;\r\n /** When true, skip renderToString for client components (faster HMR). */\r\n skipClientSSR?: boolean;\r\n}\r\n\r\n// \u2500\u2500\u2500 Top-level renderer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Recursively renders a React element (or primitive) to an HTML string.\r\n *\r\n * Handles:\r\n * null / undefined / boolean \u2192 ''\r\n * string / number \u2192 HTML-escaped text\r\n * array \u2192 rendered in parallel, joined\r\n * Fragment \u2192 renders children directly\r\n * HTML element string \u2192 renderHtmlElement()\r\n * function component \u2192 renderFunctionComponent()\r\n */\r\nexport async function renderElementToHtml(\r\n element: any,\r\n ctx: RenderContext,\r\n): Promise<string> {\r\n if (element === null || element === undefined || typeof element === 'boolean') return '';\r\n if (typeof element === 'string' || typeof element === 'number')\r\n return escapeHtml(String(element));\r\n\r\n if (Array.isArray(element)) {\r\n const parts = await Promise.all(element.map(e => renderElementToHtml(e, ctx)));\r\n return parts.join('');\r\n }\r\n\r\n if (!element.type) return '';\r\n\r\n const { type, props } = element;\r\n\r\n if (type === Fragment) return renderElementToHtml(props.children, ctx);\r\n if (typeof type === 'string') return renderHtmlElement(type, props, ctx);\r\n if (typeof type === 'function') return renderFunctionComponent(type, props, ctx);\r\n\r\n return '';\r\n}\r\n\r\n// \u2500\u2500\u2500 HTML element renderer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Renders a native HTML element (e.g. `<div className=\"foo\">`).\r\n *\r\n * Attribute conversion:\r\n * className \u2192 class\r\n * htmlFor \u2192 for\r\n * style \u2192 converted from camelCase object to CSS string\r\n * boolean \u2192 omitted when false, rendered as name-only attribute when true\r\n * dangerouslySetInnerHTML \u2192 inner HTML set verbatim (no escaping)\r\n *\r\n * Void elements (img, br, input, etc.) are self-closed.\r\n */\r\nasync function renderHtmlElement(\r\n type: string,\r\n props: any,\r\n ctx: RenderContext,\r\n): Promise<string> {\r\n const { children, ...attributes } = (props || {}) as Record<string, any>;\r\n\r\n const attrs = Object.entries(attributes as Record<string, any>)\r\n .map(([key, value]) => {\r\n // React prop name \u2192 HTML attribute name.\r\n if (key === 'className') key = 'class';\r\n if (key === 'htmlFor') key = 'for';\r\n if (key === 'dangerouslySetInnerHTML') return ''; // handled separately below\r\n\r\n if (typeof value === 'boolean') return value ? key : '';\r\n\r\n // camelCase style object \u2192 \"prop:value;\u2026\" CSS string.\r\n if (key === 'style' && typeof value === 'object') {\r\n const styleStr = Object.entries(value)\r\n .map(([k, v]) => {\r\n const prop = k.replace(/[A-Z]/g, m => `-${m.toLowerCase()}`);\r\n // Strip characters that could break out of the attribute value.\r\n const safeVal = String(v).replace(/[<>\"'`\\\\]/g, '');\r\n return `${prop}:${safeVal}`;\r\n })\r\n .join(';');\r\n return `style=\"${styleStr}\"`;\r\n }\r\n\r\n return `${key}=\"${escapeHtml(String(value))}\"`;\r\n })\r\n .filter(Boolean)\r\n .join(' ');\r\n\r\n const attrStr = attrs ? ` ${attrs}` : '';\r\n\r\n if (props?.dangerouslySetInnerHTML) {\r\n return `<${type}${attrStr}>${props.dangerouslySetInnerHTML.__html}</${type}>`;\r\n }\r\n\r\n // Void elements cannot have children.\r\n if (['img', 'br', 'hr', 'input', 'meta', 'link'].includes(type)) {\r\n return `<${type}${attrStr} />`;\r\n }\r\n\r\n const childrenHtml = children ? await renderElementToHtml(children, ctx) : '';\r\n return `<${type}${attrStr}>${childrenHtml}</${type}>`;\r\n}\r\n\r\n// \u2500\u2500\u2500 Function component renderer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Renders a function (or class) component.\r\n *\r\n * Client boundary detection:\r\n * The component cache maps file paths to ComponentInfo. We match the\r\n * component's function name against the default export of each registered\r\n * client file to determine whether this component is a client boundary.\r\n *\r\n * If it is, we emit a hydration marker and optionally run renderToString\r\n * to produce the initial HTML inside the marker (skipped when skipClientSSR\r\n * is set, e.g. during HMR navigation).\r\n *\r\n * Class components:\r\n * Instantiated via `new type(props)` and their render() method called.\r\n *\r\n * Async components:\r\n * Awaited if the return value is a Promise (standard server component pattern).\r\n */\r\nasync function renderFunctionComponent(\r\n type: Function,\r\n props: any,\r\n ctx: RenderContext,\r\n): Promise<string> {\r\n const componentCache = getComponentCache();\r\n\r\n // Check whether this component function is a registered client component.\r\n for (const [id, filePath] of ctx.registry.entries()) {\r\n const info = componentCache.get(filePath);\r\n if (!info?.isClientComponent) continue;\r\n\r\n // Match by default export function name.\r\n const content = fs.readFileSync(filePath, 'utf-8');\r\n const match = content.match(/export\\s+default\\s+(?:function\\s+)?(\\w+)/);\r\n if (!match?.[1] || type.name !== match[1]) continue;\r\n\r\n // This is a client boundary.\r\n try {\r\n ctx.hydrated.add(id);\r\n const serializedProps = serializePropsForHydration(props, ctx.registry);\r\n log.verbose(`Client component rendered for hydration: ${id} (${path.basename(filePath)})`);\r\n\r\n // Optionally SSR the component so the initial HTML is meaningful\r\n // (improves perceived performance and avoids layout shift).\r\n const html = ctx.skipClientSSR\r\n ? ''\r\n : renderToString(createElement(type as React.ComponentType<any>, props));\r\n\r\n return `<span data-hydrate-id=\"${id}\" data-hydrate-props=\"${escapeHtml(\r\n JSON.stringify(serializedProps),\r\n )}\">${html}</span>`;\r\n } catch (err) {\r\n log.error('Error rendering client component:', err);\r\n return `<div style=\"color:red\">Error rendering client component: ${escapeHtml(String(err))}</div>`;\r\n }\r\n }\r\n\r\n // Server component \u2014 call it and recurse into the result.\r\n try {\r\n const result = type(props);\r\n const resolved = result?.then ? await result : result;\r\n return renderElementToHtml(resolved, ctx);\r\n } catch (err) {\r\n log.error('Error rendering component:', err);\r\n return `<div style=\"color:red\">Error rendering component: ${escapeHtml(String(err))}</div>`;\r\n }\r\n}\r\n\r\n// \u2500\u2500\u2500 Prop serialization \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Converts props into a JSON-serializable form for the data-hydrate-props\r\n * attribute. React elements inside props are serialized to a tagged object\r\n * format ({ __re: 'html'|'client', \u2026 }) that the browser's reconstructElement\r\n * function (in bundle.ts) can turn back into real React elements.\r\n *\r\n * Functions are dropped (cannot be serialized).\r\n */\r\nfunction serializePropsForHydration(\r\n props: any,\r\n registry: Map<string, string>,\r\n): any {\r\n if (!props || typeof props !== 'object') return props;\r\n const out: any = {};\r\n for (const [key, value] of Object.entries(props as Record<string, any>)) {\r\n const s = serializeValue(value, registry);\r\n if (s !== undefined) out[key] = s;\r\n }\r\n return out;\r\n}\r\n\r\nfunction serializeValue(value: any, registry: Map<string, string>): any {\r\n if (value === null || value === undefined) return value;\r\n if (typeof value === 'function') return undefined; // not serializable\r\n if (typeof value !== 'object') return value;\r\n if (Array.isArray(value))\r\n return value.map(v => serializeValue(v, registry)).filter(v => v !== undefined);\r\n if ((value as any).$$typeof)\r\n return serializeReactElement(value, registry);\r\n\r\n const out: any = {};\r\n for (const [k, v] of Object.entries(value as Record<string, any>)) {\r\n const s = serializeValue(v, registry);\r\n if (s !== undefined) out[k] = s;\r\n }\r\n return out;\r\n}\r\n\r\n/**\r\n * Serializes a React element to its wire format:\r\n * Native element \u2192 { __re: 'html', tag, props }\r\n * Client component \u2192 { __re: 'client', componentId, props }\r\n * Server component \u2192 undefined (cannot be serialized)\r\n */\r\nfunction serializeReactElement(element: any, registry: Map<string, string>): any {\r\n const { type, props } = element;\r\n\r\n if (typeof type === 'string') {\r\n return { __re: 'html', tag: type, props: serializePropsForHydration(props, registry) };\r\n }\r\n\r\n if (typeof type === 'function') {\r\n const componentCache = getComponentCache();\r\n for (const [id, filePath] of registry.entries()) {\r\n const info = componentCache.get(filePath);\r\n if (!info?.isClientComponent) continue;\r\n const content = fs.readFileSync(filePath, 'utf-8');\r\n const match = content.match(/export\\s+default\\s+(?:function\\s+)?(\\w+)/);\r\n if (match?.[1] && type.name === match[1]) {\r\n return {\r\n __re: 'client',\r\n componentId: id,\r\n props: serializePropsForHydration(props, registry),\r\n };\r\n }\r\n }\r\n }\r\n\r\n return undefined; // Server component \u2014 not serializable\r\n}\r\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/router.d.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* router.ts — File-System Based URL Router
|
|
3
|
+
*
|
|
4
|
+
* Maps incoming URL paths to handler files using Next.js-compatible conventions:
|
|
5
|
+
*
|
|
6
|
+
* server/users/index.ts → /users
|
|
7
|
+
* server/users/[id].ts → /users/:id (dynamic segment)
|
|
8
|
+
* server/blog/[...slug].ts → /blog/* (catch-all)
|
|
9
|
+
* server/files/[[...path]].ts → /files or /files/* (optional catch-all)
|
|
10
|
+
*
|
|
11
|
+
* Route specificity (higher = wins over lower):
|
|
12
|
+
* static segment +4 (e.g. 'about')
|
|
13
|
+
* dynamic segment +3 (e.g. '[id]')
|
|
14
|
+
* catch-all +2 (e.g. '[...slug]')
|
|
15
|
+
* optional catch-all +1 (e.g. '[[...path]]')
|
|
16
|
+
*
|
|
17
|
+
* Path traversal protection:
|
|
18
|
+
* matchRoute() rejects URL segments that contain '..' or '.' and verifies
|
|
19
|
+
* that the resolved file path stays inside the base directory before
|
|
20
|
+
* checking whether the file exists.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Recursively collects all .ts/.tsx files in `dir`, returning paths relative to
|
|
24
|
+
* `baseDir` without the file extension.
|
|
25
|
+
*
|
|
26
|
+
* Example output: ['index', 'users/index', 'users/[id]', 'blog/[...slug]']
|
|
27
|
+
*/
|
|
28
|
+
export declare function findAllRoutes(dir: string, baseDir?: string): string[];
|
|
29
|
+
/**
|
|
30
|
+
* Attempts to match `urlSegments` against a route that may contain dynamic
|
|
31
|
+
* segments ([param]), catch-alls ([...slug]), and optional catch-alls ([[...path]]).
|
|
32
|
+
*
|
|
33
|
+
* Returns the captured params on success, or null if the route does not match.
|
|
34
|
+
*
|
|
35
|
+
* Param value types:
|
|
36
|
+
* [param] → string
|
|
37
|
+
* [...slug] → string[] (at least one segment required)
|
|
38
|
+
* [[...path]] → string[] (zero or more segments)
|
|
39
|
+
*/
|
|
40
|
+
export declare function matchDynamicRoute(urlSegments: string[], routePath: string): {
|
|
41
|
+
params: Record<string, string | string[]>;
|
|
42
|
+
} | null;
|
|
43
|
+
/**
|
|
44
|
+
* Computes a specificity score for a route path.
|
|
45
|
+
* Used to sort candidate routes so more specific routes shadow catch-alls.
|
|
46
|
+
*
|
|
47
|
+
* Higher score = more specific:
|
|
48
|
+
* static segment 4
|
|
49
|
+
* [dynamic] 3
|
|
50
|
+
* [...catchAll] 2
|
|
51
|
+
* [[...optCatchAll]] 1
|
|
52
|
+
*/
|
|
53
|
+
export declare function getRouteSpecificity(routePath: string): number;
|
|
54
|
+
export interface RouteMatch {
|
|
55
|
+
filePath: string;
|
|
56
|
+
params: Record<string, string | string[]>;
|
|
57
|
+
routePattern: string;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Resolves a URL path to a route file inside `baseDir`.
|
|
61
|
+
*
|
|
62
|
+
* Steps:
|
|
63
|
+
* 1. Reject '..' or '.' path segments (path traversal guard).
|
|
64
|
+
* 2. Try an exact file match (e.g. /about → baseDir/about.tsx).
|
|
65
|
+
* 3. Sort all discovered routes by specificity (most specific first).
|
|
66
|
+
* 4. Return the first dynamic route that matches.
|
|
67
|
+
*
|
|
68
|
+
* @param urlPath The URL path to match (e.g. '/users/42').
|
|
69
|
+
* @param baseDir Absolute path to the directory containing route files.
|
|
70
|
+
* @param extension File extension to look for ('.tsx' or '.ts').
|
|
71
|
+
*/
|
|
72
|
+
export declare function matchRoute(urlPath: string, baseDir: string, extension?: string): RouteMatch | null;
|
|
73
|
+
/**
|
|
74
|
+
* Returns every layout.tsx file that wraps a given route file, in
|
|
75
|
+
* outermost-first order (root layout first, nearest layout last).
|
|
76
|
+
*
|
|
77
|
+
* Layout chain example for app/pages/blog/[slug]/page.tsx:
|
|
78
|
+
* app/pages/layout.tsx ← root layout
|
|
79
|
+
* app/pages/blog/layout.tsx ← blog section layout
|
|
80
|
+
*
|
|
81
|
+
* The outermost-first order matches how wrapWithLayouts() nests them:
|
|
82
|
+
* the last layout in the array is the innermost wrapper.
|
|
83
|
+
*/
|
|
84
|
+
export declare function findLayoutsForRoute(routeFilePath: string, pagesDir: string): string[];
|