cpeak 2.3.0 → 2.4.1

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.
@@ -0,0 +1,63 @@
1
+ import http, { IncomingMessage, ServerResponse } from 'node:http';
2
+
3
+ type Cpeak$1 = InstanceType<typeof Cpeak>;
4
+ type StringMap = Record<string, string>;
5
+ interface CpeakRequest<ReqBody = any, ReqParams = any> extends IncomingMessage {
6
+ params: ReqParams;
7
+ vars?: StringMap;
8
+ body?: ReqBody;
9
+ [key: string]: any;
10
+ query: ReqParams;
11
+ }
12
+ interface CpeakResponse extends ServerResponse {
13
+ sendFile: (path: string, mime: string) => Promise<void>;
14
+ status: (code: number) => CpeakResponse;
15
+ redirect: (location: string) => CpeakResponse;
16
+ json: (data: any) => void;
17
+ [key: string]: any;
18
+ }
19
+ type Next = (err?: any) => void;
20
+ type HandleErr = (err: any) => void;
21
+ type Middleware<ReqBody = any, ReqParams = any> = (req: CpeakRequest<ReqBody, ReqParams>, res: CpeakResponse, next: Next) => void;
22
+ type RouteMiddleware<ReqBody = any, ReqParams = any> = (req: CpeakRequest<ReqBody, ReqParams>, res: CpeakResponse, next: Next, handleErr: HandleErr) => void | Promise<void>;
23
+ type Handler<ReqBody = any, ReqParams = any> = (req: CpeakRequest<ReqBody, ReqParams>, res: CpeakResponse, handleErr: HandleErr) => void | Promise<void>;
24
+ interface Route {
25
+ path: string;
26
+ regex: RegExp;
27
+ middleware: RouteMiddleware[];
28
+ cb: Handler;
29
+ }
30
+ interface RoutesMap {
31
+ [method: string]: Route[];
32
+ }
33
+
34
+ declare const parseJSON: (req: CpeakRequest, res: CpeakResponse, next: Next) => void;
35
+
36
+ declare const serveStatic: (folderPath: string, newMimeTypes?: StringMap) => (req: CpeakRequest, res: CpeakResponse, next: Next) => void | Promise<void>;
37
+
38
+ declare const render: () => (req: CpeakRequest, res: CpeakResponse, next: Next) => void;
39
+
40
+ declare function frameworkError(message: string, skipFn: Function, code?: string): Error & {
41
+ code?: string;
42
+ };
43
+ declare enum ErrorCode {
44
+ MISSING_MIME = "CPEAK_ERR_MISSING_MIME",
45
+ FILE_NOT_FOUND = "CPEAK_ERR_FILE_NOT_FOUND",
46
+ NOT_A_FILE = "CPEAK_ERR_NOT_A_FILE",
47
+ SEND_FILE_FAIL = "CPEAK_ERR_SEND_FILE_FAIL"
48
+ }
49
+ declare class Cpeak {
50
+ #private;
51
+ private server;
52
+ private routes;
53
+ private middleware;
54
+ private _handleErr?;
55
+ constructor();
56
+ route(method: string, path: string, ...args: (RouteMiddleware | Handler)[]): void;
57
+ beforeEach(cb: Middleware): void;
58
+ handleErr(cb: (err: unknown, req: CpeakRequest, res: CpeakResponse) => void): void;
59
+ listen(port: number, cb?: () => void): http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>;
60
+ close(cb?: (err?: Error) => void): void;
61
+ }
62
+
63
+ export { type Cpeak$1 as Cpeak, type CpeakRequest, type CpeakResponse, ErrorCode, type HandleErr, type Handler, type Middleware, type Next, type RouteMiddleware, type RoutesMap, Cpeak as default, frameworkError, parseJSON, render, serveStatic };
package/dist/index.js ADDED
@@ -0,0 +1,319 @@
1
+ // lib/index.ts
2
+ import http from "http";
3
+ import fs3 from "fs/promises";
4
+ import { createReadStream } from "fs";
5
+ import { pipeline } from "stream/promises";
6
+
7
+ // lib/utils/parseJSON.ts
8
+ var parseJSON = (req, res, next) => {
9
+ function isJSON(contentType = "") {
10
+ const [type] = contentType.split(";");
11
+ return type.trim().toLowerCase() === "application/json" || /\+json$/i.test(type.trim());
12
+ }
13
+ if (!isJSON(req.headers["content-type"])) return next();
14
+ let body = "";
15
+ req.on("data", (chunk) => {
16
+ body += chunk.toString("utf-8");
17
+ });
18
+ req.on("end", () => {
19
+ body = JSON.parse(body);
20
+ req.body = body;
21
+ return next();
22
+ });
23
+ };
24
+
25
+ // lib/utils/serveStatic.ts
26
+ import fs from "fs";
27
+ import path from "path";
28
+ var MIME_TYPES = {
29
+ html: "text/html",
30
+ css: "text/css",
31
+ js: "application/javascript",
32
+ jpg: "image/jpeg",
33
+ jpeg: "image/jpeg",
34
+ png: "image/png",
35
+ svg: "image/svg+xml",
36
+ txt: "text/plain",
37
+ eot: "application/vnd.ms-fontobject",
38
+ otf: "font/otf",
39
+ ttf: "font/ttf",
40
+ woff: "font/woff",
41
+ woff2: "font/woff2"
42
+ };
43
+ var serveStatic = (folderPath, newMimeTypes) => {
44
+ if (newMimeTypes) {
45
+ Object.assign(MIME_TYPES, newMimeTypes);
46
+ }
47
+ function processFolder(folderPath2, parentFolder) {
48
+ const staticFiles = [];
49
+ const files = fs.readdirSync(folderPath2);
50
+ for (const file of files) {
51
+ const fullPath = path.join(folderPath2, file);
52
+ if (fs.statSync(fullPath).isDirectory()) {
53
+ const subfolderFiles = processFolder(fullPath, parentFolder);
54
+ staticFiles.push(...subfolderFiles);
55
+ } else {
56
+ const relativePath = path.relative(parentFolder, fullPath);
57
+ const fileExtension = path.extname(file).slice(1);
58
+ if (MIME_TYPES[fileExtension]) staticFiles.push("/" + relativePath);
59
+ }
60
+ }
61
+ return staticFiles;
62
+ }
63
+ const filesArrayToFilesMap = (filesArray) => {
64
+ const filesMap2 = {};
65
+ for (const file of filesArray) {
66
+ const fileExtension = path.extname(file).slice(1);
67
+ filesMap2[file] = {
68
+ path: folderPath + file,
69
+ mime: MIME_TYPES[fileExtension]
70
+ };
71
+ }
72
+ return filesMap2;
73
+ };
74
+ const filesMap = filesArrayToFilesMap(processFolder(folderPath, folderPath));
75
+ return function(req, res, next) {
76
+ const url = req.url;
77
+ if (typeof url !== "string") return next();
78
+ if (Object.prototype.hasOwnProperty.call(filesMap, url)) {
79
+ const fileRoute = filesMap[url];
80
+ return res.sendFile(fileRoute.path, fileRoute.mime);
81
+ }
82
+ next();
83
+ };
84
+ };
85
+
86
+ // lib/utils/render.ts
87
+ import fs2 from "fs/promises";
88
+ function renderTemplate(templateStr, data) {
89
+ let result = [];
90
+ let currentIndex = 0;
91
+ while (currentIndex < templateStr.length) {
92
+ const startIdx = templateStr.indexOf("{{", currentIndex);
93
+ if (startIdx === -1) {
94
+ result.push(templateStr.slice(currentIndex));
95
+ break;
96
+ }
97
+ result.push(templateStr.slice(currentIndex, startIdx));
98
+ const endIdx = templateStr.indexOf("}}", startIdx);
99
+ if (endIdx === -1) {
100
+ result.push(templateStr.slice(startIdx));
101
+ break;
102
+ }
103
+ const varName = templateStr.slice(startIdx + 2, endIdx).trim();
104
+ const replacement = data[varName] !== void 0 ? data[varName] : "";
105
+ result.push(replacement);
106
+ currentIndex = endIdx + 2;
107
+ }
108
+ return result.join("");
109
+ }
110
+ var render = () => {
111
+ return function(req, res, next) {
112
+ res.render = async (path2, data, mime) => {
113
+ if (!mime) {
114
+ throw frameworkError(
115
+ `MIME type is missing. You called res.render("${path2}", ...) but forgot to provide the third "mime" argument.`,
116
+ res.render
117
+ );
118
+ }
119
+ let fileStr = await fs2.readFile(path2, "utf-8");
120
+ const finalStr = renderTemplate(fileStr, data);
121
+ res.setHeader("Content-Type", mime);
122
+ res.end(finalStr);
123
+ };
124
+ next();
125
+ };
126
+ };
127
+
128
+ // lib/index.ts
129
+ function frameworkError(message, skipFn, code) {
130
+ const err = new Error(message);
131
+ Error.captureStackTrace(err, skipFn);
132
+ if (code) err.code = code;
133
+ return err;
134
+ }
135
+ var ErrorCode = /* @__PURE__ */ ((ErrorCode2) => {
136
+ ErrorCode2["MISSING_MIME"] = "CPEAK_ERR_MISSING_MIME";
137
+ ErrorCode2["FILE_NOT_FOUND"] = "CPEAK_ERR_FILE_NOT_FOUND";
138
+ ErrorCode2["NOT_A_FILE"] = "CPEAK_ERR_NOT_A_FILE";
139
+ ErrorCode2["SEND_FILE_FAIL"] = "CPEAK_ERR_SEND_FILE_FAIL";
140
+ return ErrorCode2;
141
+ })(ErrorCode || {});
142
+ var Cpeak = class {
143
+ server;
144
+ routes;
145
+ middleware;
146
+ _handleErr;
147
+ constructor() {
148
+ this.server = http.createServer();
149
+ this.routes = {};
150
+ this.middleware = [];
151
+ this.server.on("request", (req, res) => {
152
+ res.sendFile = async (path2, mime) => {
153
+ if (!mime) {
154
+ throw frameworkError(
155
+ 'MIME type is missing. Use res.sendFile(path, "mime-type").',
156
+ res.sendFile,
157
+ "CPEAK_ERR_MISSING_MIME" /* MISSING_MIME */
158
+ );
159
+ }
160
+ try {
161
+ const stat = await fs3.stat(path2);
162
+ if (!stat.isFile()) {
163
+ throw frameworkError(
164
+ `Not a file: ${path2}`,
165
+ res.sendFile,
166
+ "CPEAK_ERR_NOT_A_FILE" /* NOT_A_FILE */
167
+ );
168
+ }
169
+ res.setHeader("Content-Type", mime);
170
+ res.setHeader("Content-Length", String(stat.size));
171
+ await pipeline(createReadStream(path2), res);
172
+ } catch (err) {
173
+ if (err?.code === "ENOENT") {
174
+ throw frameworkError(
175
+ `File not found: ${path2}`,
176
+ res.sendFile,
177
+ "CPEAK_ERR_FILE_NOT_FOUND" /* FILE_NOT_FOUND */
178
+ );
179
+ }
180
+ throw frameworkError(
181
+ `Failed to send file: ${path2}`,
182
+ res.sendFile,
183
+ "CPEAK_ERR_SEND_FILE_FAIL" /* SEND_FILE_FAIL */
184
+ );
185
+ }
186
+ };
187
+ res.status = (code) => {
188
+ res.statusCode = code;
189
+ return res;
190
+ };
191
+ res.redirect = (location) => {
192
+ res.writeHead(302, { Location: location });
193
+ res.end();
194
+ return res;
195
+ };
196
+ res.json = (data) => {
197
+ res.setHeader("Content-Type", "application/json");
198
+ res.end(JSON.stringify(data));
199
+ };
200
+ const urlWithoutParams = req.url?.split("?")[0];
201
+ const params = new URLSearchParams(req.url?.split("?")[1]);
202
+ const paramsObject = Object.fromEntries(params.entries());
203
+ req.params = paramsObject;
204
+ req.query = paramsObject;
205
+ const runHandler = (req2, res2, middleware, cb, index) => {
206
+ if (index === middleware.length) {
207
+ try {
208
+ const handlerResult = cb(req2, res2, (error) => {
209
+ res2.setHeader("Connection", "close");
210
+ this._handleErr?.(error, req2, res2);
211
+ });
212
+ if (handlerResult && typeof handlerResult.then === "function") {
213
+ handlerResult.catch((error) => {
214
+ res2.setHeader("Connection", "close");
215
+ this._handleErr?.(error, req2, res2);
216
+ });
217
+ }
218
+ return handlerResult;
219
+ } catch (error) {
220
+ res2.setHeader("Connection", "close");
221
+ this._handleErr?.(error, req2, res2);
222
+ }
223
+ } else {
224
+ middleware[index](
225
+ req2,
226
+ res2,
227
+ // The next function
228
+ (error) => {
229
+ if (error) {
230
+ res2.setHeader("Connection", "close");
231
+ return this._handleErr?.(error, req2, res2);
232
+ }
233
+ runHandler(req2, res2, middleware, cb, index + 1);
234
+ },
235
+ // Error handler for a route middleware
236
+ (error) => {
237
+ res2.setHeader("Connection", "close");
238
+ this._handleErr?.(error, req2, res2);
239
+ }
240
+ );
241
+ }
242
+ };
243
+ const runMiddleware = (req2, res2, middleware, index) => {
244
+ if (index === middleware.length) {
245
+ const routes = this.routes[req2.method?.toLowerCase() || ""];
246
+ if (routes && typeof routes[Symbol.iterator] === "function")
247
+ for (const route of routes) {
248
+ const match = urlWithoutParams?.match(route.regex);
249
+ if (match) {
250
+ const vars = this.#extractVars(route.path, match);
251
+ req2.vars = vars;
252
+ return runHandler(req2, res2, route.middleware, route.cb, 0);
253
+ }
254
+ }
255
+ return res2.status(404).json({ error: `Cannot ${req2.method} ${urlWithoutParams}` });
256
+ } else {
257
+ middleware[index](req2, res2, () => {
258
+ runMiddleware(req2, res2, middleware, index + 1);
259
+ });
260
+ }
261
+ };
262
+ runMiddleware(req, res, this.middleware, 0);
263
+ });
264
+ }
265
+ route(method, path2, ...args) {
266
+ if (!this.routes[method]) this.routes[method] = [];
267
+ const cb = args.pop();
268
+ if (!cb || typeof cb !== "function") {
269
+ throw new Error("Route definition must include a handler");
270
+ }
271
+ const middleware = args.flat();
272
+ const regex = this.#pathToRegex(path2);
273
+ this.routes[method].push({ path: path2, regex, middleware, cb });
274
+ }
275
+ beforeEach(cb) {
276
+ this.middleware.push(cb);
277
+ }
278
+ handleErr(cb) {
279
+ this._handleErr = cb;
280
+ }
281
+ listen(port, cb) {
282
+ return this.server.listen(port, cb);
283
+ }
284
+ close(cb) {
285
+ this.server.close(cb);
286
+ }
287
+ // ------------------------------
288
+ // PRIVATE METHODS:
289
+ // ------------------------------
290
+ #pathToRegex(path2) {
291
+ const varNames = [];
292
+ const regexString = "^" + path2.replace(/:\w+/g, (match, offset) => {
293
+ varNames.push(match.slice(1));
294
+ return "([^/]+)";
295
+ }) + "$";
296
+ const regex = new RegExp(regexString);
297
+ return regex;
298
+ }
299
+ #extractVars(path2, match) {
300
+ const varNames = (path2.match(/:\w+/g) || []).map(
301
+ (varParam) => varParam.slice(1)
302
+ );
303
+ const vars = {};
304
+ varNames.forEach((name, index) => {
305
+ vars[name] = match[index + 1];
306
+ });
307
+ return vars;
308
+ }
309
+ };
310
+ var index_default = Cpeak;
311
+ export {
312
+ ErrorCode,
313
+ index_default as default,
314
+ frameworkError,
315
+ parseJSON,
316
+ render,
317
+ serveStatic
318
+ };
319
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../lib/index.ts","../lib/utils/parseJSON.ts","../lib/utils/serveStatic.ts","../lib/utils/render.ts"],"sourcesContent":["import http from \"node:http\";\nimport fs from \"node:fs/promises\";\nimport { createReadStream } from \"node:fs\";\nimport { pipeline } from \"node:stream/promises\";\n\nimport { serveStatic, parseJSON, render } from \"./utils\";\n\nimport type {\n StringMap,\n CpeakRequest,\n CpeakResponse,\n Middleware,\n RouteMiddleware,\n Handler,\n RoutesMap,\n} from \"./types\";\n\n// A utility function to create an error with a custom stack trace\nexport function frameworkError(\n message: string,\n skipFn: Function,\n code?: string\n) {\n const err = new Error(message) as Error & { code?: string };\n Error.captureStackTrace(err, skipFn);\n\n if (code) err.code = code;\n\n return err;\n}\n\nexport enum ErrorCode {\n MISSING_MIME = \"CPEAK_ERR_MISSING_MIME\",\n FILE_NOT_FOUND = \"CPEAK_ERR_FILE_NOT_FOUND\",\n NOT_A_FILE = \"CPEAK_ERR_NOT_A_FILE\",\n SEND_FILE_FAIL = \"CPEAK_ERR_SEND_FILE_FAIL\",\n}\n\nclass Cpeak {\n private server: http.Server;\n private routes: RoutesMap;\n private middleware: Middleware[];\n private _handleErr?: (\n err: unknown,\n req: CpeakRequest,\n res: CpeakResponse\n ) => void;\n\n constructor() {\n this.server = http.createServer();\n this.routes = {};\n this.middleware = [];\n\n this.server.on(\"request\", (req: CpeakRequest, res: CpeakResponse) => {\n // Send a file back to the client\n res.sendFile = async (path: string, mime: string) => {\n if (!mime) {\n throw frameworkError(\n 'MIME type is missing. Use res.sendFile(path, \"mime-type\").',\n res.sendFile,\n ErrorCode.MISSING_MIME\n );\n }\n\n try {\n const stat = await fs.stat(path);\n if (!stat.isFile()) {\n throw frameworkError(\n `Not a file: ${path}`,\n res.sendFile,\n ErrorCode.NOT_A_FILE\n );\n }\n\n res.setHeader(\"Content-Type\", mime);\n res.setHeader(\"Content-Length\", String(stat.size));\n\n // Properly propagate stream errors and respect backpressure\n await pipeline(createReadStream(path), res);\n } catch (err: any) {\n if (err?.code === \"ENOENT\") {\n throw frameworkError(\n `File not found: ${path}`,\n res.sendFile,\n ErrorCode.FILE_NOT_FOUND\n );\n }\n\n throw frameworkError(\n `Failed to send file: ${path}`,\n res.sendFile,\n ErrorCode.SEND_FILE_FAIL\n );\n }\n };\n\n // Set the status code of the response\n res.status = (code: number) => {\n res.statusCode = code;\n return res;\n };\n\n // Redirects to a new URL\n res.redirect = (location: string) => {\n res.writeHead(302, { Location: location });\n res.end();\n return res;\n };\n\n // Send a json data back to the client (for small json data, less than the highWaterMark)\n res.json = (data: any) => {\n // This is only good for bodies that their size is less than the highWaterMark value\n res.setHeader(\"Content-Type\", \"application/json\");\n res.end(JSON.stringify(data));\n };\n\n // Get the url without the URL parameters\n const urlWithoutParams = req.url?.split(\"?\")[0];\n\n // Parse the URL parameters (like /users?key1=value1&key2=value2)\n // We put this here to also parse them for all the middleware functions\n const params = new URLSearchParams(req.url?.split(\"?\")[1]);\n\n const paramsObject = Object.fromEntries(params.entries());\n\n req.params = paramsObject;\n req.query = paramsObject; // only for compatibility with frameworks built for express\n\n // Run all the specific middleware functions for that router only and then run the handler\n const runHandler = (\n req: CpeakRequest,\n res: CpeakResponse,\n middleware: RouteMiddleware[],\n cb: Handler,\n index: number\n ) => {\n // Our exit point...\n if (index === middleware.length) {\n // Call the route handler with the modified req and res objects.\n // Also handle the promise errors by passing them to the handleErr to save developers from having to manually wrap every handler in try catch.\n try {\n const handlerResult = cb(req, res, (error) => {\n res.setHeader(\"Connection\", \"close\");\n this._handleErr?.(error, req, res);\n });\n\n if (handlerResult && typeof handlerResult.then === \"function\") {\n handlerResult.catch((error) => {\n res.setHeader(\"Connection\", \"close\");\n this._handleErr?.(error, req, res);\n });\n }\n\n return handlerResult;\n } catch (error) {\n res.setHeader(\"Connection\", \"close\");\n this._handleErr?.(error, req, res);\n }\n } else {\n middleware[index](\n req,\n res,\n // The next function\n (error) => {\n // this function only accepts an error argument to be more compatible with NPM modules that are built for express\n if (error) {\n res.setHeader(\"Connection\", \"close\");\n return this._handleErr?.(error, req, res);\n }\n runHandler(req, res, middleware, cb, index + 1);\n },\n // Error handler for a route middleware\n (error) => {\n res.setHeader(\"Connection\", \"close\");\n this._handleErr?.(error, req, res);\n }\n );\n }\n };\n\n // Run all the middleware functions (beforeEach functions) before we run the corresponding route\n const runMiddleware = (\n req: CpeakRequest,\n res: CpeakResponse,\n middleware: Middleware[],\n index: number\n ) => {\n // Our exit point...\n if (index === middleware.length) {\n const routes = this.routes[req.method?.toLowerCase() || \"\"];\n if (routes && typeof routes[Symbol.iterator] === \"function\")\n for (const route of routes) {\n const match = urlWithoutParams?.match(route.regex);\n\n if (match) {\n // Parse the URL variables from the matched route (like /users/:id)\n const vars = this.#extractVars(route.path, match);\n req.vars = vars;\n\n return runHandler(req, res, route.middleware, route.cb, 0);\n }\n }\n\n // If the requested route dose not exist, return 404\n return res\n .status(404)\n .json({ error: `Cannot ${req.method} ${urlWithoutParams}` });\n } else {\n middleware[index](req, res, () => {\n runMiddleware(req, res, middleware, index + 1);\n });\n }\n };\n\n runMiddleware(req, res, this.middleware, 0);\n });\n }\n\n route(method: string, path: string, ...args: (RouteMiddleware | Handler)[]) {\n if (!this.routes[method]) this.routes[method] = [];\n\n // The last argument should always be our handler\n const cb = args.pop() as Handler;\n\n if (!cb || typeof cb !== \"function\") {\n throw new Error(\"Route definition must include a handler\");\n }\n\n // Rest will be our middleware functions\n const middleware = args.flat() as RouteMiddleware[];\n\n const regex = this.#pathToRegex(path);\n this.routes[method].push({ path, regex, middleware, cb });\n }\n\n beforeEach(cb: Middleware) {\n this.middleware.push(cb);\n }\n\n handleErr(cb: (err: unknown, req: CpeakRequest, res: CpeakResponse) => void) {\n this._handleErr = cb;\n }\n\n listen(port: number, cb?: () => void) {\n return this.server.listen(port, cb);\n }\n\n close(cb?: (err?: Error) => void) {\n this.server.close(cb);\n }\n\n // ------------------------------\n // PRIVATE METHODS:\n // ------------------------------\n #pathToRegex(path: string) {\n const varNames: string[] = [];\n const regexString =\n \"^\" +\n path.replace(/:\\w+/g, (match, offset) => {\n varNames.push(match.slice(1));\n return \"([^/]+)\";\n }) +\n \"$\";\n\n const regex = new RegExp(regexString);\n return regex;\n }\n\n #extractVars(path: string, match: RegExpMatchArray) {\n // Extract url variable values from the matched route\n const varNames = (path.match(/:\\w+/g) || []).map((varParam) =>\n varParam.slice(1)\n );\n const vars: StringMap = {};\n varNames.forEach((name, index) => {\n vars[name] = match[index + 1];\n });\n return vars;\n }\n}\n\n// Util functions\nexport { serveStatic, parseJSON, render };\n\nexport type {\n Cpeak,\n CpeakRequest,\n CpeakResponse,\n Next,\n HandleErr,\n Middleware,\n RouteMiddleware,\n Handler,\n RoutesMap,\n} from \"./types\";\n\nexport default Cpeak;\n","import type { CpeakRequest, CpeakResponse, Next } from \"../types\";\n\n// Parsing JSON\nconst parseJSON = (req: CpeakRequest, res: CpeakResponse, next: Next) => {\n // This is only good for bodies that their size is less than the highWaterMark value\n\n function isJSON(contentType: string = \"\") {\n // Remove any params like \"; charset=UTF-8\"\n const [type] = contentType.split(\";\");\n return (\n type.trim().toLowerCase() === \"application/json\" ||\n /\\+json$/i.test(type.trim())\n );\n }\n\n if (!isJSON(req.headers[\"content-type\"] as string)) return next();\n\n let body = \"\";\n req.on(\"data\", (chunk: Buffer) => {\n body += chunk.toString(\"utf-8\");\n });\n\n req.on(\"end\", () => {\n body = JSON.parse(body);\n req.body = body;\n return next();\n });\n};\n\nexport { parseJSON };\n","import fs from \"node:fs\";\nimport path from \"node:path\";\n\nimport type { StringMap, CpeakRequest, CpeakResponse, Next } from \"../types\";\n\nconst MIME_TYPES: StringMap = {\n html: \"text/html\",\n css: \"text/css\",\n js: \"application/javascript\",\n jpg: \"image/jpeg\",\n jpeg: \"image/jpeg\",\n png: \"image/png\",\n svg: \"image/svg+xml\",\n txt: \"text/plain\",\n eot: \"application/vnd.ms-fontobject\",\n otf: \"font/otf\",\n ttf: \"font/ttf\",\n woff: \"font/woff\",\n woff2: \"font/woff2\",\n};\n\nconst serveStatic = (folderPath: string, newMimeTypes?: StringMap) => {\n // For new user defined mime types\n if (newMimeTypes) {\n Object.assign(MIME_TYPES, newMimeTypes);\n }\n\n function processFolder(folderPath: string, parentFolder: string) {\n const staticFiles: string[] = [];\n\n // Read the contents of the folder\n const files = fs.readdirSync(folderPath);\n\n // Loop through the files and subfolders\n for (const file of files) {\n const fullPath = path.join(folderPath, file);\n\n // Check if it's a directory\n if (fs.statSync(fullPath).isDirectory()) {\n // If it's a directory, recursively process it\n const subfolderFiles = processFolder(fullPath, parentFolder);\n staticFiles.push(...subfolderFiles);\n } else {\n // If it's a file, add it to the array\n const relativePath = path.relative(parentFolder, fullPath);\n const fileExtension = path.extname(file).slice(1);\n if (MIME_TYPES[fileExtension]) staticFiles.push(\"/\" + relativePath);\n }\n }\n\n return staticFiles;\n }\n\n const filesArrayToFilesMap = (filesArray: string[]) => {\n const filesMap: Record<string, { path: string; mime: string }> = {};\n for (const file of filesArray) {\n const fileExtension = path.extname(file).slice(1);\n filesMap[file] = {\n path: folderPath + file,\n mime: MIME_TYPES[fileExtension],\n };\n }\n return filesMap;\n };\n\n // Start processing the folder\n const filesMap = filesArrayToFilesMap(processFolder(folderPath, folderPath));\n\n return function (req: CpeakRequest, res: CpeakResponse, next: Next) {\n const url = req.url;\n if (typeof url !== \"string\") return next();\n\n if (Object.prototype.hasOwnProperty.call(filesMap, url)) {\n const fileRoute = filesMap[url];\n return res.sendFile(fileRoute.path, fileRoute.mime);\n }\n\n next();\n };\n};\n\nexport { serveStatic };\n","import fs from \"node:fs/promises\";\nimport { frameworkError } from \"../\";\nimport type { CpeakRequest, CpeakResponse, Next } from \"../types\";\n\nfunction renderTemplate(\n templateStr: string,\n data: Record<string, unknown>\n): string {\n // Initialize variables\n let result: (string | unknown)[] = [];\n\n let currentIndex = 0;\n\n while (currentIndex < templateStr.length) {\n // Find the next opening placeholder\n const startIdx = templateStr.indexOf(\"{{\", currentIndex);\n if (startIdx === -1) {\n // No more placeholders, push the remaining string\n result.push(templateStr.slice(currentIndex));\n break;\n }\n\n // Push the part before the placeholder\n result.push(templateStr.slice(currentIndex, startIdx));\n\n // Find the closing placeholder\n const endIdx = templateStr.indexOf(\"}}\", startIdx);\n if (endIdx === -1) {\n // No closing brace found, treat the rest as plain text\n result.push(templateStr.slice(startIdx));\n break;\n }\n\n // Extract the variable name\n const varName = templateStr.slice(startIdx + 2, endIdx).trim();\n\n // Replace the variable with its value from the data, or use an empty string if not found\n const replacement = data[varName] !== undefined ? data[varName] : \"\";\n\n // Push the replacement to the result array\n result.push(replacement);\n\n // Move the index past the current closing placeholder\n currentIndex = endIdx + 2;\n }\n\n // Join all parts into a final string\n return result.join(\"\");\n}\n\n// Errors to return: recommend to not render files larger than 100KB\n// To Explore: Doing the operation in C++ and return the data as stream back to the client\n// @TODO: remove the file from static map\n// @TODO: escape the string to prevent XSS\n// @TODO: add another {{{ }}} option to not escape the string\nconst render = () => {\n return function (req: CpeakRequest, res: CpeakResponse, next: Next): void {\n res.render = async (\n path: string,\n data: Record<string, unknown>,\n mime: string\n ) => {\n // check if mime is specified, if not return an error\n if (!mime) {\n throw frameworkError(\n `MIME type is missing. You called res.render(\"${path}\", ...) but forgot to provide the third \"mime\" argument.`,\n res.render\n );\n }\n\n let fileStr = await fs.readFile(path, \"utf-8\");\n const finalStr = renderTemplate(fileStr, data);\n res.setHeader(\"Content-Type\", mime);\n res.end(finalStr);\n };\n\n next();\n };\n};\n\nexport { render };\n"],"mappings":";AAAA,OAAO,UAAU;AACjB,OAAOA,SAAQ;AACf,SAAS,wBAAwB;AACjC,SAAS,gBAAgB;;;ACAzB,IAAM,YAAY,CAAC,KAAmB,KAAoB,SAAe;AAGvE,WAAS,OAAO,cAAsB,IAAI;AAExC,UAAM,CAAC,IAAI,IAAI,YAAY,MAAM,GAAG;AACpC,WACE,KAAK,KAAK,EAAE,YAAY,MAAM,sBAC9B,WAAW,KAAK,KAAK,KAAK,CAAC;AAAA,EAE/B;AAEA,MAAI,CAAC,OAAO,IAAI,QAAQ,cAAc,CAAW,EAAG,QAAO,KAAK;AAEhE,MAAI,OAAO;AACX,MAAI,GAAG,QAAQ,CAAC,UAAkB;AAChC,YAAQ,MAAM,SAAS,OAAO;AAAA,EAChC,CAAC;AAED,MAAI,GAAG,OAAO,MAAM;AAClB,WAAO,KAAK,MAAM,IAAI;AACtB,QAAI,OAAO;AACX,WAAO,KAAK;AAAA,EACd,CAAC;AACH;;;AC3BA,OAAO,QAAQ;AACf,OAAO,UAAU;AAIjB,IAAM,aAAwB;AAAA,EAC5B,MAAM;AAAA,EACN,KAAK;AAAA,EACL,IAAI;AAAA,EACJ,KAAK;AAAA,EACL,MAAM;AAAA,EACN,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,MAAM;AAAA,EACN,OAAO;AACT;AAEA,IAAM,cAAc,CAAC,YAAoB,iBAA6B;AAEpE,MAAI,cAAc;AAChB,WAAO,OAAO,YAAY,YAAY;AAAA,EACxC;AAEA,WAAS,cAAcC,aAAoB,cAAsB;AAC/D,UAAM,cAAwB,CAAC;AAG/B,UAAM,QAAQ,GAAG,YAAYA,WAAU;AAGvC,eAAW,QAAQ,OAAO;AACxB,YAAM,WAAW,KAAK,KAAKA,aAAY,IAAI;AAG3C,UAAI,GAAG,SAAS,QAAQ,EAAE,YAAY,GAAG;AAEvC,cAAM,iBAAiB,cAAc,UAAU,YAAY;AAC3D,oBAAY,KAAK,GAAG,cAAc;AAAA,MACpC,OAAO;AAEL,cAAM,eAAe,KAAK,SAAS,cAAc,QAAQ;AACzD,cAAM,gBAAgB,KAAK,QAAQ,IAAI,EAAE,MAAM,CAAC;AAChD,YAAI,WAAW,aAAa,EAAG,aAAY,KAAK,MAAM,YAAY;AAAA,MACpE;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAEA,QAAM,uBAAuB,CAAC,eAAyB;AACrD,UAAMC,YAA2D,CAAC;AAClE,eAAW,QAAQ,YAAY;AAC7B,YAAM,gBAAgB,KAAK,QAAQ,IAAI,EAAE,MAAM,CAAC;AAChD,MAAAA,UAAS,IAAI,IAAI;AAAA,QACf,MAAM,aAAa;AAAA,QACnB,MAAM,WAAW,aAAa;AAAA,MAChC;AAAA,IACF;AACA,WAAOA;AAAA,EACT;AAGA,QAAM,WAAW,qBAAqB,cAAc,YAAY,UAAU,CAAC;AAE3E,SAAO,SAAU,KAAmB,KAAoB,MAAY;AAClE,UAAM,MAAM,IAAI;AAChB,QAAI,OAAO,QAAQ,SAAU,QAAO,KAAK;AAEzC,QAAI,OAAO,UAAU,eAAe,KAAK,UAAU,GAAG,GAAG;AACvD,YAAM,YAAY,SAAS,GAAG;AAC9B,aAAO,IAAI,SAAS,UAAU,MAAM,UAAU,IAAI;AAAA,IACpD;AAEA,SAAK;AAAA,EACP;AACF;;;AC/EA,OAAOC,SAAQ;AAIf,SAAS,eACP,aACA,MACQ;AAER,MAAI,SAA+B,CAAC;AAEpC,MAAI,eAAe;AAEnB,SAAO,eAAe,YAAY,QAAQ;AAExC,UAAM,WAAW,YAAY,QAAQ,MAAM,YAAY;AACvD,QAAI,aAAa,IAAI;AAEnB,aAAO,KAAK,YAAY,MAAM,YAAY,CAAC;AAC3C;AAAA,IACF;AAGA,WAAO,KAAK,YAAY,MAAM,cAAc,QAAQ,CAAC;AAGrD,UAAM,SAAS,YAAY,QAAQ,MAAM,QAAQ;AACjD,QAAI,WAAW,IAAI;AAEjB,aAAO,KAAK,YAAY,MAAM,QAAQ,CAAC;AACvC;AAAA,IACF;AAGA,UAAM,UAAU,YAAY,MAAM,WAAW,GAAG,MAAM,EAAE,KAAK;AAG7D,UAAM,cAAc,KAAK,OAAO,MAAM,SAAY,KAAK,OAAO,IAAI;AAGlE,WAAO,KAAK,WAAW;AAGvB,mBAAe,SAAS;AAAA,EAC1B;AAGA,SAAO,OAAO,KAAK,EAAE;AACvB;AAOA,IAAM,SAAS,MAAM;AACnB,SAAO,SAAU,KAAmB,KAAoB,MAAkB;AACxE,QAAI,SAAS,OACXC,OACA,MACA,SACG;AAEH,UAAI,CAAC,MAAM;AACT,cAAM;AAAA,UACJ,gDAAgDA,KAAI;AAAA,UACpD,IAAI;AAAA,QACN;AAAA,MACF;AAEA,UAAI,UAAU,MAAMC,IAAG,SAASD,OAAM,OAAO;AAC7C,YAAM,WAAW,eAAe,SAAS,IAAI;AAC7C,UAAI,UAAU,gBAAgB,IAAI;AAClC,UAAI,IAAI,QAAQ;AAAA,IAClB;AAEA,SAAK;AAAA,EACP;AACF;;;AH5DO,SAAS,eACd,SACA,QACA,MACA;AACA,QAAM,MAAM,IAAI,MAAM,OAAO;AAC7B,QAAM,kBAAkB,KAAK,MAAM;AAEnC,MAAI,KAAM,KAAI,OAAO;AAErB,SAAO;AACT;AAEO,IAAK,YAAL,kBAAKE,eAAL;AACL,EAAAA,WAAA,kBAAe;AACf,EAAAA,WAAA,oBAAiB;AACjB,EAAAA,WAAA,gBAAa;AACb,EAAAA,WAAA,oBAAiB;AAJP,SAAAA;AAAA,GAAA;AAOZ,IAAM,QAAN,MAAY;AAAA,EACF;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAMR,cAAc;AACZ,SAAK,SAAS,KAAK,aAAa;AAChC,SAAK,SAAS,CAAC;AACf,SAAK,aAAa,CAAC;AAEnB,SAAK,OAAO,GAAG,WAAW,CAAC,KAAmB,QAAuB;AAEnE,UAAI,WAAW,OAAOC,OAAc,SAAiB;AACnD,YAAI,CAAC,MAAM;AACT,gBAAM;AAAA,YACJ;AAAA,YACA,IAAI;AAAA,YACJ;AAAA,UACF;AAAA,QACF;AAEA,YAAI;AACF,gBAAM,OAAO,MAAMC,IAAG,KAAKD,KAAI;AAC/B,cAAI,CAAC,KAAK,OAAO,GAAG;AAClB,kBAAM;AAAA,cACJ,eAAeA,KAAI;AAAA,cACnB,IAAI;AAAA,cACJ;AAAA,YACF;AAAA,UACF;AAEA,cAAI,UAAU,gBAAgB,IAAI;AAClC,cAAI,UAAU,kBAAkB,OAAO,KAAK,IAAI,CAAC;AAGjD,gBAAM,SAAS,iBAAiBA,KAAI,GAAG,GAAG;AAAA,QAC5C,SAAS,KAAU;AACjB,cAAI,KAAK,SAAS,UAAU;AAC1B,kBAAM;AAAA,cACJ,mBAAmBA,KAAI;AAAA,cACvB,IAAI;AAAA,cACJ;AAAA,YACF;AAAA,UACF;AAEA,gBAAM;AAAA,YACJ,wBAAwBA,KAAI;AAAA,YAC5B,IAAI;AAAA,YACJ;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAGA,UAAI,SAAS,CAAC,SAAiB;AAC7B,YAAI,aAAa;AACjB,eAAO;AAAA,MACT;AAGA,UAAI,WAAW,CAAC,aAAqB;AACnC,YAAI,UAAU,KAAK,EAAE,UAAU,SAAS,CAAC;AACzC,YAAI,IAAI;AACR,eAAO;AAAA,MACT;AAGA,UAAI,OAAO,CAAC,SAAc;AAExB,YAAI,UAAU,gBAAgB,kBAAkB;AAChD,YAAI,IAAI,KAAK,UAAU,IAAI,CAAC;AAAA,MAC9B;AAGA,YAAM,mBAAmB,IAAI,KAAK,MAAM,GAAG,EAAE,CAAC;AAI9C,YAAM,SAAS,IAAI,gBAAgB,IAAI,KAAK,MAAM,GAAG,EAAE,CAAC,CAAC;AAEzD,YAAM,eAAe,OAAO,YAAY,OAAO,QAAQ,CAAC;AAExD,UAAI,SAAS;AACb,UAAI,QAAQ;AAGZ,YAAM,aAAa,CACjBE,MACAC,MACA,YACA,IACA,UACG;AAEH,YAAI,UAAU,WAAW,QAAQ;AAG/B,cAAI;AACF,kBAAM,gBAAgB,GAAGD,MAAKC,MAAK,CAAC,UAAU;AAC5C,cAAAA,KAAI,UAAU,cAAc,OAAO;AACnC,mBAAK,aAAa,OAAOD,MAAKC,IAAG;AAAA,YACnC,CAAC;AAED,gBAAI,iBAAiB,OAAO,cAAc,SAAS,YAAY;AAC7D,4BAAc,MAAM,CAAC,UAAU;AAC7B,gBAAAA,KAAI,UAAU,cAAc,OAAO;AACnC,qBAAK,aAAa,OAAOD,MAAKC,IAAG;AAAA,cACnC,CAAC;AAAA,YACH;AAEA,mBAAO;AAAA,UACT,SAAS,OAAO;AACd,YAAAA,KAAI,UAAU,cAAc,OAAO;AACnC,iBAAK,aAAa,OAAOD,MAAKC,IAAG;AAAA,UACnC;AAAA,QACF,OAAO;AACL,qBAAW,KAAK;AAAA,YACdD;AAAA,YACAC;AAAA;AAAA,YAEA,CAAC,UAAU;AAET,kBAAI,OAAO;AACT,gBAAAA,KAAI,UAAU,cAAc,OAAO;AACnC,uBAAO,KAAK,aAAa,OAAOD,MAAKC,IAAG;AAAA,cAC1C;AACA,yBAAWD,MAAKC,MAAK,YAAY,IAAI,QAAQ,CAAC;AAAA,YAChD;AAAA;AAAA,YAEA,CAAC,UAAU;AACT,cAAAA,KAAI,UAAU,cAAc,OAAO;AACnC,mBAAK,aAAa,OAAOD,MAAKC,IAAG;AAAA,YACnC;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAGA,YAAM,gBAAgB,CACpBD,MACAC,MACA,YACA,UACG;AAEH,YAAI,UAAU,WAAW,QAAQ;AAC/B,gBAAM,SAAS,KAAK,OAAOD,KAAI,QAAQ,YAAY,KAAK,EAAE;AAC1D,cAAI,UAAU,OAAO,OAAO,OAAO,QAAQ,MAAM;AAC/C,uBAAW,SAAS,QAAQ;AAC1B,oBAAM,QAAQ,kBAAkB,MAAM,MAAM,KAAK;AAEjD,kBAAI,OAAO;AAET,sBAAM,OAAO,KAAK,aAAa,MAAM,MAAM,KAAK;AAChD,gBAAAA,KAAI,OAAO;AAEX,uBAAO,WAAWA,MAAKC,MAAK,MAAM,YAAY,MAAM,IAAI,CAAC;AAAA,cAC3D;AAAA,YACF;AAGF,iBAAOA,KACJ,OAAO,GAAG,EACV,KAAK,EAAE,OAAO,UAAUD,KAAI,MAAM,IAAI,gBAAgB,GAAG,CAAC;AAAA,QAC/D,OAAO;AACL,qBAAW,KAAK,EAAEA,MAAKC,MAAK,MAAM;AAChC,0BAAcD,MAAKC,MAAK,YAAY,QAAQ,CAAC;AAAA,UAC/C,CAAC;AAAA,QACH;AAAA,MACF;AAEA,oBAAc,KAAK,KAAK,KAAK,YAAY,CAAC;AAAA,IAC5C,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,QAAgBH,UAAiB,MAAqC;AAC1E,QAAI,CAAC,KAAK,OAAO,MAAM,EAAG,MAAK,OAAO,MAAM,IAAI,CAAC;AAGjD,UAAM,KAAK,KAAK,IAAI;AAEpB,QAAI,CAAC,MAAM,OAAO,OAAO,YAAY;AACnC,YAAM,IAAI,MAAM,yCAAyC;AAAA,IAC3D;AAGA,UAAM,aAAa,KAAK,KAAK;AAE7B,UAAM,QAAQ,KAAK,aAAaA,KAAI;AACpC,SAAK,OAAO,MAAM,EAAE,KAAK,EAAE,MAAAA,OAAM,OAAO,YAAY,GAAG,CAAC;AAAA,EAC1D;AAAA,EAEA,WAAW,IAAgB;AACzB,SAAK,WAAW,KAAK,EAAE;AAAA,EACzB;AAAA,EAEA,UAAU,IAAmE;AAC3E,SAAK,aAAa;AAAA,EACpB;AAAA,EAEA,OAAO,MAAc,IAAiB;AACpC,WAAO,KAAK,OAAO,OAAO,MAAM,EAAE;AAAA,EACpC;AAAA,EAEA,MAAM,IAA4B;AAChC,SAAK,OAAO,MAAM,EAAE;AAAA,EACtB;AAAA;AAAA;AAAA;AAAA,EAKA,aAAaA,OAAc;AACzB,UAAM,WAAqB,CAAC;AAC5B,UAAM,cACJ,MACAA,MAAK,QAAQ,SAAS,CAAC,OAAO,WAAW;AACvC,eAAS,KAAK,MAAM,MAAM,CAAC,CAAC;AAC5B,aAAO;AAAA,IACT,CAAC,IACD;AAEF,UAAM,QAAQ,IAAI,OAAO,WAAW;AACpC,WAAO;AAAA,EACT;AAAA,EAEA,aAAaA,OAAc,OAAyB;AAElD,UAAM,YAAYA,MAAK,MAAM,OAAO,KAAK,CAAC,GAAG;AAAA,MAAI,CAAC,aAChD,SAAS,MAAM,CAAC;AAAA,IAClB;AACA,UAAM,OAAkB,CAAC;AACzB,aAAS,QAAQ,CAAC,MAAM,UAAU;AAChC,WAAK,IAAI,IAAI,MAAM,QAAQ,CAAC;AAAA,IAC9B,CAAC;AACD,WAAO;AAAA,EACT;AACF;AAiBA,IAAO,gBAAQ;","names":["fs","folderPath","filesMap","fs","path","fs","ErrorCode","path","fs","req","res"]}
package/lib/index.ts ADDED
@@ -0,0 +1,297 @@
1
+ import http from "node:http";
2
+ import fs from "node:fs/promises";
3
+ import { createReadStream } from "node:fs";
4
+ import { pipeline } from "node:stream/promises";
5
+
6
+ import { serveStatic, parseJSON, render } from "./utils";
7
+
8
+ import type {
9
+ StringMap,
10
+ CpeakRequest,
11
+ CpeakResponse,
12
+ Middleware,
13
+ RouteMiddleware,
14
+ Handler,
15
+ RoutesMap,
16
+ } from "./types";
17
+
18
+ // A utility function to create an error with a custom stack trace
19
+ export function frameworkError(
20
+ message: string,
21
+ skipFn: Function,
22
+ code?: string
23
+ ) {
24
+ const err = new Error(message) as Error & { code?: string };
25
+ Error.captureStackTrace(err, skipFn);
26
+
27
+ if (code) err.code = code;
28
+
29
+ return err;
30
+ }
31
+
32
+ export enum ErrorCode {
33
+ MISSING_MIME = "CPEAK_ERR_MISSING_MIME",
34
+ FILE_NOT_FOUND = "CPEAK_ERR_FILE_NOT_FOUND",
35
+ NOT_A_FILE = "CPEAK_ERR_NOT_A_FILE",
36
+ SEND_FILE_FAIL = "CPEAK_ERR_SEND_FILE_FAIL",
37
+ }
38
+
39
+ class Cpeak {
40
+ private server: http.Server;
41
+ private routes: RoutesMap;
42
+ private middleware: Middleware[];
43
+ private _handleErr?: (
44
+ err: unknown,
45
+ req: CpeakRequest,
46
+ res: CpeakResponse
47
+ ) => void;
48
+
49
+ constructor() {
50
+ this.server = http.createServer();
51
+ this.routes = {};
52
+ this.middleware = [];
53
+
54
+ this.server.on("request", (req: CpeakRequest, res: CpeakResponse) => {
55
+ // Send a file back to the client
56
+ res.sendFile = async (path: string, mime: string) => {
57
+ if (!mime) {
58
+ throw frameworkError(
59
+ 'MIME type is missing. Use res.sendFile(path, "mime-type").',
60
+ res.sendFile,
61
+ ErrorCode.MISSING_MIME
62
+ );
63
+ }
64
+
65
+ try {
66
+ const stat = await fs.stat(path);
67
+ if (!stat.isFile()) {
68
+ throw frameworkError(
69
+ `Not a file: ${path}`,
70
+ res.sendFile,
71
+ ErrorCode.NOT_A_FILE
72
+ );
73
+ }
74
+
75
+ res.setHeader("Content-Type", mime);
76
+ res.setHeader("Content-Length", String(stat.size));
77
+
78
+ // Properly propagate stream errors and respect backpressure
79
+ await pipeline(createReadStream(path), res);
80
+ } catch (err: any) {
81
+ if (err?.code === "ENOENT") {
82
+ throw frameworkError(
83
+ `File not found: ${path}`,
84
+ res.sendFile,
85
+ ErrorCode.FILE_NOT_FOUND
86
+ );
87
+ }
88
+
89
+ throw frameworkError(
90
+ `Failed to send file: ${path}`,
91
+ res.sendFile,
92
+ ErrorCode.SEND_FILE_FAIL
93
+ );
94
+ }
95
+ };
96
+
97
+ // Set the status code of the response
98
+ res.status = (code: number) => {
99
+ res.statusCode = code;
100
+ return res;
101
+ };
102
+
103
+ // Redirects to a new URL
104
+ res.redirect = (location: string) => {
105
+ res.writeHead(302, { Location: location });
106
+ res.end();
107
+ return res;
108
+ };
109
+
110
+ // Send a json data back to the client (for small json data, less than the highWaterMark)
111
+ res.json = (data: any) => {
112
+ // This is only good for bodies that their size is less than the highWaterMark value
113
+ res.setHeader("Content-Type", "application/json");
114
+ res.end(JSON.stringify(data));
115
+ };
116
+
117
+ // Get the url without the URL parameters
118
+ const urlWithoutParams = req.url?.split("?")[0];
119
+
120
+ // Parse the URL parameters (like /users?key1=value1&key2=value2)
121
+ // We put this here to also parse them for all the middleware functions
122
+ const params = new URLSearchParams(req.url?.split("?")[1]);
123
+
124
+ const paramsObject = Object.fromEntries(params.entries());
125
+
126
+ req.params = paramsObject;
127
+ req.query = paramsObject; // only for compatibility with frameworks built for express
128
+
129
+ // Run all the specific middleware functions for that router only and then run the handler
130
+ const runHandler = (
131
+ req: CpeakRequest,
132
+ res: CpeakResponse,
133
+ middleware: RouteMiddleware[],
134
+ cb: Handler,
135
+ index: number
136
+ ) => {
137
+ // Our exit point...
138
+ if (index === middleware.length) {
139
+ // Call the route handler with the modified req and res objects.
140
+ // Also handle the promise errors by passing them to the handleErr to save developers from having to manually wrap every handler in try catch.
141
+ try {
142
+ const handlerResult = cb(req, res, (error) => {
143
+ res.setHeader("Connection", "close");
144
+ this._handleErr?.(error, req, res);
145
+ });
146
+
147
+ if (handlerResult && typeof handlerResult.then === "function") {
148
+ handlerResult.catch((error) => {
149
+ res.setHeader("Connection", "close");
150
+ this._handleErr?.(error, req, res);
151
+ });
152
+ }
153
+
154
+ return handlerResult;
155
+ } catch (error) {
156
+ res.setHeader("Connection", "close");
157
+ this._handleErr?.(error, req, res);
158
+ }
159
+ } else {
160
+ middleware[index](
161
+ req,
162
+ res,
163
+ // The next function
164
+ (error) => {
165
+ // this function only accepts an error argument to be more compatible with NPM modules that are built for express
166
+ if (error) {
167
+ res.setHeader("Connection", "close");
168
+ return this._handleErr?.(error, req, res);
169
+ }
170
+ runHandler(req, res, middleware, cb, index + 1);
171
+ },
172
+ // Error handler for a route middleware
173
+ (error) => {
174
+ res.setHeader("Connection", "close");
175
+ this._handleErr?.(error, req, res);
176
+ }
177
+ );
178
+ }
179
+ };
180
+
181
+ // Run all the middleware functions (beforeEach functions) before we run the corresponding route
182
+ const runMiddleware = (
183
+ req: CpeakRequest,
184
+ res: CpeakResponse,
185
+ middleware: Middleware[],
186
+ index: number
187
+ ) => {
188
+ // Our exit point...
189
+ if (index === middleware.length) {
190
+ const routes = this.routes[req.method?.toLowerCase() || ""];
191
+ if (routes && typeof routes[Symbol.iterator] === "function")
192
+ for (const route of routes) {
193
+ const match = urlWithoutParams?.match(route.regex);
194
+
195
+ if (match) {
196
+ // Parse the URL variables from the matched route (like /users/:id)
197
+ const vars = this.#extractVars(route.path, match);
198
+ req.vars = vars;
199
+
200
+ return runHandler(req, res, route.middleware, route.cb, 0);
201
+ }
202
+ }
203
+
204
+ // If the requested route dose not exist, return 404
205
+ return res
206
+ .status(404)
207
+ .json({ error: `Cannot ${req.method} ${urlWithoutParams}` });
208
+ } else {
209
+ middleware[index](req, res, () => {
210
+ runMiddleware(req, res, middleware, index + 1);
211
+ });
212
+ }
213
+ };
214
+
215
+ runMiddleware(req, res, this.middleware, 0);
216
+ });
217
+ }
218
+
219
+ route(method: string, path: string, ...args: (RouteMiddleware | Handler)[]) {
220
+ if (!this.routes[method]) this.routes[method] = [];
221
+
222
+ // The last argument should always be our handler
223
+ const cb = args.pop() as Handler;
224
+
225
+ if (!cb || typeof cb !== "function") {
226
+ throw new Error("Route definition must include a handler");
227
+ }
228
+
229
+ // Rest will be our middleware functions
230
+ const middleware = args.flat() as RouteMiddleware[];
231
+
232
+ const regex = this.#pathToRegex(path);
233
+ this.routes[method].push({ path, regex, middleware, cb });
234
+ }
235
+
236
+ beforeEach(cb: Middleware) {
237
+ this.middleware.push(cb);
238
+ }
239
+
240
+ handleErr(cb: (err: unknown, req: CpeakRequest, res: CpeakResponse) => void) {
241
+ this._handleErr = cb;
242
+ }
243
+
244
+ listen(port: number, cb?: () => void) {
245
+ return this.server.listen(port, cb);
246
+ }
247
+
248
+ close(cb?: (err?: Error) => void) {
249
+ this.server.close(cb);
250
+ }
251
+
252
+ // ------------------------------
253
+ // PRIVATE METHODS:
254
+ // ------------------------------
255
+ #pathToRegex(path: string) {
256
+ const varNames: string[] = [];
257
+ const regexString =
258
+ "^" +
259
+ path.replace(/:\w+/g, (match, offset) => {
260
+ varNames.push(match.slice(1));
261
+ return "([^/]+)";
262
+ }) +
263
+ "$";
264
+
265
+ const regex = new RegExp(regexString);
266
+ return regex;
267
+ }
268
+
269
+ #extractVars(path: string, match: RegExpMatchArray) {
270
+ // Extract url variable values from the matched route
271
+ const varNames = (path.match(/:\w+/g) || []).map((varParam) =>
272
+ varParam.slice(1)
273
+ );
274
+ const vars: StringMap = {};
275
+ varNames.forEach((name, index) => {
276
+ vars[name] = match[index + 1];
277
+ });
278
+ return vars;
279
+ }
280
+ }
281
+
282
+ // Util functions
283
+ export { serveStatic, parseJSON, render };
284
+
285
+ export type {
286
+ Cpeak,
287
+ CpeakRequest,
288
+ CpeakResponse,
289
+ Next,
290
+ HandleErr,
291
+ Middleware,
292
+ RouteMiddleware,
293
+ Handler,
294
+ RoutesMap,
295
+ } from "./types";
296
+
297
+ export default Cpeak;
package/lib/types.ts ADDED
@@ -0,0 +1,64 @@
1
+ import { IncomingMessage, ServerResponse } from "node:http";
2
+ import CpeakClass from "./index";
3
+
4
+ export type Cpeak = InstanceType<typeof CpeakClass>;
5
+
6
+ // Extending Node.js's Request and Response objects to add our custom properties
7
+ export type StringMap = Record<string, string>;
8
+
9
+ export interface CpeakRequest<ReqBody = any, ReqParams = any>
10
+ extends IncomingMessage {
11
+ params: ReqParams;
12
+ vars?: StringMap;
13
+ body?: ReqBody;
14
+ [key: string]: any; // allow developers to add their onw extensions (e.g. req.test)
15
+
16
+ // For express frameworks compatibility:
17
+ query: ReqParams;
18
+ }
19
+
20
+ export interface CpeakResponse extends ServerResponse {
21
+ sendFile: (path: string, mime: string) => Promise<void>;
22
+ status: (code: number) => CpeakResponse;
23
+ redirect: (location: string) => CpeakResponse;
24
+ json: (data: any) => void;
25
+ [key: string]: any; // allow developers to add their onw extensions (e.g. res.test)
26
+ }
27
+
28
+ export type Next = (err?: any) => void;
29
+ export type HandleErr = (err: any) => void;
30
+
31
+ // beforeEach middleware: (req, res, next)
32
+ export type Middleware<ReqBody = any, ReqParams = any> = (
33
+ req: CpeakRequest<ReqBody, ReqParams>,
34
+ res: CpeakResponse,
35
+ next: Next
36
+ ) => void;
37
+
38
+ // Route middleware: (req, res, next, handleErr)
39
+ export type RouteMiddleware<ReqBody = any, ReqParams = any> = (
40
+ req: CpeakRequest<ReqBody, ReqParams>,
41
+ res: CpeakResponse,
42
+ next: Next,
43
+ handleErr: HandleErr
44
+ ) => void | Promise<void>;
45
+
46
+ // Route handlers: (req, res, handleErr)
47
+ export type Handler<ReqBody = any, ReqParams = any> = (
48
+ req: CpeakRequest<ReqBody, ReqParams>,
49
+ res: CpeakResponse,
50
+ handleErr: HandleErr
51
+ ) => void | Promise<void>;
52
+
53
+ // For a route object value in Cpeak.routes. The key is the method name.
54
+ export interface Route {
55
+ path: string;
56
+ regex: RegExp;
57
+ middleware: RouteMiddleware[];
58
+ cb: Handler;
59
+ }
60
+
61
+ // For Cpeak.routes:
62
+ export interface RoutesMap {
63
+ [method: string]: Route[];
64
+ }
@@ -0,0 +1,5 @@
1
+ import { parseJSON } from "./parseJSON";
2
+ import { serveStatic } from "./serveStatic";
3
+ import { render } from "./render";
4
+
5
+ export { serveStatic, parseJSON, render };
@@ -1,8 +1,10 @@
1
+ import type { CpeakRequest, CpeakResponse, Next } from "../types";
2
+
1
3
  // Parsing JSON
2
- const parseJSON = (req, res, next) => {
4
+ const parseJSON = (req: CpeakRequest, res: CpeakResponse, next: Next) => {
3
5
  // This is only good for bodies that their size is less than the highWaterMark value
4
6
 
5
- function isJSON(contentType = "") {
7
+ function isJSON(contentType: string = "") {
6
8
  // Remove any params like "; charset=UTF-8"
7
9
  const [type] = contentType.split(";");
8
10
  return (
@@ -11,10 +13,10 @@ const parseJSON = (req, res, next) => {
11
13
  );
12
14
  }
13
15
 
14
- if (!isJSON(req.headers["content-type"])) return next();
16
+ if (!isJSON(req.headers["content-type"] as string)) return next();
15
17
 
16
18
  let body = "";
17
- req.on("data", (chunk) => {
19
+ req.on("data", (chunk: Buffer) => {
18
20
  body += chunk.toString("utf-8");
19
21
  });
20
22
 
@@ -1,8 +1,13 @@
1
1
  import fs from "node:fs/promises";
2
+ import { frameworkError } from "../";
3
+ import type { CpeakRequest, CpeakResponse, Next } from "../types";
2
4
 
3
- function renderTemplate(templateStr, data) {
5
+ function renderTemplate(
6
+ templateStr: string,
7
+ data: Record<string, unknown>
8
+ ): string {
4
9
  // Initialize variables
5
- let result = [];
10
+ let result: (string | unknown)[] = [];
6
11
 
7
12
  let currentIndex = 0;
8
13
 
@@ -49,8 +54,20 @@ function renderTemplate(templateStr, data) {
49
54
  // @TODO: escape the string to prevent XSS
50
55
  // @TODO: add another {{{ }}} option to not escape the string
51
56
  const render = () => {
52
- return function (req, res, next) {
53
- res.render = async (path, data, mime) => {
57
+ return function (req: CpeakRequest, res: CpeakResponse, next: Next): void {
58
+ res.render = async (
59
+ path: string,
60
+ data: Record<string, unknown>,
61
+ mime: string
62
+ ) => {
63
+ // check if mime is specified, if not return an error
64
+ if (!mime) {
65
+ throw frameworkError(
66
+ `MIME type is missing. You called res.render("${path}", ...) but forgot to provide the third "mime" argument.`,
67
+ res.render
68
+ );
69
+ }
70
+
54
71
  let fileStr = await fs.readFile(path, "utf-8");
55
72
  const finalStr = renderTemplate(fileStr, data);
56
73
  res.setHeader("Content-Type", mime);
@@ -1,7 +1,9 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
 
4
- const MIME_TYPES = {
4
+ import type { StringMap, CpeakRequest, CpeakResponse, Next } from "../types";
5
+
6
+ const MIME_TYPES: StringMap = {
5
7
  html: "text/html",
6
8
  css: "text/css",
7
9
  js: "application/javascript",
@@ -17,14 +19,14 @@ const MIME_TYPES = {
17
19
  woff2: "font/woff2",
18
20
  };
19
21
 
20
- const serveStatic = (folderPath, newMimeTypes) => {
22
+ const serveStatic = (folderPath: string, newMimeTypes?: StringMap) => {
21
23
  // For new user defined mime types
22
24
  if (newMimeTypes) {
23
25
  Object.assign(MIME_TYPES, newMimeTypes);
24
26
  }
25
27
 
26
- function processFolder(folderPath, parentFolder) {
27
- const staticFiles = [];
28
+ function processFolder(folderPath: string, parentFolder: string) {
29
+ const staticFiles: string[] = [];
28
30
 
29
31
  // Read the contents of the folder
30
32
  const files = fs.readdirSync(folderPath);
@@ -49,8 +51,8 @@ const serveStatic = (folderPath, newMimeTypes) => {
49
51
  return staticFiles;
50
52
  }
51
53
 
52
- const filesArrayToFilesMap = (filesArray) => {
53
- const filesMap = {};
54
+ const filesArrayToFilesMap = (filesArray: string[]) => {
55
+ const filesMap: Record<string, { path: string; mime: string }> = {};
54
56
  for (const file of filesArray) {
55
57
  const fileExtension = path.extname(file).slice(1);
56
58
  filesMap[file] = {
@@ -64,13 +66,16 @@ const serveStatic = (folderPath, newMimeTypes) => {
64
66
  // Start processing the folder
65
67
  const filesMap = filesArrayToFilesMap(processFolder(folderPath, folderPath));
66
68
 
67
- return function (req, res, next) {
68
- if (filesMap.hasOwnProperty(req.url)) {
69
- const fileRoute = filesMap[req.url];
69
+ return function (req: CpeakRequest, res: CpeakResponse, next: Next) {
70
+ const url = req.url;
71
+ if (typeof url !== "string") return next();
72
+
73
+ if (Object.prototype.hasOwnProperty.call(filesMap, url)) {
74
+ const fileRoute = filesMap[url];
70
75
  return res.sendFile(fileRoute.path, fileRoute.mime);
71
- } else {
72
- next();
73
76
  }
77
+
78
+ next();
74
79
  };
75
80
  };
76
81
 
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "cpeak",
3
- "version": "2.3.0",
3
+ "version": "2.4.1",
4
4
  "description": "A minimal and fast Node.js HTTP framework.",
5
5
  "type": "module",
6
- "main": "./lib/index.js",
7
6
  "scripts": {
8
- "test": "mocha test/**/*.js"
7
+ "build": "tsup lib/index.ts --format esm --dts --sourcemap --out-dir dist --clean",
8
+ "dev": "tsup lib/index.ts --watch --format esm --dts --sourcemap --out-dir dist",
9
+ "test": "tsx ./node_modules/mocha/bin/_mocha --extension ts \"test/**/*.test.ts\""
9
10
  },
10
11
  "repository": {
11
12
  "type": "git",
@@ -15,6 +16,21 @@
15
16
  "url": "https://github.com/agile8118/cpeak/issues"
16
17
  },
17
18
  "homepage": "https://github.com/agile8118/cpeak#readme",
19
+ "main": "./dist/index.js",
20
+ "module": "./dist/index.js",
21
+ "types": "./dist/index.d.ts",
22
+ "exports": {
23
+ ".": {
24
+ "import": "./dist/index.js",
25
+ "types": "./dist/index.d.ts"
26
+ }
27
+ },
28
+ "files": [
29
+ "dist",
30
+ "lib",
31
+ "README.md",
32
+ "LICENSE"
33
+ ],
18
34
  "author": "Cododev Technology",
19
35
  "license": "MIT",
20
36
  "keywords": [
@@ -26,7 +42,14 @@
26
42
  "framework"
27
43
  ],
28
44
  "devDependencies": {
29
- "mocha": "^10.7.3",
30
- "supertest": "^7.0.0"
45
+ "@types/mocha": "^10.0.10",
46
+ "@types/node": "^24.3.0",
47
+ "@types/supertest": "^6.0.3",
48
+ "mocha": "^10.8.2",
49
+ "supertest": "^7.1.4",
50
+ "ts-node": "^10.9.2",
51
+ "tsup": "^8.5.0",
52
+ "tsx": "^4.20.5",
53
+ "typescript": "^5.9.2"
31
54
  }
32
55
  }
package/lib/index.js DELETED
@@ -1,172 +0,0 @@
1
- import http from "node:http";
2
- import fs from "node:fs/promises";
3
-
4
- import { serveStatic, parseJSON, render } from "./utils/index.js";
5
-
6
- class Cpeak {
7
- constructor() {
8
- this.server = http.createServer();
9
- this.routes = {};
10
- this.middleware = [];
11
- this.handleErr;
12
-
13
- this.server.on("request", (req, res) => {
14
- // Send a file back to the client
15
- res.sendFile = async (path, mime) => {
16
- const fileHandle = await fs.open(path, "r");
17
- const fileStream = fileHandle.createReadStream();
18
-
19
- res.setHeader("Content-Type", mime);
20
-
21
- fileStream.pipe(res);
22
- };
23
-
24
- // Set the status code of the response
25
- res.status = (code) => {
26
- res.statusCode = code;
27
- return res;
28
- };
29
-
30
- // Redirects to a new URL
31
- res.redirect = (location) => {
32
- res.writeHead(302, { Location: location });
33
- res.end();
34
- return res;
35
- };
36
-
37
- // Send a json data back to the client (for small json data, less than the highWaterMark)
38
- res.json = (data) => {
39
- // This is only good for bodies that their size is less than the highWaterMark value
40
- res.setHeader("Content-Type", "application/json");
41
- res.end(JSON.stringify(data));
42
- };
43
-
44
- // Get the url without the URL parameters
45
- const urlWithoutParams = req.url.split("?")[0];
46
-
47
- // Parse the URL parameters (like /users?key1=value1&key2=value2)
48
- // We put this here to also parse them for all the middleware functions
49
- const params = new URLSearchParams(req.url.split("?")[1]);
50
- req.params = Object.fromEntries(params.entries());
51
-
52
- // Run all the specific middleware functions for that router only and then run the handler
53
- const runHandler = (req, res, middleware, cb, index) => {
54
- // Our exit point...
55
- if (index === middleware.length) {
56
- // Call the route handler with the modified req and res objects
57
- return cb(req, res, (error) => {
58
- res.setHeader("Connection", "close");
59
- this.handleErr(error, req, res);
60
- });
61
- } else {
62
- middleware[index](
63
- req,
64
- res,
65
- // The next function
66
- () => {
67
- runHandler(req, res, middleware, cb, index + 1);
68
- },
69
- // Error handler for a route middleware
70
- (error) => {
71
- res.setHeader("Connection", "close");
72
- this.handleErr(error, req, res);
73
- }
74
- );
75
- }
76
- };
77
-
78
- // Run all the middleware functions (beforeEach functions) before we run the corresponding route
79
- const runMiddleware = (req, res, middleware, index) => {
80
- // Our exit point...
81
- if (index === middleware.length) {
82
- const routes = this.routes[req.method.toLowerCase()];
83
- if (routes && typeof routes[Symbol.iterator] === "function")
84
- for (const route of routes) {
85
- const match = urlWithoutParams.match(route.regex);
86
-
87
- if (match) {
88
- // Parse the URL variables from the matched route (like /users/:id)
89
- const vars = this.#extractVars(route.path, match);
90
- req.vars = vars;
91
-
92
- return runHandler(req, res, route.middleware, route.cb, 0);
93
- }
94
- }
95
-
96
- // If the requested route dose not exist, return 404
97
- return res
98
- .status(404)
99
- .json({ error: `Cannot ${req.method} ${urlWithoutParams}` });
100
- } else {
101
- middleware[index](req, res, () => {
102
- runMiddleware(req, res, middleware, index + 1);
103
- });
104
- }
105
- };
106
-
107
- runMiddleware(req, res, this.middleware, 0);
108
- });
109
- }
110
-
111
- route(method, path, ...args) {
112
- if (!this.routes[method]) this.routes[method] = [];
113
-
114
- // The last argument is always our handler
115
- const cb = args.pop();
116
-
117
- // Rest will be our middleware functions
118
- const middleware = args.flat();
119
-
120
- const regex = this.#pathToRegex(path);
121
- this.routes[method].push({ path, regex, middleware, cb });
122
- }
123
-
124
- beforeEach(cb) {
125
- this.middleware.push(cb);
126
- }
127
-
128
- handleErr(cb) {
129
- this.handleErr = cb;
130
- }
131
-
132
- listen(port, cb) {
133
- this.server.listen(port, cb);
134
- }
135
-
136
- close(cb) {
137
- this.server.close(cb);
138
- }
139
-
140
- // ------------------------------
141
- // PRIVATE METHODS:
142
- // ------------------------------
143
- #pathToRegex(path) {
144
- const varNames = [];
145
- const regexString =
146
- "^" +
147
- path.replace(/:\w+/g, (match, offset) => {
148
- varNames.push(match.slice(1));
149
- return "([^/]+)";
150
- }) +
151
- "$";
152
-
153
- const regex = new RegExp(regexString);
154
- return regex;
155
- }
156
-
157
- #extractVars(path, match) {
158
- // Extract url variable values from the matched route
159
- const varNames = (path.match(/:\w+/g) || []).map((varParam) =>
160
- varParam.slice(1)
161
- );
162
- const vars = {};
163
- varNames.forEach((name, index) => {
164
- vars[name] = match[index + 1];
165
- });
166
- return vars;
167
- }
168
- }
169
-
170
- export { serveStatic, parseJSON, render };
171
-
172
- export default Cpeak;
@@ -1,5 +0,0 @@
1
- import { parseJSON } from "./parseJSON.js";
2
- import { serveStatic } from "./serveStatic.js";
3
- import { render } from "./render.js";
4
-
5
- export { serveStatic, parseJSON, render };
package/playground.js DELETED
@@ -1,63 +0,0 @@
1
- function renderTemplate(templateStr, data) {
2
- // Initialize variables
3
- let result = [];
4
- let currentIndex = 0;
5
-
6
- while (currentIndex < templateStr.length) {
7
- // Find the next opening placeholder
8
- const startIdx = templateStr.indexOf("{{", currentIndex);
9
- if (startIdx === -1) {
10
- // No more placeholders, push the remaining string
11
- result.push(templateStr.slice(currentIndex));
12
- break;
13
- }
14
-
15
- // Push the part before the placeholder
16
- result.push(templateStr.slice(currentIndex, startIdx));
17
-
18
- // Find the closing placeholder
19
- const endIdx = templateStr.indexOf("}}", startIdx);
20
- if (endIdx === -1) {
21
- // No closing brace found, treat the rest as plain text
22
- result.push(templateStr.slice(startIdx));
23
- break;
24
- }
25
-
26
- // Extract the variable name
27
- const varName = templateStr.slice(startIdx + 2, endIdx).trim();
28
-
29
- // Replace the variable with its value from the data, or use an empty string if not found
30
- const replacement = data[varName] !== undefined ? data[varName] : "";
31
-
32
- // Push the replacement to the result array
33
- result.push(replacement);
34
-
35
- // Move the index past the current closing placeholder
36
- currentIndex = endIdx + 2;
37
- }
38
-
39
- // Join all parts into a final string
40
- return result.join("");
41
- }
42
-
43
- // Example usage:
44
- const templateStr = `
45
- <!DOCTYPE html>
46
- <html>
47
- <head>
48
- <title>Poster</title>
49
-
50
- <link rel="stylesheet" href="/styles.css" />
51
- </head>
52
- <body style="color: #333">
53
- <h1>{{ msg1 }} {{ msg2 }}</h1>
54
-
55
- <!-- <strong>{{msgBar}}</strong> -->
56
-
57
- <p>
58
- `;
59
-
60
- const data = { msg1: "Alice", msg2: 30 };
61
-
62
- const output = renderTemplate(templateStr, data);
63
- console.log(output); // Output: "Hello, Alice! You are 30 years old."