cpeak 2.7.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/README.md +120 -72
- package/dist/index.d.ts +59 -48
- package/dist/index.js +484 -174
- package/dist/index.js.map +1 -1
- package/lib/index.ts +132 -121
- package/lib/internal/errors.ts +51 -0
- package/lib/internal/mimeTypes.ts +31 -0
- package/lib/internal/router.ts +259 -0
- package/lib/types.ts +29 -25
- package/lib/utils/render.ts +142 -59
- package/lib/utils/serveStatic.ts +35 -27
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
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
|
-
// 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,226 @@ 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
|
+
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"
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// lib/internal/errors.ts
|
|
148
|
+
function frameworkError(message, skipFn, code, status, clientDisconnect) {
|
|
149
|
+
const err = new Error(message);
|
|
150
|
+
Error.captureStackTrace(err, skipFn);
|
|
151
|
+
err.cpeak_err = true;
|
|
152
|
+
if (code) err.code = code;
|
|
153
|
+
if (status) err.status = status;
|
|
154
|
+
if (clientDisconnect) err.clientDisconnect = true;
|
|
155
|
+
return err;
|
|
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
|
+
}
|
|
166
|
+
var ErrorCode = /* @__PURE__ */ ((ErrorCode2) => {
|
|
167
|
+
ErrorCode2["MISSING_MIME"] = "CPEAK_ERR_MISSING_MIME";
|
|
168
|
+
ErrorCode2["FILE_NOT_FOUND"] = "CPEAK_ERR_FILE_NOT_FOUND";
|
|
169
|
+
ErrorCode2["NOT_A_FILE"] = "CPEAK_ERR_NOT_A_FILE";
|
|
170
|
+
ErrorCode2["SEND_FILE_FAIL"] = "CPEAK_ERR_SEND_FILE_FAIL";
|
|
171
|
+
ErrorCode2["INVALID_JSON"] = "CPEAK_ERR_INVALID_JSON";
|
|
172
|
+
ErrorCode2["PAYLOAD_TOO_LARGE"] = "CPEAK_ERR_PAYLOAD_TOO_LARGE";
|
|
173
|
+
ErrorCode2["WEAK_SECRET"] = "CPEAK_ERR_WEAK_SECRET";
|
|
174
|
+
ErrorCode2["COMPRESSION_NOT_ENABLED"] = "CPEAK_ERR_COMPRESSION_NOT_ENABLED";
|
|
175
|
+
ErrorCode2["RENDER_NOT_ENABLED"] = "CPEAK_ERR_RENDER_NOT_ENABLED";
|
|
176
|
+
ErrorCode2["DUPLICATE_ROUTE"] = "CPEAK_ERR_DUPLICATE_ROUTE";
|
|
177
|
+
ErrorCode2["INVALID_ROUTE"] = "CPEAK_ERR_INVALID_ROUTE";
|
|
178
|
+
ErrorCode2["DUPLICATE_FALLBACK"] = "CPEAK_ERR_DUPLICATE_FALLBACK";
|
|
179
|
+
ErrorCode2["RENDER_FAIL"] = "CPEAK_ERR_RENDER_FAIL";
|
|
180
|
+
return ErrorCode2;
|
|
181
|
+
})(ErrorCode || {});
|
|
182
|
+
|
|
183
|
+
// lib/internal/router.ts
|
|
184
|
+
function createNode() {
|
|
185
|
+
return { staticChildren: /* @__PURE__ */ new Map() };
|
|
186
|
+
}
|
|
187
|
+
var Router = class {
|
|
188
|
+
#treesByMethod = /* @__PURE__ */ new Map();
|
|
189
|
+
add(method, path3, middleware, handler) {
|
|
190
|
+
const methodKey = method.toLowerCase();
|
|
191
|
+
let root = this.#treesByMethod.get(methodKey);
|
|
192
|
+
if (!root) {
|
|
193
|
+
root = createNode();
|
|
194
|
+
this.#treesByMethod.set(methodKey, root);
|
|
195
|
+
}
|
|
196
|
+
const segments = splitPath(path3);
|
|
197
|
+
const paramNames = [];
|
|
198
|
+
let currentNode = root;
|
|
199
|
+
for (let i = 0; i < segments.length; i++) {
|
|
200
|
+
const segment = segments[i];
|
|
201
|
+
const isLastSegment = i === segments.length - 1;
|
|
202
|
+
if (segment.length > 1 && segment.startsWith("*")) {
|
|
203
|
+
throw frameworkError(
|
|
204
|
+
`Invalid route "${path3}": named wildcards (e.g. "*name") are not supported. Use a plain "*" at the end of the path.`,
|
|
205
|
+
this.add,
|
|
206
|
+
"CPEAK_ERR_INVALID_ROUTE" /* INVALID_ROUTE */
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
if (segment === "*") {
|
|
210
|
+
if (!isLastSegment) {
|
|
211
|
+
throw frameworkError(
|
|
212
|
+
`Invalid route "${path3}": "*" is only allowed as the final path segment.`,
|
|
213
|
+
this.add,
|
|
214
|
+
"CPEAK_ERR_INVALID_ROUTE" /* INVALID_ROUTE */
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
if (currentNode.wildcardChild) {
|
|
218
|
+
throw frameworkError(
|
|
219
|
+
`Duplicate route: ${method.toUpperCase()} ${path3}`,
|
|
220
|
+
this.add,
|
|
221
|
+
"CPEAK_ERR_DUPLICATE_ROUTE" /* DUPLICATE_ROUTE */
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
currentNode.wildcardChild = { handler, middleware, paramNames };
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (segment.startsWith(":")) {
|
|
228
|
+
const paramName = segment.slice(1);
|
|
229
|
+
if (!paramName) {
|
|
230
|
+
throw frameworkError(
|
|
231
|
+
`Invalid route "${path3}": empty parameter name.`,
|
|
232
|
+
this.add,
|
|
233
|
+
"CPEAK_ERR_INVALID_ROUTE" /* INVALID_ROUTE */
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
paramNames.push(paramName);
|
|
237
|
+
if (!currentNode.paramChild) {
|
|
238
|
+
currentNode.paramChild = createNode();
|
|
239
|
+
}
|
|
240
|
+
currentNode = currentNode.paramChild;
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
let staticChild = currentNode.staticChildren.get(segment);
|
|
244
|
+
if (!staticChild) {
|
|
245
|
+
staticChild = createNode();
|
|
246
|
+
currentNode.staticChildren.set(segment, staticChild);
|
|
247
|
+
}
|
|
248
|
+
currentNode = staticChild;
|
|
249
|
+
}
|
|
250
|
+
if (currentNode.handler) {
|
|
251
|
+
throw frameworkError(
|
|
252
|
+
`Duplicate route: ${method.toUpperCase()} ${path3}`,
|
|
253
|
+
this.add,
|
|
254
|
+
"CPEAK_ERR_DUPLICATE_ROUTE" /* DUPLICATE_ROUTE */
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
currentNode.handler = handler;
|
|
258
|
+
currentNode.middleware = middleware;
|
|
259
|
+
currentNode.paramNames = paramNames;
|
|
260
|
+
}
|
|
261
|
+
find(method, path3) {
|
|
262
|
+
const root = this.#treesByMethod.get(method.toLowerCase());
|
|
263
|
+
if (!root) return null;
|
|
264
|
+
const segments = splitPath(path3);
|
|
265
|
+
return matchSegments(root, segments, 0, []);
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
function matchSegments(node, segments, segmentIndex, capturedValues) {
|
|
269
|
+
if (segmentIndex === segments.length) {
|
|
270
|
+
if (node.handler) {
|
|
271
|
+
return {
|
|
272
|
+
middleware: node.middleware,
|
|
273
|
+
handler: node.handler,
|
|
274
|
+
params: zipParams(node.paramNames, capturedValues)
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
if (node.wildcardChild) {
|
|
278
|
+
return {
|
|
279
|
+
middleware: node.wildcardChild.middleware,
|
|
280
|
+
handler: node.wildcardChild.handler,
|
|
281
|
+
params: zipParams(node.wildcardChild.paramNames, capturedValues)
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
const segment = segments[segmentIndex];
|
|
287
|
+
const staticChild = node.staticChildren.get(segment);
|
|
288
|
+
if (staticChild) {
|
|
289
|
+
const foundMatch = matchSegments(
|
|
290
|
+
staticChild,
|
|
291
|
+
segments,
|
|
292
|
+
segmentIndex + 1,
|
|
293
|
+
capturedValues
|
|
294
|
+
);
|
|
295
|
+
if (foundMatch) return foundMatch;
|
|
296
|
+
}
|
|
297
|
+
if (node.paramChild) {
|
|
298
|
+
capturedValues.push(safeDecode(segment));
|
|
299
|
+
const foundMatch = matchSegments(
|
|
300
|
+
node.paramChild,
|
|
301
|
+
segments,
|
|
302
|
+
segmentIndex + 1,
|
|
303
|
+
capturedValues
|
|
304
|
+
);
|
|
305
|
+
if (foundMatch) return foundMatch;
|
|
306
|
+
capturedValues.pop();
|
|
307
|
+
}
|
|
308
|
+
if (node.wildcardChild) {
|
|
309
|
+
return {
|
|
310
|
+
middleware: node.wildcardChild.middleware,
|
|
311
|
+
handler: node.wildcardChild.handler,
|
|
312
|
+
params: zipParams(node.wildcardChild.paramNames, capturedValues)
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
function zipParams(names, values) {
|
|
318
|
+
const params = {};
|
|
319
|
+
for (let i = 0; i < names.length; i++) {
|
|
320
|
+
params[names[i]] = values[i];
|
|
321
|
+
}
|
|
322
|
+
return params;
|
|
323
|
+
}
|
|
324
|
+
function safeDecode(segment) {
|
|
325
|
+
try {
|
|
326
|
+
return decodeURIComponent(segment);
|
|
327
|
+
} catch {
|
|
328
|
+
return segment;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
function splitPath(path3) {
|
|
332
|
+
if (path3 === "" || path3 === "/") return [];
|
|
333
|
+
const withoutLeadingSlash = path3.startsWith("/") ? path3.slice(1) : path3;
|
|
334
|
+
return withoutLeadingSlash.split("/");
|
|
335
|
+
}
|
|
336
|
+
|
|
117
337
|
// lib/utils/parseJSON.ts
|
|
118
338
|
import { Buffer as Buffer3 } from "buffer";
|
|
119
339
|
function isJSON(contentType) {
|
|
@@ -171,39 +391,40 @@ var parseJSON = (options = {}) => {
|
|
|
171
391
|
// lib/utils/serveStatic.ts
|
|
172
392
|
import fs from "fs";
|
|
173
393
|
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
|
-
}
|
|
394
|
+
var serveStatic = (folderPath, options) => {
|
|
197
395
|
const prefix = options?.prefix ?? "";
|
|
396
|
+
const live = options?.live ?? false;
|
|
397
|
+
if (live) {
|
|
398
|
+
const resolvedFolder2 = path.resolve(folderPath);
|
|
399
|
+
const excludes2 = (options?.exclude ?? []).map((e) => path.join(resolvedFolder2, e));
|
|
400
|
+
return async function(req, res, next) {
|
|
401
|
+
const url = req.url;
|
|
402
|
+
if (typeof url !== "string") return next();
|
|
403
|
+
const pathname = url.split("?")[0];
|
|
404
|
+
const unprefixed = prefix ? pathname.slice(prefix.length) : pathname;
|
|
405
|
+
const filePath = path.join(resolvedFolder2, unprefixed);
|
|
406
|
+
const fileExtension = path.extname(filePath).slice(1);
|
|
407
|
+
const mime = MIME_TYPES[fileExtension];
|
|
408
|
+
if (!mime || !filePath.startsWith(resolvedFolder2)) return next();
|
|
409
|
+
if (excludes2.some((e) => filePath.startsWith(e))) return next();
|
|
410
|
+
const stat = await fs.promises.stat(filePath).catch(() => null);
|
|
411
|
+
if (stat?.isFile()) return res.sendFile(filePath, mime);
|
|
412
|
+
next();
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
const resolvedFolder = path.resolve(folderPath);
|
|
416
|
+
const excludes = (options?.exclude ?? []).map((e) => path.join(resolvedFolder, e));
|
|
198
417
|
function processFolder(folderPath2, parentFolder) {
|
|
199
418
|
const staticFiles = [];
|
|
200
419
|
const files = fs.readdirSync(folderPath2);
|
|
201
420
|
for (const file of files) {
|
|
202
421
|
const fullPath = path.join(folderPath2, file);
|
|
203
422
|
if (fs.statSync(fullPath).isDirectory()) {
|
|
423
|
+
if (excludes.some((e) => fullPath.startsWith(e))) continue;
|
|
204
424
|
const subfolderFiles = processFolder(fullPath, parentFolder);
|
|
205
425
|
staticFiles.push(...subfolderFiles);
|
|
206
426
|
} else {
|
|
427
|
+
if (excludes.some((e) => fullPath.startsWith(e))) continue;
|
|
207
428
|
const relativePath = path.relative(parentFolder, fullPath);
|
|
208
429
|
const fileExtension = path.extname(file).slice(1);
|
|
209
430
|
if (MIME_TYPES[fileExtension]) staticFiles.push("/" + relativePath);
|
|
@@ -236,47 +457,126 @@ var serveStatic = (folderPath, newMimeTypes, options) => {
|
|
|
236
457
|
};
|
|
237
458
|
|
|
238
459
|
// lib/utils/render.ts
|
|
239
|
-
import
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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;
|
|
248
482
|
}
|
|
249
|
-
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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);
|
|
254
493
|
}
|
|
255
|
-
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
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();
|
|
259
499
|
}
|
|
260
|
-
|
|
261
|
-
|
|
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
|
+
};
|
|
262
539
|
var render = () => {
|
|
263
540
|
return function(req, res, next) {
|
|
264
|
-
res.render = async (
|
|
541
|
+
res.render = async (filePath, data, mime) => {
|
|
542
|
+
if (res.headersSent) return;
|
|
265
543
|
if (!mime) {
|
|
544
|
+
const dotIndex = filePath.lastIndexOf(".");
|
|
545
|
+
const fileExtension = dotIndex >= 0 ? filePath.slice(dotIndex + 1) : "";
|
|
546
|
+
mime = MIME_TYPES[fileExtension];
|
|
547
|
+
if (!mime) {
|
|
548
|
+
throw frameworkError(
|
|
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 */
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
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) {
|
|
266
572
|
throw frameworkError(
|
|
267
|
-
`
|
|
268
|
-
res.render
|
|
573
|
+
`Failed to render "${filePath}." Error: ${err}`,
|
|
574
|
+
res.render,
|
|
575
|
+
"CPEAK_ERR_RENDER_FAIL" /* RENDER_FAIL */,
|
|
576
|
+
void 0,
|
|
577
|
+
isClientDisconnect(err)
|
|
269
578
|
);
|
|
270
579
|
}
|
|
271
|
-
let fileStr = await fs2.readFile(path2, "utf-8");
|
|
272
|
-
const finalStr = renderTemplate(fileStr, data);
|
|
273
|
-
const config = res.socket?.server?._cpeakCompression;
|
|
274
|
-
if (config) {
|
|
275
|
-
await compressAndSend(res, mime, finalStr, config);
|
|
276
|
-
return;
|
|
277
|
-
}
|
|
278
|
-
res.setHeader("Content-Type", mime);
|
|
279
|
-
res.end(finalStr);
|
|
280
580
|
};
|
|
281
581
|
next();
|
|
282
582
|
};
|
|
@@ -468,8 +768,8 @@ function parseRawCookies(header) {
|
|
|
468
768
|
}
|
|
469
769
|
function buildSetCookieHeader(name, value, options) {
|
|
470
770
|
const parts = [`${name}=${encodeURIComponent(value)}`];
|
|
471
|
-
const
|
|
472
|
-
parts.push(`Path=${
|
|
771
|
+
const path3 = options.path ?? "/";
|
|
772
|
+
parts.push(`Path=${path3}`);
|
|
473
773
|
if (options.domain) parts.push(`Domain=${options.domain}`);
|
|
474
774
|
if (options.maxAge !== void 0)
|
|
475
775
|
parts.push(`Max-Age=${Math.floor(options.maxAge / 1e3)}`);
|
|
@@ -603,28 +903,6 @@ var cors = (options = {}) => {
|
|
|
603
903
|
};
|
|
604
904
|
|
|
605
905
|
// 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
906
|
var CpeakIncomingMessage = class extends http.IncomingMessage {
|
|
629
907
|
// We define body and params here for better V8 optimization (not changing the shape of the object at runtime)
|
|
630
908
|
body = void 0;
|
|
@@ -647,44 +925,59 @@ var CpeakIncomingMessage = class extends http.IncomingMessage {
|
|
|
647
925
|
}
|
|
648
926
|
};
|
|
649
927
|
var CpeakServerResponse = class extends http.ServerResponse {
|
|
928
|
+
// Set per-request from the Cpeak instance. Undefined when compression isn't enabled.
|
|
929
|
+
_compression;
|
|
650
930
|
// Send a file back to the client
|
|
651
|
-
async sendFile(
|
|
931
|
+
async sendFile(path3, mime) {
|
|
932
|
+
if (this.headersSent) return;
|
|
652
933
|
if (!mime) {
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
934
|
+
const dotIndex = path3.lastIndexOf(".");
|
|
935
|
+
const fileExtension = dotIndex >= 0 ? path3.slice(dotIndex + 1) : "";
|
|
936
|
+
mime = MIME_TYPES[fileExtension];
|
|
937
|
+
if (!mime) {
|
|
938
|
+
throw frameworkError(
|
|
939
|
+
`MIME type is missing for "${path3}". Pass it as the second argument or register the extension via cpeak({ mimeTypes: { ${fileExtension || "ext"}: "..." } }).`,
|
|
940
|
+
this.sendFile,
|
|
941
|
+
"CPEAK_ERR_MISSING_MIME" /* MISSING_MIME */
|
|
942
|
+
);
|
|
943
|
+
}
|
|
658
944
|
}
|
|
659
945
|
try {
|
|
660
|
-
const stat = await
|
|
946
|
+
const stat = await fs2.stat(path3);
|
|
661
947
|
if (!stat.isFile()) {
|
|
662
948
|
throw frameworkError(
|
|
663
|
-
`Not a file: ${
|
|
949
|
+
`Not a file: ${path3}`,
|
|
664
950
|
this.sendFile,
|
|
665
951
|
"CPEAK_ERR_NOT_A_FILE" /* NOT_A_FILE */
|
|
666
952
|
);
|
|
667
953
|
}
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
954
|
+
if (this._compression) {
|
|
955
|
+
await compressAndSend(
|
|
956
|
+
this,
|
|
957
|
+
mime,
|
|
958
|
+
createReadStream2(path3),
|
|
959
|
+
this._compression,
|
|
960
|
+
stat.size
|
|
961
|
+
);
|
|
671
962
|
return;
|
|
672
963
|
}
|
|
673
964
|
this.setHeader("Content-Type", mime);
|
|
674
965
|
this.setHeader("Content-Length", String(stat.size));
|
|
675
|
-
await
|
|
966
|
+
await pipeline3(createReadStream2(path3), this);
|
|
676
967
|
} catch (err) {
|
|
677
968
|
if (err?.code === "ENOENT") {
|
|
678
969
|
throw frameworkError(
|
|
679
|
-
`File not found: ${
|
|
970
|
+
`File not found: ${path3}`,
|
|
680
971
|
this.sendFile,
|
|
681
972
|
"CPEAK_ERR_FILE_NOT_FOUND" /* FILE_NOT_FOUND */
|
|
682
973
|
);
|
|
683
974
|
}
|
|
684
975
|
throw frameworkError(
|
|
685
|
-
`Failed to send file: ${
|
|
976
|
+
`Failed to send file: ${path3}`,
|
|
686
977
|
this.sendFile,
|
|
687
|
-
"CPEAK_ERR_SEND_FILE_FAIL" /* SEND_FILE_FAIL
|
|
978
|
+
"CPEAK_ERR_SEND_FILE_FAIL" /* SEND_FILE_FAIL */,
|
|
979
|
+
void 0,
|
|
980
|
+
isClientDisconnect(err)
|
|
688
981
|
);
|
|
689
982
|
}
|
|
690
983
|
}
|
|
@@ -704,84 +997,104 @@ var CpeakServerResponse = class extends http.ServerResponse {
|
|
|
704
997
|
this.writeHead(302, { Location: location });
|
|
705
998
|
this.end();
|
|
706
999
|
}
|
|
707
|
-
// Send a json data back to the client.
|
|
708
|
-
//
|
|
709
|
-
// (async) when compression was enabled at cpeak() construction.
|
|
1000
|
+
// Send a json data back to the client.
|
|
1001
|
+
// This is only good for bodies that their size is less than the highWaterMark value.
|
|
710
1002
|
json(data) {
|
|
1003
|
+
if (this.headersSent) return Promise.resolve();
|
|
711
1004
|
const body = JSON.stringify(data);
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
return compressAndSend(this, "application/json", body, config);
|
|
1005
|
+
if (this._compression) {
|
|
1006
|
+
return compressAndSend(this, "application/json", body, this._compression);
|
|
715
1007
|
}
|
|
716
1008
|
this.setHeader("Content-Type", "application/json");
|
|
717
1009
|
this.end(body);
|
|
1010
|
+
return Promise.resolve();
|
|
718
1011
|
}
|
|
719
|
-
|
|
720
|
-
|
|
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
|
+
}
|
|
1019
|
+
// Explicit compression entry point. A developer can use this in any custom handler to compress arbitrary responses
|
|
721
1020
|
compress(mime, body, size) {
|
|
722
|
-
|
|
723
|
-
if (!
|
|
1021
|
+
if (this.headersSent) return Promise.resolve();
|
|
1022
|
+
if (!this._compression) {
|
|
724
1023
|
throw frameworkError(
|
|
725
1024
|
"compression is not enabled. Pass `compression` to cpeak({ compression: true | { ... } }) to use res.compress.",
|
|
726
1025
|
this.compress,
|
|
727
1026
|
"CPEAK_ERR_COMPRESSION_NOT_ENABLED" /* COMPRESSION_NOT_ENABLED */
|
|
728
1027
|
);
|
|
729
1028
|
}
|
|
730
|
-
return compressAndSend(this, mime, body,
|
|
1029
|
+
return compressAndSend(this, mime, body, this._compression, size);
|
|
731
1030
|
}
|
|
732
1031
|
};
|
|
733
1032
|
var Cpeak = class {
|
|
734
1033
|
#server;
|
|
735
|
-
#
|
|
1034
|
+
#router;
|
|
736
1035
|
#middleware;
|
|
737
1036
|
#handleErr;
|
|
1037
|
+
#fallback;
|
|
1038
|
+
#compression;
|
|
738
1039
|
constructor(options = {}) {
|
|
739
1040
|
this.#server = http.createServer({
|
|
740
1041
|
IncomingMessage: CpeakIncomingMessage,
|
|
741
1042
|
ServerResponse: CpeakServerResponse
|
|
742
1043
|
});
|
|
743
|
-
this.#
|
|
1044
|
+
this.#router = new Router();
|
|
744
1045
|
this.#middleware = [];
|
|
745
1046
|
if (options.compression) {
|
|
746
|
-
this.#
|
|
747
|
-
options.compression
|
|
748
|
-
);
|
|
1047
|
+
this.#compression = resolveCompressionOptions(options.compression);
|
|
749
1048
|
}
|
|
1049
|
+
if (options.mimeTypes) Object.assign(MIME_TYPES, options.mimeTypes);
|
|
750
1050
|
this.#server.on(
|
|
751
1051
|
"request",
|
|
752
1052
|
async (req, res) => {
|
|
1053
|
+
res._compression = this.#compression;
|
|
753
1054
|
const qIndex = req.url?.indexOf("?");
|
|
754
1055
|
const urlWithoutQueries = qIndex === -1 ? req.url || "" : req.url?.substring(0, qIndex);
|
|
755
|
-
const dispatchError = (error) => {
|
|
1056
|
+
const dispatchError = async (error) => {
|
|
756
1057
|
if (res.headersSent) {
|
|
757
1058
|
req.socket?.destroy();
|
|
758
|
-
|
|
1059
|
+
} else {
|
|
1060
|
+
res.setHeader("Connection", "close");
|
|
1061
|
+
}
|
|
1062
|
+
if (isClientDisconnect(error) && !error.clientDisconnect) {
|
|
1063
|
+
error.clientDisconnect = true;
|
|
1064
|
+
}
|
|
1065
|
+
try {
|
|
1066
|
+
await this.#handleErr?.(error, req, res);
|
|
1067
|
+
} catch (handlerFailure) {
|
|
1068
|
+
console.error(
|
|
1069
|
+
"[cpeak] handleErr failed while processing:",
|
|
1070
|
+
error,
|
|
1071
|
+
"\nReason:",
|
|
1072
|
+
handlerFailure
|
|
1073
|
+
);
|
|
1074
|
+
if (!res.headersSent) {
|
|
1075
|
+
try {
|
|
1076
|
+
res.statusCode = 500;
|
|
1077
|
+
res.end();
|
|
1078
|
+
} catch {
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
759
1081
|
}
|
|
760
|
-
res.setHeader("Connection", "close");
|
|
761
|
-
this.#handleErr?.(error, req, res);
|
|
762
1082
|
};
|
|
763
1083
|
const runHandler = async (req2, res2, middleware, cb, index) => {
|
|
764
1084
|
if (index === middleware.length) {
|
|
765
1085
|
try {
|
|
766
|
-
await cb(req2, res2
|
|
1086
|
+
await cb(req2, res2);
|
|
767
1087
|
} catch (error) {
|
|
768
1088
|
dispatchError(error);
|
|
769
1089
|
}
|
|
770
1090
|
} else {
|
|
771
1091
|
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
|
-
);
|
|
1092
|
+
await middleware[index](req2, res2, async (error) => {
|
|
1093
|
+
if (error) {
|
|
1094
|
+
return dispatchError(error);
|
|
1095
|
+
}
|
|
1096
|
+
await runHandler(req2, res2, middleware, cb, index + 1);
|
|
1097
|
+
});
|
|
785
1098
|
} catch (error) {
|
|
786
1099
|
dispatchError(error);
|
|
787
1100
|
}
|
|
@@ -789,25 +1102,25 @@ var Cpeak = class {
|
|
|
789
1102
|
};
|
|
790
1103
|
const runMiddleware = async (req2, res2, middleware, index) => {
|
|
791
1104
|
if (index === middleware.length) {
|
|
792
|
-
const
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
}
|
|
1105
|
+
const method = req2.method?.toLowerCase() || "";
|
|
1106
|
+
const found = this.#router.find(method, urlWithoutQueries || "");
|
|
1107
|
+
if (found) {
|
|
1108
|
+
req2.params = found.params;
|
|
1109
|
+
return await runHandler(
|
|
1110
|
+
req2,
|
|
1111
|
+
res2,
|
|
1112
|
+
found.middleware,
|
|
1113
|
+
found.handler,
|
|
1114
|
+
0
|
|
1115
|
+
);
|
|
1116
|
+
}
|
|
1117
|
+
if (this.#fallback) {
|
|
1118
|
+
try {
|
|
1119
|
+
return await this.#fallback(req2, res2);
|
|
1120
|
+
} catch (error) {
|
|
1121
|
+
return dispatchError(error);
|
|
810
1122
|
}
|
|
1123
|
+
}
|
|
811
1124
|
return res2.status(404).json({ error: `Cannot ${req2.method} ${urlWithoutQueries}` });
|
|
812
1125
|
} else {
|
|
813
1126
|
try {
|
|
@@ -826,15 +1139,13 @@ var Cpeak = class {
|
|
|
826
1139
|
}
|
|
827
1140
|
);
|
|
828
1141
|
}
|
|
829
|
-
route(method,
|
|
830
|
-
if (!this.#routes[method]) this.#routes[method] = [];
|
|
1142
|
+
route(method, path3, ...args) {
|
|
831
1143
|
const cb = args.pop();
|
|
832
1144
|
if (!cb || typeof cb !== "function") {
|
|
833
1145
|
throw new Error("Route definition must include a handler");
|
|
834
1146
|
}
|
|
835
1147
|
const middleware = args.flat();
|
|
836
|
-
|
|
837
|
-
this.#routes[method].push({ path: path2, regex, middleware, cb });
|
|
1148
|
+
this.#router.add(method, path3, middleware, cb);
|
|
838
1149
|
}
|
|
839
1150
|
beforeEach(cb) {
|
|
840
1151
|
this.#middleware.push(cb);
|
|
@@ -842,31 +1153,29 @@ var Cpeak = class {
|
|
|
842
1153
|
handleErr(cb) {
|
|
843
1154
|
this.#handleErr = cb;
|
|
844
1155
|
}
|
|
845
|
-
|
|
846
|
-
|
|
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
|
+
}
|
|
1167
|
+
listen(...args) {
|
|
1168
|
+
return this.#server.listen(...args);
|
|
847
1169
|
}
|
|
848
1170
|
address() {
|
|
849
1171
|
return this.#server.address();
|
|
850
1172
|
}
|
|
851
1173
|
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;
|
|
1174
|
+
return this.#server.close(cb);
|
|
1175
|
+
}
|
|
1176
|
+
// A getter for developers who want to access the underlying http server instance for advanced use cases that aren't covered by Cpeak
|
|
1177
|
+
get server() {
|
|
1178
|
+
return this.#server;
|
|
870
1179
|
}
|
|
871
1180
|
};
|
|
872
1181
|
function cpeak(options) {
|
|
@@ -883,6 +1192,7 @@ export {
|
|
|
883
1192
|
cpeak as default,
|
|
884
1193
|
frameworkError,
|
|
885
1194
|
hashPassword,
|
|
1195
|
+
isClientDisconnect,
|
|
886
1196
|
parseJSON,
|
|
887
1197
|
render,
|
|
888
1198
|
serveStatic,
|