@orieljs/http 0.1.0-alpha.10 → 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.d.ts CHANGED
@@ -412,6 +412,59 @@ declare function stackFrames(error: Error): StackFrame[];
412
412
  /** The APP_DEBUG page: class, message, request, a source excerpt of the first app frame, the stack and causes. */
413
413
  declare function debugPage(error: unknown, method: string, url: string): Promise<string>;
414
414
 
415
+ interface ExportOptions {
416
+ /** The folder the site is written to. */
417
+ output: string;
418
+ /** The URL the pages are requested at (the app's `app.url`); only its origin's links are followed. */
419
+ baseUrl: string;
420
+ /** Follow every same-site link from `/` (and from `paths`). */
421
+ crawl: boolean;
422
+ /** Paths exported besides `/` (pages nothing links to, a feed, …). */
423
+ paths: string[];
424
+ /** Paths left out; `*` matches anything (`/drafts/*`). */
425
+ exclude: string[];
426
+ /** `/docs/routing` → `docs/routing.html` (`'html'`) or `docs/routing/index.html` (`'directory'`). */
427
+ fileNames: 'html' | 'directory';
428
+ /** Copied into the output first (the built assets, images); false copies nothing. */
429
+ publicPath: string | false;
430
+ }
431
+ interface ExportResult {
432
+ /** The paths written, in the order they were found. */
433
+ pages: string[];
434
+ /** Redirects met while following links: listed, not written. */
435
+ redirects: Array<{
436
+ path: string;
437
+ location: string;
438
+ }>;
439
+ /** Whether the app's 404 page was written as `404.html`. */
440
+ notFoundPage: boolean;
441
+ }
442
+ /** A page answered with an error: the export stops, and the last export stays as it was. */
443
+ declare class ExportError extends Error {
444
+ readonly path: string;
445
+ readonly status: number;
446
+ readonly name = "ExportError";
447
+ constructor(path: string, status: number);
448
+ }
449
+ /**
450
+ * Exports an app's pages as files: requests `/` and `paths` inside the process (as `serve` handles a request),
451
+ * follows same-site links, and writes every page into `output`, after a copy of `publicPath`. The site is written
452
+ * into a temporary folder beside `output` and swapped in at the end, so a failed export leaves the last one intact.
453
+ */
454
+ declare function exportSite(app: Application, options: ExportOptions): Promise<ExportResult>;
455
+
456
+ /**
457
+ * The file a page is written to, relative to the output folder: `/` → `index.html`, `/docs/routing` →
458
+ * `docs/routing.html` (or `docs/routing/index.html`). A path with an extension (`/feed.xml`) keeps its name.
459
+ */
460
+ declare function exportFileName(path: string, style: 'html' | 'directory'): string;
461
+ /**
462
+ * The paths a page links to on the same site (the origin of `baseUrl`): resolved against the page's URL, decoded,
463
+ * without query, hash or a trailing slash, each once, in document order. Links to other origins, `mailto:`, `tel:`,
464
+ * `javascript:` and to an anchor on the same page are left out.
465
+ */
466
+ declare function sameSiteLinks(html: string, pageUrl: string, baseUrl: string): string[];
467
+
415
468
  type Runtime = 'node' | 'bun';
416
469
  interface ServeOptions {
417
470
  /** Default: PORT, then SERVER_PORT, then 8000 (Laravel's serve). 0 picks a free port. */
@@ -472,6 +525,15 @@ declare function serveBun(app: Application, options: ResolvedServeOptions): Prom
472
525
  declare function toWebRequest(req: IncomingMessage, res: ServerResponse, fallbackHost: string): Request;
473
526
  /** Web Response → ServerResponse, streaming the body with backpressure. Several Set-Cookie headers stay separate. */
474
527
  declare function writeResponse(res: ServerResponse, response: Response): Promise<void>;
528
+ /**
529
+ * A node:http request listener for the app: a public file when one matches, else the app's answer, then the
530
+ * terminable middleware. `serve` uses it, and so does any host that hands Oriel Node's request and response (Vercel's
531
+ * functions). `fallbackHost` is the Host used when a request has none.
532
+ */
533
+ declare function nodeRequestHandler(app: Application, options: {
534
+ publicPath?: string;
535
+ fallbackHost: string;
536
+ }): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
475
537
  declare function serveNode(app: Application, options: ResolvedServeOptions): Promise<RunningServer>;
476
538
 
477
539
  declare function detectRuntime(): Runtime;
@@ -549,7 +611,7 @@ interface HttpCommandOptions {
549
611
  /** Replaces `serve --watch`'s restarting server (tests). Resolves the command's exit code. */
550
612
  watch?: (options: WatchedServerOptions) => Promise<number>;
551
613
  }
552
- /** Registers `serve`, `route:list` and `install:api` as closure commands (PC1). */
614
+ /** Registers `serve`, `route:list`, `export` and `install:api` as closure commands (PC1). */
553
615
  declare function registerHttpCommands(kernel: ConsoleKernel, app: Application, options?: HttpCommandOptions): void;
554
616
 
555
617
  /**
@@ -565,4 +627,4 @@ declare class HttpServiceProvider extends ServiceProvider {
565
627
  boot(): void;
566
628
  }
567
629
 
568
- export { API_ROUTES_STUB, ConvertEmptyStringsToNull, type ErrorClass, ErrorHandler, ExceptionsConfiguration, FormUploadedFile, type HttpCommandOptions, HttpException, HttpRedirectResponse, HttpRedirector, HttpResponseException, HttpResponseFactory, HttpServerError, HttpServiceProvider, IncomingRequest, type IncomingRequestOptions, type JsonWhenCallback, Kernel, MiddlewareConfiguration, type RenderCallback, type ReportCallback, type ResolvedServeOptions, type RouteListFilters, type RouteListRow, type RunningServer, type Runtime, type ServeOptions, type StackFrame, TrimStrings, type TypedRedirector, abort, abortIf, abortUnless, addApiRouting, applyExceptionCallbacks, applyMiddlewareCallbacks, back, bunRuntime, clientAddress, contentDisposition, currentRequest, debugPage, detectRuntime, escapeHtml, exceedsJsonDepth, extensionFromMime, filterRouteList, formatRouteList, fromEntries, ipMatches, isBlank, mergeRecursive, mimeFromExtension, minimalPage, normalizeLists, optionalCookieJar, optionalSession, parseBracketKey, readBody, redirect, registerHttpCommands, request, requestScope, response, route, routeListRows, sanitize, serve, serveBun, serveNode, servePublicFile, setClientAddress, stackFrames, statusFor, statusText, summarizeValidation, toWebRequest, transformLeaves, url, writeResponse };
630
+ export { API_ROUTES_STUB, ConvertEmptyStringsToNull, type ErrorClass, ErrorHandler, ExceptionsConfiguration, ExportError, type ExportOptions, type ExportResult, FormUploadedFile, type HttpCommandOptions, HttpException, HttpRedirectResponse, HttpRedirector, HttpResponseException, HttpResponseFactory, HttpServerError, HttpServiceProvider, IncomingRequest, type IncomingRequestOptions, type JsonWhenCallback, Kernel, MiddlewareConfiguration, type RenderCallback, type ReportCallback, type ResolvedServeOptions, type RouteListFilters, type RouteListRow, type RunningServer, type Runtime, type ServeOptions, type StackFrame, TrimStrings, type TypedRedirector, abort, abortIf, abortUnless, addApiRouting, applyExceptionCallbacks, applyMiddlewareCallbacks, back, bunRuntime, clientAddress, contentDisposition, currentRequest, debugPage, detectRuntime, escapeHtml, exceedsJsonDepth, exportFileName, exportSite, extensionFromMime, filterRouteList, formatRouteList, fromEntries, ipMatches, isBlank, mergeRecursive, mimeFromExtension, minimalPage, nodeRequestHandler, normalizeLists, optionalCookieJar, optionalSession, parseBracketKey, readBody, redirect, registerHttpCommands, request, requestScope, response, route, routeListRows, sameSiteLinks, sanitize, serve, serveBun, serveNode, servePublicFile, setClientAddress, stackFrames, statusFor, statusText, summarizeValidation, toWebRequest, transformLeaves, url, writeResponse };
package/dist/index.js CHANGED
@@ -1562,6 +1562,11 @@ function applyExceptionCallbacks(app, handler) {
1562
1562
  for (const callback of app.make(ExceptionCallbacks)) callback(configurator);
1563
1563
  }
1564
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
+
1565
1570
  // server/types.ts
1566
1571
  import { HttpKernel } from "@orieljs/contracts";
1567
1572
 
@@ -1631,6 +1636,136 @@ function exitOnSignals(close, closed, app) {
1631
1636
  }
1632
1637
  var displayHost = (hostname) => hostname.includes(":") ? `[${hostname}]` : hostname;
1633
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
+
1634
1769
  // server/bun.ts
1635
1770
  function bunRuntime() {
1636
1771
  return globalThis.Bun;
@@ -1639,7 +1774,7 @@ async function serveBun(app, options) {
1639
1774
  const bun = bunRuntime();
1640
1775
  if (bun === void 0) throw new HttpServerError("Bun.serve is not available: the process is not running on Bun.");
1641
1776
  let markClosed = () => void 0;
1642
- const closed = new Promise((resolve2) => markClosed = resolve2);
1777
+ const closed = new Promise((resolve4) => markClosed = resolve4);
1643
1778
  const server = bun.serve({
1644
1779
  port: options.port,
1645
1780
  hostname: options.hostname,
@@ -1657,8 +1792,8 @@ async function serveBun(app, options) {
1657
1792
  let closing;
1658
1793
  const close = () => closing ??= (async () => {
1659
1794
  let timer;
1660
- const forced = new Promise((resolve2) => {
1661
- timer = setTimeout(() => void Promise.resolve(server.stop(true)).then(resolve2), options.shutdownTimeout);
1795
+ const forced = new Promise((resolve4) => {
1796
+ timer = setTimeout(() => void Promise.resolve(server.stop(true)).then(resolve4), options.shutdownTimeout);
1662
1797
  });
1663
1798
  await Promise.race([Promise.resolve(server.stop(false)), forced]);
1664
1799
  clearTimeout(timer);
@@ -1761,40 +1896,44 @@ async function writeResponse(res, response2) {
1761
1896
  }
1762
1897
  await pipeline(Readable3.fromWeb(response2.body), res);
1763
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
+ };
1918
+ }
1764
1919
  async function serveNode(app, options) {
1765
- const fallbackHost = `${displayHost(options.hostname)}:${options.port}`;
1766
- const server = createServer((req, res) => {
1767
- void (async () => {
1768
- let request2;
1769
- try {
1770
- request2 = toWebRequest(req, res, fallbackHost);
1771
- } catch {
1772
- res.statusCode = 400;
1773
- res.end("Bad Request");
1774
- return;
1775
- }
1776
- setClientAddress(request2, req.socket.remoteAddress);
1777
- const response2 = await handleSafely(app, request2, options.publicPath);
1778
- try {
1779
- await writeResponse(res, response2);
1780
- } catch {
1781
- res.destroy();
1782
- }
1783
- await terminateRequest(app, request2, response2);
1784
- })();
1920
+ const handle = nodeRequestHandler(app, {
1921
+ ...options.publicPath !== void 0 && { publicPath: options.publicPath },
1922
+ fallbackHost: `${displayHost(options.hostname)}:${options.port}`
1785
1923
  });
1786
- await new Promise((resolve2, reject) => {
1924
+ const server = createServer((req, res) => void handle(req, res));
1925
+ await new Promise((resolve4, reject) => {
1787
1926
  server.once("error", reject);
1788
1927
  server.listen(options.port, options.hostname, () => {
1789
1928
  server.off("error", reject);
1790
- resolve2();
1929
+ resolve4();
1791
1930
  });
1792
1931
  });
1793
1932
  const address = server.address();
1794
- const closed = new Promise((resolve2) => server.once("close", () => resolve2()));
1933
+ const closed = new Promise((resolve4) => server.once("close", () => resolve4()));
1795
1934
  let closing;
1796
1935
  const close = () => closing ??= (async () => {
1797
- const done = new Promise((resolve2) => server.close(() => resolve2()));
1936
+ const done = new Promise((resolve4) => server.close(() => resolve4()));
1798
1937
  server.closeIdleConnections();
1799
1938
  const idle = setInterval(() => server.closeIdleConnections(), 50);
1800
1939
  const force = setTimeout(() => server.closeAllConnections(), options.shutdownTimeout);
@@ -1834,16 +1973,17 @@ async function serve(app, options = {}) {
1834
1973
  }
1835
1974
 
1836
1975
  // commands.ts
1837
- import { existsSync as existsSync3 } from "fs";
1838
- import { mkdir, readFile as readFile2, writeFile } from "fs/promises";
1839
- 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";
1840
1979
  import {
1980
+ ConfigRepository as ConfigRepository4,
1841
1981
  Router as Router3
1842
1982
  } from "@orieljs/contracts";
1843
1983
 
1844
1984
  // watch/environment.ts
1845
- import { existsSync, readFileSync } from "fs";
1846
- import { join } from "path";
1985
+ import { existsSync as existsSync2, readFileSync } from "fs";
1986
+ import { join as join2 } from "path";
1847
1987
  import * as util from "util";
1848
1988
  var launchEnvironment = Object.freeze({ ...process.env });
1849
1989
  var BUN_ENV_FILES = [
@@ -1865,8 +2005,8 @@ function serverEnvironment(options = {}) {
1865
2005
  if (typeof parseEnv2 !== "function") return env;
1866
2006
  const cwd = options.cwd ?? process.cwd();
1867
2007
  for (const name of BUN_ENV_FILES) {
1868
- const file = join(cwd, name);
1869
- if (!existsSync(file)) continue;
2008
+ const file = join2(cwd, name);
2009
+ if (!existsSync2(file)) continue;
1870
2010
  let values;
1871
2011
  try {
1872
2012
  values = parseEnv2(readFileSync(file, "utf8"));
@@ -1882,8 +2022,8 @@ function serverEnvironment(options = {}) {
1882
2022
  }
1883
2023
 
1884
2024
  // watch/files.ts
1885
- import { existsSync as existsSync2, statSync, watch as watchPath } from "fs";
1886
- import { join as join2 } from "path";
2025
+ import { existsSync as existsSync3, statSync as statSync2, watch as watchPath } from "fs";
2026
+ import { join as join3 } from "path";
1887
2027
  var WATCHED_FOLDERS = ["app", "bootstrap", "config", "lang", "resources/lang", "resources/views", "routes"];
1888
2028
  var WATCHED_ROOT_FILES = ["package.json", "tsconfig.json"];
1889
2029
  var IGNORED_FOLDERS = ["bootstrap/ssr", "bootstrap/cache"];
@@ -1914,7 +2054,7 @@ function watchFiles(basePath, onChange, options = {}) {
1914
2054
  const reportedLate = (path) => {
1915
2055
  if (Date.now() - startedAt >= settle) return false;
1916
2056
  try {
1917
- const modified = statSync(join2(basePath, path), { throwIfNoEntry: false })?.mtimeMs;
2057
+ const modified = statSync2(join3(basePath, path), { throwIfNoEntry: false })?.mtimeMs;
1918
2058
  return modified === void 0 || modified < startedAt + 1;
1919
2059
  } catch {
1920
2060
  return false;
@@ -1933,7 +2073,7 @@ function watchFiles(basePath, onChange, options = {}) {
1933
2073
  const handles = [];
1934
2074
  const start = (folder, recursive, toPath) => {
1935
2075
  try {
1936
- const handle = watch(folder === "." ? basePath : join2(basePath, folder), { recursive }, (_event, file) => {
2076
+ const handle = watch(folder === "." ? basePath : join3(basePath, folder), { recursive }, (_event, file) => {
1937
2077
  const path = toPath(file);
1938
2078
  if (path !== void 0) record(path);
1939
2079
  });
@@ -1945,7 +2085,7 @@ function watchFiles(basePath, onChange, options = {}) {
1945
2085
  };
1946
2086
  start(".", false, (file) => file === null ? void 0 : toSlashes(file));
1947
2087
  for (const folder of WATCHED_FOLDERS) {
1948
- if (existsSync2(join2(basePath, folder))) start(folder, true, (file) => `${folder}/${file === null ? "" : toSlashes(file)}`);
2088
+ if (existsSync3(join3(basePath, folder))) start(folder, true, (file) => `${folder}/${file === null ? "" : toSlashes(file)}`);
1949
2089
  }
1950
2090
  return {
1951
2091
  close() {
@@ -1959,13 +2099,13 @@ function watchFiles(basePath, onChange, options = {}) {
1959
2099
  // watch/port.ts
1960
2100
  import { createServer as createServer2 } from "net";
1961
2101
  function claimPort(hostname, port) {
1962
- return new Promise((resolve2, reject) => {
2102
+ return new Promise((resolve4, reject) => {
1963
2103
  const server = createServer2();
1964
2104
  server.once("error", reject);
1965
2105
  try {
1966
2106
  server.listen({ port, host: hostname, exclusive: true }, () => {
1967
2107
  const { port: claimed } = server.address();
1968
- server.close(() => resolve2(claimed));
2108
+ server.close(() => resolve4(claimed));
1969
2109
  });
1970
2110
  } catch (error) {
1971
2111
  reject(error);
@@ -1996,7 +2136,7 @@ var WatchedServer = class {
1996
2136
  };
1997
2137
  /** Starts the server and keeps it current until stopped. Resolves 0 once the last server has exited. */
1998
2138
  run() {
1999
- const done = new Promise((resolve2) => this.finish = resolve2);
2139
+ const done = new Promise((resolve4) => this.finish = resolve4);
2000
2140
  const stopListening = this.listenForSignals();
2001
2141
  const watcher = this.options.watch((files) => void this.changed(files));
2002
2142
  this.start();
@@ -2035,14 +2175,14 @@ var WatchedServer = class {
2035
2175
  stdio: ["ignore", "inherit", "inherit"]
2036
2176
  });
2037
2177
  const running = { child, stopping: false, killed: false, exited: Promise.resolve() };
2038
- running.exited = new Promise((resolve2) => {
2178
+ running.exited = new Promise((resolve4) => {
2039
2179
  let settled = false;
2040
2180
  const settle = (report) => {
2041
2181
  if (settled) return;
2042
2182
  settled = true;
2043
2183
  if (this.running === running) this.running = void 0;
2044
2184
  if (!running.stopping && !this.stopping) this.options.error(`${report}. Waiting for changes before restarting...`);
2045
- resolve2();
2185
+ resolve4();
2046
2186
  };
2047
2187
  child.once("exit", (code, signal) => settle(signal !== null ? `The server stopped (signal ${signal})` : `The server stopped (exit code ${code})`));
2048
2188
  child.once("error", (error) => {
@@ -2180,6 +2320,38 @@ async function serveWatching(app, out, port, run) {
2180
2320
  error: (message) => out.error(message)
2181
2321
  });
2182
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
+ }
2183
2355
  function registerHttpCommands(kernel, app, options = {}) {
2184
2356
  const serveApp = options.serve ?? serve;
2185
2357
  const terminalWidth = options.terminalWidth ?? (() => process.stdout.columns ?? 120);
@@ -2238,25 +2410,28 @@ function registerHttpCommands(kernel, app, options = {}) {
2238
2410
  return 0;
2239
2411
  }
2240
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");
2241
2416
  kernel.command("install:api {--force : Overwrite any existing API routes file}", async function() {
2242
2417
  const out = io(this);
2243
2418
  const routesFile = app.basePath("routes/api.ts");
2244
- if (existsSync3(routesFile) && !out.flag("force")) {
2419
+ if (existsSync4(routesFile) && !out.flag("force")) {
2245
2420
  out.error("API routes file already exists.");
2246
2421
  } else {
2247
- await mkdir(dirname(routesFile), { recursive: true });
2248
- await writeFile(routesFile, API_ROUTES_STUB);
2422
+ await mkdir2(dirname2(routesFile), { recursive: true });
2423
+ await writeFile2(routesFile, API_ROUTES_STUB);
2249
2424
  out.info("Published API routes file.");
2250
2425
  }
2251
2426
  const bootstrapFile = app.bootstrapPath("app.ts");
2252
- const source = existsSync3(bootstrapFile) ? await readFile2(bootstrapFile, "utf8") : void 0;
2427
+ const source = existsSync4(bootstrapFile) ? await readFile2(bootstrapFile, "utf8") : void 0;
2253
2428
  const updated = source === void 0 ? void 0 : addApiRouting(source);
2254
2429
  if (updated === void 0) {
2255
2430
  out.warn(
2256
2431
  "Unable to automatically add API route definition to bootstrap file. Add api: 'routes/api.ts' to withRouting() in bootstrap/app.ts."
2257
2432
  );
2258
2433
  } else if (updated !== source) {
2259
- await writeFile(bootstrapFile, updated);
2434
+ await writeFile2(bootstrapFile, updated);
2260
2435
  }
2261
2436
  out.info("API scaffolding installed.");
2262
2437
  return 0;
@@ -2301,6 +2476,7 @@ export {
2301
2476
  ConvertEmptyStringsToNull,
2302
2477
  ErrorHandler,
2303
2478
  ExceptionsConfiguration,
2479
+ ExportError,
2304
2480
  FormUploadedFile,
2305
2481
  HttpException,
2306
2482
  HttpRedirectResponse,
@@ -2328,6 +2504,8 @@ export {
2328
2504
  detectRuntime,
2329
2505
  escapeHtml,
2330
2506
  exceedsJsonDepth,
2507
+ exportFileName,
2508
+ exportSite,
2331
2509
  extensionFromMime,
2332
2510
  filterRouteList,
2333
2511
  formatRouteList,
@@ -2337,6 +2515,7 @@ export {
2337
2515
  mergeRecursive,
2338
2516
  mimeFromExtension,
2339
2517
  minimalPage,
2518
+ nodeRequestHandler,
2340
2519
  normalizeLists,
2341
2520
  optionalCookieJar,
2342
2521
  optionalSession,
@@ -2349,6 +2528,7 @@ export {
2349
2528
  response,
2350
2529
  route,
2351
2530
  routeListRows,
2531
+ sameSiteLinks,
2352
2532
  sanitize,
2353
2533
  serve,
2354
2534
  serveBun,