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,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* http-server.ts — API Route Dispatcher
|
|
3
|
+
*
|
|
4
|
+
* Handles discovery and dispatch of API routes inside `serverDir`.
|
|
5
|
+
*
|
|
6
|
+
* Directory conventions (mirrors Next.js):
|
|
7
|
+
* server/
|
|
8
|
+
* users/ → prefix /users (directory)
|
|
9
|
+
* index.ts → GET /users (method exports: GET, POST, …)
|
|
10
|
+
* [id].ts → GET /users/:id
|
|
11
|
+
* auth.ts → prefix /auth (top-level file)
|
|
12
|
+
* index.ts → prefix / (root handler)
|
|
13
|
+
*
|
|
14
|
+
* Route handler exports:
|
|
15
|
+
* export function GET(req, res) { … }
|
|
16
|
+
* export function POST(req, res) { … }
|
|
17
|
+
* export default function(req, res) { … } // matches any method
|
|
18
|
+
*
|
|
19
|
+
* Request augmentation:
|
|
20
|
+
* req.body — parsed JSON or raw string (10 MB limit)
|
|
21
|
+
* req.params — dynamic route segments (e.g. { id: '42' })
|
|
22
|
+
* req.query — URL search params
|
|
23
|
+
*
|
|
24
|
+
* Response augmentation:
|
|
25
|
+
* res.json(data, status?) — JSON response shorthand
|
|
26
|
+
* res.status(code) — sets statusCode, returns res for chaining
|
|
27
|
+
*/
|
|
28
|
+
import type { IncomingMessage, ServerResponse } from 'http';
|
|
29
|
+
/** Describes a single API prefix discovered in serverDir. */
|
|
30
|
+
export interface ApiPrefixInfo {
|
|
31
|
+
/** URL prefix this entry handles (e.g. '/users', ''). */
|
|
32
|
+
prefix: string;
|
|
33
|
+
/** Directory to scan for route files. */
|
|
34
|
+
directory: string;
|
|
35
|
+
/** Set when the prefix comes from a top-level file (not a directory). */
|
|
36
|
+
filePath?: string;
|
|
37
|
+
}
|
|
38
|
+
/** Node's IncomingMessage with parsed body, params, and query. */
|
|
39
|
+
export interface ApiRequest extends IncomingMessage {
|
|
40
|
+
params?: Record<string, string | string[]>;
|
|
41
|
+
query?: Record<string, string>;
|
|
42
|
+
body?: any;
|
|
43
|
+
}
|
|
44
|
+
/** Node's ServerResponse with json() and status() convenience methods. */
|
|
45
|
+
export interface ApiResponse extends ServerResponse {
|
|
46
|
+
json: (data: any, status?: number) => void;
|
|
47
|
+
status: (code: number) => ApiResponse;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Scans `serverDir` and returns one ApiPrefixInfo per directory, top-level
|
|
51
|
+
* file, and root index.ts. Directories are returned before same-stem files
|
|
52
|
+
* so `/a/b` routes resolve to the directory tree before any flat file.
|
|
53
|
+
*
|
|
54
|
+
* Called at startup and again whenever the server directory changes (in dev).
|
|
55
|
+
*/
|
|
56
|
+
export declare function discoverApiPrefixes(serverDir: string): ApiPrefixInfo[];
|
|
57
|
+
/**
|
|
58
|
+
* Buffers the request body and returns:
|
|
59
|
+
* - Parsed JSON object if Content-Type is application/json.
|
|
60
|
+
* - Raw string otherwise.
|
|
61
|
+
*
|
|
62
|
+
* Rejects with an error if the body exceeds MAX_BODY_BYTES to prevent
|
|
63
|
+
* memory exhaustion attacks. Deletes __proto__ and constructor from parsed
|
|
64
|
+
* JSON objects to guard against prototype pollution.
|
|
65
|
+
*/
|
|
66
|
+
export declare function parseBody(req: IncomingMessage): Promise<any>;
|
|
67
|
+
/** Extracts URL search params into a plain string map. */
|
|
68
|
+
export declare function parseQuery(url: string, port: number): Record<string, string>;
|
|
69
|
+
/**
|
|
70
|
+
* Adds `json()` and `status()` convenience methods to a raw ServerResponse,
|
|
71
|
+
* mirroring the Express API surface that most API handlers expect.
|
|
72
|
+
*/
|
|
73
|
+
export declare function enhanceResponse(res: ServerResponse): ApiResponse;
|
|
74
|
+
/**
|
|
75
|
+
* Finds the first ApiPrefixInfo whose prefix is a prefix of `url`.
|
|
76
|
+
*
|
|
77
|
+
* The empty-string prefix ('') acts as a catch-all and only matches when no
|
|
78
|
+
* other prefix claims the URL.
|
|
79
|
+
*
|
|
80
|
+
* Returns `null` when no prefix matches (request should fall through to SSR).
|
|
81
|
+
*/
|
|
82
|
+
export declare function matchApiPrefix(url: string, apiPrefixes: ApiPrefixInfo[]): {
|
|
83
|
+
prefix: ApiPrefixInfo;
|
|
84
|
+
apiPath: string;
|
|
85
|
+
} | null;
|
|
86
|
+
interface ApiHandlerOptions {
|
|
87
|
+
apiPrefixes: ApiPrefixInfo[];
|
|
88
|
+
port: number;
|
|
89
|
+
}
|
|
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>;
|
|
99
|
+
export {};
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import { pathToFileURL } from "url";
|
|
4
|
+
import { log } from "./logger.js";
|
|
5
|
+
import { matchRoute } from "./router.js";
|
|
6
|
+
function discoverApiPrefixes(serverDir) {
|
|
7
|
+
if (!fs.existsSync(serverDir)) {
|
|
8
|
+
log.warn("Server directory not found:", serverDir);
|
|
9
|
+
return [];
|
|
10
|
+
}
|
|
11
|
+
const entries = fs.readdirSync(serverDir, { withFileTypes: true });
|
|
12
|
+
const prefixes = [];
|
|
13
|
+
for (const e of entries) {
|
|
14
|
+
if (e.isDirectory()) {
|
|
15
|
+
prefixes.push({ prefix: `/${e.name}`, directory: path.join(serverDir, e.name) });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
for (const e of entries) {
|
|
19
|
+
if (e.isFile() && (e.name.endsWith(".ts") || e.name.endsWith(".tsx")) && e.name !== "index.ts" && e.name !== "index.tsx") {
|
|
20
|
+
const stem = e.name.replace(/\.tsx?$/, "");
|
|
21
|
+
prefixes.push({
|
|
22
|
+
prefix: `/${stem}`,
|
|
23
|
+
directory: serverDir,
|
|
24
|
+
filePath: path.join(serverDir, e.name)
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (fs.existsSync(path.join(serverDir, "index.ts"))) {
|
|
29
|
+
prefixes.push({ prefix: "", directory: serverDir });
|
|
30
|
+
}
|
|
31
|
+
return prefixes;
|
|
32
|
+
}
|
|
33
|
+
const MAX_BODY_BYTES = 10 * 1024 * 1024;
|
|
34
|
+
async function parseBody(req) {
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
let body = "";
|
|
37
|
+
let bytes = 0;
|
|
38
|
+
req.on("data", (chunk) => {
|
|
39
|
+
bytes += chunk.length;
|
|
40
|
+
if (bytes > MAX_BODY_BYTES) {
|
|
41
|
+
req.destroy();
|
|
42
|
+
return reject(new Error("Request body too large"));
|
|
43
|
+
}
|
|
44
|
+
body += chunk.toString();
|
|
45
|
+
});
|
|
46
|
+
req.on("end", () => {
|
|
47
|
+
try {
|
|
48
|
+
if (body && req.headers["content-type"]?.includes("application/json")) {
|
|
49
|
+
const parsed = JSON.parse(body);
|
|
50
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
51
|
+
delete parsed.__proto__;
|
|
52
|
+
delete parsed.constructor;
|
|
53
|
+
}
|
|
54
|
+
resolve(parsed);
|
|
55
|
+
} else {
|
|
56
|
+
resolve(body);
|
|
57
|
+
}
|
|
58
|
+
} catch (err) {
|
|
59
|
+
reject(err);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
req.on("error", reject);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
function parseQuery(url, port) {
|
|
66
|
+
const query = {};
|
|
67
|
+
new URL(url, `http://localhost:${port}`).searchParams.forEach((v, k) => {
|
|
68
|
+
query[k] = v;
|
|
69
|
+
});
|
|
70
|
+
return query;
|
|
71
|
+
}
|
|
72
|
+
function enhanceResponse(res) {
|
|
73
|
+
const apiRes = res;
|
|
74
|
+
apiRes.json = function(data, statusCode = 200) {
|
|
75
|
+
this.statusCode = statusCode;
|
|
76
|
+
this.setHeader("Content-Type", "application/json");
|
|
77
|
+
this.end(JSON.stringify(data));
|
|
78
|
+
};
|
|
79
|
+
apiRes.status = function(code) {
|
|
80
|
+
this.statusCode = code;
|
|
81
|
+
return this;
|
|
82
|
+
};
|
|
83
|
+
return apiRes;
|
|
84
|
+
}
|
|
85
|
+
function respondOptions(res) {
|
|
86
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
87
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
|
|
88
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
89
|
+
res.statusCode = 204;
|
|
90
|
+
res.end();
|
|
91
|
+
}
|
|
92
|
+
function matchApiPrefix(url, apiPrefixes) {
|
|
93
|
+
for (const prefix of apiPrefixes) {
|
|
94
|
+
if (prefix.prefix === "") {
|
|
95
|
+
const claimedByOther = apiPrefixes.some(
|
|
96
|
+
(p) => p.prefix !== "" && url.startsWith(p.prefix)
|
|
97
|
+
);
|
|
98
|
+
if (!claimedByOther) return { prefix, apiPath: url || "/" };
|
|
99
|
+
} else if (url.startsWith(prefix.prefix)) {
|
|
100
|
+
return { prefix, apiPath: url.slice(prefix.prefix.length) || "/" };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
function createApiHandler({ apiPrefixes, port }) {
|
|
106
|
+
return async function handleApiRoute(url, req, res) {
|
|
107
|
+
const apiRes = enhanceResponse(res);
|
|
108
|
+
const apiMatch = matchApiPrefix(url, apiPrefixes);
|
|
109
|
+
if (!apiMatch) {
|
|
110
|
+
apiRes.json({ error: "API endpoint not found" }, 404);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const { prefix, apiPath } = apiMatch;
|
|
114
|
+
let filePath = null;
|
|
115
|
+
let params = {};
|
|
116
|
+
if (prefix.filePath) {
|
|
117
|
+
filePath = prefix.filePath;
|
|
118
|
+
}
|
|
119
|
+
if (!filePath && prefix.prefix === "" && apiPath === "/") {
|
|
120
|
+
const indexPath = path.join(prefix.directory, "index.ts");
|
|
121
|
+
if (fs.existsSync(indexPath)) filePath = indexPath;
|
|
122
|
+
}
|
|
123
|
+
if (!filePath) {
|
|
124
|
+
const routeMatch = matchRoute(apiPath, prefix.directory, ".ts") ?? matchRoute(apiPath, prefix.directory, ".tsx");
|
|
125
|
+
if (routeMatch) {
|
|
126
|
+
filePath = routeMatch.filePath;
|
|
127
|
+
params = routeMatch.params;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (!filePath) {
|
|
131
|
+
apiRes.json({ error: "API endpoint not found" }, 404);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
const method = (req.method || "GET").toUpperCase();
|
|
136
|
+
log.verbose(`API ${method} ${url} -> ${path.relative(process.cwd(), filePath)}`);
|
|
137
|
+
if (method === "OPTIONS") {
|
|
138
|
+
respondOptions(apiRes);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const apiReq = req;
|
|
142
|
+
apiReq.body = await parseBody(req);
|
|
143
|
+
apiReq.params = params;
|
|
144
|
+
apiReq.query = parseQuery(url, port);
|
|
145
|
+
const apiModule = await import(pathToFileURL(filePath).href);
|
|
146
|
+
const handler = apiModule[method] ?? apiModule.default;
|
|
147
|
+
if (!handler) {
|
|
148
|
+
apiRes.json({ error: `Method ${method} not allowed` }, 405);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
await handler(apiReq, apiRes);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
log.error("API Error:", error);
|
|
154
|
+
apiRes.json({ error: "Internal server error" }, 500);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
export {
|
|
159
|
+
createApiHandler,
|
|
160
|
+
discoverApiPrefixes,
|
|
161
|
+
enhanceResponse,
|
|
162
|
+
matchApiPrefix,
|
|
163
|
+
parseBody,
|
|
164
|
+
parseQuery
|
|
165
|
+
};
|
|
166
|
+
//# sourceMappingURL=http-server.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 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": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { useHtml } from './use-html';
|
|
2
|
+
export type { HtmlOptions, TitleValue, HtmlAttrs, BodyAttrs, MetaTag, LinkTag, ScriptTag, StyleTag, } from './use-html';
|
|
3
|
+
export { default as useRouter } from './as-is/useRouter';
|
|
4
|
+
export { default as Link } from './as-is/Link';
|
|
5
|
+
export { setupLocationChangeMonitor, initRuntime } from './bundle';
|
|
6
|
+
export type { RuntimeData } from './bundle';
|
|
7
|
+
export { escapeHtml } from './utils';
|
|
8
|
+
export { ansi, c, log, setDebugLevel, getDebugLevel } from './logger';
|
|
9
|
+
export type { DebugLevel } from './logger';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { useHtml } from "./use-html.js";
|
|
2
|
+
import { default as default2 } from "./as-is/useRouter";
|
|
3
|
+
import { default as default3 } from "./as-is/Link";
|
|
4
|
+
import { setupLocationChangeMonitor, initRuntime } from "./bundle.js";
|
|
5
|
+
import { escapeHtml } from "./utils.js";
|
|
6
|
+
import { ansi, c, log, setDebugLevel, getDebugLevel } from "./logger.js";
|
|
7
|
+
export {
|
|
8
|
+
default3 as Link,
|
|
9
|
+
ansi,
|
|
10
|
+
c,
|
|
11
|
+
escapeHtml,
|
|
12
|
+
getDebugLevel,
|
|
13
|
+
initRuntime,
|
|
14
|
+
log,
|
|
15
|
+
setDebugLevel,
|
|
16
|
+
setupLocationChangeMonitor,
|
|
17
|
+
useHtml,
|
|
18
|
+
default2 as useRouter
|
|
19
|
+
};
|
|
20
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/index.ts"],
|
|
4
|
+
"sourcesContent": ["// \u2500\u2500\u2500 Client-side hooks & components \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\nexport { useHtml } from './use-html';\r\nexport type {\r\n HtmlOptions,\r\n TitleValue,\r\n HtmlAttrs,\r\n BodyAttrs,\r\n MetaTag,\r\n LinkTag,\r\n ScriptTag,\r\n StyleTag,\r\n} from './use-html';\r\n\r\nexport { default as useRouter } from './as-is/useRouter';\r\n\r\nexport { default as Link } from './as-is/Link';\r\n\r\n// \u2500\u2500\u2500 Client runtime (browser bootstrap) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\nexport { setupLocationChangeMonitor, initRuntime } from './bundle';\r\nexport type { RuntimeData } from './bundle';\r\n\r\n// \u2500\u2500\u2500 Shared utilities \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\nexport { escapeHtml } from './utils';\r\n\r\nexport { ansi, c, log, setDebugLevel, getDebugLevel } from './logger';\r\nexport type { DebugLevel } from './logger';\r\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,eAAe;AAYxB,SAAoB,WAAXA,gBAA4B;AAErC,SAAoB,WAAXA,gBAAuB;AAGhC,SAAS,4BAA4B,mBAAmB;AAIxD,SAAS,kBAAkB;AAE3B,SAAS,MAAM,GAAG,KAAK,eAAe,qBAAqB;",
|
|
6
|
+
"names": ["default"]
|
|
7
|
+
}
|
package/dist/logger.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* logger.ts — ANSI-Coloured Levelled Logger
|
|
3
|
+
*
|
|
4
|
+
* Provides a small set of server-side logging utilities used throughout NukeJS.
|
|
5
|
+
*
|
|
6
|
+
* Debug levels (set via nuke.config.ts `debug` field):
|
|
7
|
+
* false — silent, nothing printed
|
|
8
|
+
* 'error' — error() only
|
|
9
|
+
* 'info' — info(), warn(), error()
|
|
10
|
+
* true — verbose: all of the above plus verbose()
|
|
11
|
+
*
|
|
12
|
+
* The level can be read back with `getDebugLevel()` and is also forwarded to
|
|
13
|
+
* the browser client (as a string) so server and client log at the same level.
|
|
14
|
+
*/
|
|
15
|
+
/** Map of named ANSI colour/style escape codes. */
|
|
16
|
+
export declare const ansi: {
|
|
17
|
+
readonly reset: "\u001B[0m";
|
|
18
|
+
readonly bold: "\u001B[1m";
|
|
19
|
+
readonly dim: "\u001B[2m";
|
|
20
|
+
readonly black: "\u001B[30m";
|
|
21
|
+
readonly red: "\u001B[31m";
|
|
22
|
+
readonly green: "\u001B[32m";
|
|
23
|
+
readonly yellow: "\u001B[33m";
|
|
24
|
+
readonly blue: "\u001B[34m";
|
|
25
|
+
readonly magenta: "\u001B[35m";
|
|
26
|
+
readonly cyan: "\u001B[36m";
|
|
27
|
+
readonly white: "\u001B[37m";
|
|
28
|
+
readonly gray: "\u001B[90m";
|
|
29
|
+
readonly bgBlue: "\u001B[44m";
|
|
30
|
+
readonly bgGreen: "\u001B[42m";
|
|
31
|
+
readonly bgMagenta: "\u001B[45m";
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Wraps `text` in the ANSI sequence for `color`, optionally bold.
|
|
35
|
+
* Always appends the reset sequence so colour does not bleed into surrounding
|
|
36
|
+
* terminal output.
|
|
37
|
+
*/
|
|
38
|
+
export declare function c(color: keyof typeof ansi, text: string, bold?: boolean): string;
|
|
39
|
+
/** false = silent | 'error' = errors only | 'info' = startup + errors | true = verbose */
|
|
40
|
+
export type DebugLevel = false | 'error' | 'info' | true;
|
|
41
|
+
/** Sets the active log level. Called once after the config is loaded. */
|
|
42
|
+
export declare function setDebugLevel(level: DebugLevel): void;
|
|
43
|
+
/** Returns the currently active log level. */
|
|
44
|
+
export declare function getDebugLevel(): DebugLevel;
|
|
45
|
+
/**
|
|
46
|
+
* Structured log object with four severity methods.
|
|
47
|
+
* Each method is a no-op unless the current `_level` allows it.
|
|
48
|
+
*/
|
|
49
|
+
export declare const log: {
|
|
50
|
+
/** Trace-level detail: component IDs, route matching, bundle paths. */
|
|
51
|
+
verbose(...args: any[]): void;
|
|
52
|
+
/** Startup messages, route tables, config summary. */
|
|
53
|
+
info(...args: any[]): void;
|
|
54
|
+
/** Non-fatal issues: missing middleware, unrecognised config keys. */
|
|
55
|
+
warn(...args: any[]): void;
|
|
56
|
+
/** Errors that produce a 500 response or crash the build. */
|
|
57
|
+
error(...args: any[]): void;
|
|
58
|
+
};
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const ansi = {
|
|
2
|
+
reset: "\x1B[0m",
|
|
3
|
+
bold: "\x1B[1m",
|
|
4
|
+
dim: "\x1B[2m",
|
|
5
|
+
black: "\x1B[30m",
|
|
6
|
+
red: "\x1B[31m",
|
|
7
|
+
green: "\x1B[32m",
|
|
8
|
+
yellow: "\x1B[33m",
|
|
9
|
+
blue: "\x1B[34m",
|
|
10
|
+
magenta: "\x1B[35m",
|
|
11
|
+
cyan: "\x1B[36m",
|
|
12
|
+
white: "\x1B[37m",
|
|
13
|
+
gray: "\x1B[90m",
|
|
14
|
+
bgBlue: "\x1B[44m",
|
|
15
|
+
bgGreen: "\x1B[42m",
|
|
16
|
+
bgMagenta: "\x1B[45m"
|
|
17
|
+
};
|
|
18
|
+
function c(color, text, bold = false) {
|
|
19
|
+
return `${bold ? ansi.bold : ""}${ansi[color]}${text}${ansi.reset}`;
|
|
20
|
+
}
|
|
21
|
+
let _level = false;
|
|
22
|
+
function setDebugLevel(level) {
|
|
23
|
+
_level = level;
|
|
24
|
+
}
|
|
25
|
+
function getDebugLevel() {
|
|
26
|
+
return _level;
|
|
27
|
+
}
|
|
28
|
+
const log = {
|
|
29
|
+
/** Trace-level detail: component IDs, route matching, bundle paths. */
|
|
30
|
+
verbose(...args) {
|
|
31
|
+
if (_level === true) console.log(c("gray", "[verbose]"), ...args);
|
|
32
|
+
},
|
|
33
|
+
/** Startup messages, route tables, config summary. */
|
|
34
|
+
info(...args) {
|
|
35
|
+
if (_level === true || _level === "info") console.log(c("cyan", "[info]"), ...args);
|
|
36
|
+
},
|
|
37
|
+
/** Non-fatal issues: missing middleware, unrecognised config keys. */
|
|
38
|
+
warn(...args) {
|
|
39
|
+
if (_level === true || _level === "info") console.warn(c("yellow", "[warn]"), ...args);
|
|
40
|
+
},
|
|
41
|
+
/** Errors that produce a 500 response or crash the build. */
|
|
42
|
+
error(...args) {
|
|
43
|
+
if (_level !== false) console.error(c("red", "[error]"), ...args);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
export {
|
|
47
|
+
ansi,
|
|
48
|
+
c,
|
|
49
|
+
getDebugLevel,
|
|
50
|
+
log,
|
|
51
|
+
setDebugLevel
|
|
52
|
+
};
|
|
53
|
+
//# sourceMappingURL=logger.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/logger.ts"],
|
|
4
|
+
"sourcesContent": ["/**\r\n * logger.ts \u2014 ANSI-Coloured Levelled Logger\r\n *\r\n * Provides a small set of server-side logging utilities used throughout NukeJS.\r\n *\r\n * Debug levels (set via nuke.config.ts `debug` field):\r\n * false \u2014 silent, nothing printed\r\n * 'error' \u2014 error() only\r\n * 'info' \u2014 info(), warn(), error()\r\n * true \u2014 verbose: all of the above plus verbose()\r\n *\r\n * The level can be read back with `getDebugLevel()` and is also forwarded to\r\n * the browser client (as a string) so server and client log at the same level.\r\n */\r\n\r\n// \u2500\u2500\u2500 ANSI escape codes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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/** Map of named ANSI colour/style escape codes. */\r\nexport const ansi = {\r\n reset: '\\x1b[0m',\r\n bold: '\\x1b[1m',\r\n dim: '\\x1b[2m',\r\n black: '\\x1b[30m',\r\n red: '\\x1b[31m',\r\n green: '\\x1b[32m',\r\n yellow: '\\x1b[33m',\r\n blue: '\\x1b[34m',\r\n magenta: '\\x1b[35m',\r\n cyan: '\\x1b[36m',\r\n white: '\\x1b[37m',\r\n gray: '\\x1b[90m',\r\n bgBlue: '\\x1b[44m',\r\n bgGreen: '\\x1b[42m',\r\n bgMagenta: '\\x1b[45m',\r\n} as const;\r\n\r\n/**\r\n * Wraps `text` in the ANSI sequence for `color`, optionally bold.\r\n * Always appends the reset sequence so colour does not bleed into surrounding\r\n * terminal output.\r\n */\r\nexport function c(color: keyof typeof ansi, text: string, bold = false): string {\r\n return `${bold ? ansi.bold : ''}${ansi[color]}${text}${ansi.reset}`;\r\n}\r\n\r\n// \u2500\u2500\u2500 Debug level \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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/** false = silent | 'error' = errors only | 'info' = startup + errors | true = verbose */\r\nexport type DebugLevel = false | 'error' | 'info' | true;\r\n\r\nlet _level: DebugLevel = false;\r\n\r\n/** Sets the active log level. Called once after the config is loaded. */\r\nexport function setDebugLevel(level: DebugLevel): void { _level = level; }\r\n\r\n/** Returns the currently active log level. */\r\nexport function getDebugLevel(): DebugLevel { return _level; }\r\n\r\n// \u2500\u2500\u2500 Logger \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n\r\n/**\r\n * Structured log object with four severity methods.\r\n * Each method is a no-op unless the current `_level` allows it.\r\n */\r\nexport const log = {\r\n /** Trace-level detail: component IDs, route matching, bundle paths. */\r\n verbose(...args: any[]): void {\r\n if (_level === true) console.log(c('gray', '[verbose]'), ...args);\r\n },\r\n /** Startup messages, route tables, config summary. */\r\n info(...args: any[]): void {\r\n if (_level === true || _level === 'info') console.log(c('cyan', '[info]'), ...args);\r\n },\r\n /** Non-fatal issues: missing middleware, unrecognised config keys. */\r\n warn(...args: any[]): void {\r\n if (_level === true || _level === 'info') console.warn(c('yellow', '[warn]'), ...args);\r\n },\r\n /** Errors that produce a 500 response or crash the build. */\r\n error(...args: any[]): void {\r\n if (_level !== false) console.error(c('red', '[error]'), ...args);\r\n },\r\n};\r\n"],
|
|
5
|
+
"mappings": "AAkBO,MAAM,OAAO;AAAA,EAClB,OAAW;AAAA,EACX,MAAW;AAAA,EACX,KAAW;AAAA,EACX,OAAW;AAAA,EACX,KAAW;AAAA,EACX,OAAW;AAAA,EACX,QAAW;AAAA,EACX,MAAW;AAAA,EACX,SAAW;AAAA,EACX,MAAW;AAAA,EACX,OAAW;AAAA,EACX,MAAW;AAAA,EACX,QAAW;AAAA,EACX,SAAW;AAAA,EACX,WAAW;AACb;AAOO,SAAS,EAAE,OAA0B,MAAc,OAAO,OAAe;AAC9E,SAAO,GAAG,OAAO,KAAK,OAAO,EAAE,GAAG,KAAK,KAAK,CAAC,GAAG,IAAI,GAAG,KAAK,KAAK;AACnE;AAOA,IAAI,SAAqB;AAGlB,SAAS,cAAc,OAAyB;AAAE,WAAS;AAAO;AAGlE,SAAS,gBAA4B;AAAE,SAAO;AAAQ;AAQtD,MAAM,MAAM;AAAA;AAAA,EAEjB,WAAW,MAAmB;AAC5B,QAAI,WAAW,KAAM,SAAQ,IAAI,EAAE,QAAQ,WAAW,GAAG,GAAG,IAAI;AAAA,EAClE;AAAA;AAAA,EAEA,QAAQ,MAAmB;AACzB,QAAI,WAAW,QAAQ,WAAW,OAAQ,SAAQ,IAAI,EAAE,QAAQ,QAAQ,GAAG,GAAG,IAAI;AAAA,EACpF;AAAA;AAAA,EAEA,QAAQ,MAAmB;AACzB,QAAI,WAAW,QAAQ,WAAW,OAAQ,SAAQ,KAAK,EAAE,UAAU,QAAQ,GAAG,GAAG,IAAI;AAAA,EACvF;AAAA;AAAA,EAEA,SAAS,MAAmB;AAC1B,QAAI,WAAW,MAAO,SAAQ,MAAM,EAAE,OAAO,SAAS,GAAG,GAAG,IAAI;AAAA,EAClE;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* metadata.ts — Legacy Metadata Helpers
|
|
3
|
+
*
|
|
4
|
+
* @deprecated Use the `useHtml()` hook instead.
|
|
5
|
+
*
|
|
6
|
+
* This module provided an earlier metadata API where pages exported a
|
|
7
|
+
* `metadata` object alongside their default component. It is retained for
|
|
8
|
+
* backwards compatibility but new projects should use useHtml().
|
|
9
|
+
*
|
|
10
|
+
* Example (old pattern):
|
|
11
|
+
* export const metadata = {
|
|
12
|
+
* title: 'My Page',
|
|
13
|
+
* scripts: [{ src: '/analytics.js', defer: true }],
|
|
14
|
+
* };
|
|
15
|
+
*/
|
|
16
|
+
export interface ScriptTag {
|
|
17
|
+
src?: string;
|
|
18
|
+
content?: string;
|
|
19
|
+
type?: string;
|
|
20
|
+
defer?: boolean;
|
|
21
|
+
async?: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface StyleTag {
|
|
24
|
+
href?: string;
|
|
25
|
+
content?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface Metadata {
|
|
28
|
+
title?: string;
|
|
29
|
+
scripts?: ScriptTag[];
|
|
30
|
+
styles?: StyleTag[];
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Dynamically imports a page/layout module and returns its exported `metadata`
|
|
34
|
+
* object, or an empty object if none is found or the import fails.
|
|
35
|
+
*/
|
|
36
|
+
export declare function loadMetadata(filePath: string): Promise<Metadata>;
|
|
37
|
+
/**
|
|
38
|
+
* Merges metadata from an array of modules in render order (outermost layout
|
|
39
|
+
* first, page last).
|
|
40
|
+
*
|
|
41
|
+
* Merge strategy:
|
|
42
|
+
* title — last non-empty value wins (page overrides layout)
|
|
43
|
+
* scripts — concatenated in order
|
|
44
|
+
* styles — concatenated in order
|
|
45
|
+
*/
|
|
46
|
+
export declare function mergeMetadata(ordered: Metadata[]): Required<Metadata>;
|
|
47
|
+
/** Renders a ScriptTag to an HTML string. */
|
|
48
|
+
export declare function renderScriptTag(s: ScriptTag): string;
|
|
49
|
+
/** Renders a StyleTag to an HTML string. */
|
|
50
|
+
export declare function renderStyleTag(s: StyleTag): string;
|
package/dist/metadata.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { pathToFileURL } from "url";
|
|
2
|
+
import { escapeHtml } from "./utils.js";
|
|
3
|
+
async function loadMetadata(filePath) {
|
|
4
|
+
try {
|
|
5
|
+
const mod = await import(pathToFileURL(filePath).href);
|
|
6
|
+
return mod.metadata ?? {};
|
|
7
|
+
} catch {
|
|
8
|
+
return {};
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function mergeMetadata(ordered) {
|
|
12
|
+
const result = { title: "", scripts: [], styles: [] };
|
|
13
|
+
for (const m of ordered) {
|
|
14
|
+
if (m.title) result.title = m.title;
|
|
15
|
+
if (m.scripts?.length) result.scripts.push(...m.scripts);
|
|
16
|
+
if (m.styles?.length) result.styles.push(...m.styles);
|
|
17
|
+
}
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
function renderScriptTag(s) {
|
|
21
|
+
if (s.src) {
|
|
22
|
+
const attrs = [
|
|
23
|
+
`src="${escapeHtml(s.src)}"`,
|
|
24
|
+
s.type ? `type="${escapeHtml(s.type)}"` : "",
|
|
25
|
+
s.defer ? "defer" : "",
|
|
26
|
+
s.async ? "async" : ""
|
|
27
|
+
].filter(Boolean).join(" ");
|
|
28
|
+
return `<script ${attrs}></script>`;
|
|
29
|
+
}
|
|
30
|
+
const typeAttr = s.type ? ` type="${escapeHtml(s.type)}"` : "";
|
|
31
|
+
return `<script${typeAttr}>${s.content ?? ""}</script>`;
|
|
32
|
+
}
|
|
33
|
+
function renderStyleTag(s) {
|
|
34
|
+
if (s.href) return `<link rel="stylesheet" href="${escapeHtml(s.href)}" />`;
|
|
35
|
+
return `<style>${s.content ?? ""}</style>`;
|
|
36
|
+
}
|
|
37
|
+
export {
|
|
38
|
+
loadMetadata,
|
|
39
|
+
mergeMetadata,
|
|
40
|
+
renderScriptTag,
|
|
41
|
+
renderStyleTag
|
|
42
|
+
};
|
|
43
|
+
//# sourceMappingURL=metadata.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/metadata.ts"],
|
|
4
|
+
"sourcesContent": ["/**\r\n * metadata.ts \u2014 Legacy Metadata Helpers\r\n *\r\n * @deprecated Use the `useHtml()` hook instead.\r\n *\r\n * This module provided an earlier metadata API where pages exported a\r\n * `metadata` object alongside their default component. It is retained for\r\n * backwards compatibility but new projects should use useHtml().\r\n *\r\n * Example (old pattern):\r\n * export const metadata = {\r\n * title: 'My Page',\r\n * scripts: [{ src: '/analytics.js', defer: true }],\r\n * };\r\n */\r\n\r\nimport { pathToFileURL } from 'url';\r\nimport { escapeHtml } from './utils';\r\n\r\nexport interface ScriptTag {\r\n src?: string;\r\n content?: string;\r\n type?: string;\r\n defer?: boolean;\r\n async?: boolean;\r\n}\r\n\r\nexport interface StyleTag {\r\n href?: string;\r\n content?: string;\r\n}\r\n\r\nexport interface Metadata {\r\n title?: string;\r\n scripts?: ScriptTag[];\r\n styles?: StyleTag[];\r\n}\r\n\r\n/**\r\n * Dynamically imports a page/layout module and returns its exported `metadata`\r\n * object, or an empty object if none is found or the import fails.\r\n */\r\nexport async function loadMetadata(filePath: string): Promise<Metadata> {\r\n try {\r\n const mod = await import(pathToFileURL(filePath).href);\r\n return (mod.metadata as Metadata) ?? {};\r\n } catch {\r\n return {};\r\n }\r\n}\r\n\r\n/**\r\n * Merges metadata from an array of modules in render order (outermost layout\r\n * first, page last).\r\n *\r\n * Merge strategy:\r\n * title \u2014 last non-empty value wins (page overrides layout)\r\n * scripts \u2014 concatenated in order\r\n * styles \u2014 concatenated in order\r\n */\r\nexport function mergeMetadata(ordered: Metadata[]): Required<Metadata> {\r\n const result: Required<Metadata> = { title: '', scripts: [], styles: [] };\r\n for (const m of ordered) {\r\n if (m.title) result.title = m.title;\r\n if (m.scripts?.length) result.scripts.push(...m.scripts);\r\n if (m.styles?.length) result.styles.push(...m.styles);\r\n }\r\n return result;\r\n}\r\n\r\n/** Renders a ScriptTag to an HTML string. */\r\nexport function renderScriptTag(s: ScriptTag): string {\r\n if (s.src) {\r\n const attrs = [\r\n `src=\"${escapeHtml(s.src)}\"`,\r\n s.type ? `type=\"${escapeHtml(s.type)}\"` : '',\r\n s.defer ? 'defer' : '',\r\n s.async ? 'async' : '',\r\n ].filter(Boolean).join(' ');\r\n return `<script ${attrs}></script>`;\r\n }\r\n const typeAttr = s.type ? ` type=\"${escapeHtml(s.type)}\"` : '';\r\n return `<script${typeAttr}>${s.content ?? ''}</script>`;\r\n}\r\n\r\n/** Renders a StyleTag to an HTML string. */\r\nexport function renderStyleTag(s: StyleTag): string {\r\n if (s.href) return `<link rel=\"stylesheet\" href=\"${escapeHtml(s.href)}\" />`;\r\n return `<style>${s.content ?? ''}</style>`;\r\n}\r\n"],
|
|
5
|
+
"mappings": "AAgBA,SAAS,qBAAqB;AAC9B,SAAS,kBAAkB;AAyB3B,eAAsB,aAAa,UAAqC;AACtE,MAAI;AACF,UAAM,MAAM,MAAM,OAAO,cAAc,QAAQ,EAAE;AACjD,WAAQ,IAAI,YAAyB,CAAC;AAAA,EACxC,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AAWO,SAAS,cAAc,SAAyC;AACrE,QAAM,SAA6B,EAAE,OAAO,IAAI,SAAS,CAAC,GAAG,QAAQ,CAAC,EAAE;AACxE,aAAW,KAAK,SAAS;AACvB,QAAI,EAAE,MAAgB,QAAO,QAAQ,EAAE;AACvC,QAAI,EAAE,SAAS,OAAQ,QAAO,QAAQ,KAAK,GAAG,EAAE,OAAO;AACvD,QAAI,EAAE,QAAQ,OAAS,QAAO,OAAO,KAAK,GAAG,EAAE,MAAM;AAAA,EACvD;AACA,SAAO;AACT;AAGO,SAAS,gBAAgB,GAAsB;AACpD,MAAI,EAAE,KAAK;AACT,UAAM,QAAQ;AAAA,MACZ,QAAQ,WAAW,EAAE,GAAG,CAAC;AAAA,MACzB,EAAE,OAAQ,SAAS,WAAW,EAAE,IAAI,CAAC,MAAM;AAAA,MAC3C,EAAE,QAAQ,UAAW;AAAA,MACrB,EAAE,QAAQ,UAAW;AAAA,IACvB,EAAE,OAAO,OAAO,EAAE,KAAK,GAAG;AAC1B,WAAO,WAAW,KAAK;AAAA,EACzB;AACA,QAAM,WAAW,EAAE,OAAO,UAAU,WAAW,EAAE,IAAI,CAAC,MAAM;AAC5D,SAAO,UAAU,QAAQ,IAAI,EAAE,WAAW,EAAE;AAC9C;AAGO,SAAS,eAAe,GAAqB;AAClD,MAAI,EAAE,KAAM,QAAO,gCAAgC,WAAW,EAAE,IAAI,CAAC;AACrE,SAAO,UAAU,EAAE,WAAW,EAAE;AAClC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* middleware-loader.ts — Middleware Chain Manager
|
|
3
|
+
*
|
|
4
|
+
* Loads and runs the NukeJS middleware stack. Two layers are supported:
|
|
5
|
+
*
|
|
6
|
+
* 1. Built-in middleware — shipped with the nukejs package.
|
|
7
|
+
* Currently handles the /__hmr and /__hmr.js
|
|
8
|
+
* routes required by the HMR client.
|
|
9
|
+
* Located next to this file as `middleware.ts`
|
|
10
|
+
* (or `middleware.js` in the compiled dist/).
|
|
11
|
+
*
|
|
12
|
+
* 2. User middleware — `middleware.ts` in the project root (cwd).
|
|
13
|
+
* Runs after the built-in layer so it can inspect
|
|
14
|
+
* or short-circuit every incoming request, including
|
|
15
|
+
* API and page routes.
|
|
16
|
+
*
|
|
17
|
+
* Each middleware function receives (req, res) and may either:
|
|
18
|
+
* - End the response (res.end / res.json) to short-circuit further handling.
|
|
19
|
+
* - Return without touching res to pass control to the next layer.
|
|
20
|
+
*
|
|
21
|
+
* runMiddleware() returns `true` if any middleware ended the response,
|
|
22
|
+
* allowing app.ts to skip its own routing logic.
|
|
23
|
+
*
|
|
24
|
+
* Restart behaviour:
|
|
25
|
+
* When nuke.config.ts or middleware.ts change in dev, app.ts restarts the
|
|
26
|
+
* process. The new process calls loadMiddleware() fresh so stale module
|
|
27
|
+
* caches are not an issue.
|
|
28
|
+
*/
|
|
29
|
+
import type { IncomingMessage, ServerResponse } from 'http';
|
|
30
|
+
export type MiddlewareFunction = (req: IncomingMessage, res: ServerResponse) => Promise<void> | void;
|
|
31
|
+
/**
|
|
32
|
+
* Discovers and loads all middleware in priority order:
|
|
33
|
+
* 1. Built-in (this package's own middleware.ts / middleware.js)
|
|
34
|
+
* 2. User-supplied (cwd/middleware.ts)
|
|
35
|
+
*
|
|
36
|
+
* Duplicate paths (e.g. if cwd === package dir in a monorepo) are deduplicated
|
|
37
|
+
* via a Set so the same file is never loaded twice.
|
|
38
|
+
*
|
|
39
|
+
* Should be called once at startup after the config is loaded.
|
|
40
|
+
*/
|
|
41
|
+
export declare function loadMiddleware(): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* Runs all loaded middleware in registration order.
|
|
44
|
+
*
|
|
45
|
+
* Stops and returns `true` as soon as any middleware ends or sends a response
|
|
46
|
+
* (res.writableEnded or res.headersSent), allowing app.ts to skip routing.
|
|
47
|
+
*
|
|
48
|
+
* Returns `false` if no middleware handled the request.
|
|
49
|
+
*/
|
|
50
|
+
export declare function runMiddleware(req: IncomingMessage, res: ServerResponse): Promise<boolean>;
|