@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/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
- /** Remembers where a guest was going (a GET page, else the previous URL), then redirects to `path`. */
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 FormUploadedFile = class {
243
- constructor(file) {
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
- /** Guessed from the MIME type, falling back to the client's extension (Laravel's extension()). */
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
- return extensionFromMime(this.mimeType) ?? this.clientExtension();
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, new FormUploadedFile(value)]);
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 : String(value);
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
- user(guard) {
692
- return this.#userResolver?.(guard);
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
- /** Validates all() through the ValidationFactory; throws its Validation error on failure. */
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, parameters, absolute) {
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, { trustedProxies: this.#trustedProxies(), maxBodySize: this.#maxBodySize() });
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 (http.expectsJson()) response2 = this.#renderJson(error);
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
- if (headers !== void 0) for (const [name, value] of Object.entries(headers)) response2.headers.set(name, value);
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 (request2.expectsJson()) {
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 (request2.expectsJson()) return this.#json({ message: error.message || "Unauthenticated." }, 401);
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((resolve) => markClosed = resolve);
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((resolve) => {
1475
- timer = setTimeout(() => void Promise.resolve(server.stop(true)).then(resolve), options.shutdownTimeout);
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 Readable2 } from "stream";
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(Readable2.fromWeb(response2.body), res);
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 fallbackHost = `${displayHost(options.hostname)}:${options.port}`;
1580
- const server = createServer((req, res) => {
1581
- void (async () => {
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
- await new Promise((resolve, reject) => {
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
- resolve();
1929
+ resolve4();
1605
1930
  });
1606
1931
  });
1607
1932
  const address = server.address();
1608
- const closed = new Promise((resolve) => server.once("close", () => resolve()));
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((resolve) => server.close(() => resolve()));
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 (existsSync(routesFile) && !out.flag("force")) {
2419
+ if (existsSync4(routesFile) && !out.flag("force")) {
1789
2420
  out.error("API routes file already exists.");
1790
2421
  } else {
1791
- await mkdir(dirname(routesFile), { recursive: true });
1792
- await writeFile(routesFile, API_ROUTES_STUB);
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 = existsSync(bootstrapFile) ? await readFile2(bootstrapFile, "utf8") : void 0;
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 writeFile(bootstrapFile, updated);
2434
+ await writeFile2(bootstrapFile, updated);
1804
2435
  }
1805
- out.info("API scaffolding installed. Run [oriel migrate] to create the personal access tokens table.");
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,