cpeak 2.7.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/README.md +120 -72
- package/dist/index.d.ts +77 -75
- package/dist/index.js +307 -135
- package/dist/index.js.map +1 -1
- package/lib/index.ts +108 -118
- package/lib/internal/errors.ts +35 -0
- package/lib/internal/mimeTypes.ts +22 -0
- package/lib/internal/router.ts +259 -0
- package/lib/types.ts +18 -20
- package/lib/utils/render.ts +11 -6
- package/lib/utils/serveStatic.ts +29 -28
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import fs3 from "fs/promises";
|
|
|
4
4
|
import { createReadStream } from "fs";
|
|
5
5
|
import { pipeline as pipeline2 } from "stream/promises";
|
|
6
6
|
|
|
7
|
-
// lib/
|
|
7
|
+
// lib/internal/compression.ts
|
|
8
8
|
import zlib from "zlib";
|
|
9
9
|
import { Readable } from "stream";
|
|
10
10
|
import { Buffer as Buffer2 } from "buffer";
|
|
@@ -114,6 +114,204 @@ async function compressAndSend(res, mime, body, config, size) {
|
|
|
114
114
|
);
|
|
115
115
|
}
|
|
116
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
|
+
}
|
|
314
|
+
|
|
117
315
|
// lib/utils/parseJSON.ts
|
|
118
316
|
import { Buffer as Buffer3 } from "buffer";
|
|
119
317
|
function isJSON(contentType) {
|
|
@@ -171,30 +369,25 @@ var parseJSON = (options = {}) => {
|
|
|
171
369
|
// lib/utils/serveStatic.ts
|
|
172
370
|
import fs from "fs";
|
|
173
371
|
import path from "path";
|
|
174
|
-
var
|
|
175
|
-
html: "text/html",
|
|
176
|
-
css: "text/css",
|
|
177
|
-
js: "application/javascript",
|
|
178
|
-
jpg: "image/jpeg",
|
|
179
|
-
jpeg: "image/jpeg",
|
|
180
|
-
png: "image/png",
|
|
181
|
-
svg: "image/svg+xml",
|
|
182
|
-
txt: "text/plain",
|
|
183
|
-
eot: "application/vnd.ms-fontobject",
|
|
184
|
-
otf: "font/otf",
|
|
185
|
-
ttf: "font/ttf",
|
|
186
|
-
woff: "font/woff",
|
|
187
|
-
woff2: "font/woff2",
|
|
188
|
-
gif: "image/gif",
|
|
189
|
-
ico: "image/x-icon",
|
|
190
|
-
json: "application/json",
|
|
191
|
-
webmanifest: "application/manifest+json"
|
|
192
|
-
};
|
|
193
|
-
var serveStatic = (folderPath, newMimeTypes, options) => {
|
|
194
|
-
if (newMimeTypes) {
|
|
195
|
-
Object.assign(MIME_TYPES, newMimeTypes);
|
|
196
|
-
}
|
|
372
|
+
var serveStatic = (folderPath, options) => {
|
|
197
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
|
+
}
|
|
198
391
|
function processFolder(folderPath2, parentFolder) {
|
|
199
392
|
const staticFiles = [];
|
|
200
393
|
const files = fs.readdirSync(folderPath2);
|
|
@@ -263,16 +456,20 @@ var render = () => {
|
|
|
263
456
|
return function(req, res, next) {
|
|
264
457
|
res.render = async (path2, data, mime) => {
|
|
265
458
|
if (!mime) {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
)
|
|
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
|
+
}
|
|
270
468
|
}
|
|
271
469
|
let fileStr = await fs2.readFile(path2, "utf-8");
|
|
272
470
|
const finalStr = renderTemplate(fileStr, data);
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
await compressAndSend(res, mime, finalStr, config);
|
|
471
|
+
if (res._compression) {
|
|
472
|
+
await compressAndSend(res, mime, finalStr, res._compression);
|
|
276
473
|
return;
|
|
277
474
|
}
|
|
278
475
|
res.setHeader("Content-Type", mime);
|
|
@@ -603,28 +800,6 @@ var cors = (options = {}) => {
|
|
|
603
800
|
};
|
|
604
801
|
|
|
605
802
|
// lib/index.ts
|
|
606
|
-
function frameworkError(message, skipFn, code, status) {
|
|
607
|
-
const err = new Error(message);
|
|
608
|
-
Error.captureStackTrace(err, skipFn);
|
|
609
|
-
err.cpeak_err = true;
|
|
610
|
-
if (code) err.code = code;
|
|
611
|
-
if (status) err.status = status;
|
|
612
|
-
return err;
|
|
613
|
-
}
|
|
614
|
-
var ErrorCode = /* @__PURE__ */ ((ErrorCode2) => {
|
|
615
|
-
ErrorCode2["MISSING_MIME"] = "CPEAK_ERR_MISSING_MIME";
|
|
616
|
-
ErrorCode2["FILE_NOT_FOUND"] = "CPEAK_ERR_FILE_NOT_FOUND";
|
|
617
|
-
ErrorCode2["NOT_A_FILE"] = "CPEAK_ERR_NOT_A_FILE";
|
|
618
|
-
ErrorCode2["SEND_FILE_FAIL"] = "CPEAK_ERR_SEND_FILE_FAIL";
|
|
619
|
-
ErrorCode2["INVALID_JSON"] = "CPEAK_ERR_INVALID_JSON";
|
|
620
|
-
ErrorCode2["PAYLOAD_TOO_LARGE"] = "CPEAK_ERR_PAYLOAD_TOO_LARGE";
|
|
621
|
-
ErrorCode2["WEAK_SECRET"] = "CPEAK_ERR_WEAK_SECRET";
|
|
622
|
-
ErrorCode2["COMPRESSION_NOT_ENABLED"] = "CPEAK_ERR_COMPRESSION_NOT_ENABLED";
|
|
623
|
-
return ErrorCode2;
|
|
624
|
-
})(ErrorCode || {});
|
|
625
|
-
function compressionConfigFor(res) {
|
|
626
|
-
return res.socket?.server?._cpeakCompression;
|
|
627
|
-
}
|
|
628
803
|
var CpeakIncomingMessage = class extends http.IncomingMessage {
|
|
629
804
|
// We define body and params here for better V8 optimization (not changing the shape of the object at runtime)
|
|
630
805
|
body = void 0;
|
|
@@ -647,14 +822,21 @@ var CpeakIncomingMessage = class extends http.IncomingMessage {
|
|
|
647
822
|
}
|
|
648
823
|
};
|
|
649
824
|
var CpeakServerResponse = class extends http.ServerResponse {
|
|
825
|
+
// Set per-request from the Cpeak instance. Undefined when compression isn't enabled.
|
|
826
|
+
_compression;
|
|
650
827
|
// Send a file back to the client
|
|
651
828
|
async sendFile(path2, mime) {
|
|
652
829
|
if (!mime) {
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
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
|
+
}
|
|
658
840
|
}
|
|
659
841
|
try {
|
|
660
842
|
const stat = await fs3.stat(path2);
|
|
@@ -665,9 +847,14 @@ var CpeakServerResponse = class extends http.ServerResponse {
|
|
|
665
847
|
"CPEAK_ERR_NOT_A_FILE" /* NOT_A_FILE */
|
|
666
848
|
);
|
|
667
849
|
}
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
850
|
+
if (this._compression) {
|
|
851
|
+
await compressAndSend(
|
|
852
|
+
this,
|
|
853
|
+
mime,
|
|
854
|
+
createReadStream(path2),
|
|
855
|
+
this._compression,
|
|
856
|
+
stat.size
|
|
857
|
+
);
|
|
671
858
|
return;
|
|
672
859
|
}
|
|
673
860
|
this.setHeader("Content-Type", mime);
|
|
@@ -704,84 +891,91 @@ var CpeakServerResponse = class extends http.ServerResponse {
|
|
|
704
891
|
this.writeHead(302, { Location: location });
|
|
705
892
|
this.end();
|
|
706
893
|
}
|
|
707
|
-
// Send a json data back to the client.
|
|
708
|
-
//
|
|
709
|
-
// (async) when compression was enabled at cpeak() construction.
|
|
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.
|
|
710
896
|
json(data) {
|
|
711
897
|
const body = JSON.stringify(data);
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
return compressAndSend(this, "application/json", body, config);
|
|
898
|
+
if (this._compression) {
|
|
899
|
+
return compressAndSend(this, "application/json", body, this._compression);
|
|
715
900
|
}
|
|
716
901
|
this.setHeader("Content-Type", "application/json");
|
|
717
902
|
this.end(body);
|
|
903
|
+
return Promise.resolve();
|
|
718
904
|
}
|
|
719
|
-
// Explicit compression entry point.
|
|
720
|
-
// the developer asked to compress but the framework was never told to.
|
|
905
|
+
// Explicit compression entry point. A developer can use this in any custom handler to compress arbitrary responses
|
|
721
906
|
compress(mime, body, size) {
|
|
722
|
-
|
|
723
|
-
if (!config) {
|
|
907
|
+
if (!this._compression) {
|
|
724
908
|
throw frameworkError(
|
|
725
909
|
"compression is not enabled. Pass `compression` to cpeak({ compression: true | { ... } }) to use res.compress.",
|
|
726
910
|
this.compress,
|
|
727
911
|
"CPEAK_ERR_COMPRESSION_NOT_ENABLED" /* COMPRESSION_NOT_ENABLED */
|
|
728
912
|
);
|
|
729
913
|
}
|
|
730
|
-
return compressAndSend(this, mime, body,
|
|
914
|
+
return compressAndSend(this, mime, body, this._compression, size);
|
|
731
915
|
}
|
|
732
916
|
};
|
|
733
917
|
var Cpeak = class {
|
|
734
918
|
#server;
|
|
735
|
-
#
|
|
919
|
+
#router;
|
|
736
920
|
#middleware;
|
|
737
921
|
#handleErr;
|
|
922
|
+
#compression;
|
|
738
923
|
constructor(options = {}) {
|
|
739
924
|
this.#server = http.createServer({
|
|
740
925
|
IncomingMessage: CpeakIncomingMessage,
|
|
741
926
|
ServerResponse: CpeakServerResponse
|
|
742
927
|
});
|
|
743
|
-
this.#
|
|
928
|
+
this.#router = new Router();
|
|
744
929
|
this.#middleware = [];
|
|
745
930
|
if (options.compression) {
|
|
746
|
-
this.#
|
|
747
|
-
options.compression
|
|
748
|
-
);
|
|
931
|
+
this.#compression = resolveCompressionOptions(options.compression);
|
|
749
932
|
}
|
|
933
|
+
if (options.mimeTypes) Object.assign(MIME_TYPES, options.mimeTypes);
|
|
750
934
|
this.#server.on(
|
|
751
935
|
"request",
|
|
752
936
|
async (req, res) => {
|
|
937
|
+
res._compression = this.#compression;
|
|
753
938
|
const qIndex = req.url?.indexOf("?");
|
|
754
939
|
const urlWithoutQueries = qIndex === -1 ? req.url || "" : req.url?.substring(0, qIndex);
|
|
755
|
-
const dispatchError = (error) => {
|
|
940
|
+
const dispatchError = async (error) => {
|
|
756
941
|
if (res.headersSent) {
|
|
757
942
|
req.socket?.destroy();
|
|
758
943
|
return;
|
|
759
944
|
}
|
|
760
945
|
res.setHeader("Connection", "close");
|
|
761
|
-
|
|
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
|
+
}
|
|
762
963
|
};
|
|
763
964
|
const runHandler = async (req2, res2, middleware, cb, index) => {
|
|
764
965
|
if (index === middleware.length) {
|
|
765
966
|
try {
|
|
766
|
-
await cb(req2, res2
|
|
967
|
+
await cb(req2, res2);
|
|
767
968
|
} catch (error) {
|
|
768
969
|
dispatchError(error);
|
|
769
970
|
}
|
|
770
971
|
} else {
|
|
771
972
|
try {
|
|
772
|
-
await middleware[index](
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
return dispatchError(error);
|
|
779
|
-
}
|
|
780
|
-
await runHandler(req2, res2, middleware, cb, index + 1);
|
|
781
|
-
},
|
|
782
|
-
// Error handler for a route middleware
|
|
783
|
-
dispatchError
|
|
784
|
-
);
|
|
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
|
+
});
|
|
785
979
|
} catch (error) {
|
|
786
980
|
dispatchError(error);
|
|
787
981
|
}
|
|
@@ -789,25 +983,18 @@ var Cpeak = class {
|
|
|
789
983
|
};
|
|
790
984
|
const runMiddleware = async (req2, res2, middleware, index) => {
|
|
791
985
|
if (index === middleware.length) {
|
|
792
|
-
const
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
res2,
|
|
805
|
-
route.middleware,
|
|
806
|
-
route.cb,
|
|
807
|
-
0
|
|
808
|
-
);
|
|
809
|
-
}
|
|
810
|
-
}
|
|
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
|
+
}
|
|
811
998
|
return res2.status(404).json({ error: `Cannot ${req2.method} ${urlWithoutQueries}` });
|
|
812
999
|
} else {
|
|
813
1000
|
try {
|
|
@@ -827,14 +1014,12 @@ var Cpeak = class {
|
|
|
827
1014
|
);
|
|
828
1015
|
}
|
|
829
1016
|
route(method, path2, ...args) {
|
|
830
|
-
if (!this.#routes[method]) this.#routes[method] = [];
|
|
831
1017
|
const cb = args.pop();
|
|
832
1018
|
if (!cb || typeof cb !== "function") {
|
|
833
1019
|
throw new Error("Route definition must include a handler");
|
|
834
1020
|
}
|
|
835
1021
|
const middleware = args.flat();
|
|
836
|
-
|
|
837
|
-
this.#routes[method].push({ path: path2, regex, middleware, cb });
|
|
1022
|
+
this.#router.add(method, path2, middleware, cb);
|
|
838
1023
|
}
|
|
839
1024
|
beforeEach(cb) {
|
|
840
1025
|
this.#middleware.push(cb);
|
|
@@ -842,31 +1027,18 @@ var Cpeak = class {
|
|
|
842
1027
|
handleErr(cb) {
|
|
843
1028
|
this.#handleErr = cb;
|
|
844
1029
|
}
|
|
845
|
-
listen(
|
|
846
|
-
return this.#server.listen(
|
|
1030
|
+
listen(...args) {
|
|
1031
|
+
return this.#server.listen(...args);
|
|
847
1032
|
}
|
|
848
1033
|
address() {
|
|
849
1034
|
return this.#server.address();
|
|
850
1035
|
}
|
|
851
1036
|
close(cb) {
|
|
852
|
-
this.#server.close(cb);
|
|
853
|
-
}
|
|
854
|
-
//
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
#pathToRegex(path2) {
|
|
858
|
-
const regexString = "^" + path2.replace(/:\w+/g, "([^/]+)").replace(/\*/g, ".*") + "$";
|
|
859
|
-
return new RegExp(regexString);
|
|
860
|
-
}
|
|
861
|
-
#extractPathVariables(path2, match) {
|
|
862
|
-
const paramNames = (path2.match(/:\w+/g) || []).map(
|
|
863
|
-
(param) => param.slice(1)
|
|
864
|
-
);
|
|
865
|
-
const params = {};
|
|
866
|
-
paramNames.forEach((name, index) => {
|
|
867
|
-
params[name] = match[index + 1];
|
|
868
|
-
});
|
|
869
|
-
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;
|
|
870
1042
|
}
|
|
871
1043
|
};
|
|
872
1044
|
function cpeak(options) {
|