cpeak 2.6.0 → 2.8.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/dist/index.js CHANGED
@@ -2,10 +2,318 @@
2
2
  import http from "http";
3
3
  import fs3 from "fs/promises";
4
4
  import { createReadStream } from "fs";
5
+ import { pipeline as pipeline2 } from "stream/promises";
6
+
7
+ // lib/internal/compression.ts
8
+ import zlib from "zlib";
9
+ import { Readable } from "stream";
10
+ import { Buffer as Buffer2 } from "buffer";
5
11
  import { pipeline } from "stream/promises";
12
+ var COMPRESSIBLE_TYPE = /text|json|javascript|css|xml|svg/i;
13
+ var NO_TRANSFORM = /(?:^|,)\s*no-transform\s*(?:,|$)/i;
14
+ function pickEncoding(header) {
15
+ if (!header) return null;
16
+ const accepted = {};
17
+ let wildcard;
18
+ for (const part of header.split(",")) {
19
+ const [rawName, ...params] = part.trim().split(";");
20
+ const name = rawName.trim().toLowerCase();
21
+ if (!name) continue;
22
+ let q = 1;
23
+ for (const p of params) {
24
+ const m = p.trim().match(/^q=([\d.]+)$/i);
25
+ if (m) q = Number(m[1]);
26
+ }
27
+ if (Number.isNaN(q)) q = 0;
28
+ if (name === "*") wildcard = q;
29
+ else accepted[name] = q;
30
+ }
31
+ const tryPick = (enc) => {
32
+ const q = enc in accepted ? accepted[enc] : wildcard;
33
+ return q !== void 0 && q > 0;
34
+ };
35
+ if (tryPick("br")) return "br";
36
+ if (tryPick("gzip")) return "gzip";
37
+ if (tryPick("deflate")) return "deflate";
38
+ return null;
39
+ }
40
+ function appendVary(res, value) {
41
+ const existing = res.getHeader("Vary");
42
+ if (!existing) return res.setHeader("Vary", value);
43
+ const current = String(existing).split(",").map((s) => s.trim()).filter(Boolean);
44
+ if (current.includes("*") || current.some((v) => v.toLowerCase() === value.toLowerCase()))
45
+ return;
46
+ res.setHeader("Vary", [...current, value].join(", "));
47
+ }
48
+ function brotliOptsFor(config) {
49
+ const userBrotli = config.brotli || {};
50
+ return {
51
+ ...userBrotli,
52
+ params: {
53
+ [zlib.constants.BROTLI_PARAM_QUALITY]: 4,
54
+ ...userBrotli.params || {}
55
+ }
56
+ };
57
+ }
58
+ function createCompressorStream(encoding, config) {
59
+ if (encoding === "br")
60
+ return zlib.createBrotliCompress(brotliOptsFor(config));
61
+ if (encoding === "gzip") return zlib.createGzip(config.gzip);
62
+ return zlib.createDeflate(config.deflate);
63
+ }
64
+ function negotiate(res, mime, size, config) {
65
+ if (!COMPRESSIBLE_TYPE.test(mime)) return { encoding: null, eligible: false };
66
+ if (res.req?.method === "HEAD") return { encoding: null, eligible: false };
67
+ const cc = res.getHeader("Cache-Control");
68
+ if (cc && NO_TRANSFORM.test(String(cc)))
69
+ return { encoding: null, eligible: false };
70
+ const existing = res.getHeader("Content-Encoding");
71
+ if (existing && existing !== "identity")
72
+ return { encoding: null, eligible: false };
73
+ if (size < config.threshold) return { encoding: null, eligible: true };
74
+ const encoding = pickEncoding(
75
+ String(res.req?.headers["accept-encoding"] || "")
76
+ );
77
+ return { encoding, eligible: true };
78
+ }
79
+ function bodyAsReadable(body) {
80
+ if (Buffer2.isBuffer(body)) return Readable.from([body]);
81
+ if (typeof body === "string") return Readable.from([Buffer2.from(body)]);
82
+ return body;
83
+ }
84
+ function resolveCompressionOptions(input) {
85
+ const options = input === true ? {} : input;
86
+ return {
87
+ threshold: options.threshold ?? 1024,
88
+ brotli: options.brotli ?? {},
89
+ gzip: options.gzip ?? {},
90
+ deflate: options.deflate ?? {}
91
+ };
92
+ }
93
+ async function compressAndSend(res, mime, body, config, size) {
94
+ res.setHeader("Content-Type", mime);
95
+ const knownSize = Buffer2.isBuffer(body) ? body.length : typeof body === "string" ? Buffer2.byteLength(body) : size ?? Infinity;
96
+ const { encoding, eligible } = negotiate(res, mime, knownSize, config);
97
+ if (!encoding) {
98
+ if (eligible) appendVary(res, "Accept-Encoding");
99
+ if (Buffer2.isBuffer(body) || typeof body === "string") {
100
+ res.setHeader("Content-Length", String(knownSize));
101
+ res.end(body);
102
+ return;
103
+ }
104
+ if (size !== void 0) res.setHeader("Content-Length", String(size));
105
+ await pipeline(body, res);
106
+ return;
107
+ }
108
+ res.setHeader("Content-Encoding", encoding);
109
+ appendVary(res, "Accept-Encoding");
110
+ await pipeline(
111
+ bodyAsReadable(body),
112
+ createCompressorStream(encoding, config),
113
+ res
114
+ );
115
+ }
116
+
117
+ // lib/internal/mimeTypes.ts
118
+ var MIME_TYPES = {
119
+ html: "text/html",
120
+ css: "text/css",
121
+ js: "application/javascript",
122
+ jpg: "image/jpeg",
123
+ jpeg: "image/jpeg",
124
+ png: "image/png",
125
+ svg: "image/svg+xml",
126
+ txt: "text/plain",
127
+ eot: "application/vnd.ms-fontobject",
128
+ otf: "font/otf",
129
+ ttf: "font/ttf",
130
+ woff: "font/woff",
131
+ woff2: "font/woff2",
132
+ gif: "image/gif",
133
+ ico: "image/x-icon",
134
+ json: "application/json",
135
+ webmanifest: "application/manifest+json"
136
+ };
137
+
138
+ // lib/internal/errors.ts
139
+ function frameworkError(message, skipFn, code, status) {
140
+ const err = new Error(message);
141
+ Error.captureStackTrace(err, skipFn);
142
+ err.cpeak_err = true;
143
+ if (code) err.code = code;
144
+ if (status) err.status = status;
145
+ return err;
146
+ }
147
+ var ErrorCode = /* @__PURE__ */ ((ErrorCode2) => {
148
+ ErrorCode2["MISSING_MIME"] = "CPEAK_ERR_MISSING_MIME";
149
+ ErrorCode2["FILE_NOT_FOUND"] = "CPEAK_ERR_FILE_NOT_FOUND";
150
+ ErrorCode2["NOT_A_FILE"] = "CPEAK_ERR_NOT_A_FILE";
151
+ ErrorCode2["SEND_FILE_FAIL"] = "CPEAK_ERR_SEND_FILE_FAIL";
152
+ ErrorCode2["INVALID_JSON"] = "CPEAK_ERR_INVALID_JSON";
153
+ ErrorCode2["PAYLOAD_TOO_LARGE"] = "CPEAK_ERR_PAYLOAD_TOO_LARGE";
154
+ ErrorCode2["WEAK_SECRET"] = "CPEAK_ERR_WEAK_SECRET";
155
+ ErrorCode2["COMPRESSION_NOT_ENABLED"] = "CPEAK_ERR_COMPRESSION_NOT_ENABLED";
156
+ ErrorCode2["DUPLICATE_ROUTE"] = "CPEAK_ERR_DUPLICATE_ROUTE";
157
+ ErrorCode2["INVALID_ROUTE"] = "CPEAK_ERR_INVALID_ROUTE";
158
+ return ErrorCode2;
159
+ })(ErrorCode || {});
160
+
161
+ // lib/internal/router.ts
162
+ function createNode() {
163
+ return { staticChildren: /* @__PURE__ */ new Map() };
164
+ }
165
+ var Router = class {
166
+ #treesByMethod = /* @__PURE__ */ new Map();
167
+ add(method, path2, middleware, handler) {
168
+ const methodKey = method.toLowerCase();
169
+ let root = this.#treesByMethod.get(methodKey);
170
+ if (!root) {
171
+ root = createNode();
172
+ this.#treesByMethod.set(methodKey, root);
173
+ }
174
+ const segments = splitPath(path2);
175
+ const paramNames = [];
176
+ let currentNode = root;
177
+ for (let i = 0; i < segments.length; i++) {
178
+ const segment = segments[i];
179
+ const isLastSegment = i === segments.length - 1;
180
+ if (segment.length > 1 && segment.startsWith("*")) {
181
+ throw frameworkError(
182
+ `Invalid route "${path2}": named wildcards (e.g. "*name") are not supported. Use a plain "*" at the end of the path.`,
183
+ this.add,
184
+ "CPEAK_ERR_INVALID_ROUTE" /* INVALID_ROUTE */
185
+ );
186
+ }
187
+ if (segment === "*") {
188
+ if (!isLastSegment) {
189
+ throw frameworkError(
190
+ `Invalid route "${path2}": "*" is only allowed as the final path segment.`,
191
+ this.add,
192
+ "CPEAK_ERR_INVALID_ROUTE" /* INVALID_ROUTE */
193
+ );
194
+ }
195
+ if (currentNode.wildcardChild) {
196
+ throw frameworkError(
197
+ `Duplicate route: ${method.toUpperCase()} ${path2}`,
198
+ this.add,
199
+ "CPEAK_ERR_DUPLICATE_ROUTE" /* DUPLICATE_ROUTE */
200
+ );
201
+ }
202
+ currentNode.wildcardChild = { handler, middleware, paramNames };
203
+ return;
204
+ }
205
+ if (segment.startsWith(":")) {
206
+ const paramName = segment.slice(1);
207
+ if (!paramName) {
208
+ throw frameworkError(
209
+ `Invalid route "${path2}": empty parameter name.`,
210
+ this.add,
211
+ "CPEAK_ERR_INVALID_ROUTE" /* INVALID_ROUTE */
212
+ );
213
+ }
214
+ paramNames.push(paramName);
215
+ if (!currentNode.paramChild) {
216
+ currentNode.paramChild = createNode();
217
+ }
218
+ currentNode = currentNode.paramChild;
219
+ continue;
220
+ }
221
+ let staticChild = currentNode.staticChildren.get(segment);
222
+ if (!staticChild) {
223
+ staticChild = createNode();
224
+ currentNode.staticChildren.set(segment, staticChild);
225
+ }
226
+ currentNode = staticChild;
227
+ }
228
+ if (currentNode.handler) {
229
+ throw frameworkError(
230
+ `Duplicate route: ${method.toUpperCase()} ${path2}`,
231
+ this.add,
232
+ "CPEAK_ERR_DUPLICATE_ROUTE" /* DUPLICATE_ROUTE */
233
+ );
234
+ }
235
+ currentNode.handler = handler;
236
+ currentNode.middleware = middleware;
237
+ currentNode.paramNames = paramNames;
238
+ }
239
+ find(method, path2) {
240
+ const root = this.#treesByMethod.get(method.toLowerCase());
241
+ if (!root) return null;
242
+ const segments = splitPath(path2);
243
+ return matchSegments(root, segments, 0, []);
244
+ }
245
+ };
246
+ function matchSegments(node, segments, segmentIndex, capturedValues) {
247
+ if (segmentIndex === segments.length) {
248
+ if (node.handler) {
249
+ return {
250
+ middleware: node.middleware,
251
+ handler: node.handler,
252
+ params: zipParams(node.paramNames, capturedValues)
253
+ };
254
+ }
255
+ if (node.wildcardChild) {
256
+ return {
257
+ middleware: node.wildcardChild.middleware,
258
+ handler: node.wildcardChild.handler,
259
+ params: zipParams(node.wildcardChild.paramNames, capturedValues)
260
+ };
261
+ }
262
+ return null;
263
+ }
264
+ const segment = segments[segmentIndex];
265
+ const staticChild = node.staticChildren.get(segment);
266
+ if (staticChild) {
267
+ const foundMatch = matchSegments(
268
+ staticChild,
269
+ segments,
270
+ segmentIndex + 1,
271
+ capturedValues
272
+ );
273
+ if (foundMatch) return foundMatch;
274
+ }
275
+ if (node.paramChild) {
276
+ capturedValues.push(safeDecode(segment));
277
+ const foundMatch = matchSegments(
278
+ node.paramChild,
279
+ segments,
280
+ segmentIndex + 1,
281
+ capturedValues
282
+ );
283
+ if (foundMatch) return foundMatch;
284
+ capturedValues.pop();
285
+ }
286
+ if (node.wildcardChild) {
287
+ return {
288
+ middleware: node.wildcardChild.middleware,
289
+ handler: node.wildcardChild.handler,
290
+ params: zipParams(node.wildcardChild.paramNames, capturedValues)
291
+ };
292
+ }
293
+ return null;
294
+ }
295
+ function zipParams(names, values) {
296
+ const params = {};
297
+ for (let i = 0; i < names.length; i++) {
298
+ params[names[i]] = values[i];
299
+ }
300
+ return params;
301
+ }
302
+ function safeDecode(segment) {
303
+ try {
304
+ return decodeURIComponent(segment);
305
+ } catch {
306
+ return segment;
307
+ }
308
+ }
309
+ function splitPath(path2) {
310
+ if (path2 === "" || path2 === "/") return [];
311
+ const withoutLeadingSlash = path2.startsWith("/") ? path2.slice(1) : path2;
312
+ return withoutLeadingSlash.split("/");
313
+ }
6
314
 
7
315
  // lib/utils/parseJSON.ts
8
- import { Buffer as Buffer2 } from "buffer";
316
+ import { Buffer as Buffer3 } from "buffer";
9
317
  function isJSON(contentType) {
10
318
  if (!contentType) return false;
11
319
  if (contentType === "application/json") return true;
@@ -38,7 +346,7 @@ var parseJSON = (options = {}) => {
38
346
  };
39
347
  const onEnd = () => {
40
348
  try {
41
- const rawBody = chunks.length === 1 ? chunks[0].toString("utf-8") : Buffer2.concat(chunks).toString("utf-8");
349
+ const rawBody = chunks.length === 1 ? chunks[0].toString("utf-8") : Buffer3.concat(chunks).toString("utf-8");
42
350
  req.body = rawBody ? JSON.parse(rawBody) : {};
43
351
  next();
44
352
  } catch (err) {
@@ -61,30 +369,25 @@ var parseJSON = (options = {}) => {
61
369
  // lib/utils/serveStatic.ts
62
370
  import fs from "fs";
63
371
  import path from "path";
64
- var MIME_TYPES = {
65
- html: "text/html",
66
- css: "text/css",
67
- js: "application/javascript",
68
- jpg: "image/jpeg",
69
- jpeg: "image/jpeg",
70
- png: "image/png",
71
- svg: "image/svg+xml",
72
- txt: "text/plain",
73
- eot: "application/vnd.ms-fontobject",
74
- otf: "font/otf",
75
- ttf: "font/ttf",
76
- woff: "font/woff",
77
- woff2: "font/woff2",
78
- gif: "image/gif",
79
- ico: "image/x-icon",
80
- json: "application/json",
81
- webmanifest: "application/manifest+json"
82
- };
83
- var serveStatic = (folderPath, newMimeTypes, options) => {
84
- if (newMimeTypes) {
85
- Object.assign(MIME_TYPES, newMimeTypes);
86
- }
372
+ var serveStatic = (folderPath, options) => {
87
373
  const prefix = options?.prefix ?? "";
374
+ const live = options?.live ?? false;
375
+ if (live) {
376
+ const resolvedFolder = path.resolve(folderPath);
377
+ return async function(req, res, next) {
378
+ const url = req.url;
379
+ if (typeof url !== "string") return next();
380
+ const pathname = url.split("?")[0];
381
+ const unprefixed = prefix ? pathname.slice(prefix.length) : pathname;
382
+ const filePath = path.join(resolvedFolder, unprefixed);
383
+ const fileExtension = path.extname(filePath).slice(1);
384
+ const mime = MIME_TYPES[fileExtension];
385
+ if (!mime || !filePath.startsWith(resolvedFolder)) return next();
386
+ const stat = await fs.promises.stat(filePath).catch(() => null);
387
+ if (stat?.isFile()) return res.sendFile(filePath, mime);
388
+ next();
389
+ };
390
+ }
88
391
  function processFolder(folderPath2, parentFolder) {
89
392
  const staticFiles = [];
90
393
  const files = fs.readdirSync(folderPath2);
@@ -153,13 +456,22 @@ var render = () => {
153
456
  return function(req, res, next) {
154
457
  res.render = async (path2, data, mime) => {
155
458
  if (!mime) {
156
- throw frameworkError(
157
- `MIME type is missing. You called res.render("${path2}", ...) but forgot to provide the third "mime" argument.`,
158
- res.render
159
- );
459
+ const dotIndex = path2.lastIndexOf(".");
460
+ const fileExtension = dotIndex >= 0 ? path2.slice(dotIndex + 1) : "";
461
+ mime = MIME_TYPES[fileExtension];
462
+ if (!mime) {
463
+ throw frameworkError(
464
+ `MIME type is missing for "${path2}". Pass it as the third argument or register the extension via cpeak({ mimeTypes: { ${fileExtension || "ext"}: "..." } }).`,
465
+ res.render
466
+ );
467
+ }
160
468
  }
161
469
  let fileStr = await fs2.readFile(path2, "utf-8");
162
470
  const finalStr = renderTemplate(fileStr, data);
471
+ if (res._compression) {
472
+ await compressAndSend(res, mime, finalStr, res._compression);
473
+ return;
474
+ }
163
475
  res.setHeader("Content-Type", mime);
164
476
  res.end(finalStr);
165
477
  };
@@ -424,25 +736,70 @@ function cookieParser(options = {}) {
424
736
  };
425
737
  }
426
738
 
427
- // lib/index.ts
428
- function frameworkError(message, skipFn, code, status) {
429
- const err = new Error(message);
430
- Error.captureStackTrace(err, skipFn);
431
- err.cpeak_err = true;
432
- if (code) err.code = code;
433
- if (status) err.status = status;
434
- return err;
739
+ // lib/utils/cors.ts
740
+ function appendVary2(res, value) {
741
+ const existing = res.getHeader("Vary");
742
+ if (!existing) return res.setHeader("Vary", value);
743
+ const current = String(existing).split(",").map((s) => s.trim()).filter(Boolean);
744
+ if (current.includes("*") || current.includes(value)) return;
745
+ res.setHeader("Vary", [...current, value].join(", "));
435
746
  }
436
- var ErrorCode = /* @__PURE__ */ ((ErrorCode2) => {
437
- ErrorCode2["MISSING_MIME"] = "CPEAK_ERR_MISSING_MIME";
438
- ErrorCode2["FILE_NOT_FOUND"] = "CPEAK_ERR_FILE_NOT_FOUND";
439
- ErrorCode2["NOT_A_FILE"] = "CPEAK_ERR_NOT_A_FILE";
440
- ErrorCode2["SEND_FILE_FAIL"] = "CPEAK_ERR_SEND_FILE_FAIL";
441
- ErrorCode2["INVALID_JSON"] = "CPEAK_ERR_INVALID_JSON";
442
- ErrorCode2["PAYLOAD_TOO_LARGE"] = "CPEAK_ERR_PAYLOAD_TOO_LARGE";
443
- ErrorCode2["WEAK_SECRET"] = "CPEAK_ERR_WEAK_SECRET";
444
- return ErrorCode2;
445
- })(ErrorCode || {});
747
+ async function isAllowed(origin, rule) {
748
+ if (rule === true || rule === "*") return true;
749
+ if (rule === false || !origin) return false;
750
+ if (typeof rule === "string") return rule === origin;
751
+ if (Array.isArray(rule)) return rule.includes(origin);
752
+ if (rule instanceof RegExp) return rule.test(origin);
753
+ if (typeof rule === "function") return await rule(origin);
754
+ return false;
755
+ }
756
+ var cors = (options = {}) => {
757
+ const {
758
+ origin = "*",
759
+ methods = "GET,HEAD,PUT,PATCH,POST,DELETE",
760
+ allowedHeaders,
761
+ exposedHeaders,
762
+ credentials = false,
763
+ maxAge = 86400,
764
+ preflightContinue = false,
765
+ optionsSuccessStatus = 204
766
+ } = options;
767
+ const methodsStr = Array.isArray(methods) ? methods.join(",") : methods;
768
+ const allowedHeadersStr = Array.isArray(allowedHeaders) ? allowedHeaders.join(",") : allowedHeaders;
769
+ const exposedHeadersStr = Array.isArray(exposedHeaders) ? exposedHeaders.join(",") : exposedHeaders;
770
+ return async (req, res, next) => {
771
+ const requestOrigin = req.headers.origin;
772
+ if (!requestOrigin) return next();
773
+ const allowed = await isAllowed(requestOrigin, origin);
774
+ if (!allowed) return next();
775
+ const allowOriginValue = origin === "*" && !credentials ? "*" : requestOrigin;
776
+ res.setHeader("Access-Control-Allow-Origin", allowOriginValue);
777
+ if (allowOriginValue !== "*") appendVary2(res, "Origin");
778
+ if (credentials) res.setHeader("Access-Control-Allow-Credentials", "true");
779
+ if (exposedHeadersStr)
780
+ res.setHeader("Access-Control-Expose-Headers", exposedHeadersStr);
781
+ const isPreflight = req.method === "OPTIONS" && req.headers["access-control-request-method"] !== void 0;
782
+ if (!isPreflight) return next();
783
+ res.setHeader("Access-Control-Allow-Methods", methodsStr);
784
+ if (allowedHeadersStr) {
785
+ res.setHeader("Access-Control-Allow-Headers", allowedHeadersStr);
786
+ } else if (origin === "*") {
787
+ const requested = req.headers["access-control-request-headers"];
788
+ if (requested) res.setHeader("Access-Control-Allow-Headers", requested);
789
+ } else {
790
+ res.setHeader(
791
+ "Access-Control-Allow-Headers",
792
+ "Content-Type, Authorization"
793
+ );
794
+ }
795
+ res.setHeader("Access-Control-Max-Age", String(maxAge));
796
+ if (preflightContinue) return next();
797
+ res.statusCode = optionsSuccessStatus;
798
+ res.end();
799
+ };
800
+ };
801
+
802
+ // lib/index.ts
446
803
  var CpeakIncomingMessage = class extends http.IncomingMessage {
447
804
  // We define body and params here for better V8 optimization (not changing the shape of the object at runtime)
448
805
  body = void 0;
@@ -465,14 +822,21 @@ var CpeakIncomingMessage = class extends http.IncomingMessage {
465
822
  }
466
823
  };
467
824
  var CpeakServerResponse = class extends http.ServerResponse {
825
+ // Set per-request from the Cpeak instance. Undefined when compression isn't enabled.
826
+ _compression;
468
827
  // Send a file back to the client
469
828
  async sendFile(path2, mime) {
470
829
  if (!mime) {
471
- throw frameworkError(
472
- 'MIME type is missing. Use res.sendFile(path, "mime-type").',
473
- this.sendFile,
474
- "CPEAK_ERR_MISSING_MIME" /* MISSING_MIME */
475
- );
830
+ const dotIndex = path2.lastIndexOf(".");
831
+ const fileExtension = dotIndex >= 0 ? path2.slice(dotIndex + 1) : "";
832
+ mime = MIME_TYPES[fileExtension];
833
+ if (!mime) {
834
+ throw frameworkError(
835
+ `MIME type is missing for "${path2}". Pass it as the second argument or register the extension via cpeak({ mimeTypes: { ${fileExtension || "ext"}: "..." } }).`,
836
+ this.sendFile,
837
+ "CPEAK_ERR_MISSING_MIME" /* MISSING_MIME */
838
+ );
839
+ }
476
840
  }
477
841
  try {
478
842
  const stat = await fs3.stat(path2);
@@ -483,9 +847,19 @@ var CpeakServerResponse = class extends http.ServerResponse {
483
847
  "CPEAK_ERR_NOT_A_FILE" /* NOT_A_FILE */
484
848
  );
485
849
  }
850
+ if (this._compression) {
851
+ await compressAndSend(
852
+ this,
853
+ mime,
854
+ createReadStream(path2),
855
+ this._compression,
856
+ stat.size
857
+ );
858
+ return;
859
+ }
486
860
  this.setHeader("Content-Type", mime);
487
861
  this.setHeader("Content-Length", String(stat.size));
488
- await pipeline(createReadStream(path2), this);
862
+ await pipeline2(createReadStream(path2), this);
489
863
  } catch (err) {
490
864
  if (err?.code === "ENOENT") {
491
865
  throw frameworkError(
@@ -517,59 +891,91 @@ var CpeakServerResponse = class extends http.ServerResponse {
517
891
  this.writeHead(302, { Location: location });
518
892
  this.end();
519
893
  }
520
- // Send a json data back to the client (for small json data, less than the highWaterMark)
894
+ // Send a json data back to the client.
895
+ // This is only good for bodies that their size is less than the highWaterMark value.
521
896
  json(data) {
897
+ const body = JSON.stringify(data);
898
+ if (this._compression) {
899
+ return compressAndSend(this, "application/json", body, this._compression);
900
+ }
522
901
  this.setHeader("Content-Type", "application/json");
523
- this.end(JSON.stringify(data));
902
+ this.end(body);
903
+ return Promise.resolve();
904
+ }
905
+ // Explicit compression entry point. A developer can use this in any custom handler to compress arbitrary responses
906
+ compress(mime, body, size) {
907
+ if (!this._compression) {
908
+ throw frameworkError(
909
+ "compression is not enabled. Pass `compression` to cpeak({ compression: true | { ... } }) to use res.compress.",
910
+ this.compress,
911
+ "CPEAK_ERR_COMPRESSION_NOT_ENABLED" /* COMPRESSION_NOT_ENABLED */
912
+ );
913
+ }
914
+ return compressAndSend(this, mime, body, this._compression, size);
524
915
  }
525
916
  };
526
917
  var Cpeak = class {
527
918
  #server;
528
- #routes;
919
+ #router;
529
920
  #middleware;
530
921
  #handleErr;
531
- constructor() {
922
+ #compression;
923
+ constructor(options = {}) {
532
924
  this.#server = http.createServer({
533
925
  IncomingMessage: CpeakIncomingMessage,
534
926
  ServerResponse: CpeakServerResponse
535
927
  });
536
- this.#routes = {};
928
+ this.#router = new Router();
537
929
  this.#middleware = [];
930
+ if (options.compression) {
931
+ this.#compression = resolveCompressionOptions(options.compression);
932
+ }
933
+ if (options.mimeTypes) Object.assign(MIME_TYPES, options.mimeTypes);
538
934
  this.#server.on(
539
935
  "request",
540
936
  async (req, res) => {
937
+ res._compression = this.#compression;
541
938
  const qIndex = req.url?.indexOf("?");
542
939
  const urlWithoutQueries = qIndex === -1 ? req.url || "" : req.url?.substring(0, qIndex);
543
- const dispatchError = (error) => {
940
+ const dispatchError = async (error) => {
544
941
  if (res.headersSent) {
545
942
  req.socket?.destroy();
546
943
  return;
547
944
  }
548
945
  res.setHeader("Connection", "close");
549
- this.#handleErr?.(error, req, res);
946
+ try {
947
+ await this.#handleErr?.(error, req, res);
948
+ } catch (handlerFailure) {
949
+ console.error(
950
+ "[cpeak] handleErr failed while processing:",
951
+ error,
952
+ "\nReason:",
953
+ handlerFailure
954
+ );
955
+ if (!res.headersSent) {
956
+ try {
957
+ res.statusCode = 500;
958
+ res.end();
959
+ } catch {
960
+ }
961
+ }
962
+ }
550
963
  };
551
964
  const runHandler = async (req2, res2, middleware, cb, index) => {
552
965
  if (index === middleware.length) {
553
966
  try {
554
- await cb(req2, res2, dispatchError);
967
+ await cb(req2, res2);
555
968
  } catch (error) {
556
969
  dispatchError(error);
557
970
  }
558
971
  } else {
559
972
  try {
560
- await middleware[index](
561
- req2,
562
- res2,
563
- // The next function
564
- async (error) => {
565
- if (error) {
566
- return dispatchError(error);
567
- }
568
- await runHandler(req2, res2, middleware, cb, index + 1);
569
- },
570
- // Error handler for a route middleware
571
- dispatchError
572
- );
973
+ await middleware[index](req2, res2, async (error) => {
974
+ if (error) {
975
+ return dispatchError(error);
976
+ }
977
+ await runHandler(req2, res2, middleware, cb, index + 1);
978
+ });
573
979
  } catch (error) {
574
980
  dispatchError(error);
575
981
  }
@@ -577,25 +983,18 @@ var Cpeak = class {
577
983
  };
578
984
  const runMiddleware = async (req2, res2, middleware, index) => {
579
985
  if (index === middleware.length) {
580
- const routes = this.#routes[req2.method?.toLowerCase() || ""];
581
- if (routes && typeof routes[Symbol.iterator] === "function")
582
- for (const route of routes) {
583
- const match = urlWithoutQueries?.match(route.regex);
584
- if (match) {
585
- const pathVariables = this.#extractPathVariables(
586
- route.path,
587
- match
588
- );
589
- req2.params = pathVariables;
590
- return await runHandler(
591
- req2,
592
- res2,
593
- route.middleware,
594
- route.cb,
595
- 0
596
- );
597
- }
598
- }
986
+ const method = req2.method?.toLowerCase() || "";
987
+ const found = this.#router.find(method, urlWithoutQueries || "");
988
+ if (found) {
989
+ req2.params = found.params;
990
+ return await runHandler(
991
+ req2,
992
+ res2,
993
+ found.middleware,
994
+ found.handler,
995
+ 0
996
+ );
997
+ }
599
998
  return res2.status(404).json({ error: `Cannot ${req2.method} ${urlWithoutQueries}` });
600
999
  } else {
601
1000
  try {
@@ -615,14 +1014,12 @@ var Cpeak = class {
615
1014
  );
616
1015
  }
617
1016
  route(method, path2, ...args) {
618
- if (!this.#routes[method]) this.#routes[method] = [];
619
1017
  const cb = args.pop();
620
1018
  if (!cb || typeof cb !== "function") {
621
1019
  throw new Error("Route definition must include a handler");
622
1020
  }
623
1021
  const middleware = args.flat();
624
- const regex = this.#pathToRegex(path2);
625
- this.#routes[method].push({ path: path2, regex, middleware, cb });
1022
+ this.#router.add(method, path2, middleware, cb);
626
1023
  }
627
1024
  beforeEach(cb) {
628
1025
  this.#middleware.push(cb);
@@ -630,35 +1027,22 @@ var Cpeak = class {
630
1027
  handleErr(cb) {
631
1028
  this.#handleErr = cb;
632
1029
  }
633
- listen(port, cb) {
634
- return this.#server.listen(port, cb);
1030
+ listen(...args) {
1031
+ return this.#server.listen(...args);
635
1032
  }
636
1033
  address() {
637
1034
  return this.#server.address();
638
1035
  }
639
1036
  close(cb) {
640
- this.#server.close(cb);
641
- }
642
- // ------------------------------
643
- // PRIVATE METHODS:
644
- // ------------------------------
645
- #pathToRegex(path2) {
646
- const regexString = "^" + path2.replace(/:\w+/g, "([^/]+)").replace(/\*/g, ".*") + "$";
647
- return new RegExp(regexString);
648
- }
649
- #extractPathVariables(path2, match) {
650
- const paramNames = (path2.match(/:\w+/g) || []).map(
651
- (param) => param.slice(1)
652
- );
653
- const params = {};
654
- paramNames.forEach((name, index) => {
655
- params[name] = match[index + 1];
656
- });
657
- return params;
1037
+ return this.#server.close(cb);
1038
+ }
1039
+ // A getter for developers who want to access the underlying http server instance for advanced use cases that aren't covered by Cpeak
1040
+ get server() {
1041
+ return this.#server;
658
1042
  }
659
1043
  };
660
- function cpeak() {
661
- return new Cpeak();
1044
+ function cpeak(options) {
1045
+ return new Cpeak(options);
662
1046
  }
663
1047
  export {
664
1048
  Cpeak,
@@ -667,6 +1051,7 @@ export {
667
1051
  ErrorCode,
668
1052
  auth,
669
1053
  cookieParser,
1054
+ cors,
670
1055
  cpeak as default,
671
1056
  frameworkError,
672
1057
  hashPassword,