cpeak 2.5.0 → 2.7.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
@@ -2,9 +2,17 @@ import http from "node:http";
2
2
  import fs from "node:fs/promises";
3
3
  import { createReadStream } from "node:fs";
4
4
  import { pipeline } from "node:stream/promises";
5
+ import type { Readable } from "node:stream";
6
+ import type { Buffer } from "node:buffer";
7
+
8
+ import {
9
+ resolveCompressionOptions,
10
+ compressAndSend
11
+ } from "./internal/compression";
5
12
 
6
13
  import type {
7
14
  StringMap,
15
+ CpeakOptions,
8
16
  CpeakRequest,
9
17
  CpeakResponse,
10
18
  Middleware,
@@ -13,6 +21,8 @@ import type {
13
21
  RoutesMap
14
22
  } from "./types";
15
23
 
24
+ import type { ResolvedCompressionConfig } from "./internal/types";
25
+
16
26
  // A utility function to create an error with a custom stack trace
17
27
  export function frameworkError(
18
28
  message: string,
@@ -40,38 +50,43 @@ export enum ErrorCode {
40
50
  NOT_A_FILE = "CPEAK_ERR_NOT_A_FILE",
41
51
  SEND_FILE_FAIL = "CPEAK_ERR_SEND_FILE_FAIL",
42
52
  INVALID_JSON = "CPEAK_ERR_INVALID_JSON",
43
- PAYLOAD_TOO_LARGE = "CPEAK_ERR_PAYLOAD_TOO_LARGE"
53
+ PAYLOAD_TOO_LARGE = "CPEAK_ERR_PAYLOAD_TOO_LARGE",
54
+ WEAK_SECRET = "CPEAK_ERR_WEAK_SECRET",
55
+ COMPRESSION_NOT_ENABLED = "CPEAK_ERR_COMPRESSION_NOT_ENABLED"
44
56
  }
45
57
 
46
- class CpeakIncomingMessage extends http.IncomingMessage {
58
+ export class CpeakIncomingMessage extends http.IncomingMessage {
47
59
  // We define body and params here for better V8 optimization (not changing the shape of the object at runtime)
48
60
  public body: any = undefined;
49
61
  public params: StringMap = {};
50
62
 
51
- private _query?: StringMap;
63
+ #query?: StringMap;
52
64
 
53
65
  // Parse the URL parameters (like /users?key1=value1&key2=value2)
54
66
  // We will call this query to be more familiar with other node.js frameworks.
55
67
  // This is a getter method (accessed like a property)
56
68
  get query(): StringMap {
57
69
  // This way if a developer writes req.query multiple times, we don't parse it multiple times
58
- if (this._query) return this._query;
70
+ if (this.#query) return this.#query;
59
71
 
60
72
  const url = this.url || "";
61
73
  const qIndex = url.indexOf("?");
62
74
 
63
75
  if (qIndex === -1) {
64
- this._query = {};
76
+ this.#query = {};
65
77
  } else {
66
78
  const searchParams = new URLSearchParams(url.substring(qIndex + 1));
67
- this._query = Object.fromEntries(searchParams.entries());
79
+ this.#query = Object.fromEntries(searchParams.entries());
68
80
  }
69
81
 
70
- return this._query;
82
+ return this.#query;
71
83
  }
72
84
  }
73
85
 
74
- class CpeakServerResponse extends http.ServerResponse<CpeakIncomingMessage> {
86
+ export class CpeakServerResponse extends http.ServerResponse<CpeakIncomingMessage> {
87
+ // Set per-request from the Cpeak instance. Undefined when compression isn't enabled.
88
+ _compression?: ResolvedCompressionConfig;
89
+
75
90
  // Send a file back to the client
76
91
  async sendFile(path: string, mime: string) {
77
92
  if (!mime) {
@@ -92,6 +107,17 @@ class CpeakServerResponse extends http.ServerResponse<CpeakIncomingMessage> {
92
107
  );
93
108
  }
94
109
 
110
+ if (this._compression) {
111
+ await compressAndSend(
112
+ this,
113
+ mime,
114
+ createReadStream(path),
115
+ this._compression,
116
+ stat.size
117
+ );
118
+ return;
119
+ }
120
+
95
121
  this.setHeader("Content-Type", mime);
96
122
  this.setHeader("Content-Length", String(stat.size));
97
123
 
@@ -120,157 +146,188 @@ class CpeakServerResponse extends http.ServerResponse<CpeakIncomingMessage> {
120
146
  return this;
121
147
  }
122
148
 
149
+ // Set the Content-Disposition header to prompt the user to download a file
150
+ attachment(filename?: string) {
151
+ const contentDisposition = filename
152
+ ? `attachment; filename="${filename}"`
153
+ : "attachment";
154
+ this.setHeader("Content-Disposition", contentDisposition);
155
+ return this;
156
+ }
157
+
123
158
  // Redirects to a new URL
124
159
  redirect(location: string) {
125
160
  this.writeHead(302, { Location: location });
126
161
  this.end();
127
- return this;
128
162
  }
129
163
 
130
- // Send a json data back to the client (for small json data, less than the highWaterMark)
131
- json(data: any) {
132
- // This is only good for bodies that their size is less than the highWaterMark value
164
+ // Send a json data back to the client.
165
+ // This is only good for bodies that their size is less than the highWaterMark value.
166
+ // Branches into compressAndSend (async) when compression was enabled at cpeak() construction.
167
+ json(data: any): void | Promise<void> {
168
+ const body = JSON.stringify(data);
169
+ if (this._compression) {
170
+ return compressAndSend(this, "application/json", body, this._compression);
171
+ }
133
172
  this.setHeader("Content-Type", "application/json");
134
- this.end(JSON.stringify(data));
173
+ this.end(body);
174
+ }
175
+
176
+ // Explicit compression entry point. A developer can use this in any custom handler to compress arbitrary responses
177
+ compress(
178
+ mime: string,
179
+ body: Buffer | string | Readable,
180
+ size?: number
181
+ ): Promise<void> {
182
+ if (!this._compression) {
183
+ throw frameworkError(
184
+ "compression is not enabled. Pass `compression` to cpeak({ compression: true | { ... } }) to use res.compress.",
185
+ this.compress,
186
+ ErrorCode.COMPRESSION_NOT_ENABLED
187
+ );
188
+ }
189
+ return compressAndSend(this, mime, body, this._compression, size);
135
190
  }
136
191
  }
137
192
 
138
- class Cpeak {
139
- private server: http.Server<
140
- typeof CpeakIncomingMessage,
141
- typeof CpeakServerResponse
142
- >;
143
- private routes: RoutesMap;
144
- private middleware: Middleware[];
145
- private _handleErr?: (
146
- err: unknown,
147
- req: CpeakRequest,
148
- res: CpeakResponse
149
- ) => void;
150
-
151
- constructor() {
152
- this.server = http.createServer({
193
+ export class Cpeak {
194
+ #server: http.Server<typeof CpeakIncomingMessage, typeof CpeakServerResponse>;
195
+ #routes: RoutesMap;
196
+ #middleware: Middleware[];
197
+ #handleErr?: (err: unknown, req: CpeakRequest, res: CpeakResponse) => void;
198
+ #compression?: ResolvedCompressionConfig;
199
+
200
+ constructor(options: CpeakOptions = {}) {
201
+ this.#server = http.createServer({
153
202
  IncomingMessage: CpeakIncomingMessage,
154
203
  ServerResponse: CpeakServerResponse
155
204
  });
156
- this.routes = {};
157
- this.middleware = [];
158
-
159
- this.server.on("request", async (req: CpeakRequest, res: CpeakResponse) => {
160
- // Get the url without the URL parameters (query strings)
161
- const qIndex = req.url?.indexOf("?");
162
- const urlWithoutQueries =
163
- qIndex === -1 ? req.url || "" : req.url?.substring(0, qIndex);
164
-
165
- // Run all the specific middleware functions for that router only and then run the handler
166
- const runHandler = async (
167
- req: CpeakRequest,
168
- res: CpeakResponse,
169
- middleware: RouteMiddleware[],
170
- cb: Handler,
171
- index: number
172
- ) => {
173
- // Our exit point...
174
- if (index === middleware.length) {
175
- // Call the route handler with the modified req and res objects.
176
- // Also handle the promise errors by passing them to the handleErr to save developers from having to manually wrap every handler in try catch.
177
- try {
178
- await cb(req, res, (error) => {
179
- res.setHeader("Connection", "close");
180
- this._handleErr?.(error, req, res);
181
- });
182
- } catch (error) {
183
- res.setHeader("Connection", "close");
184
- this._handleErr?.(error, req, res);
205
+ this.#routes = {};
206
+ this.#middleware = [];
207
+
208
+ // Resolve compression options once at app startup.
209
+ if (options.compression) {
210
+ this.#compression = resolveCompressionOptions(options.compression);
211
+ }
212
+
213
+ this.#server.on(
214
+ "request",
215
+ async (req: CpeakRequest, res: CpeakResponse) => {
216
+ res._compression = this.#compression;
217
+
218
+ // Get the url without the URL parameters (query strings)
219
+ const qIndex = req.url?.indexOf("?");
220
+ const urlWithoutQueries =
221
+ qIndex === -1 ? req.url || "" : req.url?.substring(0, qIndex);
222
+
223
+ const dispatchError = (error: unknown) => {
224
+ if (res.headersSent) {
225
+ req.socket?.destroy();
226
+ return;
185
227
  }
186
- } else {
187
- // Handle the promise errors by passing them to the handleErr to save developers from having to manually wrap every handler middleware in try catch.
188
- try {
189
- await middleware[index](
190
- req,
191
- res,
192
- // The next function
193
- async (error) => {
194
- // this function only accepts an error argument to be more compatible with NPM modules that are built for express
195
- if (error) {
196
- res.setHeader("Connection", "close");
197
- return this._handleErr?.(error, req, res);
198
- }
199
- await runHandler(req, res, middleware, cb, index + 1);
200
- },
201
- // Error handler for a route middleware
202
- (error) => {
203
- res.setHeader("Connection", "close");
204
- this._handleErr?.(error, req, res);
205
- }
206
- );
207
- } catch (error) {
208
- res.setHeader("Connection", "close");
209
- this._handleErr?.(error, req, res);
228
+ res.setHeader("Connection", "close");
229
+ this.#handleErr?.(error, req, res);
230
+ };
231
+
232
+ // Run all the specific middleware functions for that router only and then run the handler
233
+ const runHandler = async (
234
+ req: CpeakRequest,
235
+ res: CpeakResponse,
236
+ middleware: RouteMiddleware[],
237
+ cb: Handler,
238
+ index: number
239
+ ) => {
240
+ // Our exit point...
241
+ if (index === middleware.length) {
242
+ // Call the route handler with the modified req and res objects.
243
+ // Also handle the promise errors by passing them to the handleErr to save developers from having to manually wrap every handler in try catch.
244
+ try {
245
+ await cb(req, res, dispatchError);
246
+ } catch (error) {
247
+ dispatchError(error);
248
+ }
249
+ } else {
250
+ // Handle the promise errors by passing them to the handleErr to save developers from having to manually wrap every handler middleware in try catch.
251
+ try {
252
+ await middleware[index](
253
+ req,
254
+ res,
255
+ // The next function
256
+ async (error) => {
257
+ // this function only accepts an error argument to be more compatible with NPM modules that are built for express
258
+ if (error) {
259
+ return dispatchError(error);
260
+ }
261
+ await runHandler(req, res, middleware, cb, index + 1);
262
+ },
263
+ // Error handler for a route middleware
264
+ dispatchError
265
+ );
266
+ } catch (error) {
267
+ dispatchError(error);
268
+ }
210
269
  }
211
- }
212
- };
213
-
214
- // Run all the middleware functions (beforeEach functions) before we run the corresponding route
215
- const runMiddleware = async (
216
- req: CpeakRequest,
217
- res: CpeakResponse,
218
- middleware: Middleware[],
219
- index: number
220
- ) => {
221
- // Our exit point...
222
- if (index === middleware.length) {
223
- const routes = this.routes[req.method?.toLowerCase() || ""];
224
- if (routes && typeof routes[Symbol.iterator] === "function")
225
- for (const route of routes) {
226
- const match = urlWithoutQueries?.match(route.regex);
227
-
228
- if (match) {
229
- // Parse the URL path variables from the matched route (like /users/:id)
230
- const pathVariables = this.#extractPathVariables(
231
- route.path,
232
- match
233
- );
234
-
235
- // We will call this params to be more familiar with other node.js frameworks.
236
- req.params = pathVariables;
237
-
238
- return await runHandler(
239
- req,
240
- res,
241
- route.middleware,
242
- route.cb,
243
- 0
244
- );
270
+ };
271
+
272
+ // Run all the middleware functions (beforeEach functions) before we run the corresponding route
273
+ const runMiddleware = async (
274
+ req: CpeakRequest,
275
+ res: CpeakResponse,
276
+ middleware: Middleware[],
277
+ index: number
278
+ ) => {
279
+ // Our exit point...
280
+ if (index === middleware.length) {
281
+ const routes = this.#routes[req.method?.toLowerCase() || ""];
282
+ if (routes && typeof routes[Symbol.iterator] === "function")
283
+ for (const route of routes) {
284
+ const match = urlWithoutQueries?.match(route.regex);
285
+
286
+ if (match) {
287
+ // Parse the URL path variables from the matched route (like /users/:id)
288
+ const pathVariables = this.#extractPathVariables(
289
+ route.path,
290
+ match
291
+ );
292
+
293
+ // We will call this params to be more familiar with other node.js frameworks.
294
+ req.params = pathVariables;
295
+
296
+ return await runHandler(
297
+ req,
298
+ res,
299
+ route.middleware,
300
+ route.cb,
301
+ 0
302
+ );
303
+ }
245
304
  }
246
- }
247
305
 
248
- // If the requested route dose not exist, return 404
249
- return res
250
- .status(404)
251
- .json({ error: `Cannot ${req.method} ${urlWithoutQueries}` });
252
- } else {
253
- try {
254
- await middleware[index](req, res, async (err?: unknown) => {
255
- if (err) {
256
- res.setHeader("Connection", "close");
257
- return this._handleErr?.(err, req, res);
258
- }
259
- await runMiddleware(req, res, middleware, index + 1);
260
- });
261
- } catch (error) {
262
- res.setHeader("Connection", "close");
263
- this._handleErr?.(error, req, res);
306
+ // If the requested route dose not exist, return 404
307
+ return res
308
+ .status(404)
309
+ .json({ error: `Cannot ${req.method} ${urlWithoutQueries}` });
310
+ } else {
311
+ try {
312
+ await middleware[index](req, res, async (err?: unknown) => {
313
+ if (err) {
314
+ return dispatchError(err);
315
+ }
316
+ await runMiddleware(req, res, middleware, index + 1);
317
+ });
318
+ } catch (error) {
319
+ dispatchError(error);
320
+ }
264
321
  }
265
- }
266
- };
322
+ };
267
323
 
268
- await runMiddleware(req, res, this.middleware, 0);
269
- });
324
+ await runMiddleware(req, res, this.#middleware, 0);
325
+ }
326
+ );
270
327
  }
271
328
 
272
329
  route(method: string, path: string, ...args: (RouteMiddleware | Handler)[]) {
273
- if (!this.routes[method]) this.routes[method] = [];
330
+ if (!this.#routes[method]) this.#routes[method] = [];
274
331
 
275
332
  // The last argument should always be our handler
276
333
  const cb = args.pop() as Handler;
@@ -283,40 +340,37 @@ class Cpeak {
283
340
  const middleware = args.flat() as RouteMiddleware[];
284
341
 
285
342
  const regex = this.#pathToRegex(path);
286
- this.routes[method].push({ path, regex, middleware, cb });
343
+ this.#routes[method].push({ path, regex, middleware, cb });
287
344
  }
288
345
 
289
346
  beforeEach(cb: Middleware) {
290
- this.middleware.push(cb);
347
+ this.#middleware.push(cb);
291
348
  }
292
349
 
293
350
  handleErr(cb: (err: unknown, req: CpeakRequest, res: CpeakResponse) => void) {
294
- this._handleErr = cb;
351
+ this.#handleErr = cb;
295
352
  }
296
353
 
297
354
  listen(port: number, cb?: () => void) {
298
- return this.server.listen(port, cb);
355
+ return this.#server.listen(port, cb);
356
+ }
357
+
358
+ address() {
359
+ return this.#server.address();
299
360
  }
300
361
 
301
362
  close(cb?: (err?: Error) => void) {
302
- this.server.close(cb);
363
+ this.#server.close(cb);
303
364
  }
304
365
 
305
366
  // ------------------------------
306
367
  // PRIVATE METHODS:
307
368
  // ------------------------------
308
369
  #pathToRegex(path: string) {
309
- const paramNames: string[] = [];
310
370
  const regexString =
311
- "^" +
312
- path.replace(/:\w+/g, (match, offset) => {
313
- paramNames.push(match.slice(1));
314
- return "([^/]+)";
315
- }) +
316
- "$";
317
-
318
- const regex = new RegExp(regexString);
319
- return regex;
371
+ "^" + path.replace(/:\w+/g, "([^/]+)").replace(/\*/g, ".*") + "$";
372
+
373
+ return new RegExp(regexString);
320
374
  }
321
375
 
322
376
  #extractPathVariables(path: string, match: RegExpMatchArray) {
@@ -333,12 +387,29 @@ class Cpeak {
333
387
  }
334
388
 
335
389
  // Util functions
336
- export { serveStatic } from "./utils/serveStatic.js";
337
- export { parseJSON } from "./utils/paseJSON.js";
338
- export { render } from "./utils/render.js";
390
+ export {
391
+ serveStatic,
392
+ parseJSON,
393
+ render,
394
+ swagger,
395
+ auth,
396
+ hashPassword,
397
+ verifyPassword,
398
+ cookieParser,
399
+ cors
400
+ } from "./utils";
401
+
402
+ export type {
403
+ AuthOptions,
404
+ PbkdfOptions,
405
+ CookieOptions,
406
+ CorsOptions
407
+ } from "./utils/types";
408
+
409
+ export type { CompressionOptions } from "./internal/types";
339
410
 
340
411
  export type {
341
- Cpeak,
412
+ CpeakOptions,
342
413
  CpeakRequest,
343
414
  CpeakResponse,
344
415
  Next,
@@ -349,6 +420,6 @@ export type {
349
420
  RoutesMap
350
421
  } from "./types";
351
422
 
352
- export default function cpeak() {
353
- return new Cpeak();
423
+ export default function cpeak(options?: CpeakOptions): Cpeak {
424
+ return new Cpeak(options);
354
425
  }