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 +54 -45
- package/dist/index.js +207 -69
- package/dist/index.js.map +1 -1
- package/lib/index.ts +26 -5
- package/lib/internal/errors.ts +18 -2
- package/lib/internal/mimeTypes.ts +10 -1
- package/lib/types.ts +14 -8
- package/lib/utils/render.ts +134 -56
- package/lib/utils/serveStatic.ts +8 -1
- package/package.json +1 -1
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
|
|
4
|
-
import { createReadStream } from "fs";
|
|
5
|
-
import { pipeline as
|
|
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
|
-
|
|
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,
|
|
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(
|
|
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 "${
|
|
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 "${
|
|
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()} ${
|
|
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 "${
|
|
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()} ${
|
|
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,
|
|
261
|
+
find(method, path3) {
|
|
240
262
|
const root = this.#treesByMethod.get(method.toLowerCase());
|
|
241
263
|
if (!root) return null;
|
|
242
|
-
const segments = splitPath(
|
|
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(
|
|
310
|
-
if (
|
|
311
|
-
const withoutLeadingSlash =
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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
|
-
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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
|
-
|
|
449
|
-
const
|
|
450
|
-
|
|
451
|
-
|
|
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
|
-
|
|
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 (
|
|
541
|
+
res.render = async (filePath, data, mime) => {
|
|
542
|
+
if (res.headersSent) return;
|
|
458
543
|
if (!mime) {
|
|
459
|
-
const dotIndex =
|
|
460
|
-
const fileExtension = dotIndex >= 0 ?
|
|
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 "${
|
|
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
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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
|
|
669
|
-
parts.push(`Path=${
|
|
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(
|
|
931
|
+
async sendFile(path3, mime) {
|
|
932
|
+
if (this.headersSent) return;
|
|
829
933
|
if (!mime) {
|
|
830
|
-
const dotIndex =
|
|
831
|
-
const fileExtension = dotIndex >= 0 ?
|
|
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 "${
|
|
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
|
|
946
|
+
const stat = await fs2.stat(path3);
|
|
843
947
|
if (!stat.isFile()) {
|
|
844
948
|
throw frameworkError(
|
|
845
|
-
`Not a file: ${
|
|
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
|
-
|
|
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
|
|
966
|
+
await pipeline3(createReadStream2(path3), this);
|
|
863
967
|
} catch (err) {
|
|
864
968
|
if (err?.code === "ENOENT") {
|
|
865
969
|
throw frameworkError(
|
|
866
|
-
`File not found: ${
|
|
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: ${
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|