@orieljs/http 0.1.0-alpha.1 → 0.1.0-alpha.11
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 +170 -20
- package/dist/index.js +707 -68
- 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;
|
|
@@ -1415,8 +1562,48 @@ function applyExceptionCallbacks(app, handler) {
|
|
|
1415
1562
|
for (const callback of app.make(ExceptionCallbacks)) callback(configurator);
|
|
1416
1563
|
}
|
|
1417
1564
|
|
|
1565
|
+
// export/export.ts
|
|
1566
|
+
import { existsSync, lstatSync, readdirSync, statSync } from "fs";
|
|
1567
|
+
import { cp, mkdir, mkdtemp, rename, rm, writeFile } from "fs/promises";
|
|
1568
|
+
import { basename as basename2, dirname, join, resolve as resolve2 } from "path";
|
|
1569
|
+
|
|
1418
1570
|
// server/types.ts
|
|
1419
1571
|
import { HttpKernel } from "@orieljs/contracts";
|
|
1572
|
+
|
|
1573
|
+
// server/static.ts
|
|
1574
|
+
import { createReadStream as createReadStream2 } from "fs";
|
|
1575
|
+
import { stat as stat2 } from "fs/promises";
|
|
1576
|
+
import { Readable as Readable2 } from "stream";
|
|
1577
|
+
import { relative, resolve, sep } from "path";
|
|
1578
|
+
var HIDDEN_SEGMENT = /^\.|\.(ts|tsx|mts|cts)$|[. ]$/i;
|
|
1579
|
+
async function servePublicFile(publicPath, request2) {
|
|
1580
|
+
if (request2.method !== "GET" && request2.method !== "HEAD") return void 0;
|
|
1581
|
+
let pathname;
|
|
1582
|
+
try {
|
|
1583
|
+
pathname = decodeURIComponent(new URL(request2.url).pathname);
|
|
1584
|
+
} catch {
|
|
1585
|
+
return void 0;
|
|
1586
|
+
}
|
|
1587
|
+
if (pathname === "/" || pathname.includes("\0")) return void 0;
|
|
1588
|
+
const root = resolve(publicPath);
|
|
1589
|
+
const file = resolve(root, `.${pathname}`);
|
|
1590
|
+
if (!file.startsWith(root + sep)) return void 0;
|
|
1591
|
+
if (relative(root, file).split(/[\\/]/).some((segment) => HIDDEN_SEGMENT.test(segment))) return void 0;
|
|
1592
|
+
const info = await stat2(file).catch(() => void 0);
|
|
1593
|
+
if (info === void 0 || !info.isFile()) return void 0;
|
|
1594
|
+
const type = mimeFromExtension(file);
|
|
1595
|
+
const headers = new Headers({
|
|
1596
|
+
"content-type": type.startsWith("text/") || type === "application/json" ? `${type}; charset=utf-8` : type,
|
|
1597
|
+
"content-length": String(info.size),
|
|
1598
|
+
"last-modified": info.mtime.toUTCString()
|
|
1599
|
+
});
|
|
1600
|
+
if (pathname.startsWith("/build/assets/")) headers.set("cache-control", "public, max-age=31536000, immutable");
|
|
1601
|
+
if (request2.method === "HEAD") return new Response(null, { status: 200, headers });
|
|
1602
|
+
const body = Readable2.toWeb(createReadStream2(file));
|
|
1603
|
+
return new Response(body, { status: 200, headers });
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
// server/types.ts
|
|
1420
1607
|
async function terminateRequest(app, request2, response2) {
|
|
1421
1608
|
try {
|
|
1422
1609
|
if (app.bound(HttpKernel)) await app.make(HttpKernel).terminate?.(request2, response2);
|
|
@@ -1424,17 +1611,21 @@ async function terminateRequest(app, request2, response2) {
|
|
|
1424
1611
|
console.error(error);
|
|
1425
1612
|
}
|
|
1426
1613
|
}
|
|
1427
|
-
async function handleSafely(app, request2) {
|
|
1614
|
+
async function handleSafely(app, request2, publicPath) {
|
|
1428
1615
|
try {
|
|
1616
|
+
if (publicPath !== void 0) {
|
|
1617
|
+
const file = await servePublicFile(publicPath, request2);
|
|
1618
|
+
if (file !== void 0) return file;
|
|
1619
|
+
}
|
|
1429
1620
|
return await app.handle(request2);
|
|
1430
1621
|
} catch (error) {
|
|
1431
1622
|
console.error(error);
|
|
1432
1623
|
return plainServerError();
|
|
1433
1624
|
}
|
|
1434
1625
|
}
|
|
1435
|
-
function exitOnSignals(close, closed) {
|
|
1626
|
+
function exitOnSignals(close, closed, app) {
|
|
1436
1627
|
const onSignal = () => {
|
|
1437
|
-
void close().then(() => process.exit(0));
|
|
1628
|
+
void close().then(() => app.shutdown()).then(() => process.exit(0));
|
|
1438
1629
|
};
|
|
1439
1630
|
process.once("SIGINT", onSignal);
|
|
1440
1631
|
process.once("SIGTERM", onSignal);
|
|
@@ -1445,6 +1636,136 @@ function exitOnSignals(close, closed) {
|
|
|
1445
1636
|
}
|
|
1446
1637
|
var displayHost = (hostname) => hostname.includes(":") ? `[${hostname}]` : hostname;
|
|
1447
1638
|
|
|
1639
|
+
// export/paths.ts
|
|
1640
|
+
import { posix } from "path";
|
|
1641
|
+
var ANCHOR = /<a\b[^>]*?\shref\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>]+))/gi;
|
|
1642
|
+
var decode = (path) => {
|
|
1643
|
+
try {
|
|
1644
|
+
return decodeURIComponent(path);
|
|
1645
|
+
} catch {
|
|
1646
|
+
return path;
|
|
1647
|
+
}
|
|
1648
|
+
};
|
|
1649
|
+
function exportFileName(path, style) {
|
|
1650
|
+
const trimmed = decode(path).replace(/^\/+|\/+$/g, "");
|
|
1651
|
+
if (trimmed === "") return "index.html";
|
|
1652
|
+
if (posix.extname(trimmed) !== "") return trimmed;
|
|
1653
|
+
return style === "directory" ? `${trimmed}/index.html` : `${trimmed}.html`;
|
|
1654
|
+
}
|
|
1655
|
+
function sameSiteLinks(html, pageUrl, baseUrl) {
|
|
1656
|
+
const origin = new URL(baseUrl).origin;
|
|
1657
|
+
const found = [];
|
|
1658
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1659
|
+
for (const match of html.matchAll(ANCHOR)) {
|
|
1660
|
+
const href = (match[1] ?? match[2] ?? match[3] ?? "").trim();
|
|
1661
|
+
if (href === "" || href.startsWith("#")) continue;
|
|
1662
|
+
const path = sitePath(href, pageUrl, origin);
|
|
1663
|
+
if (path === void 0 || seen.has(path)) continue;
|
|
1664
|
+
seen.add(path);
|
|
1665
|
+
found.push(path);
|
|
1666
|
+
}
|
|
1667
|
+
return found;
|
|
1668
|
+
}
|
|
1669
|
+
function sitePath(href, pageUrl, origin) {
|
|
1670
|
+
let url2;
|
|
1671
|
+
try {
|
|
1672
|
+
url2 = new URL(href, pageUrl);
|
|
1673
|
+
} catch {
|
|
1674
|
+
return void 0;
|
|
1675
|
+
}
|
|
1676
|
+
if (url2.origin !== new URL(origin).origin) return void 0;
|
|
1677
|
+
return decode(url2.pathname.length > 1 ? url2.pathname.replace(/\/+$/, "") : url2.pathname);
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
// export/export.ts
|
|
1681
|
+
var ExportError = class extends Error {
|
|
1682
|
+
constructor(path, status) {
|
|
1683
|
+
super(`[${path}] answered ${status}.`);
|
|
1684
|
+
this.path = path;
|
|
1685
|
+
this.status = status;
|
|
1686
|
+
}
|
|
1687
|
+
path;
|
|
1688
|
+
status;
|
|
1689
|
+
name = "ExportError";
|
|
1690
|
+
};
|
|
1691
|
+
var MARKER = ".oriel-export";
|
|
1692
|
+
var NOT_FOUND_PATH = "/__oriel-export-not-found__";
|
|
1693
|
+
async function exportSite(app, options) {
|
|
1694
|
+
const output = resolve2(options.output);
|
|
1695
|
+
const publicPath = options.publicPath === false ? void 0 : resolve2(options.publicPath);
|
|
1696
|
+
if (publicPath !== void 0 && existsSync(join(publicPath, "hot"))) {
|
|
1697
|
+
throw new Error("The Vite dev server is running (public/hot exists): stop `npm run dev`, build the assets with `npm run build`, then export.");
|
|
1698
|
+
}
|
|
1699
|
+
assertReplaceable(output);
|
|
1700
|
+
const staging = await mkdtemp(join(dirname(output), `.${basename2(output)}-`));
|
|
1701
|
+
try {
|
|
1702
|
+
if (publicPath !== void 0 && existsSync(publicPath)) {
|
|
1703
|
+
await cp(publicPath, staging, { recursive: true, filter: (source) => basename2(source) !== "hot" && !lstatSync(source).isSymbolicLink() });
|
|
1704
|
+
}
|
|
1705
|
+
const excluded = (path) => options.exclude.some((pattern) => wildcard(pattern).test(path));
|
|
1706
|
+
const isPublicFile = (path) => publicPath !== void 0 && path !== "/" && isFile(join(publicPath, path));
|
|
1707
|
+
const queue = [.../* @__PURE__ */ new Set(["/", ...options.paths.map(rooted)])].filter((path) => path === "/" || !excluded(path));
|
|
1708
|
+
const seen = new Set(queue);
|
|
1709
|
+
const pages = [];
|
|
1710
|
+
const redirects = [];
|
|
1711
|
+
while (queue.length > 0) {
|
|
1712
|
+
const path = queue.shift();
|
|
1713
|
+
const url2 = new URL(path, options.baseUrl).href;
|
|
1714
|
+
const answer = await fetchPage(app, url2);
|
|
1715
|
+
if (answer.status >= 300 && answer.status < 400) {
|
|
1716
|
+
const location = new URL(answer.headers.get("location") ?? "/", url2).href;
|
|
1717
|
+
redirects.push({ path, location });
|
|
1718
|
+
const target = sitePath(location, url2, options.baseUrl);
|
|
1719
|
+
if (target !== void 0 && !seen.has(target) && !excluded(target) && !isPublicFile(target)) {
|
|
1720
|
+
seen.add(target);
|
|
1721
|
+
queue.push(target);
|
|
1722
|
+
}
|
|
1723
|
+
continue;
|
|
1724
|
+
}
|
|
1725
|
+
if (answer.status >= 400) throw new ExportError(path, answer.status);
|
|
1726
|
+
await writeInto(staging, exportFileName(path, options.fileNames), answer.body);
|
|
1727
|
+
pages.push(path);
|
|
1728
|
+
if (!options.crawl || !isHtml(answer.headers)) continue;
|
|
1729
|
+
for (const link of sameSiteLinks(answer.body.toString("utf8"), url2, options.baseUrl)) {
|
|
1730
|
+
if (seen.has(link) || excluded(link) || isPublicFile(link)) continue;
|
|
1731
|
+
seen.add(link);
|
|
1732
|
+
queue.push(link);
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
const notFound = await fetchPage(app, new URL(NOT_FOUND_PATH, options.baseUrl).href);
|
|
1736
|
+
const notFoundPage = notFound.status === 404 && isHtml(notFound.headers);
|
|
1737
|
+
if (notFoundPage) await writeInto(staging, "404.html", notFound.body);
|
|
1738
|
+
await writeFile(join(staging, MARKER), "");
|
|
1739
|
+
await rm(output, { recursive: true, force: true });
|
|
1740
|
+
await rename(staging, output);
|
|
1741
|
+
return { pages, redirects, notFoundPage };
|
|
1742
|
+
} catch (error) {
|
|
1743
|
+
await rm(staging, { recursive: true, force: true });
|
|
1744
|
+
throw error;
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
function assertReplaceable(output) {
|
|
1748
|
+
if (!existsSync(output)) return;
|
|
1749
|
+
if (statSync(output).isDirectory() && (existsSync(join(output, MARKER)) || readdirSync(output).length === 0)) return;
|
|
1750
|
+
throw new Error(`[${output}] exists and wasn't made by oriel export: remove it, or export somewhere else with --output.`);
|
|
1751
|
+
}
|
|
1752
|
+
async function fetchPage(app, url2) {
|
|
1753
|
+
const request2 = new Request(url2, { headers: { accept: "text/html" } });
|
|
1754
|
+
const response2 = await handleSafely(app, request2);
|
|
1755
|
+
const body = Buffer.from(await response2.arrayBuffer());
|
|
1756
|
+
await terminateRequest(app, request2, response2);
|
|
1757
|
+
return { status: response2.status, headers: response2.headers, body };
|
|
1758
|
+
}
|
|
1759
|
+
async function writeInto(folder, file, body) {
|
|
1760
|
+
const path = join(folder, file);
|
|
1761
|
+
await mkdir(dirname(path), { recursive: true });
|
|
1762
|
+
await writeFile(path, body);
|
|
1763
|
+
}
|
|
1764
|
+
var isHtml = (headers) => (headers.get("content-type") ?? "").includes("text/html");
|
|
1765
|
+
var rooted = (path) => path.startsWith("/") ? path : `/${path}`;
|
|
1766
|
+
var isFile = (path) => existsSync(path) && statSync(path).isFile();
|
|
1767
|
+
var wildcard = (pattern) => new RegExp(`^${rooted(pattern).replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*")}$`);
|
|
1768
|
+
|
|
1448
1769
|
// server/bun.ts
|
|
1449
1770
|
function bunRuntime() {
|
|
1450
1771
|
return globalThis.Bun;
|
|
@@ -1453,13 +1774,13 @@ async function serveBun(app, options) {
|
|
|
1453
1774
|
const bun = bunRuntime();
|
|
1454
1775
|
if (bun === void 0) throw new HttpServerError("Bun.serve is not available: the process is not running on Bun.");
|
|
1455
1776
|
let markClosed = () => void 0;
|
|
1456
|
-
const closed = new Promise((
|
|
1777
|
+
const closed = new Promise((resolve4) => markClosed = resolve4);
|
|
1457
1778
|
const server = bun.serve({
|
|
1458
1779
|
port: options.port,
|
|
1459
1780
|
hostname: options.hostname,
|
|
1460
1781
|
async fetch(request2, current) {
|
|
1461
1782
|
setClientAddress(request2, current.requestIP(request2)?.address);
|
|
1462
|
-
const response2 = await handleSafely(app, request2);
|
|
1783
|
+
const response2 = await handleSafely(app, request2, options.publicPath);
|
|
1463
1784
|
setTimeout(() => void terminateRequest(app, request2, response2), 0);
|
|
1464
1785
|
return response2;
|
|
1465
1786
|
},
|
|
@@ -1471,14 +1792,14 @@ async function serveBun(app, options) {
|
|
|
1471
1792
|
let closing;
|
|
1472
1793
|
const close = () => closing ??= (async () => {
|
|
1473
1794
|
let timer;
|
|
1474
|
-
const forced = new Promise((
|
|
1475
|
-
timer = setTimeout(() => void Promise.resolve(server.stop(true)).then(
|
|
1795
|
+
const forced = new Promise((resolve4) => {
|
|
1796
|
+
timer = setTimeout(() => void Promise.resolve(server.stop(true)).then(resolve4), options.shutdownTimeout);
|
|
1476
1797
|
});
|
|
1477
1798
|
await Promise.race([Promise.resolve(server.stop(false)), forced]);
|
|
1478
1799
|
clearTimeout(timer);
|
|
1479
1800
|
markClosed();
|
|
1480
1801
|
})();
|
|
1481
|
-
if (options.signals) exitOnSignals(close, closed);
|
|
1802
|
+
if (options.signals) exitOnSignals(close, closed, app);
|
|
1482
1803
|
return {
|
|
1483
1804
|
runtime: "bun",
|
|
1484
1805
|
hostname: options.hostname,
|
|
@@ -1491,7 +1812,7 @@ async function serveBun(app, options) {
|
|
|
1491
1812
|
|
|
1492
1813
|
// server/node.ts
|
|
1493
1814
|
import { createServer } from "http";
|
|
1494
|
-
import { Readable as
|
|
1815
|
+
import { Readable as Readable3 } from "stream";
|
|
1495
1816
|
import { pipeline } from "stream/promises";
|
|
1496
1817
|
function requestBody(req) {
|
|
1497
1818
|
let finished = false;
|
|
@@ -1573,42 +1894,46 @@ async function writeResponse(res, response2) {
|
|
|
1573
1894
|
res.end();
|
|
1574
1895
|
return;
|
|
1575
1896
|
}
|
|
1576
|
-
await pipeline(
|
|
1897
|
+
await pipeline(Readable3.fromWeb(response2.body), res);
|
|
1898
|
+
}
|
|
1899
|
+
function nodeRequestHandler(app, options) {
|
|
1900
|
+
return async (req, res) => {
|
|
1901
|
+
let request2;
|
|
1902
|
+
try {
|
|
1903
|
+
request2 = toWebRequest(req, res, options.fallbackHost);
|
|
1904
|
+
} catch {
|
|
1905
|
+
res.statusCode = 400;
|
|
1906
|
+
res.end("Bad Request");
|
|
1907
|
+
return;
|
|
1908
|
+
}
|
|
1909
|
+
setClientAddress(request2, req.socket.remoteAddress);
|
|
1910
|
+
const response2 = await handleSafely(app, request2, options.publicPath);
|
|
1911
|
+
try {
|
|
1912
|
+
await writeResponse(res, response2);
|
|
1913
|
+
} catch {
|
|
1914
|
+
res.destroy();
|
|
1915
|
+
}
|
|
1916
|
+
await terminateRequest(app, request2, response2);
|
|
1917
|
+
};
|
|
1577
1918
|
}
|
|
1578
1919
|
async function serveNode(app, options) {
|
|
1579
|
-
const
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
let request2;
|
|
1583
|
-
try {
|
|
1584
|
-
request2 = toWebRequest(req, res, fallbackHost);
|
|
1585
|
-
} catch {
|
|
1586
|
-
res.statusCode = 400;
|
|
1587
|
-
res.end("Bad Request");
|
|
1588
|
-
return;
|
|
1589
|
-
}
|
|
1590
|
-
setClientAddress(request2, req.socket.remoteAddress);
|
|
1591
|
-
const response2 = await handleSafely(app, request2);
|
|
1592
|
-
try {
|
|
1593
|
-
await writeResponse(res, response2);
|
|
1594
|
-
} catch {
|
|
1595
|
-
res.destroy();
|
|
1596
|
-
}
|
|
1597
|
-
await terminateRequest(app, request2, response2);
|
|
1598
|
-
})();
|
|
1920
|
+
const handle = nodeRequestHandler(app, {
|
|
1921
|
+
...options.publicPath !== void 0 && { publicPath: options.publicPath },
|
|
1922
|
+
fallbackHost: `${displayHost(options.hostname)}:${options.port}`
|
|
1599
1923
|
});
|
|
1600
|
-
|
|
1924
|
+
const server = createServer((req, res) => void handle(req, res));
|
|
1925
|
+
await new Promise((resolve4, reject) => {
|
|
1601
1926
|
server.once("error", reject);
|
|
1602
1927
|
server.listen(options.port, options.hostname, () => {
|
|
1603
1928
|
server.off("error", reject);
|
|
1604
|
-
|
|
1929
|
+
resolve4();
|
|
1605
1930
|
});
|
|
1606
1931
|
});
|
|
1607
1932
|
const address = server.address();
|
|
1608
|
-
const closed = new Promise((
|
|
1933
|
+
const closed = new Promise((resolve4) => server.once("close", () => resolve4()));
|
|
1609
1934
|
let closing;
|
|
1610
1935
|
const close = () => closing ??= (async () => {
|
|
1611
|
-
const done = new Promise((
|
|
1936
|
+
const done = new Promise((resolve4) => server.close(() => resolve4()));
|
|
1612
1937
|
server.closeIdleConnections();
|
|
1613
1938
|
const idle = setInterval(() => server.closeIdleConnections(), 50);
|
|
1614
1939
|
const force = setTimeout(() => server.closeAllConnections(), options.shutdownTimeout);
|
|
@@ -1616,7 +1941,7 @@ async function serveNode(app, options) {
|
|
|
1616
1941
|
clearInterval(idle);
|
|
1617
1942
|
clearTimeout(force);
|
|
1618
1943
|
})();
|
|
1619
|
-
if (options.signals) exitOnSignals(close, closed);
|
|
1944
|
+
if (options.signals) exitOnSignals(close, closed, app);
|
|
1620
1945
|
return {
|
|
1621
1946
|
runtime: "node",
|
|
1622
1947
|
hostname: options.hostname,
|
|
@@ -1636,7 +1961,8 @@ async function serve(app, options = {}) {
|
|
|
1636
1961
|
port: options.port ?? Number(process.env.PORT ?? process.env.SERVER_PORT ?? 8e3),
|
|
1637
1962
|
hostname: options.hostname ?? "127.0.0.1",
|
|
1638
1963
|
signals: options.signals ?? true,
|
|
1639
|
-
shutdownTimeout: options.shutdownTimeout ?? 1e4
|
|
1964
|
+
shutdownTimeout: options.shutdownTimeout ?? 1e4,
|
|
1965
|
+
...options.publicPath !== false && { publicPath: options.publicPath ?? app.publicPath() }
|
|
1640
1966
|
};
|
|
1641
1967
|
await app.boot();
|
|
1642
1968
|
const runtime = options.runtime ?? detectRuntime();
|
|
@@ -1647,12 +1973,256 @@ async function serve(app, options = {}) {
|
|
|
1647
1973
|
}
|
|
1648
1974
|
|
|
1649
1975
|
// commands.ts
|
|
1650
|
-
import { existsSync } from "fs";
|
|
1651
|
-
import { mkdir, readFile as readFile2, writeFile } from "fs/promises";
|
|
1652
|
-
import { dirname } from "path";
|
|
1976
|
+
import { existsSync as existsSync4 } from "fs";
|
|
1977
|
+
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
1978
|
+
import { dirname as dirname2, resolve as resolve3 } from "path";
|
|
1653
1979
|
import {
|
|
1980
|
+
ConfigRepository as ConfigRepository4,
|
|
1654
1981
|
Router as Router3
|
|
1655
1982
|
} from "@orieljs/contracts";
|
|
1983
|
+
|
|
1984
|
+
// watch/environment.ts
|
|
1985
|
+
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
1986
|
+
import { join as join2 } from "path";
|
|
1987
|
+
import * as util from "util";
|
|
1988
|
+
var launchEnvironment = Object.freeze({ ...process.env });
|
|
1989
|
+
var BUN_ENV_FILES = [
|
|
1990
|
+
".env",
|
|
1991
|
+
".env.development",
|
|
1992
|
+
".env.production",
|
|
1993
|
+
".env.test",
|
|
1994
|
+
".env.local",
|
|
1995
|
+
".env.development.local",
|
|
1996
|
+
".env.production.local",
|
|
1997
|
+
".env.test.local"
|
|
1998
|
+
];
|
|
1999
|
+
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] ?? "");
|
|
2000
|
+
function serverEnvironment(options = {}) {
|
|
2001
|
+
const launched = options.launched ?? launchEnvironment;
|
|
2002
|
+
const env = { ...launched };
|
|
2003
|
+
if (!(options.bun ?? process.versions.bun !== void 0)) return env;
|
|
2004
|
+
const parseEnv2 = "parseEnv" in options ? options.parseEnv : util.parseEnv;
|
|
2005
|
+
if (typeof parseEnv2 !== "function") return env;
|
|
2006
|
+
const cwd = options.cwd ?? process.cwd();
|
|
2007
|
+
for (const name of BUN_ENV_FILES) {
|
|
2008
|
+
const file = join2(cwd, name);
|
|
2009
|
+
if (!existsSync2(file)) continue;
|
|
2010
|
+
let values;
|
|
2011
|
+
try {
|
|
2012
|
+
values = parseEnv2(readFileSync(file, "utf8"));
|
|
2013
|
+
} catch {
|
|
2014
|
+
continue;
|
|
2015
|
+
}
|
|
2016
|
+
for (const [key, raw] of Object.entries(values)) {
|
|
2017
|
+
const value = launched[key];
|
|
2018
|
+
if (raw !== void 0 && value !== void 0 && (value === raw || value === expand(raw, launched))) delete env[key];
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
return env;
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
// watch/files.ts
|
|
2025
|
+
import { existsSync as existsSync3, statSync as statSync2, watch as watchPath } from "fs";
|
|
2026
|
+
import { join as join3 } from "path";
|
|
2027
|
+
var WATCHED_FOLDERS = ["app", "bootstrap", "config", "lang", "resources/lang", "resources/views", "routes"];
|
|
2028
|
+
var WATCHED_ROOT_FILES = ["package.json", "tsconfig.json"];
|
|
2029
|
+
var IGNORED_FOLDERS = ["bootstrap/ssr", "bootstrap/cache"];
|
|
2030
|
+
var CODE_FILE = /\.(?:[cm]?[jt]sx?|json)$/;
|
|
2031
|
+
var DECLARATION_FILE = /\.d\.[cm]?ts$/;
|
|
2032
|
+
var EDITOR_FILE = /(?:\.sw[a-px]|~|___jb_(?:tmp|old)___)$/;
|
|
2033
|
+
var isEnvironmentFile = (name) => (name === ".env" || name.startsWith(".env.")) && name !== ".env.example" && !EDITOR_FILE.test(name);
|
|
2034
|
+
function isWatchedPath(path) {
|
|
2035
|
+
const segments = path.split("/");
|
|
2036
|
+
if (segments.length === 1) return isEnvironmentFile(path) || WATCHED_ROOT_FILES.includes(path);
|
|
2037
|
+
if (!WATCHED_FOLDERS.some((folder) => path.startsWith(`${folder}/`))) return false;
|
|
2038
|
+
if (IGNORED_FOLDERS.some((folder) => path === folder || path.startsWith(`${folder}/`))) return false;
|
|
2039
|
+
if (segments.some((segment) => segment.startsWith(".") || segment === "node_modules")) return false;
|
|
2040
|
+
const name = segments.at(-1);
|
|
2041
|
+
if (DECLARATION_FILE.test(name)) return false;
|
|
2042
|
+
return CODE_FILE.test(name) || !name.includes(".");
|
|
2043
|
+
}
|
|
2044
|
+
var toSlashes = (file) => file.split("\\").join("/");
|
|
2045
|
+
function watchFiles(basePath, onChange, options = {}) {
|
|
2046
|
+
const watch = options.watch ?? ((path, watchOptions, listener) => watchPath(path, watchOptions, listener));
|
|
2047
|
+
const debounce = options.debounce ?? 200;
|
|
2048
|
+
const onError = options.onError ?? (() => {
|
|
2049
|
+
});
|
|
2050
|
+
const changed = /* @__PURE__ */ new Set();
|
|
2051
|
+
let timer;
|
|
2052
|
+
const settle = options.settle ?? 1e3;
|
|
2053
|
+
const startedAt = Date.now();
|
|
2054
|
+
const reportedLate = (path) => {
|
|
2055
|
+
if (Date.now() - startedAt >= settle) return false;
|
|
2056
|
+
try {
|
|
2057
|
+
const modified = statSync2(join3(basePath, path), { throwIfNoEntry: false })?.mtimeMs;
|
|
2058
|
+
return modified === void 0 || modified < startedAt + 1;
|
|
2059
|
+
} catch {
|
|
2060
|
+
return false;
|
|
2061
|
+
}
|
|
2062
|
+
};
|
|
2063
|
+
const record = (path) => {
|
|
2064
|
+
if (!isWatchedPath(path) || reportedLate(path)) return;
|
|
2065
|
+
changed.add(path);
|
|
2066
|
+
clearTimeout(timer);
|
|
2067
|
+
timer = setTimeout(() => {
|
|
2068
|
+
const files = [...changed];
|
|
2069
|
+
changed.clear();
|
|
2070
|
+
onChange(files);
|
|
2071
|
+
}, debounce);
|
|
2072
|
+
};
|
|
2073
|
+
const handles = [];
|
|
2074
|
+
const start = (folder, recursive, toPath) => {
|
|
2075
|
+
try {
|
|
2076
|
+
const handle = watch(folder === "." ? basePath : join3(basePath, folder), { recursive }, (_event, file) => {
|
|
2077
|
+
const path = toPath(file);
|
|
2078
|
+
if (path !== void 0) record(path);
|
|
2079
|
+
});
|
|
2080
|
+
handle.on("error", (error) => onError(folder, error));
|
|
2081
|
+
handles.push(handle);
|
|
2082
|
+
} catch (error) {
|
|
2083
|
+
onError(folder, error);
|
|
2084
|
+
}
|
|
2085
|
+
};
|
|
2086
|
+
start(".", false, (file) => file === null ? void 0 : toSlashes(file));
|
|
2087
|
+
for (const folder of WATCHED_FOLDERS) {
|
|
2088
|
+
if (existsSync3(join3(basePath, folder))) start(folder, true, (file) => `${folder}/${file === null ? "" : toSlashes(file)}`);
|
|
2089
|
+
}
|
|
2090
|
+
return {
|
|
2091
|
+
close() {
|
|
2092
|
+
clearTimeout(timer);
|
|
2093
|
+
changed.clear();
|
|
2094
|
+
for (const handle of handles) handle.close();
|
|
2095
|
+
}
|
|
2096
|
+
};
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
// watch/port.ts
|
|
2100
|
+
import { createServer as createServer2 } from "net";
|
|
2101
|
+
function claimPort(hostname, port) {
|
|
2102
|
+
return new Promise((resolve4, reject) => {
|
|
2103
|
+
const server = createServer2();
|
|
2104
|
+
server.once("error", reject);
|
|
2105
|
+
try {
|
|
2106
|
+
server.listen({ port, host: hostname, exclusive: true }, () => {
|
|
2107
|
+
const { port: claimed } = server.address();
|
|
2108
|
+
server.close(() => resolve4(claimed));
|
|
2109
|
+
});
|
|
2110
|
+
} catch (error) {
|
|
2111
|
+
reject(error);
|
|
2112
|
+
}
|
|
2113
|
+
});
|
|
2114
|
+
}
|
|
2115
|
+
var REASONS = {
|
|
2116
|
+
EADDRINUSE: "Address already in use",
|
|
2117
|
+
EACCES: "Permission denied",
|
|
2118
|
+
EADDRNOTAVAIL: "Can't assign requested address"
|
|
2119
|
+
};
|
|
2120
|
+
function listenFailure(hostname, port, error) {
|
|
2121
|
+
const { code, message } = error;
|
|
2122
|
+
return `Failed to listen on ${displayHost(hostname)}:${port} (reason: ${code !== void 0 && REASONS[code] || message})`;
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
// watch/WatchedServer.ts
|
|
2126
|
+
import { spawn as spawnProcess } from "child_process";
|
|
2127
|
+
var WatchedServer = class {
|
|
2128
|
+
constructor(options) {
|
|
2129
|
+
this.options = options;
|
|
2130
|
+
}
|
|
2131
|
+
options;
|
|
2132
|
+
running;
|
|
2133
|
+
restarting = false;
|
|
2134
|
+
stopping = false;
|
|
2135
|
+
finish = () => {
|
|
2136
|
+
};
|
|
2137
|
+
/** Starts the server and keeps it current until stopped. Resolves 0 once the last server has exited. */
|
|
2138
|
+
run() {
|
|
2139
|
+
const done = new Promise((resolve4) => this.finish = resolve4);
|
|
2140
|
+
const stopListening = this.listenForSignals();
|
|
2141
|
+
const watcher = this.options.watch((files) => void this.changed(files));
|
|
2142
|
+
this.start();
|
|
2143
|
+
return done.finally(() => {
|
|
2144
|
+
watcher.close();
|
|
2145
|
+
stopListening();
|
|
2146
|
+
});
|
|
2147
|
+
}
|
|
2148
|
+
/** Stops the server and ends run(); called again, kills the server at once. */
|
|
2149
|
+
stop() {
|
|
2150
|
+
if (this.stopping) {
|
|
2151
|
+
this.running?.child.kill("SIGKILL");
|
|
2152
|
+
return;
|
|
2153
|
+
}
|
|
2154
|
+
this.stopping = true;
|
|
2155
|
+
void this.stopServer().then(() => this.finish(0));
|
|
2156
|
+
}
|
|
2157
|
+
async changed(files) {
|
|
2158
|
+
if (this.stopping || this.restarting || files.length === 0) return;
|
|
2159
|
+
this.restarting = true;
|
|
2160
|
+
try {
|
|
2161
|
+
this.options.info(
|
|
2162
|
+
files.some(isEnvironmentFile) ? "Environment modified. Restarting server..." : `Application change detected in [${files[0]}]. Restarting server...`
|
|
2163
|
+
);
|
|
2164
|
+
await this.stopServer();
|
|
2165
|
+
if (!this.stopping) this.start();
|
|
2166
|
+
} finally {
|
|
2167
|
+
this.restarting = false;
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
start() {
|
|
2171
|
+
const [command, ...args] = this.options.command;
|
|
2172
|
+
const child = (this.options.spawn ?? spawnProcess)(command, args, {
|
|
2173
|
+
cwd: this.options.cwd,
|
|
2174
|
+
env: this.options.env,
|
|
2175
|
+
stdio: ["ignore", "inherit", "inherit"]
|
|
2176
|
+
});
|
|
2177
|
+
const running = { child, stopping: false, killed: false, exited: Promise.resolve() };
|
|
2178
|
+
running.exited = new Promise((resolve4) => {
|
|
2179
|
+
let settled = false;
|
|
2180
|
+
const settle = (report) => {
|
|
2181
|
+
if (settled) return;
|
|
2182
|
+
settled = true;
|
|
2183
|
+
if (this.running === running) this.running = void 0;
|
|
2184
|
+
if (!running.stopping && !this.stopping) this.options.error(`${report}. Waiting for changes before restarting...`);
|
|
2185
|
+
resolve4();
|
|
2186
|
+
};
|
|
2187
|
+
child.once("exit", (code, signal) => settle(signal !== null ? `The server stopped (signal ${signal})` : `The server stopped (exit code ${code})`));
|
|
2188
|
+
child.once("error", (error) => {
|
|
2189
|
+
if (child.pid === void 0) settle(`The server could not start (${error.message})`);
|
|
2190
|
+
});
|
|
2191
|
+
});
|
|
2192
|
+
this.running = running;
|
|
2193
|
+
}
|
|
2194
|
+
/** Sends SIGTERM once and waits for the exit, killing the server after stopTimeout. */
|
|
2195
|
+
async stopServer() {
|
|
2196
|
+
const running = this.running;
|
|
2197
|
+
if (running === void 0) return;
|
|
2198
|
+
if (!running.stopping) {
|
|
2199
|
+
running.stopping = true;
|
|
2200
|
+
running.child.kill("SIGTERM");
|
|
2201
|
+
}
|
|
2202
|
+
const timeout = this.options.stopTimeout ?? 15e3;
|
|
2203
|
+
const timer = setTimeout(() => {
|
|
2204
|
+
if (running.killed) return;
|
|
2205
|
+
running.killed = true;
|
|
2206
|
+
this.options.error(`The server did not stop within ${timeout / 1e3} seconds and was killed.`);
|
|
2207
|
+
running.child.kill("SIGKILL");
|
|
2208
|
+
}, timeout);
|
|
2209
|
+
try {
|
|
2210
|
+
await running.exited;
|
|
2211
|
+
} finally {
|
|
2212
|
+
clearTimeout(timer);
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
listenForSignals() {
|
|
2216
|
+
const signals = this.options.signals ?? ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
2217
|
+
const handler = () => this.stop();
|
|
2218
|
+
for (const signal of signals) process.on(signal, handler);
|
|
2219
|
+
return () => {
|
|
2220
|
+
for (const signal of signals) process.off(signal, handler);
|
|
2221
|
+
};
|
|
2222
|
+
}
|
|
2223
|
+
};
|
|
2224
|
+
|
|
2225
|
+
// commands.ts
|
|
1656
2226
|
function io(command) {
|
|
1657
2227
|
return {
|
|
1658
2228
|
option: (name) => {
|
|
@@ -1726,11 +2296,68 @@ ${web[1]}api: 'routes/api.ts',`);
|
|
|
1726
2296
|
${create[0]}`);
|
|
1727
2297
|
return void 0;
|
|
1728
2298
|
}
|
|
2299
|
+
async function serveWatching(app, out, port, run) {
|
|
2300
|
+
const hostname = out.option("host") ?? "127.0.0.1";
|
|
2301
|
+
const requested = port !== void 0 ? Number(port) : Number(process.env.PORT ?? process.env.SERVER_PORT ?? 8e3);
|
|
2302
|
+
let claimed;
|
|
2303
|
+
try {
|
|
2304
|
+
claimed = await claimPort(hostname, requested);
|
|
2305
|
+
} catch (error) {
|
|
2306
|
+
out.error(listenFailure(hostname, requested, error));
|
|
2307
|
+
return 1;
|
|
2308
|
+
}
|
|
2309
|
+
const script = process.argv[1];
|
|
2310
|
+
if (script === void 0) {
|
|
2311
|
+
out.error("serve --watch can't tell which script started this process, so it can't start the server.");
|
|
2312
|
+
return 1;
|
|
2313
|
+
}
|
|
2314
|
+
return run({
|
|
2315
|
+
command: [process.execPath, ...process.execArgv, script, "serve", `--host=${hostname}`, `--port=${claimed}`],
|
|
2316
|
+
cwd: process.cwd(),
|
|
2317
|
+
env: serverEnvironment(),
|
|
2318
|
+
watch: (onChange) => watchFiles(app.basePath(), onChange, { onError: (folder, error) => out.warn(`Unable to watch [${folder}] for changes: ${error.message}`) }),
|
|
2319
|
+
info: (message) => out.info(message),
|
|
2320
|
+
error: (message) => out.error(message)
|
|
2321
|
+
});
|
|
2322
|
+
}
|
|
2323
|
+
async function exportCommand(app, out) {
|
|
2324
|
+
const config = (key, fallback) => app.bound(ConfigRepository4) ? app.make(ConfigRepository4).get(key, fallback) : fallback;
|
|
2325
|
+
const fileNames = config("export.file_names", "html");
|
|
2326
|
+
if (fileNames !== "html" && fileNames !== "directory") {
|
|
2327
|
+
out.error(`export.file_names is "html" or "directory", not "${fileNames}".`);
|
|
2328
|
+
return 1;
|
|
2329
|
+
}
|
|
2330
|
+
const output = out.option("output") ?? config("export.output", "dist");
|
|
2331
|
+
const baseUrl = config("app.url", "http://localhost");
|
|
2332
|
+
const started = performance.now();
|
|
2333
|
+
try {
|
|
2334
|
+
const result = await exportSite(app, {
|
|
2335
|
+
output: resolve3(app.basePath(), output),
|
|
2336
|
+
baseUrl,
|
|
2337
|
+
crawl: config("export.crawl", true),
|
|
2338
|
+
paths: config("export.paths", []),
|
|
2339
|
+
exclude: config("export.exclude", []),
|
|
2340
|
+
fileNames,
|
|
2341
|
+
publicPath: app.publicPath()
|
|
2342
|
+
});
|
|
2343
|
+
const origin = new URL(baseUrl).origin;
|
|
2344
|
+
for (const { path, location } of result.redirects) {
|
|
2345
|
+
out.line(`Redirect ${path} \u2192 ${location.startsWith(origin) ? location.slice(origin.length) || "/" : location}, not written.`);
|
|
2346
|
+
}
|
|
2347
|
+
const pages = result.pages.length === 1 ? "1 page" : `${result.pages.length} pages`;
|
|
2348
|
+
out.info(`Exported ${pages} to [${output}] in ${Math.round(performance.now() - started)} ms.`);
|
|
2349
|
+
return 0;
|
|
2350
|
+
} catch (error) {
|
|
2351
|
+
out.error(error instanceof ExportError ? error.message : error instanceof Error ? error.message : String(error));
|
|
2352
|
+
return 1;
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
1729
2355
|
function registerHttpCommands(kernel, app, options = {}) {
|
|
1730
2356
|
const serveApp = options.serve ?? serve;
|
|
1731
2357
|
const terminalWidth = options.terminalWidth ?? (() => process.stdout.columns ?? 120);
|
|
2358
|
+
const runWatching = options.watch ?? ((watchOptions) => new WatchedServer(watchOptions).run());
|
|
1732
2359
|
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}",
|
|
2360
|
+
"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
2361
|
async function() {
|
|
1735
2362
|
const out = io(this);
|
|
1736
2363
|
const port = out.option("port");
|
|
@@ -1738,6 +2365,7 @@ function registerHttpCommands(kernel, app, options = {}) {
|
|
|
1738
2365
|
out.error("The --port option must be a number.");
|
|
1739
2366
|
return 1;
|
|
1740
2367
|
}
|
|
2368
|
+
if (out.flag("watch")) return serveWatching(app, out, port, runWatching);
|
|
1741
2369
|
const server = await serveApp(app, {
|
|
1742
2370
|
hostname: out.option("host") ?? "127.0.0.1",
|
|
1743
2371
|
...port !== void 0 && { port: Number(port) },
|
|
@@ -1782,27 +2410,30 @@ function registerHttpCommands(kernel, app, options = {}) {
|
|
|
1782
2410
|
return 0;
|
|
1783
2411
|
}
|
|
1784
2412
|
).purpose("List all registered routes");
|
|
2413
|
+
kernel.command("export {--output= : The folder to write the site to (default: config export.output, else dist)}", function() {
|
|
2414
|
+
return exportCommand(app, io(this));
|
|
2415
|
+
}).purpose("Export the application's pages as static files");
|
|
1785
2416
|
kernel.command("install:api {--force : Overwrite any existing API routes file}", async function() {
|
|
1786
2417
|
const out = io(this);
|
|
1787
2418
|
const routesFile = app.basePath("routes/api.ts");
|
|
1788
|
-
if (
|
|
2419
|
+
if (existsSync4(routesFile) && !out.flag("force")) {
|
|
1789
2420
|
out.error("API routes file already exists.");
|
|
1790
2421
|
} else {
|
|
1791
|
-
await
|
|
1792
|
-
await
|
|
2422
|
+
await mkdir2(dirname2(routesFile), { recursive: true });
|
|
2423
|
+
await writeFile2(routesFile, API_ROUTES_STUB);
|
|
1793
2424
|
out.info("Published API routes file.");
|
|
1794
2425
|
}
|
|
1795
2426
|
const bootstrapFile = app.bootstrapPath("app.ts");
|
|
1796
|
-
const source =
|
|
2427
|
+
const source = existsSync4(bootstrapFile) ? await readFile2(bootstrapFile, "utf8") : void 0;
|
|
1797
2428
|
const updated = source === void 0 ? void 0 : addApiRouting(source);
|
|
1798
2429
|
if (updated === void 0) {
|
|
1799
2430
|
out.warn(
|
|
1800
2431
|
"Unable to automatically add API route definition to bootstrap file. Add api: 'routes/api.ts' to withRouting() in bootstrap/app.ts."
|
|
1801
2432
|
);
|
|
1802
2433
|
} else if (updated !== source) {
|
|
1803
|
-
await
|
|
2434
|
+
await writeFile2(bootstrapFile, updated);
|
|
1804
2435
|
}
|
|
1805
|
-
out.info("API scaffolding installed.
|
|
2436
|
+
out.info("API scaffolding installed.");
|
|
1806
2437
|
return 0;
|
|
1807
2438
|
}).purpose("Create an API routes file and prepare the application for API authentication");
|
|
1808
2439
|
}
|
|
@@ -1816,6 +2447,7 @@ var HttpServiceProvider = class extends ServiceProvider {
|
|
|
1816
2447
|
{
|
|
1817
2448
|
trusted_proxies: [],
|
|
1818
2449
|
max_body_size: 8 * 1024 * 1024,
|
|
2450
|
+
max_json_depth: 512,
|
|
1819
2451
|
csrf_except: [],
|
|
1820
2452
|
redirect_guests_to: void 0,
|
|
1821
2453
|
redirect_users_to: void 0
|
|
@@ -1844,6 +2476,7 @@ export {
|
|
|
1844
2476
|
ConvertEmptyStringsToNull,
|
|
1845
2477
|
ErrorHandler,
|
|
1846
2478
|
ExceptionsConfiguration,
|
|
2479
|
+
ExportError,
|
|
1847
2480
|
FormUploadedFile,
|
|
1848
2481
|
HttpException,
|
|
1849
2482
|
HttpRedirectResponse,
|
|
@@ -1870,6 +2503,9 @@ export {
|
|
|
1870
2503
|
debugPage,
|
|
1871
2504
|
detectRuntime,
|
|
1872
2505
|
escapeHtml,
|
|
2506
|
+
exceedsJsonDepth,
|
|
2507
|
+
exportFileName,
|
|
2508
|
+
exportSite,
|
|
1873
2509
|
extensionFromMime,
|
|
1874
2510
|
filterRouteList,
|
|
1875
2511
|
formatRouteList,
|
|
@@ -1879,6 +2515,7 @@ export {
|
|
|
1879
2515
|
mergeRecursive,
|
|
1880
2516
|
mimeFromExtension,
|
|
1881
2517
|
minimalPage,
|
|
2518
|
+
nodeRequestHandler,
|
|
1882
2519
|
normalizeLists,
|
|
1883
2520
|
optionalCookieJar,
|
|
1884
2521
|
optionalSession,
|
|
@@ -1891,10 +2528,12 @@ export {
|
|
|
1891
2528
|
response,
|
|
1892
2529
|
route,
|
|
1893
2530
|
routeListRows,
|
|
2531
|
+
sameSiteLinks,
|
|
1894
2532
|
sanitize,
|
|
1895
2533
|
serve,
|
|
1896
2534
|
serveBun,
|
|
1897
2535
|
serveNode,
|
|
2536
|
+
servePublicFile,
|
|
1898
2537
|
setClientAddress,
|
|
1899
2538
|
stackFrames,
|
|
1900
2539
|
statusFor,
|