cpeak 2.8.0 → 2.9.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.d.ts CHANGED
@@ -4,10 +4,12 @@ import { Readable } from 'node:stream';
4
4
  import { Buffer } from 'node:buffer';
5
5
  import * as node_zlib from 'node:zlib';
6
6
 
7
- declare function frameworkError(message: string, skipFn: Function, code?: string, status?: number): Error & {
7
+ declare function frameworkError(message: string, skipFn: Function, code?: string, status?: number, clientDisconnect?: boolean): Error & {
8
8
  code?: string;
9
9
  cpeak_err?: boolean;
10
+ clientDisconnect?: boolean;
10
11
  };
12
+ declare function isClientDisconnect(err: unknown): boolean;
11
13
  declare enum ErrorCode {
12
14
  MISSING_MIME = "CPEAK_ERR_MISSING_MIME",
13
15
  FILE_NOT_FOUND = "CPEAK_ERR_FILE_NOT_FOUND",
@@ -17,8 +19,11 @@ declare enum ErrorCode {
17
19
  PAYLOAD_TOO_LARGE = "CPEAK_ERR_PAYLOAD_TOO_LARGE",
18
20
  WEAK_SECRET = "CPEAK_ERR_WEAK_SECRET",
19
21
  COMPRESSION_NOT_ENABLED = "CPEAK_ERR_COMPRESSION_NOT_ENABLED",
22
+ RENDER_NOT_ENABLED = "CPEAK_ERR_RENDER_NOT_ENABLED",
20
23
  DUPLICATE_ROUTE = "CPEAK_ERR_DUPLICATE_ROUTE",
21
- INVALID_ROUTE = "CPEAK_ERR_INVALID_ROUTE"
24
+ INVALID_ROUTE = "CPEAK_ERR_INVALID_ROUTE",
25
+ DUPLICATE_FALLBACK = "CPEAK_ERR_DUPLICATE_FALLBACK",
26
+ RENDER_FAIL = "CPEAK_ERR_RENDER_FAIL"
22
27
  }
23
28
 
24
29
  interface CompressionOptions {
@@ -29,48 +34,6 @@ interface CompressionOptions {
29
34
  }
30
35
  type ResolvedCompressionConfig = Required<CompressionOptions>;
31
36
 
32
- type CpeakHttpServer = Server<typeof CpeakIncomingMessage, typeof CpeakServerResponse>;
33
- interface CpeakOptions {
34
- compression?: boolean | CompressionOptions;
35
- mimeTypes?: StringMap;
36
- }
37
- type StringMap = Record<string, string>;
38
- interface CpeakRequest<ReqBody = any, ReqQueries = any> extends IncomingMessage {
39
- params: StringMap;
40
- query: ReqQueries;
41
- body?: ReqBody;
42
- cookies?: StringMap;
43
- signedCookies?: Record<string, string | false>;
44
- [key: string]: any;
45
- }
46
- interface CpeakResponse extends ServerResponse {
47
- sendFile: (path: string, mime?: string) => Promise<void>;
48
- status: (code: number) => CpeakResponse;
49
- attachment: (filename?: string) => CpeakResponse;
50
- cookie: (name: string, value: string, options?: any) => CpeakResponse;
51
- redirect: (location: string) => void;
52
- json: (data: any) => Promise<void>;
53
- compress: (mime: string, body: Buffer | string | Readable, size?: number) => Promise<void>;
54
- [key: string]: any;
55
- }
56
- type Next = (err?: any) => void;
57
- type Middleware<ReqBody = any, ReqParams = any> = (req: CpeakRequest<ReqBody, ReqParams>, res: CpeakResponse, next: Next) => void;
58
- type RouteMiddleware<ReqBody = any, ReqParams = any> = (req: CpeakRequest<ReqBody, ReqParams>, res: CpeakResponse, next: Next) => void | Promise<void>;
59
- type Handler<ReqBody = any, ReqParams = any> = (req: CpeakRequest<ReqBody, ReqParams>, res: CpeakResponse) => void | Promise<void>;
60
-
61
- declare const parseJSON: (options?: {
62
- limit?: number;
63
- }) => (req: CpeakRequest, res: CpeakResponse, next: Next) => void;
64
-
65
- declare const serveStatic: (folderPath: string, options?: {
66
- prefix?: string;
67
- live?: boolean;
68
- }) => (req: CpeakRequest, res: CpeakResponse, next: Next) => void | Promise<void>;
69
-
70
- declare const render: () => (req: CpeakRequest, res: CpeakResponse, next: Next) => void;
71
-
72
- declare const swagger: (spec: object, prefix?: string) => (req: CpeakRequest, res: CpeakResponse, next: Next) => Promise<void> | undefined;
73
-
74
37
  interface PbkdfOptions {
75
38
  iterations?: number;
76
39
  keylen?: number;
@@ -111,6 +74,50 @@ interface CorsOptions {
111
74
  optionsSuccessStatus?: number;
112
75
  }
113
76
 
77
+ type CpeakHttpServer = Server<typeof CpeakIncomingMessage, typeof CpeakServerResponse>;
78
+ interface CpeakOptions {
79
+ compression?: boolean | CompressionOptions;
80
+ mimeTypes?: StringMap;
81
+ }
82
+ type StringMap = Record<string, string>;
83
+ interface CpeakRequest<ReqBody = any, ReqQueries = any> extends IncomingMessage {
84
+ params: StringMap;
85
+ query: ReqQueries;
86
+ body?: ReqBody;
87
+ cookies?: StringMap;
88
+ signedCookies?: Record<string, string | false>;
89
+ [key: string]: any;
90
+ }
91
+ interface CpeakResponse<ResBody = any> extends ServerResponse {
92
+ sendFile: (path: string, mime?: string) => Promise<void>;
93
+ status: (code: number) => CpeakResponse<ResBody>;
94
+ attachment: (filename?: string) => CpeakResponse<ResBody>;
95
+ cookie: (name: string, value: string, options?: CookieOptions) => CpeakResponse<ResBody>;
96
+ redirect: (location: string) => void;
97
+ json: (data: ResBody) => Promise<void>;
98
+ compress: (mime: string, body: Buffer | string | Readable, size?: number) => Promise<void>;
99
+ render: (filePath: string, data: Record<string, unknown>, mime?: string) => Promise<void>;
100
+ [key: string]: any;
101
+ }
102
+ type Next = (err?: any) => void;
103
+ type Middleware<ReqBody = any, ReqParams = any> = (req: CpeakRequest<ReqBody, ReqParams>, res: CpeakResponse, next: Next) => unknown;
104
+ type RouteMiddleware<ReqBody = any, ReqParams = any> = (req: CpeakRequest<ReqBody, ReqParams>, res: CpeakResponse, next: Next) => unknown;
105
+ type Handler<ReqBody = any, ReqParams = any, ResBody = any> = (req: CpeakRequest<ReqBody, ReqParams>, res: CpeakResponse<ResBody>) => unknown;
106
+
107
+ declare const parseJSON: (options?: {
108
+ limit?: number;
109
+ }) => (req: CpeakRequest, res: CpeakResponse, next: Next) => void;
110
+
111
+ declare const serveStatic: (folderPath: string, options?: {
112
+ prefix?: string;
113
+ live?: boolean;
114
+ exclude?: string[];
115
+ }) => (req: CpeakRequest, res: CpeakResponse, next: Next) => void | Promise<void>;
116
+
117
+ declare const render: () => (req: CpeakRequest, res: CpeakResponse, next: Next) => void;
118
+
119
+ declare const swagger: (spec: object, prefix?: string) => (req: CpeakRequest, res: CpeakResponse, next: Next) => Promise<void> | undefined;
120
+
114
121
  declare function hashPassword(password: string, options?: PbkdfOptions): Promise<string>;
115
122
  declare function verifyPassword(password: string, stored: string): Promise<boolean>;
116
123
  declare function auth(options: AuthOptions): Middleware;
@@ -134,6 +141,7 @@ declare class CpeakServerResponse extends http.ServerResponse<CpeakIncomingMessa
134
141
  attachment(filename?: string): this;
135
142
  redirect(location: string): void;
136
143
  json(data: any): Promise<void>;
144
+ render(): Promise<void>;
137
145
  compress(mime: string, body: Buffer | string | Readable, size?: number): Promise<void>;
138
146
  }
139
147
  declare class Cpeak {
@@ -142,6 +150,7 @@ declare class Cpeak {
142
150
  route(method: string, path: string, ...args: (RouteMiddleware | Handler)[]): void;
143
151
  beforeEach(cb: Middleware): void;
144
152
  handleErr(cb: (err: unknown, req: CpeakRequest, res: CpeakResponse) => void): void;
153
+ fallback(cb: Handler): void;
145
154
  listen(port: number, cb?: () => void): CpeakHttpServer;
146
155
  listen(port: number, host: string, cb?: () => void): CpeakHttpServer;
147
156
  listen(options: net.ListenOptions, cb?: () => void): CpeakHttpServer;
@@ -152,4 +161,4 @@ declare class Cpeak {
152
161
 
153
162
  declare function cpeak(options?: CpeakOptions): Cpeak;
154
163
 
155
- export { type AuthOptions, type CompressionOptions, type CookieOptions, type CorsOptions, Cpeak, type CpeakHttpServer, CpeakIncomingMessage, type CpeakOptions, type CpeakRequest, type CpeakResponse, CpeakServerResponse, ErrorCode, type Handler, type Middleware, type Next, type PbkdfOptions, type RouteMiddleware, auth, cookieParser, cors, cpeak as default, frameworkError, hashPassword, parseJSON, render, serveStatic, swagger, verifyPassword };
164
+ export { type AuthOptions, type CompressionOptions, type CookieOptions, type CorsOptions, Cpeak, type CpeakHttpServer, CpeakIncomingMessage, type CpeakOptions, type CpeakRequest, type CpeakResponse, CpeakServerResponse, ErrorCode, type Handler, type Middleware, type Next, type PbkdfOptions, type RouteMiddleware, auth, cookieParser, cors, cpeak as default, frameworkError, hashPassword, isClientDisconnect, parseJSON, render, serveStatic, swagger, verifyPassword };
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  // lib/index.ts
2
2
  import http from "http";
3
- import fs3 from "fs/promises";
4
- import { createReadStream } from "fs";
5
- import { pipeline as pipeline2 } from "stream/promises";
3
+ import fs2 from "fs/promises";
4
+ import { createReadStream as createReadStream2 } from "fs";
5
+ import { pipeline as pipeline3 } from "stream/promises";
6
6
 
7
7
  // lib/internal/compression.ts
8
8
  import zlib from "zlib";
@@ -132,18 +132,37 @@ var MIME_TYPES = {
132
132
  gif: "image/gif",
133
133
  ico: "image/x-icon",
134
134
  json: "application/json",
135
- webmanifest: "application/manifest+json"
135
+ map: "application/json",
136
+ webmanifest: "application/manifest+json",
137
+ xml: "application/xml",
138
+ pdf: "application/pdf",
139
+ mp4: "video/mp4",
140
+ webm: "video/webm",
141
+ mp3: "audio/mpeg",
142
+ wav: "audio/wav",
143
+ webp: "image/webp",
144
+ avif: "image/avif"
136
145
  };
137
146
 
138
147
  // lib/internal/errors.ts
139
- function frameworkError(message, skipFn, code, status) {
148
+ function frameworkError(message, skipFn, code, status, clientDisconnect) {
140
149
  const err = new Error(message);
141
150
  Error.captureStackTrace(err, skipFn);
142
151
  err.cpeak_err = true;
143
152
  if (code) err.code = code;
144
153
  if (status) err.status = status;
154
+ if (clientDisconnect) err.clientDisconnect = true;
145
155
  return err;
146
156
  }
157
+ var CLIENT_DISCONNECT_CODES = /* @__PURE__ */ new Set([
158
+ "ERR_STREAM_PREMATURE_CLOSE",
159
+ "ERR_STREAM_DESTROYED",
160
+ "ECONNRESET",
161
+ "EPIPE"
162
+ ]);
163
+ function isClientDisconnect(err) {
164
+ return CLIENT_DISCONNECT_CODES.has(err?.code);
165
+ }
147
166
  var ErrorCode = /* @__PURE__ */ ((ErrorCode2) => {
148
167
  ErrorCode2["MISSING_MIME"] = "CPEAK_ERR_MISSING_MIME";
149
168
  ErrorCode2["FILE_NOT_FOUND"] = "CPEAK_ERR_FILE_NOT_FOUND";
@@ -153,8 +172,11 @@ var ErrorCode = /* @__PURE__ */ ((ErrorCode2) => {
153
172
  ErrorCode2["PAYLOAD_TOO_LARGE"] = "CPEAK_ERR_PAYLOAD_TOO_LARGE";
154
173
  ErrorCode2["WEAK_SECRET"] = "CPEAK_ERR_WEAK_SECRET";
155
174
  ErrorCode2["COMPRESSION_NOT_ENABLED"] = "CPEAK_ERR_COMPRESSION_NOT_ENABLED";
175
+ ErrorCode2["RENDER_NOT_ENABLED"] = "CPEAK_ERR_RENDER_NOT_ENABLED";
156
176
  ErrorCode2["DUPLICATE_ROUTE"] = "CPEAK_ERR_DUPLICATE_ROUTE";
157
177
  ErrorCode2["INVALID_ROUTE"] = "CPEAK_ERR_INVALID_ROUTE";
178
+ ErrorCode2["DUPLICATE_FALLBACK"] = "CPEAK_ERR_DUPLICATE_FALLBACK";
179
+ ErrorCode2["RENDER_FAIL"] = "CPEAK_ERR_RENDER_FAIL";
158
180
  return ErrorCode2;
159
181
  })(ErrorCode || {});
160
182
 
@@ -164,14 +186,14 @@ function createNode() {
164
186
  }
165
187
  var Router = class {
166
188
  #treesByMethod = /* @__PURE__ */ new Map();
167
- add(method, path2, middleware, handler) {
189
+ add(method, path3, middleware, handler) {
168
190
  const methodKey = method.toLowerCase();
169
191
  let root = this.#treesByMethod.get(methodKey);
170
192
  if (!root) {
171
193
  root = createNode();
172
194
  this.#treesByMethod.set(methodKey, root);
173
195
  }
174
- const segments = splitPath(path2);
196
+ const segments = splitPath(path3);
175
197
  const paramNames = [];
176
198
  let currentNode = root;
177
199
  for (let i = 0; i < segments.length; i++) {
@@ -179,7 +201,7 @@ var Router = class {
179
201
  const isLastSegment = i === segments.length - 1;
180
202
  if (segment.length > 1 && segment.startsWith("*")) {
181
203
  throw frameworkError(
182
- `Invalid route "${path2}": named wildcards (e.g. "*name") are not supported. Use a plain "*" at the end of the path.`,
204
+ `Invalid route "${path3}": named wildcards (e.g. "*name") are not supported. Use a plain "*" at the end of the path.`,
183
205
  this.add,
184
206
  "CPEAK_ERR_INVALID_ROUTE" /* INVALID_ROUTE */
185
207
  );
@@ -187,14 +209,14 @@ var Router = class {
187
209
  if (segment === "*") {
188
210
  if (!isLastSegment) {
189
211
  throw frameworkError(
190
- `Invalid route "${path2}": "*" is only allowed as the final path segment.`,
212
+ `Invalid route "${path3}": "*" is only allowed as the final path segment.`,
191
213
  this.add,
192
214
  "CPEAK_ERR_INVALID_ROUTE" /* INVALID_ROUTE */
193
215
  );
194
216
  }
195
217
  if (currentNode.wildcardChild) {
196
218
  throw frameworkError(
197
- `Duplicate route: ${method.toUpperCase()} ${path2}`,
219
+ `Duplicate route: ${method.toUpperCase()} ${path3}`,
198
220
  this.add,
199
221
  "CPEAK_ERR_DUPLICATE_ROUTE" /* DUPLICATE_ROUTE */
200
222
  );
@@ -206,7 +228,7 @@ var Router = class {
206
228
  const paramName = segment.slice(1);
207
229
  if (!paramName) {
208
230
  throw frameworkError(
209
- `Invalid route "${path2}": empty parameter name.`,
231
+ `Invalid route "${path3}": empty parameter name.`,
210
232
  this.add,
211
233
  "CPEAK_ERR_INVALID_ROUTE" /* INVALID_ROUTE */
212
234
  );
@@ -227,7 +249,7 @@ var Router = class {
227
249
  }
228
250
  if (currentNode.handler) {
229
251
  throw frameworkError(
230
- `Duplicate route: ${method.toUpperCase()} ${path2}`,
252
+ `Duplicate route: ${method.toUpperCase()} ${path3}`,
231
253
  this.add,
232
254
  "CPEAK_ERR_DUPLICATE_ROUTE" /* DUPLICATE_ROUTE */
233
255
  );
@@ -236,10 +258,10 @@ var Router = class {
236
258
  currentNode.middleware = middleware;
237
259
  currentNode.paramNames = paramNames;
238
260
  }
239
- find(method, path2) {
261
+ find(method, path3) {
240
262
  const root = this.#treesByMethod.get(method.toLowerCase());
241
263
  if (!root) return null;
242
- const segments = splitPath(path2);
264
+ const segments = splitPath(path3);
243
265
  return matchSegments(root, segments, 0, []);
244
266
  }
245
267
  };
@@ -306,9 +328,9 @@ function safeDecode(segment) {
306
328
  return segment;
307
329
  }
308
330
  }
309
- function splitPath(path2) {
310
- if (path2 === "" || path2 === "/") return [];
311
- const withoutLeadingSlash = path2.startsWith("/") ? path2.slice(1) : path2;
331
+ function splitPath(path3) {
332
+ if (path3 === "" || path3 === "/") return [];
333
+ const withoutLeadingSlash = path3.startsWith("/") ? path3.slice(1) : path3;
312
334
  return withoutLeadingSlash.split("/");
313
335
  }
314
336
 
@@ -373,30 +395,36 @@ var serveStatic = (folderPath, options) => {
373
395
  const prefix = options?.prefix ?? "";
374
396
  const live = options?.live ?? false;
375
397
  if (live) {
376
- const resolvedFolder = path.resolve(folderPath);
398
+ const resolvedFolder2 = path.resolve(folderPath);
399
+ const excludes2 = (options?.exclude ?? []).map((e) => path.join(resolvedFolder2, e));
377
400
  return async function(req, res, next) {
378
401
  const url = req.url;
379
402
  if (typeof url !== "string") return next();
380
403
  const pathname = url.split("?")[0];
381
404
  const unprefixed = prefix ? pathname.slice(prefix.length) : pathname;
382
- const filePath = path.join(resolvedFolder, unprefixed);
405
+ const filePath = path.join(resolvedFolder2, unprefixed);
383
406
  const fileExtension = path.extname(filePath).slice(1);
384
407
  const mime = MIME_TYPES[fileExtension];
385
- if (!mime || !filePath.startsWith(resolvedFolder)) return next();
408
+ if (!mime || !filePath.startsWith(resolvedFolder2)) return next();
409
+ if (excludes2.some((e) => filePath.startsWith(e))) return next();
386
410
  const stat = await fs.promises.stat(filePath).catch(() => null);
387
411
  if (stat?.isFile()) return res.sendFile(filePath, mime);
388
412
  next();
389
413
  };
390
414
  }
415
+ const resolvedFolder = path.resolve(folderPath);
416
+ const excludes = (options?.exclude ?? []).map((e) => path.join(resolvedFolder, e));
391
417
  function processFolder(folderPath2, parentFolder) {
392
418
  const staticFiles = [];
393
419
  const files = fs.readdirSync(folderPath2);
394
420
  for (const file of files) {
395
421
  const fullPath = path.join(folderPath2, file);
396
422
  if (fs.statSync(fullPath).isDirectory()) {
423
+ if (excludes.some((e) => fullPath.startsWith(e))) continue;
397
424
  const subfolderFiles = processFolder(fullPath, parentFolder);
398
425
  staticFiles.push(...subfolderFiles);
399
426
  } else {
427
+ if (excludes.some((e) => fullPath.startsWith(e))) continue;
400
428
  const relativePath = path.relative(parentFolder, fullPath);
401
429
  const fileExtension = path.extname(file).slice(1);
402
430
  if (MIME_TYPES[fileExtension]) staticFiles.push("/" + relativePath);
@@ -429,51 +457,126 @@ var serveStatic = (folderPath, options) => {
429
457
  };
430
458
 
431
459
  // lib/utils/render.ts
432
- import fs2 from "fs/promises";
433
- function renderTemplate(templateStr, data) {
434
- let result = [];
435
- let currentIndex = 0;
436
- while (currentIndex < templateStr.length) {
437
- const startIdx = templateStr.indexOf("{{", currentIndex);
438
- if (startIdx === -1) {
439
- result.push(templateStr.slice(currentIndex));
440
- break;
460
+ import path2 from "path";
461
+ import { createReadStream } from "fs";
462
+ import { readFile } from "fs/promises";
463
+ import { Transform } from "stream";
464
+ import { pipeline as pipeline2 } from "stream/promises";
465
+ var MAX_PATTERN = 128;
466
+ function escapeHtml(value) {
467
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
468
+ }
469
+ var TemplateTransform = class _TemplateTransform extends Transform {
470
+ constructor(data, baseDir) {
471
+ super();
472
+ this.data = data;
473
+ this.baseDir = baseDir;
474
+ }
475
+ tail = "";
476
+ _transform(chunk, _, callback) {
477
+ const str = this.tail + chunk.toString("utf8");
478
+ if (str.length <= MAX_PATTERN) {
479
+ this.tail = str;
480
+ callback();
481
+ return;
441
482
  }
442
- result.push(templateStr.slice(currentIndex, startIdx));
443
- const endIdx = templateStr.indexOf("}}", startIdx);
444
- if (endIdx === -1) {
445
- result.push(templateStr.slice(startIdx));
446
- break;
483
+ let boundary = str.length - MAX_PATTERN;
484
+ for (const [opener, closer] of [
485
+ ["{{", "}}"],
486
+ ["<cpeak", ">"]
487
+ ]) {
488
+ const last = str.lastIndexOf(opener, boundary - 1);
489
+ if (last === -1) continue;
490
+ const closeIdx = str.indexOf(closer, last + opener.length);
491
+ if (closeIdx === -1 || closeIdx >= boundary)
492
+ boundary = Math.min(boundary, last);
447
493
  }
448
- const varName = templateStr.slice(startIdx + 2, endIdx).trim();
449
- const replacement = data[varName] !== void 0 ? data[varName] : "";
450
- result.push(replacement);
451
- currentIndex = endIdx + 2;
494
+ this.tail = str.slice(boundary);
495
+ const safe = str.slice(0, boundary);
496
+ if (safe)
497
+ this.process(safe).then(() => callback()).catch(callback);
498
+ else callback();
452
499
  }
453
- return result.join("");
454
- }
500
+ _flush(callback) {
501
+ if (this.tail)
502
+ this.process(this.tail).then(() => callback()).catch(callback);
503
+ else callback();
504
+ }
505
+ async process(str) {
506
+ const RE = /<cpeak\s+include="([^"]+)"\s*\/?>|<cpeak\s+html=\{([^}]+)\}\s*\/?>|\{\{([^}]+)\}\}/g;
507
+ let last = 0;
508
+ for (const match of str.matchAll(RE)) {
509
+ const idx = match.index;
510
+ if (idx > last) this.push(str.slice(last, idx));
511
+ const [, includeSrc, rawKey, escapedKey] = match;
512
+ if (includeSrc !== void 0) {
513
+ const includePath = path2.resolve(this.baseDir, includeSrc);
514
+ const content = await readFile(includePath, "utf8");
515
+ const chunks = [];
516
+ const nested = new _TemplateTransform(
517
+ this.data,
518
+ path2.dirname(includePath)
519
+ );
520
+ await new Promise((resolve, reject) => {
521
+ nested.on("data", (c) => chunks.push(c));
522
+ nested.on("end", resolve);
523
+ nested.on("error", reject);
524
+ nested.end(Buffer.from(content, "utf8"));
525
+ });
526
+ this.push(Buffer.concat(chunks));
527
+ } else if (rawKey !== void 0) {
528
+ const val = this.data[rawKey.trim()];
529
+ if (val !== void 0) this.push(String(val));
530
+ } else {
531
+ const val = this.data[escapedKey.trim()];
532
+ if (val !== void 0) this.push(escapeHtml(String(val)));
533
+ }
534
+ last = idx + match[0].length;
535
+ }
536
+ if (last < str.length) this.push(str.slice(last));
537
+ }
538
+ };
455
539
  var render = () => {
456
540
  return function(req, res, next) {
457
- res.render = async (path2, data, mime) => {
541
+ res.render = async (filePath, data, mime) => {
542
+ if (res.headersSent) return;
458
543
  if (!mime) {
459
- const dotIndex = path2.lastIndexOf(".");
460
- const fileExtension = dotIndex >= 0 ? path2.slice(dotIndex + 1) : "";
544
+ const dotIndex = filePath.lastIndexOf(".");
545
+ const fileExtension = dotIndex >= 0 ? filePath.slice(dotIndex + 1) : "";
461
546
  mime = MIME_TYPES[fileExtension];
462
547
  if (!mime) {
463
548
  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
549
+ `MIME type is missing for "${filePath}". Pass it as the third argument or register the extension via cpeak({ mimeTypes: { ${fileExtension || "ext"}: "..." } }).`,
550
+ res.render,
551
+ "CPEAK_ERR_MISSING_MIME" /* MISSING_MIME */
466
552
  );
467
553
  }
468
554
  }
469
- let fileStr = await fs2.readFile(path2, "utf-8");
470
- const finalStr = renderTemplate(fileStr, data);
471
- if (res._compression) {
472
- await compressAndSend(res, mime, finalStr, res._compression);
473
- return;
555
+ const resolved = path2.resolve(filePath);
556
+ try {
557
+ if (res._compression) {
558
+ const readStream = createReadStream(resolved);
559
+ const transform = new TemplateTransform(data, path2.dirname(resolved));
560
+ pipeline2(readStream, transform).catch(() => {
561
+ });
562
+ await compressAndSend(res, mime, transform, res._compression);
563
+ return;
564
+ }
565
+ res.setHeader("Content-Type", mime);
566
+ await pipeline2(
567
+ createReadStream(resolved),
568
+ new TemplateTransform(data, path2.dirname(resolved)),
569
+ res
570
+ );
571
+ } catch (err) {
572
+ throw frameworkError(
573
+ `Failed to render "${filePath}." Error: ${err}`,
574
+ res.render,
575
+ "CPEAK_ERR_RENDER_FAIL" /* RENDER_FAIL */,
576
+ void 0,
577
+ isClientDisconnect(err)
578
+ );
474
579
  }
475
- res.setHeader("Content-Type", mime);
476
- res.end(finalStr);
477
580
  };
478
581
  next();
479
582
  };
@@ -665,8 +768,8 @@ function parseRawCookies(header) {
665
768
  }
666
769
  function buildSetCookieHeader(name, value, options) {
667
770
  const parts = [`${name}=${encodeURIComponent(value)}`];
668
- const path2 = options.path ?? "/";
669
- parts.push(`Path=${path2}`);
771
+ const path3 = options.path ?? "/";
772
+ parts.push(`Path=${path3}`);
670
773
  if (options.domain) parts.push(`Domain=${options.domain}`);
671
774
  if (options.maxAge !== void 0)
672
775
  parts.push(`Max-Age=${Math.floor(options.maxAge / 1e3)}`);
@@ -825,24 +928,25 @@ var CpeakServerResponse = class extends http.ServerResponse {
825
928
  // Set per-request from the Cpeak instance. Undefined when compression isn't enabled.
826
929
  _compression;
827
930
  // Send a file back to the client
828
- async sendFile(path2, mime) {
931
+ async sendFile(path3, mime) {
932
+ if (this.headersSent) return;
829
933
  if (!mime) {
830
- const dotIndex = path2.lastIndexOf(".");
831
- const fileExtension = dotIndex >= 0 ? path2.slice(dotIndex + 1) : "";
934
+ const dotIndex = path3.lastIndexOf(".");
935
+ const fileExtension = dotIndex >= 0 ? path3.slice(dotIndex + 1) : "";
832
936
  mime = MIME_TYPES[fileExtension];
833
937
  if (!mime) {
834
938
  throw frameworkError(
835
- `MIME type is missing for "${path2}". Pass it as the second argument or register the extension via cpeak({ mimeTypes: { ${fileExtension || "ext"}: "..." } }).`,
939
+ `MIME type is missing for "${path3}". Pass it as the second argument or register the extension via cpeak({ mimeTypes: { ${fileExtension || "ext"}: "..." } }).`,
836
940
  this.sendFile,
837
941
  "CPEAK_ERR_MISSING_MIME" /* MISSING_MIME */
838
942
  );
839
943
  }
840
944
  }
841
945
  try {
842
- const stat = await fs3.stat(path2);
946
+ const stat = await fs2.stat(path3);
843
947
  if (!stat.isFile()) {
844
948
  throw frameworkError(
845
- `Not a file: ${path2}`,
949
+ `Not a file: ${path3}`,
846
950
  this.sendFile,
847
951
  "CPEAK_ERR_NOT_A_FILE" /* NOT_A_FILE */
848
952
  );
@@ -851,7 +955,7 @@ var CpeakServerResponse = class extends http.ServerResponse {
851
955
  await compressAndSend(
852
956
  this,
853
957
  mime,
854
- createReadStream(path2),
958
+ createReadStream2(path3),
855
959
  this._compression,
856
960
  stat.size
857
961
  );
@@ -859,19 +963,21 @@ var CpeakServerResponse = class extends http.ServerResponse {
859
963
  }
860
964
  this.setHeader("Content-Type", mime);
861
965
  this.setHeader("Content-Length", String(stat.size));
862
- await pipeline2(createReadStream(path2), this);
966
+ await pipeline3(createReadStream2(path3), this);
863
967
  } catch (err) {
864
968
  if (err?.code === "ENOENT") {
865
969
  throw frameworkError(
866
- `File not found: ${path2}`,
970
+ `File not found: ${path3}`,
867
971
  this.sendFile,
868
972
  "CPEAK_ERR_FILE_NOT_FOUND" /* FILE_NOT_FOUND */
869
973
  );
870
974
  }
871
975
  throw frameworkError(
872
- `Failed to send file: ${path2}`,
976
+ `Failed to send file: ${path3}`,
873
977
  this.sendFile,
874
- "CPEAK_ERR_SEND_FILE_FAIL" /* SEND_FILE_FAIL */
978
+ "CPEAK_ERR_SEND_FILE_FAIL" /* SEND_FILE_FAIL */,
979
+ void 0,
980
+ isClientDisconnect(err)
875
981
  );
876
982
  }
877
983
  }
@@ -894,6 +1000,7 @@ var CpeakServerResponse = class extends http.ServerResponse {
894
1000
  // Send a json data back to the client.
895
1001
  // This is only good for bodies that their size is less than the highWaterMark value.
896
1002
  json(data) {
1003
+ if (this.headersSent) return Promise.resolve();
897
1004
  const body = JSON.stringify(data);
898
1005
  if (this._compression) {
899
1006
  return compressAndSend(this, "application/json", body, this._compression);
@@ -902,8 +1009,16 @@ var CpeakServerResponse = class extends http.ServerResponse {
902
1009
  this.end(body);
903
1010
  return Promise.resolve();
904
1011
  }
1012
+ render() {
1013
+ throw frameworkError(
1014
+ "render middleware not registered. Add render() via app.beforeEach(render()) to use res.render.",
1015
+ this.render,
1016
+ "CPEAK_ERR_RENDER_NOT_ENABLED" /* RENDER_NOT_ENABLED */
1017
+ );
1018
+ }
905
1019
  // Explicit compression entry point. A developer can use this in any custom handler to compress arbitrary responses
906
1020
  compress(mime, body, size) {
1021
+ if (this.headersSent) return Promise.resolve();
907
1022
  if (!this._compression) {
908
1023
  throw frameworkError(
909
1024
  "compression is not enabled. Pass `compression` to cpeak({ compression: true | { ... } }) to use res.compress.",
@@ -919,6 +1034,7 @@ var Cpeak = class {
919
1034
  #router;
920
1035
  #middleware;
921
1036
  #handleErr;
1037
+ #fallback;
922
1038
  #compression;
923
1039
  constructor(options = {}) {
924
1040
  this.#server = http.createServer({
@@ -940,9 +1056,12 @@ var Cpeak = class {
940
1056
  const dispatchError = async (error) => {
941
1057
  if (res.headersSent) {
942
1058
  req.socket?.destroy();
943
- return;
1059
+ } else {
1060
+ res.setHeader("Connection", "close");
1061
+ }
1062
+ if (isClientDisconnect(error) && !error.clientDisconnect) {
1063
+ error.clientDisconnect = true;
944
1064
  }
945
- res.setHeader("Connection", "close");
946
1065
  try {
947
1066
  await this.#handleErr?.(error, req, res);
948
1067
  } catch (handlerFailure) {
@@ -995,6 +1114,13 @@ var Cpeak = class {
995
1114
  0
996
1115
  );
997
1116
  }
1117
+ if (this.#fallback) {
1118
+ try {
1119
+ return await this.#fallback(req2, res2);
1120
+ } catch (error) {
1121
+ return dispatchError(error);
1122
+ }
1123
+ }
998
1124
  return res2.status(404).json({ error: `Cannot ${req2.method} ${urlWithoutQueries}` });
999
1125
  } else {
1000
1126
  try {
@@ -1013,13 +1139,13 @@ var Cpeak = class {
1013
1139
  }
1014
1140
  );
1015
1141
  }
1016
- route(method, path2, ...args) {
1142
+ route(method, path3, ...args) {
1017
1143
  const cb = args.pop();
1018
1144
  if (!cb || typeof cb !== "function") {
1019
1145
  throw new Error("Route definition must include a handler");
1020
1146
  }
1021
1147
  const middleware = args.flat();
1022
- this.#router.add(method, path2, middleware, cb);
1148
+ this.#router.add(method, path3, middleware, cb);
1023
1149
  }
1024
1150
  beforeEach(cb) {
1025
1151
  this.#middleware.push(cb);
@@ -1027,6 +1153,17 @@ var Cpeak = class {
1027
1153
  handleErr(cb) {
1028
1154
  this.#handleErr = cb;
1029
1155
  }
1156
+ // This will handle any request that doesn't match any of the routes and middleware functions
1157
+ fallback(cb) {
1158
+ if (this.#fallback) {
1159
+ throw frameworkError(
1160
+ "Fallback handler is already registered. Only one fallback can be set per app.",
1161
+ this.fallback,
1162
+ "CPEAK_ERR_DUPLICATE_FALLBACK" /* DUPLICATE_FALLBACK */
1163
+ );
1164
+ }
1165
+ this.#fallback = cb;
1166
+ }
1030
1167
  listen(...args) {
1031
1168
  return this.#server.listen(...args);
1032
1169
  }
@@ -1055,6 +1192,7 @@ export {
1055
1192
  cpeak as default,
1056
1193
  frameworkError,
1057
1194
  hashPassword,
1195
+ isClientDisconnect,
1058
1196
  parseJSON,
1059
1197
  render,
1060
1198
  serveStatic,