cpeak 2.4.3 → 2.6.0

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/lib/index.ts CHANGED
@@ -10,19 +10,26 @@ import type {
10
10
  Middleware,
11
11
  RouteMiddleware,
12
12
  Handler,
13
- RoutesMap,
13
+ RoutesMap
14
14
  } from "./types";
15
15
 
16
16
  // A utility function to create an error with a custom stack trace
17
17
  export function frameworkError(
18
18
  message: string,
19
19
  skipFn: Function,
20
- code?: string
20
+ code?: string,
21
+ status?: number
21
22
  ) {
22
- const err = new Error(message) as Error & { code?: string };
23
+ const err = new Error(message) as Error & {
24
+ code?: string;
25
+ cpeak_err?: boolean;
26
+ };
23
27
  Error.captureStackTrace(err, skipFn);
24
28
 
29
+ err.cpeak_err = true;
30
+
25
31
  if (code) err.code = code;
32
+ if (status) (err as any).status = status;
26
33
 
27
34
  return err;
28
35
  }
@@ -32,207 +39,241 @@ export enum ErrorCode {
32
39
  FILE_NOT_FOUND = "CPEAK_ERR_FILE_NOT_FOUND",
33
40
  NOT_A_FILE = "CPEAK_ERR_NOT_A_FILE",
34
41
  SEND_FILE_FAIL = "CPEAK_ERR_SEND_FILE_FAIL",
42
+ INVALID_JSON = "CPEAK_ERR_INVALID_JSON",
43
+ PAYLOAD_TOO_LARGE = "CPEAK_ERR_PAYLOAD_TOO_LARGE",
44
+ WEAK_SECRET = "CPEAK_ERR_WEAK_SECRET"
35
45
  }
36
46
 
37
- class Cpeak {
38
- private server: http.Server;
39
- private routes: RoutesMap;
40
- private middleware: Middleware[];
41
- private _handleErr?: (
42
- err: unknown,
43
- req: CpeakRequest,
44
- res: CpeakResponse
45
- ) => void;
47
+ export class CpeakIncomingMessage extends http.IncomingMessage {
48
+ // We define body and params here for better V8 optimization (not changing the shape of the object at runtime)
49
+ public body: any = undefined;
50
+ public params: StringMap = {};
46
51
 
47
- constructor() {
48
- this.server = http.createServer();
49
- this.routes = {};
50
- this.middleware = [];
51
-
52
- this.server.on("request", (req: CpeakRequest, res: CpeakResponse) => {
53
- // Send a file back to the client
54
- res.sendFile = async (path: string, mime: string) => {
55
- if (!mime) {
56
- throw frameworkError(
57
- 'MIME type is missing. Use res.sendFile(path, "mime-type").',
58
- res.sendFile,
59
- ErrorCode.MISSING_MIME
60
- );
61
- }
62
-
63
- try {
64
- const stat = await fs.stat(path);
65
- if (!stat.isFile()) {
66
- throw frameworkError(
67
- `Not a file: ${path}`,
68
- res.sendFile,
69
- ErrorCode.NOT_A_FILE
70
- );
71
- }
52
+ #query?: StringMap;
72
53
 
73
- res.setHeader("Content-Type", mime);
74
- res.setHeader("Content-Length", String(stat.size));
75
-
76
- // Properly propagate stream errors and respect backpressure
77
- await pipeline(createReadStream(path), res);
78
- } catch (err: any) {
79
- if (err?.code === "ENOENT") {
80
- throw frameworkError(
81
- `File not found: ${path}`,
82
- res.sendFile,
83
- ErrorCode.FILE_NOT_FOUND
84
- );
85
- }
54
+ // Parse the URL parameters (like /users?key1=value1&key2=value2)
55
+ // We will call this query to be more familiar with other node.js frameworks.
56
+ // This is a getter method (accessed like a property)
57
+ get query(): StringMap {
58
+ // This way if a developer writes req.query multiple times, we don't parse it multiple times
59
+ if (this.#query) return this.#query;
86
60
 
87
- throw frameworkError(
88
- `Failed to send file: ${path}`,
89
- res.sendFile,
90
- ErrorCode.SEND_FILE_FAIL
91
- );
92
- }
93
- };
94
-
95
- // Set the status code of the response
96
- res.status = (code: number) => {
97
- res.statusCode = code;
98
- return res;
99
- };
100
-
101
- // Redirects to a new URL
102
- res.redirect = (location: string) => {
103
- res.writeHead(302, { Location: location });
104
- res.end();
105
- return res;
106
- };
107
-
108
- // Send a json data back to the client (for small json data, less than the highWaterMark)
109
- res.json = (data: any) => {
110
- // This is only good for bodies that their size is less than the highWaterMark value
111
- res.setHeader("Content-Type", "application/json");
112
- res.end(JSON.stringify(data));
113
- };
114
-
115
- // Get the url without the URL parameters
116
- const urlWithoutParams = req.url?.split("?")[0];
117
-
118
- // Parse the URL parameters (like /users?key1=value1&key2=value2)
119
- // We put this here to also parse them for all the middleware functions
120
- const params = new URLSearchParams(req.url?.split("?")[1]);
121
-
122
- const paramsObject = Object.fromEntries(params.entries());
123
-
124
- req.params = paramsObject;
125
- req.query = paramsObject; // only for compatibility with frameworks built for express
126
-
127
- // Run all the specific middleware functions for that router only and then run the handler
128
- const runHandler = (
129
- req: CpeakRequest,
130
- res: CpeakResponse,
131
- middleware: RouteMiddleware[],
132
- cb: Handler,
133
- index: number
134
- ) => {
135
- // Our exit point...
136
- if (index === middleware.length) {
137
- // Call the route handler with the modified req and res objects.
138
- // Also handle the promise errors by passing them to the handleErr to save developers from having to manually wrap every handler in try catch.
139
- try {
140
- const handlerResult = cb(req, res, (error) => {
141
- res.setHeader("Connection", "close");
142
- this._handleErr?.(error, req, res);
143
- });
144
-
145
- if (handlerResult && typeof handlerResult.then === "function") {
146
- handlerResult.catch((error) => {
147
- res.setHeader("Connection", "close");
148
- this._handleErr?.(error, req, res);
149
- });
150
- }
61
+ const url = this.url || "";
62
+ const qIndex = url.indexOf("?");
63
+
64
+ if (qIndex === -1) {
65
+ this.#query = {};
66
+ } else {
67
+ const searchParams = new URLSearchParams(url.substring(qIndex + 1));
68
+ this.#query = Object.fromEntries(searchParams.entries());
69
+ }
70
+
71
+ return this.#query;
72
+ }
73
+ }
74
+
75
+ export class CpeakServerResponse extends http.ServerResponse<CpeakIncomingMessage> {
76
+ // Send a file back to the client
77
+ async sendFile(path: string, mime: string) {
78
+ if (!mime) {
79
+ throw frameworkError(
80
+ 'MIME type is missing. Use res.sendFile(path, "mime-type").',
81
+ this.sendFile,
82
+ ErrorCode.MISSING_MIME
83
+ );
84
+ }
85
+
86
+ try {
87
+ const stat = await fs.stat(path);
88
+ if (!stat.isFile()) {
89
+ throw frameworkError(
90
+ `Not a file: ${path}`,
91
+ this.sendFile,
92
+ ErrorCode.NOT_A_FILE
93
+ );
94
+ }
95
+
96
+ this.setHeader("Content-Type", mime);
97
+ this.setHeader("Content-Length", String(stat.size));
98
+
99
+ // Properly propagate stream errors and respect backpressure
100
+ await pipeline(createReadStream(path), this);
101
+ } catch (err: any) {
102
+ if (err?.code === "ENOENT") {
103
+ throw frameworkError(
104
+ `File not found: ${path}`,
105
+ this.sendFile,
106
+ ErrorCode.FILE_NOT_FOUND
107
+ );
108
+ }
109
+
110
+ throw frameworkError(
111
+ `Failed to send file: ${path}`,
112
+ this.sendFile,
113
+ ErrorCode.SEND_FILE_FAIL
114
+ );
115
+ }
116
+ }
117
+
118
+ // Set the status code of the response
119
+ status(code: number) {
120
+ this.statusCode = code;
121
+ return this;
122
+ }
123
+
124
+ // Set the Content-Disposition header to prompt the user to download a file
125
+ attachment(filename?: string) {
126
+ const contentDisposition = filename
127
+ ? `attachment; filename="${filename}"`
128
+ : "attachment";
129
+ this.setHeader("Content-Disposition", contentDisposition);
130
+ return this;
131
+ }
151
132
 
152
- return handlerResult;
153
- } catch (error) {
154
- res.setHeader("Connection", "close");
155
- this._handleErr?.(error, req, res);
133
+ // Redirects to a new URL
134
+ redirect(location: string) {
135
+ this.writeHead(302, { Location: location });
136
+ this.end();
137
+ }
138
+
139
+ // Send a json data back to the client (for small json data, less than the highWaterMark)
140
+ json(data: any) {
141
+ // This is only good for bodies that their size is less than the highWaterMark value
142
+ this.setHeader("Content-Type", "application/json");
143
+ this.end(JSON.stringify(data));
144
+ }
145
+ }
146
+
147
+ export class Cpeak {
148
+ #server: http.Server<typeof CpeakIncomingMessage, typeof CpeakServerResponse>;
149
+ #routes: RoutesMap;
150
+ #middleware: Middleware[];
151
+ #handleErr?: (err: unknown, req: CpeakRequest, res: CpeakResponse) => void;
152
+
153
+ constructor() {
154
+ this.#server = http.createServer({
155
+ IncomingMessage: CpeakIncomingMessage,
156
+ ServerResponse: CpeakServerResponse
157
+ });
158
+ this.#routes = {};
159
+ this.#middleware = [];
160
+
161
+ this.#server.on(
162
+ "request",
163
+ async (req: CpeakRequest, res: CpeakResponse) => {
164
+ // Get the url without the URL parameters (query strings)
165
+ const qIndex = req.url?.indexOf("?");
166
+ const urlWithoutQueries =
167
+ qIndex === -1 ? req.url || "" : req.url?.substring(0, qIndex);
168
+
169
+ const dispatchError = (error: unknown) => {
170
+ if (res.headersSent) {
171
+ req.socket?.destroy();
172
+ return;
173
+ }
174
+ res.setHeader("Connection", "close");
175
+ this.#handleErr?.(error, req, res);
176
+ };
177
+
178
+ // Run all the specific middleware functions for that router only and then run the handler
179
+ const runHandler = async (
180
+ req: CpeakRequest,
181
+ res: CpeakResponse,
182
+ middleware: RouteMiddleware[],
183
+ cb: Handler,
184
+ index: number
185
+ ) => {
186
+ // Our exit point...
187
+ if (index === middleware.length) {
188
+ // Call the route handler with the modified req and res objects.
189
+ // Also handle the promise errors by passing them to the handleErr to save developers from having to manually wrap every handler in try catch.
190
+ try {
191
+ await cb(req, res, dispatchError);
192
+ } catch (error) {
193
+ dispatchError(error);
194
+ }
195
+ } else {
196
+ // Handle the promise errors by passing them to the handleErr to save developers from having to manually wrap every handler middleware in try catch.
197
+ try {
198
+ await middleware[index](
199
+ req,
200
+ res,
201
+ // The next function
202
+ async (error) => {
203
+ // this function only accepts an error argument to be more compatible with NPM modules that are built for express
204
+ if (error) {
205
+ return dispatchError(error);
206
+ }
207
+ await runHandler(req, res, middleware, cb, index + 1);
208
+ },
209
+ // Error handler for a route middleware
210
+ dispatchError
211
+ );
212
+ } catch (error) {
213
+ dispatchError(error);
214
+ }
156
215
  }
157
- } else {
158
- // Handle the promise errors by passing them to the handleErr to save developers from having to manually wrap every handler middleware in try catch.
159
- try {
160
- const middlewareResult = 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);
216
+ };
217
+
218
+ // Run all the middleware functions (beforeEach functions) before we run the corresponding route
219
+ const runMiddleware = async (
220
+ req: CpeakRequest,
221
+ res: CpeakResponse,
222
+ middleware: Middleware[],
223
+ index: number
224
+ ) => {
225
+ // Our exit point...
226
+ if (index === middleware.length) {
227
+ const routes = this.#routes[req.method?.toLowerCase() || ""];
228
+ if (routes && typeof routes[Symbol.iterator] === "function")
229
+ for (const route of routes) {
230
+ const match = urlWithoutQueries?.match(route.regex);
231
+
232
+ if (match) {
233
+ // Parse the URL path variables from the matched route (like /users/:id)
234
+ const pathVariables = this.#extractPathVariables(
235
+ route.path,
236
+ match
237
+ );
238
+
239
+ // We will call this params to be more familiar with other node.js frameworks.
240
+ req.params = pathVariables;
241
+
242
+ return await runHandler(
243
+ req,
244
+ res,
245
+ route.middleware,
246
+ route.cb,
247
+ 0
248
+ );
169
249
  }
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
250
  }
177
- );
178
-
179
- // If the middleware is async, handle the promise rejection
180
- if (
181
- middlewareResult &&
182
- typeof middlewareResult.then === "function"
183
- ) {
184
- middlewareResult.catch((error) => {
185
- res.setHeader("Connection", "close");
186
- this._handleErr?.(error, req, res);
251
+
252
+ // If the requested route dose not exist, return 404
253
+ return res
254
+ .status(404)
255
+ .json({ error: `Cannot ${req.method} ${urlWithoutQueries}` });
256
+ } else {
257
+ try {
258
+ await middleware[index](req, res, async (err?: unknown) => {
259
+ if (err) {
260
+ return dispatchError(err);
261
+ }
262
+ await runMiddleware(req, res, middleware, index + 1);
187
263
  });
264
+ } catch (error) {
265
+ dispatchError(error);
188
266
  }
189
- } catch (error) {
190
- res.setHeader("Connection", "close");
191
- this._handleErr?.(error, req, res);
192
267
  }
193
- }
194
- };
195
-
196
- // Run all the middleware functions (beforeEach functions) before we run the corresponding route
197
- const runMiddleware = (
198
- req: CpeakRequest,
199
- res: CpeakResponse,
200
- middleware: Middleware[],
201
- index: number
202
- ) => {
203
- // Our exit point...
204
- if (index === middleware.length) {
205
- const routes = this.routes[req.method?.toLowerCase() || ""];
206
- if (routes && typeof routes[Symbol.iterator] === "function")
207
- for (const route of routes) {
208
- const match = urlWithoutParams?.match(route.regex);
209
-
210
- if (match) {
211
- // Parse the URL variables from the matched route (like /users/:id)
212
- const vars = this.#extractVars(route.path, match);
213
- req.vars = vars;
214
-
215
- return runHandler(req, res, route.middleware, route.cb, 0);
216
- }
217
- }
268
+ };
218
269
 
219
- // If the requested route dose not exist, return 404
220
- return res
221
- .status(404)
222
- .json({ error: `Cannot ${req.method} ${urlWithoutParams}` });
223
- } else {
224
- middleware[index](req, res, () => {
225
- runMiddleware(req, res, middleware, index + 1);
226
- });
227
- }
228
- };
229
-
230
- runMiddleware(req, res, this.middleware, 0);
231
- });
270
+ await runMiddleware(req, res, this.#middleware, 0);
271
+ }
272
+ );
232
273
  }
233
274
 
234
275
  route(method: string, path: string, ...args: (RouteMiddleware | Handler)[]) {
235
- if (!this.routes[method]) this.routes[method] = [];
276
+ if (!this.#routes[method]) this.#routes[method] = [];
236
277
 
237
278
  // The last argument should always be our handler
238
279
  const cb = args.pop() as Handler;
@@ -245,62 +286,66 @@ class Cpeak {
245
286
  const middleware = args.flat() as RouteMiddleware[];
246
287
 
247
288
  const regex = this.#pathToRegex(path);
248
- this.routes[method].push({ path, regex, middleware, cb });
289
+ this.#routes[method].push({ path, regex, middleware, cb });
249
290
  }
250
291
 
251
292
  beforeEach(cb: Middleware) {
252
- this.middleware.push(cb);
293
+ this.#middleware.push(cb);
253
294
  }
254
295
 
255
296
  handleErr(cb: (err: unknown, req: CpeakRequest, res: CpeakResponse) => void) {
256
- this._handleErr = cb;
297
+ this.#handleErr = cb;
257
298
  }
258
299
 
259
300
  listen(port: number, cb?: () => void) {
260
- return this.server.listen(port, cb);
301
+ return this.#server.listen(port, cb);
302
+ }
303
+
304
+ address() {
305
+ return this.#server.address();
261
306
  }
262
307
 
263
308
  close(cb?: (err?: Error) => void) {
264
- this.server.close(cb);
309
+ this.#server.close(cb);
265
310
  }
266
311
 
267
312
  // ------------------------------
268
313
  // PRIVATE METHODS:
269
314
  // ------------------------------
270
315
  #pathToRegex(path: string) {
271
- const varNames: string[] = [];
272
316
  const regexString =
273
- "^" +
274
- path.replace(/:\w+/g, (match, offset) => {
275
- varNames.push(match.slice(1));
276
- return "([^/]+)";
277
- }) +
278
- "$";
279
-
280
- const regex = new RegExp(regexString);
281
- return regex;
317
+ "^" + path.replace(/:\w+/g, "([^/]+)").replace(/\*/g, ".*") + "$";
318
+
319
+ return new RegExp(regexString);
282
320
  }
283
321
 
284
- #extractVars(path: string, match: RegExpMatchArray) {
285
- // Extract url variable values from the matched route
286
- const varNames = (path.match(/:\w+/g) || []).map((varParam) =>
287
- varParam.slice(1)
322
+ #extractPathVariables(path: string, match: RegExpMatchArray) {
323
+ // Extract path url variable values from the matched route
324
+ const paramNames = (path.match(/:\w+/g) || []).map((param) =>
325
+ param.slice(1)
288
326
  );
289
- const vars: StringMap = {};
290
- varNames.forEach((name, index) => {
291
- vars[name] = match[index + 1];
327
+ const params: StringMap = {};
328
+ paramNames.forEach((name, index) => {
329
+ params[name] = match[index + 1];
292
330
  });
293
- return vars;
331
+ return params;
294
332
  }
295
333
  }
296
334
 
297
335
  // Util functions
298
- export { serveStatic } from "./utils/serveStatic.js";
299
- export { parseJSON } from "./utils/parseJSON.js";
300
- export { render } from "./utils/render.js";
336
+ export {
337
+ serveStatic,
338
+ parseJSON,
339
+ render,
340
+ swagger,
341
+ auth,
342
+ hashPassword,
343
+ verifyPassword,
344
+ cookieParser
345
+ } from "./utils";
346
+ export type { AuthOptions, PbkdfOptions, CookieOptions } from "./utils";
301
347
 
302
348
  export type {
303
- Cpeak,
304
349
  CpeakRequest,
305
350
  CpeakResponse,
306
351
  Next,
@@ -308,9 +353,9 @@ export type {
308
353
  Middleware,
309
354
  RouteMiddleware,
310
355
  Handler,
311
- RoutesMap,
356
+ RoutesMap
312
357
  } from "./types";
313
358
 
314
- export default function cpeak() {
359
+ export default function cpeak(): Cpeak {
315
360
  return new Cpeak();
316
361
  }
package/lib/types.ts CHANGED
@@ -1,26 +1,28 @@
1
1
  import { IncomingMessage, ServerResponse } from "node:http";
2
- import cpeak from "./index";
3
2
 
4
- export type Cpeak = ReturnType<typeof cpeak>;
3
+ export type { Cpeak } from "./index";
5
4
 
6
5
  // Extending Node.js's Request and Response objects to add our custom properties
7
6
  export type StringMap = Record<string, string>;
8
7
 
9
- export interface CpeakRequest<ReqBody = any, ReqParams = any>
10
- extends IncomingMessage {
11
- params: ReqParams;
12
- vars?: StringMap;
8
+ export interface CpeakRequest<
9
+ ReqBody = any,
10
+ ReqQueries = any
11
+ > extends IncomingMessage {
12
+ params: StringMap;
13
+ query: ReqQueries;
13
14
  body?: ReqBody;
15
+ cookies?: StringMap;
16
+ signedCookies?: Record<string, string | false>;
14
17
  [key: string]: any; // allow developers to add their onw extensions (e.g. req.test)
15
-
16
- // For express frameworks compatibility:
17
- query: ReqParams;
18
18
  }
19
19
 
20
20
  export interface CpeakResponse extends ServerResponse {
21
21
  sendFile: (path: string, mime: string) => Promise<void>;
22
22
  status: (code: number) => CpeakResponse;
23
- redirect: (location: string) => CpeakResponse;
23
+ attachment: (filename?: string) => CpeakResponse;
24
+ cookie: (name: string, value: string, options?: any) => CpeakResponse;
25
+ redirect: (location: string) => void;
24
26
  json: (data: any) => void;
25
27
  [key: string]: any; // allow developers to add their onw extensions (e.g. res.test)
26
28
  }