@orieljs/http 0.1.0-alpha.1 → 0.1.0-alpha.10
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/NOTICE +7 -0
- package/README.md +33 -0
- package/TRADEMARKS.md +33 -0
- package/dist/index.d.ts +107 -19
- package/dist/index.js +503 -44
- package/dist/index.js.map +1 -1
- package/package.json +7 -5
package/dist/index.js
CHANGED
|
@@ -28,6 +28,21 @@ var HttpServerError = class extends Error {
|
|
|
28
28
|
};
|
|
29
29
|
|
|
30
30
|
// body.ts
|
|
31
|
+
function exceedsJsonDepth(text, maxDepth) {
|
|
32
|
+
let depth = 0;
|
|
33
|
+
let inString = false;
|
|
34
|
+
for (let i = 0; i < text.length; i++) {
|
|
35
|
+
const code = text.charCodeAt(i);
|
|
36
|
+
if (inString) {
|
|
37
|
+
if (code === 92) i++;
|
|
38
|
+
else if (code === 34) inString = false;
|
|
39
|
+
} else if (code === 34) inString = true;
|
|
40
|
+
else if (code === 123 || code === 91) {
|
|
41
|
+
if (++depth > maxDepth) return true;
|
|
42
|
+
} else if (code === 125 || code === 93) depth--;
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
31
46
|
async function readBody(raw, limit) {
|
|
32
47
|
if (raw.body === null) return new Uint8Array(0);
|
|
33
48
|
const declared = Number(raw.headers.get("content-length") ?? NaN);
|
|
@@ -146,6 +161,15 @@ var HttpRedirectResponse = class {
|
|
|
146
161
|
// HttpRedirector.ts
|
|
147
162
|
import { UrlGenerator } from "@orieljs/contracts";
|
|
148
163
|
import { inject } from "@orieljs/container";
|
|
164
|
+
function pathOf(url2) {
|
|
165
|
+
let parsed;
|
|
166
|
+
try {
|
|
167
|
+
parsed = new URL(url2, "http://localhost");
|
|
168
|
+
} catch {
|
|
169
|
+
return "/";
|
|
170
|
+
}
|
|
171
|
+
return `${parsed.pathname.replace(/^\/+/, "/")}${parsed.search}${parsed.hash}`;
|
|
172
|
+
}
|
|
149
173
|
var HttpRedirector = class {
|
|
150
174
|
to(path, status = 302, headers) {
|
|
151
175
|
return new HttpRedirectResponse(inject(UrlGenerator).to(path), status, headers);
|
|
@@ -164,13 +188,16 @@ var HttpRedirector = class {
|
|
|
164
188
|
away(url2, status = 302) {
|
|
165
189
|
return new HttpRedirectResponse(url2, status);
|
|
166
190
|
}
|
|
167
|
-
/**
|
|
191
|
+
/**
|
|
192
|
+
* Remembers where a guest was going (a GET page, else the previous URL), then redirects to `path`. Only the path
|
|
193
|
+
* is stored, so intended() goes back on whichever host serves the app, and never to another site's Referer.
|
|
194
|
+
*/
|
|
168
195
|
guest(path, status = 302) {
|
|
169
196
|
const session = optionalSession();
|
|
170
197
|
const request2 = currentRequest();
|
|
171
198
|
if (session !== void 0) {
|
|
172
199
|
const intended = request2 !== void 0 && request2.method() === "GET" && !request2.expectsJson() ? request2.fullUrl() : inject(UrlGenerator).previous();
|
|
173
|
-
session.put("url.intended", intended);
|
|
200
|
+
session.put("url.intended", pathOf(intended));
|
|
174
201
|
}
|
|
175
202
|
return this.to(path, status);
|
|
176
203
|
}
|
|
@@ -195,7 +222,7 @@ function clientAddress(request2) {
|
|
|
195
222
|
// FormUploadedFile.ts
|
|
196
223
|
import { FilesystemManager } from "@orieljs/contracts";
|
|
197
224
|
import { inject as inject2 } from "@orieljs/container";
|
|
198
|
-
import { Str } from "@orieljs/support";
|
|
225
|
+
import { FileType, Str } from "@orieljs/support";
|
|
199
226
|
|
|
200
227
|
// mime.ts
|
|
201
228
|
var TYPES = {
|
|
@@ -210,13 +237,16 @@ var TYPES = {
|
|
|
210
237
|
ico: "image/vnd.microsoft.icon",
|
|
211
238
|
jpg: "image/jpeg",
|
|
212
239
|
js: "text/javascript",
|
|
240
|
+
map: "application/json",
|
|
213
241
|
json: "application/json",
|
|
214
242
|
md: "text/markdown",
|
|
215
243
|
mp3: "audio/mpeg",
|
|
216
244
|
mp4: "video/mp4",
|
|
245
|
+
otf: "font/otf",
|
|
217
246
|
pdf: "application/pdf",
|
|
218
247
|
png: "image/png",
|
|
219
248
|
svg: "image/svg+xml",
|
|
249
|
+
ttf: "font/ttf",
|
|
220
250
|
txt: "text/plain",
|
|
221
251
|
wasm: "application/wasm",
|
|
222
252
|
webm: "video/webm",
|
|
@@ -239,20 +269,60 @@ function extensionFromMime(type) {
|
|
|
239
269
|
}
|
|
240
270
|
|
|
241
271
|
// FormUploadedFile.ts
|
|
242
|
-
var
|
|
243
|
-
|
|
272
|
+
var NEVER_CLAIMED = /* @__PURE__ */ new Set([
|
|
273
|
+
...FileType.BINARY_AND_MARKUP_TYPES.flatMap(({ extensions }) => extensions),
|
|
274
|
+
"jpe",
|
|
275
|
+
"svgz",
|
|
276
|
+
"xhtml",
|
|
277
|
+
"xht",
|
|
278
|
+
"shtml",
|
|
279
|
+
"xsl",
|
|
280
|
+
"xslt",
|
|
281
|
+
"mht",
|
|
282
|
+
"mhtml",
|
|
283
|
+
"js",
|
|
284
|
+
"mjs",
|
|
285
|
+
"rss",
|
|
286
|
+
"atom",
|
|
287
|
+
"rdf",
|
|
288
|
+
"mathml",
|
|
289
|
+
"mml",
|
|
290
|
+
"xul"
|
|
291
|
+
]);
|
|
292
|
+
var PLAIN_EXTENSION = /^[a-z0-9]+$/;
|
|
293
|
+
var FormUploadedFile = class _FormUploadedFile {
|
|
294
|
+
constructor(file, detected) {
|
|
244
295
|
this.file = file;
|
|
296
|
+
this.detected = detected;
|
|
245
297
|
this.clientName = file.name ?? "";
|
|
246
298
|
this.mimeType = file.type === "" ? "application/octet-stream" : file.type;
|
|
247
299
|
this.size = file.size;
|
|
248
300
|
}
|
|
249
301
|
file;
|
|
302
|
+
detected;
|
|
250
303
|
clientName;
|
|
251
304
|
mimeType;
|
|
252
305
|
size;
|
|
253
|
-
/**
|
|
306
|
+
/**
|
|
307
|
+
* Reads the leading bytes first, so extension(), hashName() and store() name the file by its content. The request
|
|
308
|
+
* builds its uploads this way.
|
|
309
|
+
*/
|
|
310
|
+
static async fromFile(file) {
|
|
311
|
+
return new _FormUploadedFile(file, FileType.detectFileType(new Uint8Array(await file.slice(0, FileType.SNIFF_BYTES).arrayBuffer())));
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Laravel's extension(): the type the bytes show (guessExtension()), when they were read and match a known format.
|
|
315
|
+
* Text and bytes no format matches keep the client's claim (its MIME type, else its file name) when it is a plain
|
|
316
|
+
* extension, unless it names a format the bytes would have shown or one a browser renders or runs: otherwise 'txt'
|
|
317
|
+
* for text, '' for the rest. Unread (built directly), the client's claim when it is a plain extension, else ''.
|
|
318
|
+
*/
|
|
254
319
|
extension() {
|
|
255
|
-
|
|
320
|
+
const claimed = extensionFromMime(this.mimeType) ?? this.clientExtension();
|
|
321
|
+
const detected = this.detected;
|
|
322
|
+
if (detected === void 0) return PLAIN_EXTENSION.test(claimed) ? claimed : "";
|
|
323
|
+
if (detected !== FileType.TEXT && detected.extensions.length > 0) return detected.extensions[0];
|
|
324
|
+
if (PLAIN_EXTENSION.test(claimed) && !NEVER_CLAIMED.has(claimed)) return claimed;
|
|
325
|
+
return detected === FileType.TEXT ? "txt" : "";
|
|
256
326
|
}
|
|
257
327
|
clientExtension() {
|
|
258
328
|
const dot = this.clientName.lastIndexOf(".");
|
|
@@ -282,6 +352,7 @@ var FormUploadedFile = class {
|
|
|
282
352
|
|
|
283
353
|
// IncomingRequest.ts
|
|
284
354
|
import {
|
|
355
|
+
AuthFactory,
|
|
285
356
|
CurrentRoute,
|
|
286
357
|
Session as Session2,
|
|
287
358
|
ValidationFactory
|
|
@@ -404,6 +475,7 @@ var IncomingRequest = class {
|
|
|
404
475
|
this.#url = new URL(raw.url);
|
|
405
476
|
this.#trustedProxies = options.trustedProxies ?? [];
|
|
406
477
|
this.#maxBodySize = options.maxBodySize === void 0 ? 8 * 1024 * 1024 : options.maxBodySize;
|
|
478
|
+
this.#maxJsonDepth = options.maxJsonDepth === void 0 ? 512 : options.maxJsonDepth;
|
|
407
479
|
this.#query = fromEntries(this.#url.searchParams.entries());
|
|
408
480
|
this.#method = raw.method.toUpperCase();
|
|
409
481
|
}
|
|
@@ -411,6 +483,7 @@ var IncomingRequest = class {
|
|
|
411
483
|
#url;
|
|
412
484
|
#trustedProxies;
|
|
413
485
|
#maxBodySize;
|
|
486
|
+
#maxJsonDepth;
|
|
414
487
|
#query;
|
|
415
488
|
#body = {};
|
|
416
489
|
#files = {};
|
|
@@ -432,6 +505,9 @@ var IncomingRequest = class {
|
|
|
432
505
|
const isMultipart = type.startsWith("multipart/form-data");
|
|
433
506
|
if (isJson || isUrlencoded || isMultipart) {
|
|
434
507
|
const bytes = await readBody(this.raw, this.#maxBodySize);
|
|
508
|
+
if (isJson && this.#maxJsonDepth !== null && exceedsJsonDepth(new TextDecoder().decode(bytes), this.#maxJsonDepth)) {
|
|
509
|
+
throw new HttpException(400, "The request body is nested too deeply.");
|
|
510
|
+
}
|
|
435
511
|
try {
|
|
436
512
|
if (isJson) {
|
|
437
513
|
this.#content = new TextDecoder().decode(bytes);
|
|
@@ -440,7 +516,7 @@ var IncomingRequest = class {
|
|
|
440
516
|
this.#content = new TextDecoder().decode(bytes);
|
|
441
517
|
this.#body = fromEntries(new URLSearchParams(this.#content).entries());
|
|
442
518
|
} else {
|
|
443
|
-
this.#parseForm(await new Response(bytes, { headers: { "content-type": this.raw.headers.get("content-type") ?? "" } }).formData());
|
|
519
|
+
await this.#parseForm(await new Response(bytes, { headers: { "content-type": this.raw.headers.get("content-type") ?? "" } }).formData());
|
|
444
520
|
}
|
|
445
521
|
} catch {
|
|
446
522
|
}
|
|
@@ -465,12 +541,12 @@ var IncomingRequest = class {
|
|
|
465
541
|
if (Array.isArray(parsed)) return sanitize({ ...parsed });
|
|
466
542
|
return isPlainObject(parsed) ? sanitize(parsed) : {};
|
|
467
543
|
}
|
|
468
|
-
#parseForm(form) {
|
|
544
|
+
async #parseForm(form) {
|
|
469
545
|
const fields = [];
|
|
470
546
|
const files = [];
|
|
471
547
|
for (const [name, value] of form.entries()) {
|
|
472
548
|
if (typeof value === "string") fields.push([name, value]);
|
|
473
|
-
else if (!((value.name ?? "") === "" && value.size === 0)) files.push([name,
|
|
549
|
+
else if (!((value.name ?? "") === "" && value.size === 0)) files.push([name, await FormUploadedFile.fromFile(value)]);
|
|
474
550
|
}
|
|
475
551
|
this.#body = fromEntries(fields);
|
|
476
552
|
this.#files = fromEntries(files);
|
|
@@ -610,9 +686,14 @@ var IncomingRequest = class {
|
|
|
610
686
|
filled(key) {
|
|
611
687
|
return !isBlank(Arr.get(this.all(), key));
|
|
612
688
|
}
|
|
689
|
+
/** Laravel's string(): the input as a Stringable (the default when it is missing or null): request().string('q').trim(). */
|
|
613
690
|
string(key, defaultValue = "") {
|
|
614
691
|
const value = this.input(key);
|
|
615
|
-
return value === void 0 || value === null ? defaultValue :
|
|
692
|
+
return Str2.of(value === void 0 || value === null ? defaultValue : value);
|
|
693
|
+
}
|
|
694
|
+
/** Laravel's str(): string() under its short name. */
|
|
695
|
+
str(key, defaultValue = "") {
|
|
696
|
+
return this.string(key, defaultValue);
|
|
616
697
|
}
|
|
617
698
|
/** PHP's FILTER_VALIDATE_BOOLEAN: '1', 'true', 'on', 'yes' (any case) and true are true. */
|
|
618
699
|
boolean(key, defaultValue = false) {
|
|
@@ -688,13 +769,23 @@ var IncomingRequest = class {
|
|
|
688
769
|
session() {
|
|
689
770
|
return inject3(Session2);
|
|
690
771
|
}
|
|
691
|
-
|
|
692
|
-
|
|
772
|
+
/**
|
|
773
|
+
* The signed-in user, through the resolver Auth sets, else the current scope's guard (the default, or `guard`).
|
|
774
|
+
* The guard keeps the user it found, so it is looked up once per request. Undefined for a guest or without auth.
|
|
775
|
+
*/
|
|
776
|
+
async user(guard) {
|
|
777
|
+
if (this.#userResolver !== void 0) return await this.#userResolver(guard);
|
|
778
|
+
const container = currentContainer2();
|
|
779
|
+
if (container === void 0 || !container.bound(AuthFactory)) return void 0;
|
|
780
|
+
return await container.make(AuthFactory).guard(guard).user();
|
|
693
781
|
}
|
|
694
782
|
setUserResolver(resolver) {
|
|
695
783
|
this.#userResolver = resolver;
|
|
696
784
|
}
|
|
697
|
-
/**
|
|
785
|
+
/**
|
|
786
|
+
* Validates all() through the ValidationFactory; throws its Validation error on failure. The data is typed from the
|
|
787
|
+
* rules: `validate({ title: 'required|string' })` is `{ title: string }`.
|
|
788
|
+
*/
|
|
698
789
|
validate(rules, messages, attributes) {
|
|
699
790
|
return inject3(ValidationFactory).make(this.all(), rules, messages, attributes).validated();
|
|
700
791
|
}
|
|
@@ -738,7 +829,8 @@ function url(path, query, absolute) {
|
|
|
738
829
|
const generator = inject4(UrlGenerator2);
|
|
739
830
|
return path === void 0 ? generator : generator.to(path, query, absolute);
|
|
740
831
|
}
|
|
741
|
-
function route(name,
|
|
832
|
+
function route(name, ...args) {
|
|
833
|
+
const [parameters, absolute] = args;
|
|
742
834
|
return inject4(UrlGenerator2).route(name, parameters, absolute);
|
|
743
835
|
}
|
|
744
836
|
function abort(status, message = "", headers = {}) {
|
|
@@ -878,7 +970,11 @@ var Kernel = class {
|
|
|
878
970
|
let request2;
|
|
879
971
|
let response2;
|
|
880
972
|
try {
|
|
881
|
-
request2 = new IncomingRequest(raw, {
|
|
973
|
+
request2 = new IncomingRequest(raw, {
|
|
974
|
+
trustedProxies: this.#trustedProxies(),
|
|
975
|
+
maxBodySize: this.#maxBodySize(),
|
|
976
|
+
maxJsonDepth: this.#maxJsonDepth()
|
|
977
|
+
});
|
|
882
978
|
scope.instance(HttpRequest3, request2);
|
|
883
979
|
const state = { scope, request: request2, middleware: [] };
|
|
884
980
|
this.#requests.set(raw, state);
|
|
@@ -990,6 +1086,16 @@ var Kernel = class {
|
|
|
990
1086
|
`Invalid http.max_body_size ${JSON.stringify(value) ?? String(value)}: use a number of bytes (0 or more), or null for no limit.`
|
|
991
1087
|
);
|
|
992
1088
|
}
|
|
1089
|
+
/** config('http.max_json_depth'): how deep a JSON body may nest, or null for no limit. */
|
|
1090
|
+
#maxJsonDepth() {
|
|
1091
|
+
if (!this.app.bound(ConfigRepository)) return 512;
|
|
1092
|
+
const value = this.app.make(ConfigRepository).get("http.max_json_depth", 512);
|
|
1093
|
+
if (value === null) return null;
|
|
1094
|
+
if (typeof value === "number" && Number.isInteger(value) && value >= 1) return value;
|
|
1095
|
+
throw new HttpServerError(
|
|
1096
|
+
`Invalid http.max_json_depth ${JSON.stringify(value) ?? String(value)}: use a whole number (1 or more), or null for no limit.`
|
|
1097
|
+
);
|
|
1098
|
+
}
|
|
993
1099
|
/** config('http.trusted_proxies'): an array, or a comma-separated string such as '*' or '10.0.0.0/8,::1'. */
|
|
994
1100
|
#trustedProxies() {
|
|
995
1101
|
if (!this.app.bound(ConfigRepository)) return [];
|
|
@@ -1089,18 +1195,41 @@ var STATUS_TEXTS = {
|
|
|
1089
1195
|
403: "Forbidden",
|
|
1090
1196
|
404: "Not Found",
|
|
1091
1197
|
405: "Method Not Allowed",
|
|
1198
|
+
406: "Not Acceptable",
|
|
1199
|
+
407: "Proxy Authentication Required",
|
|
1092
1200
|
408: "Request Timeout",
|
|
1093
1201
|
409: "Conflict",
|
|
1094
1202
|
410: "Gone",
|
|
1203
|
+
411: "Length Required",
|
|
1204
|
+
412: "Precondition Failed",
|
|
1095
1205
|
413: "Content Too Large",
|
|
1206
|
+
414: "URI Too Long",
|
|
1096
1207
|
415: "Unsupported Media Type",
|
|
1208
|
+
416: "Range Not Satisfiable",
|
|
1209
|
+
417: "Expectation Failed",
|
|
1210
|
+
418: "I'm a teapot",
|
|
1097
1211
|
419: "Page Expired",
|
|
1212
|
+
421: "Misdirected Request",
|
|
1098
1213
|
422: "Unprocessable Content",
|
|
1214
|
+
423: "Locked",
|
|
1215
|
+
424: "Failed Dependency",
|
|
1216
|
+
425: "Too Early",
|
|
1217
|
+
426: "Upgrade Required",
|
|
1218
|
+
428: "Precondition Required",
|
|
1099
1219
|
429: "Too Many Requests",
|
|
1220
|
+
431: "Request Header Fields Too Large",
|
|
1221
|
+
451: "Unavailable For Legal Reasons",
|
|
1100
1222
|
500: "Server Error",
|
|
1223
|
+
501: "Not Implemented",
|
|
1101
1224
|
502: "Bad Gateway",
|
|
1102
1225
|
503: "Service Unavailable",
|
|
1103
|
-
504: "Gateway Timeout"
|
|
1226
|
+
504: "Gateway Timeout",
|
|
1227
|
+
505: "HTTP Version Not Supported",
|
|
1228
|
+
506: "Variant Also Negotiates",
|
|
1229
|
+
507: "Insufficient Storage",
|
|
1230
|
+
508: "Loop Detected",
|
|
1231
|
+
510: "Not Extended",
|
|
1232
|
+
511: "Network Authentication Required"
|
|
1104
1233
|
};
|
|
1105
1234
|
function statusText(status) {
|
|
1106
1235
|
return STATUS_TEXTS[status] ?? (status >= 500 ? "Server Error" : "Error");
|
|
@@ -1217,6 +1346,7 @@ var ErrorHandler = class {
|
|
|
1217
1346
|
#renderCallbacks = [];
|
|
1218
1347
|
#dontReport = [];
|
|
1219
1348
|
#reported = /* @__PURE__ */ new WeakSet();
|
|
1349
|
+
#jsonWhen;
|
|
1220
1350
|
reportUsing(callback) {
|
|
1221
1351
|
this.#reportCallbacks.push(callback);
|
|
1222
1352
|
}
|
|
@@ -1226,6 +1356,14 @@ var ErrorHandler = class {
|
|
|
1226
1356
|
ignore(...classes) {
|
|
1227
1357
|
this.#dontReport.push(...classes);
|
|
1228
1358
|
}
|
|
1359
|
+
/** Laravel's shouldRenderJsonWhen(): the callback decides JSON or HTML instead of the Accept header. */
|
|
1360
|
+
shouldRenderJsonWhen(callback) {
|
|
1361
|
+
this.#jsonWhen = callback;
|
|
1362
|
+
}
|
|
1363
|
+
/** Laravel's shouldReturnJson(): the shouldRenderJsonWhen() callback when set, else request.expectsJson(). */
|
|
1364
|
+
shouldReturnJson(request2, error) {
|
|
1365
|
+
return this.#jsonWhen !== void 0 ? this.#jsonWhen(request2, error) : request2.expectsJson();
|
|
1366
|
+
}
|
|
1229
1367
|
shouldReport(error) {
|
|
1230
1368
|
const kind = kindOf(error);
|
|
1231
1369
|
if (kind !== void 0 && NOT_REPORTED.has(kind)) return false;
|
|
@@ -1264,11 +1402,10 @@ var ErrorHandler = class {
|
|
|
1264
1402
|
let response2;
|
|
1265
1403
|
if (kind === "Validation") response2 = await this.#renderValidation(http, error);
|
|
1266
1404
|
else if (kind === "Authentication") response2 = await this.#renderAuthentication(http, error);
|
|
1267
|
-
else if (
|
|
1405
|
+
else if (this.shouldReturnJson(http, error)) response2 = this.#renderJson(error);
|
|
1268
1406
|
else response2 = await this.#renderHtml(http, error);
|
|
1269
1407
|
const headers = error?.headers;
|
|
1270
|
-
|
|
1271
|
-
return response2;
|
|
1408
|
+
return headers === void 0 ? response2 : withHeaders(response2, headers);
|
|
1272
1409
|
}
|
|
1273
1410
|
/** An error with its own render(request) or toResponse(request) (Laravel's renderable exceptions / Responsable). */
|
|
1274
1411
|
async #renderCustom(request2, error) {
|
|
@@ -1282,7 +1419,7 @@ var ErrorHandler = class {
|
|
|
1282
1419
|
return void 0;
|
|
1283
1420
|
}
|
|
1284
1421
|
async #renderValidation(request2, error) {
|
|
1285
|
-
if (
|
|
1422
|
+
if (this.shouldReturnJson(request2, error)) {
|
|
1286
1423
|
return this.#json({ message: summarizeValidation(error.errors), errors: error.errors }, statusFor(error));
|
|
1287
1424
|
}
|
|
1288
1425
|
const input = { ...request2.input() };
|
|
@@ -1297,7 +1434,7 @@ var ErrorHandler = class {
|
|
|
1297
1434
|
return response2;
|
|
1298
1435
|
}
|
|
1299
1436
|
async #renderAuthentication(request2, error) {
|
|
1300
|
-
if (
|
|
1437
|
+
if (this.shouldReturnJson(request2, error)) return this.#json({ message: error.message || "Unauthenticated." }, 401);
|
|
1301
1438
|
const target = error.redirectTo ?? this.#guestRedirect(request2);
|
|
1302
1439
|
const container = this.#container();
|
|
1303
1440
|
if (!container.bound(Redirector2)) return this.#redirect(target);
|
|
@@ -1388,6 +1525,11 @@ var ErrorHandler = class {
|
|
|
1388
1525
|
return currentContainer4() ?? this.app;
|
|
1389
1526
|
}
|
|
1390
1527
|
};
|
|
1528
|
+
function withHeaders(response2, extra) {
|
|
1529
|
+
const headers = new Headers(response2.headers);
|
|
1530
|
+
for (const [name, value] of Object.entries(extra)) headers.set(name, value);
|
|
1531
|
+
return new Response(response2.body, { status: response2.status, statusText: response2.statusText, headers });
|
|
1532
|
+
}
|
|
1391
1533
|
|
|
1392
1534
|
// ExceptionsConfiguration.ts
|
|
1393
1535
|
import { ExceptionCallbacks } from "@orieljs/contracts";
|
|
@@ -1408,6 +1550,11 @@ var ExceptionsConfiguration = class {
|
|
|
1408
1550
|
this.handler.ignore(...classes);
|
|
1409
1551
|
return this;
|
|
1410
1552
|
}
|
|
1553
|
+
/** shouldRenderJsonWhen((request) => request.is('api/*')): JSON errors for those requests, whatever they accept. */
|
|
1554
|
+
shouldRenderJsonWhen(callback) {
|
|
1555
|
+
this.handler.shouldRenderJsonWhen(callback);
|
|
1556
|
+
return this;
|
|
1557
|
+
}
|
|
1411
1558
|
};
|
|
1412
1559
|
function applyExceptionCallbacks(app, handler) {
|
|
1413
1560
|
if (!app.bound(ExceptionCallbacks)) return;
|
|
@@ -1417,6 +1564,41 @@ function applyExceptionCallbacks(app, handler) {
|
|
|
1417
1564
|
|
|
1418
1565
|
// server/types.ts
|
|
1419
1566
|
import { HttpKernel } from "@orieljs/contracts";
|
|
1567
|
+
|
|
1568
|
+
// server/static.ts
|
|
1569
|
+
import { createReadStream as createReadStream2 } from "fs";
|
|
1570
|
+
import { stat as stat2 } from "fs/promises";
|
|
1571
|
+
import { Readable as Readable2 } from "stream";
|
|
1572
|
+
import { relative, resolve, sep } from "path";
|
|
1573
|
+
var HIDDEN_SEGMENT = /^\.|\.(ts|tsx|mts|cts)$|[. ]$/i;
|
|
1574
|
+
async function servePublicFile(publicPath, request2) {
|
|
1575
|
+
if (request2.method !== "GET" && request2.method !== "HEAD") return void 0;
|
|
1576
|
+
let pathname;
|
|
1577
|
+
try {
|
|
1578
|
+
pathname = decodeURIComponent(new URL(request2.url).pathname);
|
|
1579
|
+
} catch {
|
|
1580
|
+
return void 0;
|
|
1581
|
+
}
|
|
1582
|
+
if (pathname === "/" || pathname.includes("\0")) return void 0;
|
|
1583
|
+
const root = resolve(publicPath);
|
|
1584
|
+
const file = resolve(root, `.${pathname}`);
|
|
1585
|
+
if (!file.startsWith(root + sep)) return void 0;
|
|
1586
|
+
if (relative(root, file).split(/[\\/]/).some((segment) => HIDDEN_SEGMENT.test(segment))) return void 0;
|
|
1587
|
+
const info = await stat2(file).catch(() => void 0);
|
|
1588
|
+
if (info === void 0 || !info.isFile()) return void 0;
|
|
1589
|
+
const type = mimeFromExtension(file);
|
|
1590
|
+
const headers = new Headers({
|
|
1591
|
+
"content-type": type.startsWith("text/") || type === "application/json" ? `${type}; charset=utf-8` : type,
|
|
1592
|
+
"content-length": String(info.size),
|
|
1593
|
+
"last-modified": info.mtime.toUTCString()
|
|
1594
|
+
});
|
|
1595
|
+
if (pathname.startsWith("/build/assets/")) headers.set("cache-control", "public, max-age=31536000, immutable");
|
|
1596
|
+
if (request2.method === "HEAD") return new Response(null, { status: 200, headers });
|
|
1597
|
+
const body = Readable2.toWeb(createReadStream2(file));
|
|
1598
|
+
return new Response(body, { status: 200, headers });
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
// server/types.ts
|
|
1420
1602
|
async function terminateRequest(app, request2, response2) {
|
|
1421
1603
|
try {
|
|
1422
1604
|
if (app.bound(HttpKernel)) await app.make(HttpKernel).terminate?.(request2, response2);
|
|
@@ -1424,17 +1606,21 @@ async function terminateRequest(app, request2, response2) {
|
|
|
1424
1606
|
console.error(error);
|
|
1425
1607
|
}
|
|
1426
1608
|
}
|
|
1427
|
-
async function handleSafely(app, request2) {
|
|
1609
|
+
async function handleSafely(app, request2, publicPath) {
|
|
1428
1610
|
try {
|
|
1611
|
+
if (publicPath !== void 0) {
|
|
1612
|
+
const file = await servePublicFile(publicPath, request2);
|
|
1613
|
+
if (file !== void 0) return file;
|
|
1614
|
+
}
|
|
1429
1615
|
return await app.handle(request2);
|
|
1430
1616
|
} catch (error) {
|
|
1431
1617
|
console.error(error);
|
|
1432
1618
|
return plainServerError();
|
|
1433
1619
|
}
|
|
1434
1620
|
}
|
|
1435
|
-
function exitOnSignals(close, closed) {
|
|
1621
|
+
function exitOnSignals(close, closed, app) {
|
|
1436
1622
|
const onSignal = () => {
|
|
1437
|
-
void close().then(() => process.exit(0));
|
|
1623
|
+
void close().then(() => app.shutdown()).then(() => process.exit(0));
|
|
1438
1624
|
};
|
|
1439
1625
|
process.once("SIGINT", onSignal);
|
|
1440
1626
|
process.once("SIGTERM", onSignal);
|
|
@@ -1453,13 +1639,13 @@ async function serveBun(app, options) {
|
|
|
1453
1639
|
const bun = bunRuntime();
|
|
1454
1640
|
if (bun === void 0) throw new HttpServerError("Bun.serve is not available: the process is not running on Bun.");
|
|
1455
1641
|
let markClosed = () => void 0;
|
|
1456
|
-
const closed = new Promise((
|
|
1642
|
+
const closed = new Promise((resolve2) => markClosed = resolve2);
|
|
1457
1643
|
const server = bun.serve({
|
|
1458
1644
|
port: options.port,
|
|
1459
1645
|
hostname: options.hostname,
|
|
1460
1646
|
async fetch(request2, current) {
|
|
1461
1647
|
setClientAddress(request2, current.requestIP(request2)?.address);
|
|
1462
|
-
const response2 = await handleSafely(app, request2);
|
|
1648
|
+
const response2 = await handleSafely(app, request2, options.publicPath);
|
|
1463
1649
|
setTimeout(() => void terminateRequest(app, request2, response2), 0);
|
|
1464
1650
|
return response2;
|
|
1465
1651
|
},
|
|
@@ -1471,14 +1657,14 @@ async function serveBun(app, options) {
|
|
|
1471
1657
|
let closing;
|
|
1472
1658
|
const close = () => closing ??= (async () => {
|
|
1473
1659
|
let timer;
|
|
1474
|
-
const forced = new Promise((
|
|
1475
|
-
timer = setTimeout(() => void Promise.resolve(server.stop(true)).then(
|
|
1660
|
+
const forced = new Promise((resolve2) => {
|
|
1661
|
+
timer = setTimeout(() => void Promise.resolve(server.stop(true)).then(resolve2), options.shutdownTimeout);
|
|
1476
1662
|
});
|
|
1477
1663
|
await Promise.race([Promise.resolve(server.stop(false)), forced]);
|
|
1478
1664
|
clearTimeout(timer);
|
|
1479
1665
|
markClosed();
|
|
1480
1666
|
})();
|
|
1481
|
-
if (options.signals) exitOnSignals(close, closed);
|
|
1667
|
+
if (options.signals) exitOnSignals(close, closed, app);
|
|
1482
1668
|
return {
|
|
1483
1669
|
runtime: "bun",
|
|
1484
1670
|
hostname: options.hostname,
|
|
@@ -1491,7 +1677,7 @@ async function serveBun(app, options) {
|
|
|
1491
1677
|
|
|
1492
1678
|
// server/node.ts
|
|
1493
1679
|
import { createServer } from "http";
|
|
1494
|
-
import { Readable as
|
|
1680
|
+
import { Readable as Readable3 } from "stream";
|
|
1495
1681
|
import { pipeline } from "stream/promises";
|
|
1496
1682
|
function requestBody(req) {
|
|
1497
1683
|
let finished = false;
|
|
@@ -1573,7 +1759,7 @@ async function writeResponse(res, response2) {
|
|
|
1573
1759
|
res.end();
|
|
1574
1760
|
return;
|
|
1575
1761
|
}
|
|
1576
|
-
await pipeline(
|
|
1762
|
+
await pipeline(Readable3.fromWeb(response2.body), res);
|
|
1577
1763
|
}
|
|
1578
1764
|
async function serveNode(app, options) {
|
|
1579
1765
|
const fallbackHost = `${displayHost(options.hostname)}:${options.port}`;
|
|
@@ -1588,7 +1774,7 @@ async function serveNode(app, options) {
|
|
|
1588
1774
|
return;
|
|
1589
1775
|
}
|
|
1590
1776
|
setClientAddress(request2, req.socket.remoteAddress);
|
|
1591
|
-
const response2 = await handleSafely(app, request2);
|
|
1777
|
+
const response2 = await handleSafely(app, request2, options.publicPath);
|
|
1592
1778
|
try {
|
|
1593
1779
|
await writeResponse(res, response2);
|
|
1594
1780
|
} catch {
|
|
@@ -1597,18 +1783,18 @@ async function serveNode(app, options) {
|
|
|
1597
1783
|
await terminateRequest(app, request2, response2);
|
|
1598
1784
|
})();
|
|
1599
1785
|
});
|
|
1600
|
-
await new Promise((
|
|
1786
|
+
await new Promise((resolve2, reject) => {
|
|
1601
1787
|
server.once("error", reject);
|
|
1602
1788
|
server.listen(options.port, options.hostname, () => {
|
|
1603
1789
|
server.off("error", reject);
|
|
1604
|
-
|
|
1790
|
+
resolve2();
|
|
1605
1791
|
});
|
|
1606
1792
|
});
|
|
1607
1793
|
const address = server.address();
|
|
1608
|
-
const closed = new Promise((
|
|
1794
|
+
const closed = new Promise((resolve2) => server.once("close", () => resolve2()));
|
|
1609
1795
|
let closing;
|
|
1610
1796
|
const close = () => closing ??= (async () => {
|
|
1611
|
-
const done = new Promise((
|
|
1797
|
+
const done = new Promise((resolve2) => server.close(() => resolve2()));
|
|
1612
1798
|
server.closeIdleConnections();
|
|
1613
1799
|
const idle = setInterval(() => server.closeIdleConnections(), 50);
|
|
1614
1800
|
const force = setTimeout(() => server.closeAllConnections(), options.shutdownTimeout);
|
|
@@ -1616,7 +1802,7 @@ async function serveNode(app, options) {
|
|
|
1616
1802
|
clearInterval(idle);
|
|
1617
1803
|
clearTimeout(force);
|
|
1618
1804
|
})();
|
|
1619
|
-
if (options.signals) exitOnSignals(close, closed);
|
|
1805
|
+
if (options.signals) exitOnSignals(close, closed, app);
|
|
1620
1806
|
return {
|
|
1621
1807
|
runtime: "node",
|
|
1622
1808
|
hostname: options.hostname,
|
|
@@ -1636,7 +1822,8 @@ async function serve(app, options = {}) {
|
|
|
1636
1822
|
port: options.port ?? Number(process.env.PORT ?? process.env.SERVER_PORT ?? 8e3),
|
|
1637
1823
|
hostname: options.hostname ?? "127.0.0.1",
|
|
1638
1824
|
signals: options.signals ?? true,
|
|
1639
|
-
shutdownTimeout: options.shutdownTimeout ?? 1e4
|
|
1825
|
+
shutdownTimeout: options.shutdownTimeout ?? 1e4,
|
|
1826
|
+
...options.publicPath !== false && { publicPath: options.publicPath ?? app.publicPath() }
|
|
1640
1827
|
};
|
|
1641
1828
|
await app.boot();
|
|
1642
1829
|
const runtime = options.runtime ?? detectRuntime();
|
|
@@ -1647,12 +1834,255 @@ async function serve(app, options = {}) {
|
|
|
1647
1834
|
}
|
|
1648
1835
|
|
|
1649
1836
|
// commands.ts
|
|
1650
|
-
import { existsSync } from "fs";
|
|
1837
|
+
import { existsSync as existsSync3 } from "fs";
|
|
1651
1838
|
import { mkdir, readFile as readFile2, writeFile } from "fs/promises";
|
|
1652
1839
|
import { dirname } from "path";
|
|
1653
1840
|
import {
|
|
1654
1841
|
Router as Router3
|
|
1655
1842
|
} from "@orieljs/contracts";
|
|
1843
|
+
|
|
1844
|
+
// watch/environment.ts
|
|
1845
|
+
import { existsSync, readFileSync } from "fs";
|
|
1846
|
+
import { join } from "path";
|
|
1847
|
+
import * as util from "util";
|
|
1848
|
+
var launchEnvironment = Object.freeze({ ...process.env });
|
|
1849
|
+
var BUN_ENV_FILES = [
|
|
1850
|
+
".env",
|
|
1851
|
+
".env.development",
|
|
1852
|
+
".env.production",
|
|
1853
|
+
".env.test",
|
|
1854
|
+
".env.local",
|
|
1855
|
+
".env.development.local",
|
|
1856
|
+
".env.production.local",
|
|
1857
|
+
".env.test.local"
|
|
1858
|
+
];
|
|
1859
|
+
var expand = (value, env) => value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g, (_match, braced, bare) => env[braced ?? bare] ?? "");
|
|
1860
|
+
function serverEnvironment(options = {}) {
|
|
1861
|
+
const launched = options.launched ?? launchEnvironment;
|
|
1862
|
+
const env = { ...launched };
|
|
1863
|
+
if (!(options.bun ?? process.versions.bun !== void 0)) return env;
|
|
1864
|
+
const parseEnv2 = "parseEnv" in options ? options.parseEnv : util.parseEnv;
|
|
1865
|
+
if (typeof parseEnv2 !== "function") return env;
|
|
1866
|
+
const cwd = options.cwd ?? process.cwd();
|
|
1867
|
+
for (const name of BUN_ENV_FILES) {
|
|
1868
|
+
const file = join(cwd, name);
|
|
1869
|
+
if (!existsSync(file)) continue;
|
|
1870
|
+
let values;
|
|
1871
|
+
try {
|
|
1872
|
+
values = parseEnv2(readFileSync(file, "utf8"));
|
|
1873
|
+
} catch {
|
|
1874
|
+
continue;
|
|
1875
|
+
}
|
|
1876
|
+
for (const [key, raw] of Object.entries(values)) {
|
|
1877
|
+
const value = launched[key];
|
|
1878
|
+
if (raw !== void 0 && value !== void 0 && (value === raw || value === expand(raw, launched))) delete env[key];
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
return env;
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
// watch/files.ts
|
|
1885
|
+
import { existsSync as existsSync2, statSync, watch as watchPath } from "fs";
|
|
1886
|
+
import { join as join2 } from "path";
|
|
1887
|
+
var WATCHED_FOLDERS = ["app", "bootstrap", "config", "lang", "resources/lang", "resources/views", "routes"];
|
|
1888
|
+
var WATCHED_ROOT_FILES = ["package.json", "tsconfig.json"];
|
|
1889
|
+
var IGNORED_FOLDERS = ["bootstrap/ssr", "bootstrap/cache"];
|
|
1890
|
+
var CODE_FILE = /\.(?:[cm]?[jt]sx?|json)$/;
|
|
1891
|
+
var DECLARATION_FILE = /\.d\.[cm]?ts$/;
|
|
1892
|
+
var EDITOR_FILE = /(?:\.sw[a-px]|~|___jb_(?:tmp|old)___)$/;
|
|
1893
|
+
var isEnvironmentFile = (name) => (name === ".env" || name.startsWith(".env.")) && name !== ".env.example" && !EDITOR_FILE.test(name);
|
|
1894
|
+
function isWatchedPath(path) {
|
|
1895
|
+
const segments = path.split("/");
|
|
1896
|
+
if (segments.length === 1) return isEnvironmentFile(path) || WATCHED_ROOT_FILES.includes(path);
|
|
1897
|
+
if (!WATCHED_FOLDERS.some((folder) => path.startsWith(`${folder}/`))) return false;
|
|
1898
|
+
if (IGNORED_FOLDERS.some((folder) => path === folder || path.startsWith(`${folder}/`))) return false;
|
|
1899
|
+
if (segments.some((segment) => segment.startsWith(".") || segment === "node_modules")) return false;
|
|
1900
|
+
const name = segments.at(-1);
|
|
1901
|
+
if (DECLARATION_FILE.test(name)) return false;
|
|
1902
|
+
return CODE_FILE.test(name) || !name.includes(".");
|
|
1903
|
+
}
|
|
1904
|
+
var toSlashes = (file) => file.split("\\").join("/");
|
|
1905
|
+
function watchFiles(basePath, onChange, options = {}) {
|
|
1906
|
+
const watch = options.watch ?? ((path, watchOptions, listener) => watchPath(path, watchOptions, listener));
|
|
1907
|
+
const debounce = options.debounce ?? 200;
|
|
1908
|
+
const onError = options.onError ?? (() => {
|
|
1909
|
+
});
|
|
1910
|
+
const changed = /* @__PURE__ */ new Set();
|
|
1911
|
+
let timer;
|
|
1912
|
+
const settle = options.settle ?? 1e3;
|
|
1913
|
+
const startedAt = Date.now();
|
|
1914
|
+
const reportedLate = (path) => {
|
|
1915
|
+
if (Date.now() - startedAt >= settle) return false;
|
|
1916
|
+
try {
|
|
1917
|
+
const modified = statSync(join2(basePath, path), { throwIfNoEntry: false })?.mtimeMs;
|
|
1918
|
+
return modified === void 0 || modified < startedAt + 1;
|
|
1919
|
+
} catch {
|
|
1920
|
+
return false;
|
|
1921
|
+
}
|
|
1922
|
+
};
|
|
1923
|
+
const record = (path) => {
|
|
1924
|
+
if (!isWatchedPath(path) || reportedLate(path)) return;
|
|
1925
|
+
changed.add(path);
|
|
1926
|
+
clearTimeout(timer);
|
|
1927
|
+
timer = setTimeout(() => {
|
|
1928
|
+
const files = [...changed];
|
|
1929
|
+
changed.clear();
|
|
1930
|
+
onChange(files);
|
|
1931
|
+
}, debounce);
|
|
1932
|
+
};
|
|
1933
|
+
const handles = [];
|
|
1934
|
+
const start = (folder, recursive, toPath) => {
|
|
1935
|
+
try {
|
|
1936
|
+
const handle = watch(folder === "." ? basePath : join2(basePath, folder), { recursive }, (_event, file) => {
|
|
1937
|
+
const path = toPath(file);
|
|
1938
|
+
if (path !== void 0) record(path);
|
|
1939
|
+
});
|
|
1940
|
+
handle.on("error", (error) => onError(folder, error));
|
|
1941
|
+
handles.push(handle);
|
|
1942
|
+
} catch (error) {
|
|
1943
|
+
onError(folder, error);
|
|
1944
|
+
}
|
|
1945
|
+
};
|
|
1946
|
+
start(".", false, (file) => file === null ? void 0 : toSlashes(file));
|
|
1947
|
+
for (const folder of WATCHED_FOLDERS) {
|
|
1948
|
+
if (existsSync2(join2(basePath, folder))) start(folder, true, (file) => `${folder}/${file === null ? "" : toSlashes(file)}`);
|
|
1949
|
+
}
|
|
1950
|
+
return {
|
|
1951
|
+
close() {
|
|
1952
|
+
clearTimeout(timer);
|
|
1953
|
+
changed.clear();
|
|
1954
|
+
for (const handle of handles) handle.close();
|
|
1955
|
+
}
|
|
1956
|
+
};
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
// watch/port.ts
|
|
1960
|
+
import { createServer as createServer2 } from "net";
|
|
1961
|
+
function claimPort(hostname, port) {
|
|
1962
|
+
return new Promise((resolve2, reject) => {
|
|
1963
|
+
const server = createServer2();
|
|
1964
|
+
server.once("error", reject);
|
|
1965
|
+
try {
|
|
1966
|
+
server.listen({ port, host: hostname, exclusive: true }, () => {
|
|
1967
|
+
const { port: claimed } = server.address();
|
|
1968
|
+
server.close(() => resolve2(claimed));
|
|
1969
|
+
});
|
|
1970
|
+
} catch (error) {
|
|
1971
|
+
reject(error);
|
|
1972
|
+
}
|
|
1973
|
+
});
|
|
1974
|
+
}
|
|
1975
|
+
var REASONS = {
|
|
1976
|
+
EADDRINUSE: "Address already in use",
|
|
1977
|
+
EACCES: "Permission denied",
|
|
1978
|
+
EADDRNOTAVAIL: "Can't assign requested address"
|
|
1979
|
+
};
|
|
1980
|
+
function listenFailure(hostname, port, error) {
|
|
1981
|
+
const { code, message } = error;
|
|
1982
|
+
return `Failed to listen on ${displayHost(hostname)}:${port} (reason: ${code !== void 0 && REASONS[code] || message})`;
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
// watch/WatchedServer.ts
|
|
1986
|
+
import { spawn as spawnProcess } from "child_process";
|
|
1987
|
+
var WatchedServer = class {
|
|
1988
|
+
constructor(options) {
|
|
1989
|
+
this.options = options;
|
|
1990
|
+
}
|
|
1991
|
+
options;
|
|
1992
|
+
running;
|
|
1993
|
+
restarting = false;
|
|
1994
|
+
stopping = false;
|
|
1995
|
+
finish = () => {
|
|
1996
|
+
};
|
|
1997
|
+
/** Starts the server and keeps it current until stopped. Resolves 0 once the last server has exited. */
|
|
1998
|
+
run() {
|
|
1999
|
+
const done = new Promise((resolve2) => this.finish = resolve2);
|
|
2000
|
+
const stopListening = this.listenForSignals();
|
|
2001
|
+
const watcher = this.options.watch((files) => void this.changed(files));
|
|
2002
|
+
this.start();
|
|
2003
|
+
return done.finally(() => {
|
|
2004
|
+
watcher.close();
|
|
2005
|
+
stopListening();
|
|
2006
|
+
});
|
|
2007
|
+
}
|
|
2008
|
+
/** Stops the server and ends run(); called again, kills the server at once. */
|
|
2009
|
+
stop() {
|
|
2010
|
+
if (this.stopping) {
|
|
2011
|
+
this.running?.child.kill("SIGKILL");
|
|
2012
|
+
return;
|
|
2013
|
+
}
|
|
2014
|
+
this.stopping = true;
|
|
2015
|
+
void this.stopServer().then(() => this.finish(0));
|
|
2016
|
+
}
|
|
2017
|
+
async changed(files) {
|
|
2018
|
+
if (this.stopping || this.restarting || files.length === 0) return;
|
|
2019
|
+
this.restarting = true;
|
|
2020
|
+
try {
|
|
2021
|
+
this.options.info(
|
|
2022
|
+
files.some(isEnvironmentFile) ? "Environment modified. Restarting server..." : `Application change detected in [${files[0]}]. Restarting server...`
|
|
2023
|
+
);
|
|
2024
|
+
await this.stopServer();
|
|
2025
|
+
if (!this.stopping) this.start();
|
|
2026
|
+
} finally {
|
|
2027
|
+
this.restarting = false;
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
start() {
|
|
2031
|
+
const [command, ...args] = this.options.command;
|
|
2032
|
+
const child = (this.options.spawn ?? spawnProcess)(command, args, {
|
|
2033
|
+
cwd: this.options.cwd,
|
|
2034
|
+
env: this.options.env,
|
|
2035
|
+
stdio: ["ignore", "inherit", "inherit"]
|
|
2036
|
+
});
|
|
2037
|
+
const running = { child, stopping: false, killed: false, exited: Promise.resolve() };
|
|
2038
|
+
running.exited = new Promise((resolve2) => {
|
|
2039
|
+
let settled = false;
|
|
2040
|
+
const settle = (report) => {
|
|
2041
|
+
if (settled) return;
|
|
2042
|
+
settled = true;
|
|
2043
|
+
if (this.running === running) this.running = void 0;
|
|
2044
|
+
if (!running.stopping && !this.stopping) this.options.error(`${report}. Waiting for changes before restarting...`);
|
|
2045
|
+
resolve2();
|
|
2046
|
+
};
|
|
2047
|
+
child.once("exit", (code, signal) => settle(signal !== null ? `The server stopped (signal ${signal})` : `The server stopped (exit code ${code})`));
|
|
2048
|
+
child.once("error", (error) => {
|
|
2049
|
+
if (child.pid === void 0) settle(`The server could not start (${error.message})`);
|
|
2050
|
+
});
|
|
2051
|
+
});
|
|
2052
|
+
this.running = running;
|
|
2053
|
+
}
|
|
2054
|
+
/** Sends SIGTERM once and waits for the exit, killing the server after stopTimeout. */
|
|
2055
|
+
async stopServer() {
|
|
2056
|
+
const running = this.running;
|
|
2057
|
+
if (running === void 0) return;
|
|
2058
|
+
if (!running.stopping) {
|
|
2059
|
+
running.stopping = true;
|
|
2060
|
+
running.child.kill("SIGTERM");
|
|
2061
|
+
}
|
|
2062
|
+
const timeout = this.options.stopTimeout ?? 15e3;
|
|
2063
|
+
const timer = setTimeout(() => {
|
|
2064
|
+
if (running.killed) return;
|
|
2065
|
+
running.killed = true;
|
|
2066
|
+
this.options.error(`The server did not stop within ${timeout / 1e3} seconds and was killed.`);
|
|
2067
|
+
running.child.kill("SIGKILL");
|
|
2068
|
+
}, timeout);
|
|
2069
|
+
try {
|
|
2070
|
+
await running.exited;
|
|
2071
|
+
} finally {
|
|
2072
|
+
clearTimeout(timer);
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
listenForSignals() {
|
|
2076
|
+
const signals = this.options.signals ?? ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
2077
|
+
const handler = () => this.stop();
|
|
2078
|
+
for (const signal of signals) process.on(signal, handler);
|
|
2079
|
+
return () => {
|
|
2080
|
+
for (const signal of signals) process.off(signal, handler);
|
|
2081
|
+
};
|
|
2082
|
+
}
|
|
2083
|
+
};
|
|
2084
|
+
|
|
2085
|
+
// commands.ts
|
|
1656
2086
|
function io(command) {
|
|
1657
2087
|
return {
|
|
1658
2088
|
option: (name) => {
|
|
@@ -1726,11 +2156,36 @@ ${web[1]}api: 'routes/api.ts',`);
|
|
|
1726
2156
|
${create[0]}`);
|
|
1727
2157
|
return void 0;
|
|
1728
2158
|
}
|
|
2159
|
+
async function serveWatching(app, out, port, run) {
|
|
2160
|
+
const hostname = out.option("host") ?? "127.0.0.1";
|
|
2161
|
+
const requested = port !== void 0 ? Number(port) : Number(process.env.PORT ?? process.env.SERVER_PORT ?? 8e3);
|
|
2162
|
+
let claimed;
|
|
2163
|
+
try {
|
|
2164
|
+
claimed = await claimPort(hostname, requested);
|
|
2165
|
+
} catch (error) {
|
|
2166
|
+
out.error(listenFailure(hostname, requested, error));
|
|
2167
|
+
return 1;
|
|
2168
|
+
}
|
|
2169
|
+
const script = process.argv[1];
|
|
2170
|
+
if (script === void 0) {
|
|
2171
|
+
out.error("serve --watch can't tell which script started this process, so it can't start the server.");
|
|
2172
|
+
return 1;
|
|
2173
|
+
}
|
|
2174
|
+
return run({
|
|
2175
|
+
command: [process.execPath, ...process.execArgv, script, "serve", `--host=${hostname}`, `--port=${claimed}`],
|
|
2176
|
+
cwd: process.cwd(),
|
|
2177
|
+
env: serverEnvironment(),
|
|
2178
|
+
watch: (onChange) => watchFiles(app.basePath(), onChange, { onError: (folder, error) => out.warn(`Unable to watch [${folder}] for changes: ${error.message}`) }),
|
|
2179
|
+
info: (message) => out.info(message),
|
|
2180
|
+
error: (message) => out.error(message)
|
|
2181
|
+
});
|
|
2182
|
+
}
|
|
1729
2183
|
function registerHttpCommands(kernel, app, options = {}) {
|
|
1730
2184
|
const serveApp = options.serve ?? serve;
|
|
1731
2185
|
const terminalWidth = options.terminalWidth ?? (() => process.stdout.columns ?? 120);
|
|
2186
|
+
const runWatching = options.watch ?? ((watchOptions) => new WatchedServer(watchOptions).run());
|
|
1732
2187
|
kernel.command(
|
|
1733
|
-
"serve {--host=127.0.0.1 : The host address to serve the application on} {--port= : The port to serve the application on}",
|
|
2188
|
+
"serve {--host=127.0.0.1 : The host address to serve the application on} {--port= : The port to serve the application on} {--watch : Automatically restart the server when the application is modified}",
|
|
1734
2189
|
async function() {
|
|
1735
2190
|
const out = io(this);
|
|
1736
2191
|
const port = out.option("port");
|
|
@@ -1738,6 +2193,7 @@ function registerHttpCommands(kernel, app, options = {}) {
|
|
|
1738
2193
|
out.error("The --port option must be a number.");
|
|
1739
2194
|
return 1;
|
|
1740
2195
|
}
|
|
2196
|
+
if (out.flag("watch")) return serveWatching(app, out, port, runWatching);
|
|
1741
2197
|
const server = await serveApp(app, {
|
|
1742
2198
|
hostname: out.option("host") ?? "127.0.0.1",
|
|
1743
2199
|
...port !== void 0 && { port: Number(port) },
|
|
@@ -1785,7 +2241,7 @@ function registerHttpCommands(kernel, app, options = {}) {
|
|
|
1785
2241
|
kernel.command("install:api {--force : Overwrite any existing API routes file}", async function() {
|
|
1786
2242
|
const out = io(this);
|
|
1787
2243
|
const routesFile = app.basePath("routes/api.ts");
|
|
1788
|
-
if (
|
|
2244
|
+
if (existsSync3(routesFile) && !out.flag("force")) {
|
|
1789
2245
|
out.error("API routes file already exists.");
|
|
1790
2246
|
} else {
|
|
1791
2247
|
await mkdir(dirname(routesFile), { recursive: true });
|
|
@@ -1793,7 +2249,7 @@ function registerHttpCommands(kernel, app, options = {}) {
|
|
|
1793
2249
|
out.info("Published API routes file.");
|
|
1794
2250
|
}
|
|
1795
2251
|
const bootstrapFile = app.bootstrapPath("app.ts");
|
|
1796
|
-
const source =
|
|
2252
|
+
const source = existsSync3(bootstrapFile) ? await readFile2(bootstrapFile, "utf8") : void 0;
|
|
1797
2253
|
const updated = source === void 0 ? void 0 : addApiRouting(source);
|
|
1798
2254
|
if (updated === void 0) {
|
|
1799
2255
|
out.warn(
|
|
@@ -1802,7 +2258,7 @@ function registerHttpCommands(kernel, app, options = {}) {
|
|
|
1802
2258
|
} else if (updated !== source) {
|
|
1803
2259
|
await writeFile(bootstrapFile, updated);
|
|
1804
2260
|
}
|
|
1805
|
-
out.info("API scaffolding installed.
|
|
2261
|
+
out.info("API scaffolding installed.");
|
|
1806
2262
|
return 0;
|
|
1807
2263
|
}).purpose("Create an API routes file and prepare the application for API authentication");
|
|
1808
2264
|
}
|
|
@@ -1816,6 +2272,7 @@ var HttpServiceProvider = class extends ServiceProvider {
|
|
|
1816
2272
|
{
|
|
1817
2273
|
trusted_proxies: [],
|
|
1818
2274
|
max_body_size: 8 * 1024 * 1024,
|
|
2275
|
+
max_json_depth: 512,
|
|
1819
2276
|
csrf_except: [],
|
|
1820
2277
|
redirect_guests_to: void 0,
|
|
1821
2278
|
redirect_users_to: void 0
|
|
@@ -1870,6 +2327,7 @@ export {
|
|
|
1870
2327
|
debugPage,
|
|
1871
2328
|
detectRuntime,
|
|
1872
2329
|
escapeHtml,
|
|
2330
|
+
exceedsJsonDepth,
|
|
1873
2331
|
extensionFromMime,
|
|
1874
2332
|
filterRouteList,
|
|
1875
2333
|
formatRouteList,
|
|
@@ -1895,6 +2353,7 @@ export {
|
|
|
1895
2353
|
serve,
|
|
1896
2354
|
serveBun,
|
|
1897
2355
|
serveNode,
|
|
2356
|
+
servePublicFile,
|
|
1898
2357
|
setClientAddress,
|
|
1899
2358
|
stackFrames,
|
|
1900
2359
|
statusFor,
|