@moonsjs/vite 0.2.5 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,15 +5,31 @@ import {
5
5
  injectShell2,
6
6
  createDevShell2,
7
7
  createFileShell2,
8
+ scanPayloadForSecrets,
9
+ MAX_TRANSPORT_BODY_BYTES,
8
10
  InvalidRequestError,
9
11
  BodyLimitError,
12
+ DEFAULT_SECURITY_HEADERS,
13
+ forwardedProtocol,
14
+ forwardedHost,
15
+ resolveClientIpValues,
16
+ MIME_TYPES,
17
+ COMPRESSIBLE_FILE,
18
+ GZIP_MIN_BYTES,
19
+ gzipCached,
20
+ acceptsGzipHeader,
21
+ rpcBodyTooLargeText,
22
+ applyStaticHeaders,
23
+ readBodyWithLimit,
24
+ resolveStaticFile2,
25
+ staticFileTarget,
26
+ scheduleRevalidation,
10
27
  resolveRequestUrl,
11
28
  resolveClientIp,
12
29
  toWebRequest2,
13
- DEFAULT_SECURITY_HEADERS,
14
30
  sendNodeResponse2,
15
31
  installDevMiddleware2
16
- } from "../index-0agjty3z.js";
32
+ } from "../index-df6b8rqt.js";
17
33
 
18
34
  // src/server/handler.ts
19
35
  import {
@@ -32,31 +48,6 @@ import {
32
48
  import { encodeSsrPayload, reviveError, serializeError } from "@moonsjs/core/serialize";
33
49
  import { API_METHODS, resolveApiRoute } from "@moonsjs/core";
34
50
  import { createRpcHandler, runWithContext, runWithRequestCache, runWithResponseCollector, takeResponseMutations } from "@moonsjs/server";
35
-
36
- // src/payload-scan.ts
37
- var SECRET_KEY_PATTERN = /(SECRET|TOKEN|PASSWORD|PASSWD|PRIVATE|CREDENTIAL|AUTH|API_KEY|ACCESS_KEY|SIGNING)/i;
38
- var MIN_SECRET_LENGTH = 12;
39
- function scanPayloadForSecrets(payload, options = {}) {
40
- const findings = [];
41
- const canary = options.canary;
42
- if (canary !== undefined && canary.length > 0 && payload.includes(canary)) {
43
- findings.push({ kind: "canary", label: "canary" });
44
- }
45
- for (const [key, value] of Object.entries(options.env ?? {})) {
46
- if (typeof value !== "string" || value.length < MIN_SECRET_LENGTH) {
47
- continue;
48
- }
49
- if (!SECRET_KEY_PATTERN.test(key) || /PUBLIC/i.test(key)) {
50
- continue;
51
- }
52
- if (payload.includes(value)) {
53
- findings.push({ kind: "env", label: key });
54
- }
55
- }
56
- return findings;
57
- }
58
-
59
- // src/server/handler.ts
60
51
  function loadDefaultRenderer() {
61
52
  return import("@moonsjs/react/ssr").then((mod) => mod.renderToMarkup).catch(() => null);
62
53
  }
@@ -519,78 +510,21 @@ function warnPayloadFindings(payload, scan, warned) {
519
510
  }
520
511
  }
521
512
  // src/server/node-server.ts
522
- import { existsSync, readFileSync as readFileSync2, statSync as statSync2 } from "node:fs";
523
- import { gzipSync } from "node:zlib";
513
+ import { readFileSync as readFileSync2, statSync as statSync2 } from "node:fs";
524
514
  import { createServer } from "node:http";
525
- import { extname as extname2, join, resolve, sep } from "node:path";
515
+ import { extname as extname2, join, resolve } from "node:path";
526
516
  import {
527
517
  DEFAULT_BODY_LIMIT_BYTES,
528
- MoonsError as MoonsError2,
529
518
  trailingSlashRedirect as trailingSlashRedirect2
530
519
  } from "@moonsjs/core";
531
520
 
532
- // src/server/isr.ts
533
- import { renameSync, writeFileSync } from "node:fs";
534
- function shouldRevalidate(input) {
535
- const now = input.now ?? Date.now();
536
- return now - input.mtimeMs >= input.revalidateSeconds * 1000;
537
- }
538
- var inflight = new Map;
539
- function revalidatePrerendered(options) {
540
- const existing = inflight.get(options.pathname);
541
- if (existing !== undefined) {
542
- return existing;
543
- }
544
- const task = runRevalidate(options).finally(() => {
545
- inflight.delete(options.pathname);
546
- });
547
- inflight.set(options.pathname, task);
548
- return task;
549
- }
550
- async function runRevalidate(options) {
551
- const log = options.log ?? ((message) => console.log(message));
552
- try {
553
- const response = await options.handler(new Request(`http://localhost${options.pathname}`, {
554
- method: "GET",
555
- headers: { "x-moons-prerender": "1" }
556
- }));
557
- if (response.status !== 200) {
558
- log(`[moons] isr: skipped ${options.pathname} (status ${response.status})`);
559
- return;
560
- }
561
- const html = await response.text();
562
- const findings = scanPayloadForSecrets(html, {
563
- ...options.canary === undefined ? {} : { canary: options.canary },
564
- env: options.env ?? process.env
565
- });
566
- if (findings.length > 0) {
567
- const labels = findings.map((finding) => finding.kind === "canary" ? "canary" : `env(${finding.label})`).join(", ");
568
- log(`[moons] isr: NOT writing ${options.pathname} — contains ${labels}`);
569
- return;
570
- }
571
- if (options.storage !== undefined) {
572
- await options.storage.write(options.pathname, new TextEncoder().encode(html));
573
- return;
574
- }
575
- if (options.file === undefined) {
576
- log(`[moons] isr: skipped ${options.pathname} — no destination file/storage`);
577
- return;
578
- }
579
- const tmp = `${options.file}.tmp-${process.pid}`;
580
- writeFileSync(tmp, html);
581
- renameSync(tmp, options.file);
582
- } catch (error) {
583
- log(`[moons] isr: failed to regenerate ${options.pathname}: ${String(error)}`);
584
- }
585
- }
586
-
587
521
  // src/server/storage.ts
588
- import { mkdirSync, readFileSync, renameSync as renameSync2, statSync, writeFileSync as writeFileSync2 } from "node:fs";
522
+ import { mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
589
523
  import { dirname, extname } from "node:path";
590
524
  function createFileStorage(clientDir) {
591
525
  return {
592
526
  async read(pathname) {
593
- const file = resolveStaticFile(clientDir, pathname);
527
+ const file = resolveStaticFile2(clientDir, pathname);
594
528
  if (file === null) {
595
529
  return null;
596
530
  }
@@ -604,8 +538,8 @@ function createFileStorage(clientDir) {
604
538
  }
605
539
  mkdirSync(dirname(target), { recursive: true });
606
540
  const tmp = `${target}.tmp-${process.pid}`;
607
- writeFileSync2(tmp, body);
608
- renameSync2(tmp, target);
541
+ writeFileSync(tmp, body);
542
+ renameSync(tmp, target);
609
543
  }
610
544
  };
611
545
  }
@@ -719,27 +653,6 @@ function installGracefulShutdown(server, options = {}) {
719
653
  }
720
654
 
721
655
  // src/server/node-server.ts
722
- var MIME_TYPES = {
723
- ".js": "text/javascript; charset=utf-8",
724
- ".mjs": "text/javascript; charset=utf-8",
725
- ".css": "text/css; charset=utf-8",
726
- ".html": "text/html; charset=utf-8",
727
- ".json": "application/json; charset=utf-8",
728
- ".map": "application/json; charset=utf-8",
729
- ".ico": "image/x-icon",
730
- ".png": "image/png",
731
- ".jpg": "image/jpeg",
732
- ".jpeg": "image/jpeg",
733
- ".gif": "image/gif",
734
- ".svg": "image/svg+xml",
735
- ".webp": "image/webp",
736
- ".avif": "image/avif",
737
- ".woff": "font/woff",
738
- ".woff2": "font/woff2",
739
- ".ttf": "font/ttf",
740
- ".txt": "text/plain; charset=utf-8",
741
- ".wasm": "application/wasm"
742
- };
743
656
  function startNodeServer(handler, options = {}) {
744
657
  const clientDir = resolve(options.clientDir ?? join(process.cwd(), "dist/client"));
745
658
  const port = options.port ?? Number(process.env["PORT"] ?? 3000);
@@ -800,13 +713,13 @@ function startNodeServer(handler, options = {}) {
800
713
  });
801
714
  res.statusCode = 200;
802
715
  res.setHeader("content-type", contentTypeForPathname(url.pathname));
803
- applyStaticHeaders(res, url.pathname, options.headers);
716
+ applyStaticHeaders({ has: (name) => res.hasHeader(name), set: (name, value) => res.setHeader(name, value) }, url.pathname, options.headers);
804
717
  res.end(req.method === "HEAD" ? undefined : Buffer.from(entry.body));
805
718
  return;
806
719
  }
807
720
  }
808
721
  {
809
- const staticFile = resolveStaticFile(clientDir, url.pathname);
722
+ const staticFile = resolveStaticFile2(clientDir, url.pathname);
810
723
  if (staticFile !== null) {
811
724
  scheduleRevalidation({
812
725
  pathname: url.pathname,
@@ -822,7 +735,7 @@ function startNodeServer(handler, options = {}) {
822
735
  const stat = statSync2(staticFile);
823
736
  res.statusCode = 200;
824
737
  res.setHeader("content-type", MIME_TYPES[extname2(staticFile).toLowerCase()] ?? "application/octet-stream");
825
- applyStaticHeaders(res, url.pathname, options.headers);
738
+ applyStaticHeaders({ has: (name) => res.hasHeader(name), set: (name, value) => res.setHeader(name, value) }, url.pathname, options.headers);
826
739
  const etag = `W/"${stat.size}-${Math.floor(stat.mtimeMs)}"`;
827
740
  res.setHeader("etag", etag);
828
741
  const ifNoneMatch = req.headers["if-none-match"];
@@ -832,7 +745,9 @@ function startNodeServer(handler, options = {}) {
832
745
  return;
833
746
  }
834
747
  let payload = body;
835
- if (req.method === "GET" && COMPRESSIBLE_FILE.test(staticFile) && body.byteLength >= GZIP_MIN_BYTES && acceptsGzip(req)) {
748
+ const encodings = req.headers["accept-encoding"];
749
+ const acceptsGzip = acceptsGzipHeader(Array.isArray(encodings) ? encodings[0] ?? null : encodings ?? null);
750
+ if (req.method === "GET" && COMPRESSIBLE_FILE.test(staticFile) && body.byteLength >= GZIP_MIN_BYTES && acceptsGzip) {
836
751
  payload = gzipCached(staticFile, stat.mtimeMs, body);
837
752
  res.setHeader("content-encoding", "gzip");
838
753
  appendVary(res, "accept-encoding");
@@ -899,47 +814,6 @@ function startNodeServer(handler, options = {}) {
899
814
  }
900
815
  return server;
901
816
  }
902
- function applyStaticHeaders(res, pathname, configured) {
903
- for (const [name, value] of Object.entries(configured ?? {})) {
904
- if (!res.hasHeader(name)) {
905
- res.setHeader(name, value);
906
- }
907
- }
908
- for (const [name, value] of Object.entries(DEFAULT_SECURITY_HEADERS)) {
909
- if (!res.hasHeader(name)) {
910
- res.setHeader(name, value);
911
- }
912
- }
913
- if (pathname.startsWith("/assets/") && !res.hasHeader("cache-control")) {
914
- res.setHeader("cache-control", "public, max-age=31536000, immutable");
915
- }
916
- }
917
- var COMPRESSIBLE_FILE = /\.(js|mjs|css|html|json|svg|txt|map|xml)$/i;
918
- var GZIP_MIN_BYTES = 1024;
919
- var GZIP_CACHE_MAX = 64;
920
- var gzipCache = new Map;
921
- function acceptsGzip(req) {
922
- const header = req.headers["accept-encoding"];
923
- if (typeof header !== "string") {
924
- return false;
925
- }
926
- return header.toLowerCase().split(",").map((part) => part.trim()).some((part) => part === "gzip" || part === "*" || part.startsWith("gzip;"));
927
- }
928
- function gzipCached(file, mtimeMs, body) {
929
- const cached = gzipCache.get(file);
930
- if (cached !== undefined && cached.mtimeMs === mtimeMs) {
931
- return cached.gzip;
932
- }
933
- const gzip = gzipSync(body);
934
- if (gzipCache.size >= GZIP_CACHE_MAX) {
935
- const oldest = gzipCache.keys().next().value;
936
- if (oldest !== undefined) {
937
- gzipCache.delete(oldest);
938
- }
939
- }
940
- gzipCache.set(file, { mtimeMs, gzip });
941
- return gzip;
942
- }
943
817
  function appendVary(res, value) {
944
818
  const existing = res.getHeader("vary");
945
819
  if (typeof existing === "string" && existing.length > 0) {
@@ -955,126 +829,292 @@ function respondBadRequest(res) {
955
829
  res.setHeader("content-type", "text/plain; charset=utf-8");
956
830
  res.end("Bad Request");
957
831
  }
958
- function rpcBodyTooLargeText(limit, size) {
959
- const megabytes = (bytes) => (bytes / 1e6).toFixed(1);
960
- return new MoonsError2({
961
- code: "E_RPC_BODY_TOO_LARGE",
962
- message: size === null ? `body exceeds the ${megabytes(limit)} MB limit` : `body ${megabytes(size)} MB exceeds the ${megabytes(limit)} MB limit`,
963
- details: "Body exceeds `security.bodyLimitBytes`; reading stops at the transport before the body is fully buffered.",
964
- suggestions: [
965
- "Reduce the payload size, or",
966
- "Increase security.bodyLimitBytes if you really need to"
967
- ]
968
- }).format();
832
+ // src/server/bun-server.ts
833
+ import { readFileSync as readFileSync3, statSync as statSync3 } from "node:fs";
834
+ import { extname as extname3, join as join2, resolve as resolve2 } from "node:path";
835
+ import { CLIENT_IP_HEADER, DEFAULT_BODY_LIMIT_BYTES as DEFAULT_BODY_LIMIT_BYTES2, trailingSlashRedirect as trailingSlashRedirect3 } from "@moonsjs/core";
836
+ var textResponse = (status, text) => new Response(text, { status, headers: { "content-type": "text/plain; charset=utf-8" } });
837
+ function appendVaryHeader(headers, value) {
838
+ const existing = headers.get("vary");
839
+ if (existing !== null && existing.length > 0) {
840
+ if (!existing.toLowerCase().includes(value)) {
841
+ headers.set("vary", `${existing}, ${value}`);
842
+ }
843
+ return;
844
+ }
845
+ headers.set("vary", value);
969
846
  }
970
- function resolveStaticFile(clientDir, pathname) {
971
- const target = staticFileTarget(clientDir, pathname);
972
- return target !== null && isFile(target) ? target : null;
847
+ function withDefaultSecurityHeaders(response) {
848
+ let missing = false;
849
+ for (const name of Object.keys(DEFAULT_SECURITY_HEADERS)) {
850
+ if (!response.headers.has(name)) {
851
+ missing = true;
852
+ break;
853
+ }
854
+ }
855
+ if (!missing) {
856
+ return response;
857
+ }
858
+ try {
859
+ for (const [name, value] of Object.entries(DEFAULT_SECURITY_HEADERS)) {
860
+ if (!response.headers.has(name)) {
861
+ response.headers.set(name, value);
862
+ }
863
+ }
864
+ return response;
865
+ } catch {
866
+ const headers = new Headers(response.headers);
867
+ for (const [name, value] of Object.entries(DEFAULT_SECURITY_HEADERS)) {
868
+ if (!headers.has(name)) {
869
+ headers.set(name, value);
870
+ }
871
+ }
872
+ return new Response(response.body, {
873
+ status: response.status,
874
+ statusText: response.statusText,
875
+ headers
876
+ });
877
+ }
973
878
  }
974
- function staticFileTarget(clientDir, pathname) {
975
- let decoded;
879
+ function resolveBunUrl(request, trustProxy) {
880
+ let url;
976
881
  try {
977
- decoded = decodeURIComponent(pathname);
882
+ url = new URL(request.url);
978
883
  } catch {
979
- return null;
884
+ throw new InvalidRequestError(`invalid request URL: ${request.url}`);
980
885
  }
981
- const exact = safeWithin(clientDir, `.${decoded}`);
982
- if (exact !== null && isFile(exact)) {
983
- return exact;
886
+ if (!trustProxy) {
887
+ return url;
984
888
  }
985
- const normalized = decoded.length > 1 ? decoded.replace(/\/+$/, "") : decoded;
986
- const manifest = readPrerenderManifest(clientDir);
987
- if (manifest.paths.has(normalized)) {
988
- const target = normalized === "/" ? "index.html" : `${normalized.slice(1)}/index.html`;
989
- return safeWithin(clientDir, `./${target}`);
889
+ const proto = forwardedProtocol(request.headers.get("x-forwarded-proto") ?? undefined);
890
+ const host = forwardedHost(request.headers.get("x-forwarded-host") ?? undefined);
891
+ if (proto === null && host === null) {
892
+ return url;
990
893
  }
991
- return null;
992
- }
993
- function safeWithin(clientDir, relative) {
994
- const candidate = resolve(clientDir, relative);
995
- if (candidate !== clientDir && !candidate.startsWith(clientDir + sep)) {
996
- return null;
894
+ const effectiveProto = proto ?? (url.protocol === "https:" ? "https" : "http");
895
+ const effectiveHost = host ?? url.host;
896
+ try {
897
+ return new URL(`${effectiveProto}://${effectiveHost}${url.pathname}${url.search}${url.hash}`);
898
+ } catch {
899
+ throw new InvalidRequestError(`invalid forwarded host: ${JSON.stringify(effectiveHost)}`);
997
900
  }
998
- return candidate;
999
901
  }
1000
- function isFile(path) {
1001
- return existsSync(path) && statSync2(path).isFile();
902
+ async function toBoundedRequest(request, url, clientIp, maxBodyBytes) {
903
+ const headers = new Headers(request.headers);
904
+ headers.delete("content-length");
905
+ if (clientIp !== null) {
906
+ headers.set(CLIENT_IP_HEADER, clientIp);
907
+ }
908
+ if (request.method === "GET" || request.method === "HEAD") {
909
+ return new Request(url, { method: request.method, headers });
910
+ }
911
+ const body = await readBodyWithLimit(request.body, maxBodyBytes);
912
+ return new Request(url, { method: request.method, headers, body });
1002
913
  }
1003
- var prerenderManifestCache = null;
1004
- function readPrerenderManifest(clientDir) {
1005
- const file = join(clientDir, "_moons_prerender.json");
1006
- const empty = { paths: new Set, revalidate: new Map };
1007
- try {
1008
- const mtimeMs = statSync2(file).mtimeMs;
1009
- if (prerenderManifestCache !== null && prerenderManifestCache.file === file && prerenderManifestCache.mtimeMs === mtimeMs) {
1010
- return prerenderManifestCache.info;
914
+ function installBunShutdown(server, options) {
915
+ const timeoutMs = options.timeoutMs ?? 1e4;
916
+ let shuttingDown = false;
917
+ return (signal) => {
918
+ if (shuttingDown) {
919
+ return;
1011
920
  }
1012
- const json = JSON.parse(readFileSync2(file, "utf8"));
1013
- const paths = new Set;
1014
- const revalidate = new Map;
1015
- if (Array.isArray(json.paths)) {
1016
- for (const path of json.paths) {
1017
- if (typeof path === "string") {
1018
- paths.add(path);
921
+ shuttingDown = true;
922
+ console.log(`[moons] received ${signal} — no longer accepting connections, draining...`);
923
+ const timer = setTimeout(() => {
924
+ console.log(`[moons] drain exceeded ${timeoutMs}ms — forcing exit`);
925
+ server.stop(true).catch(() => {});
926
+ process.exit(1);
927
+ }, timeoutMs);
928
+ timer.unref?.();
929
+ (async () => {
930
+ try {
931
+ await server.stop(false);
932
+ } catch (error) {
933
+ clearTimeout(timer);
934
+ console.log(`[moons] server close failed: ${error instanceof Error ? error.message : String(error)}`);
935
+ process.exit(1);
936
+ return;
937
+ }
938
+ if (options.plugins !== undefined) {
939
+ timer.ref?.();
940
+ try {
941
+ await options.plugins.shutdown();
942
+ } catch (error) {
943
+ console.log(`[moons] onShutdown failed: ${String(error)}`);
1019
944
  }
1020
945
  }
946
+ clearTimeout(timer);
947
+ console.log("[moons] shutdown complete (full drain)");
948
+ process.exit(0);
949
+ })();
950
+ };
951
+ }
952
+ function startBunServer(handler, options = {}) {
953
+ if (typeof Bun === "undefined") {
954
+ throw new Error("[moons] startBunServer requires the Bun runtime");
955
+ }
956
+ const clientDir = resolve2(options.clientDir ?? join2(process.cwd(), "dist/client"));
957
+ const port = options.port ?? Number(process.env["PORT"] ?? 3000);
958
+ const trustProxy = options.trustProxy === true;
959
+ const accessLogger = options.logging?.accessLog === true ? createLogger({
960
+ json: options.logging.json !== false,
961
+ redact: options.logging.redact ?? []
962
+ }) : null;
963
+ const serveStatic = async (request, url) => {
964
+ if (options.storage !== undefined) {
965
+ const entry = await options.storage.read(url.pathname);
966
+ if (entry !== null) {
967
+ scheduleRevalidation({
968
+ pathname: url.pathname,
969
+ mtimeMs: entry.mtimeMs,
970
+ storage: options.storage,
971
+ handler,
972
+ options,
973
+ clientDir,
974
+ logger: accessLogger
975
+ });
976
+ const headers = new Headers;
977
+ headers.set("content-type", contentTypeForPathname(url.pathname));
978
+ applyStaticHeaders(headers, url.pathname, options.headers);
979
+ return new Response(request.method === "HEAD" ? null : entry.body, { status: 200, headers });
980
+ }
1021
981
  }
1022
- if (Array.isArray(json.entries)) {
1023
- for (const entry of json.entries) {
1024
- if (entry === null || typeof entry !== "object" || typeof entry["path"] !== "string") {
1025
- continue;
982
+ const staticFile = resolveStaticFile2(clientDir, url.pathname);
983
+ if (staticFile === null) {
984
+ return null;
985
+ }
986
+ const stat = statSync3(staticFile);
987
+ scheduleRevalidation({
988
+ pathname: url.pathname,
989
+ mtimeMs: stat.mtimeMs,
990
+ file: staticFile,
991
+ ...options.storage === undefined ? {} : { storage: options.storage },
992
+ handler,
993
+ options,
994
+ clientDir,
995
+ logger: accessLogger
996
+ });
997
+ const headers = new Headers;
998
+ headers.set("content-type", MIME_TYPES[extname3(staticFile).toLowerCase()] ?? "application/octet-stream");
999
+ applyStaticHeaders(headers, url.pathname, options.headers);
1000
+ const etag = `W/"${stat.size}-${Math.floor(stat.mtimeMs)}"`;
1001
+ headers.set("etag", etag);
1002
+ const ifNoneMatch = request.headers.get("if-none-match");
1003
+ if (ifNoneMatch !== null && ifNoneMatch.split(",").some((value) => value.trim() === etag)) {
1004
+ return new Response(null, { status: 304, headers });
1005
+ }
1006
+ let payload = request.method === "HEAD" ? null : readFileSync3(staticFile);
1007
+ if (payload !== null && COMPRESSIBLE_FILE.test(staticFile) && payload.byteLength >= GZIP_MIN_BYTES && acceptsGzipHeader(request.headers.get("accept-encoding"))) {
1008
+ payload = gzipCached(staticFile, stat.mtimeMs, Buffer.from(payload));
1009
+ headers.set("content-encoding", "gzip");
1010
+ appendVaryHeader(headers, "accept-encoding");
1011
+ }
1012
+ return new Response(payload, { status: 200, headers });
1013
+ };
1014
+ const server = Bun.serve({
1015
+ port,
1016
+ fetch: async (request, bunServer) => {
1017
+ const requestStartedAt = performance.now();
1018
+ let url;
1019
+ try {
1020
+ url = resolveBunUrl(request, trustProxy);
1021
+ } catch (error) {
1022
+ if (error instanceof InvalidRequestError) {
1023
+ console.warn(`[moons] ${error.message} — 400`);
1024
+ return textResponse(400, "Bad Request");
1026
1025
  }
1027
- const path = entry["path"];
1028
- paths.add(path);
1029
- const seconds = entry["revalidate"];
1030
- if (typeof seconds === "number" && Number.isFinite(seconds) && seconds >= 0) {
1031
- revalidate.set(path, seconds);
1026
+ throw error;
1027
+ }
1028
+ let clientIp = null;
1029
+ const finish = (response) => {
1030
+ if (accessLogger !== null) {
1031
+ accessLogger({
1032
+ msg: "request",
1033
+ method: request.method,
1034
+ path: url.pathname + url.search,
1035
+ status: response.status,
1036
+ durationMs: Math.round(performance.now() - requestStartedAt),
1037
+ ...clientIp === null ? {} : { ip: clientIp }
1038
+ });
1032
1039
  }
1040
+ return response;
1041
+ };
1042
+ try {
1043
+ if (options.trailingSlash !== undefined) {
1044
+ const canonical = trailingSlashRedirect3(url.pathname, options.trailingSlash);
1045
+ if (canonical !== null) {
1046
+ url.pathname = canonical;
1047
+ return new Response(null, { status: 308, headers: { location: url.toString() } });
1048
+ }
1049
+ }
1050
+ if (request.method === "GET" || request.method === "HEAD") {
1051
+ const staticResponse = await serveStatic(request, url);
1052
+ if (staticResponse !== null) {
1053
+ return finish(staticResponse);
1054
+ }
1055
+ }
1056
+ const isRpc = url.pathname.startsWith("/_moons/fn/");
1057
+ const rpcLimit = options.bodyLimitBytes ?? DEFAULT_BODY_LIMIT_BYTES2;
1058
+ if (isRpc) {
1059
+ const contentLength = Number(request.headers.get("content-length") ?? "0");
1060
+ if (Number.isFinite(contentLength) && contentLength > rpcLimit) {
1061
+ return finish(textResponse(413, rpcBodyTooLargeText(rpcLimit, contentLength)));
1062
+ }
1063
+ }
1064
+ const remoteAddress = bunServer.requestIP(request)?.address ?? null;
1065
+ clientIp = resolveClientIpValues(request.headers.get("x-forwarded-for") ?? undefined, remoteAddress, trustProxy);
1066
+ const needsRewrite = clientIp !== null || url.toString() !== request.url;
1067
+ const webRequest = (request.method === "GET" || request.method === "HEAD") && !needsRewrite ? request : await toBoundedRequest(request, url, clientIp, isRpc ? rpcLimit : MAX_TRANSPORT_BODY_BYTES);
1068
+ const response = await handler(webRequest);
1069
+ return finish(withDefaultSecurityHeaders(response));
1070
+ } catch (error) {
1071
+ if (error instanceof BodyLimitError) {
1072
+ const isRpc = url.pathname.startsWith("/_moons/fn/");
1073
+ return finish(textResponse(413, isRpc ? rpcBodyTooLargeText(error.limit, null) : "Payload Too Large"));
1074
+ }
1075
+ console.error("[moons] server error:", error);
1076
+ return finish(textResponse(500, "Internal Server Error"));
1033
1077
  }
1034
1078
  }
1035
- const info = { paths, revalidate };
1036
- prerenderManifestCache = { file, mtimeMs, info };
1037
- return info;
1038
- } catch {
1039
- return empty;
1079
+ });
1080
+ const listenAfterStart = () => {
1081
+ console.log(`[moons] server listening on http://localhost:${server.port}`);
1082
+ options.plugins?.ready();
1083
+ };
1084
+ if (options.plugins !== undefined) {
1085
+ options.plugins.start().then(listenAfterStart);
1086
+ } else {
1087
+ listenAfterStart();
1040
1088
  }
1089
+ if (options.handleSignals !== false) {
1090
+ const shutdown = installBunShutdown(server, {
1091
+ ...options.shutdownTimeoutMs === undefined ? {} : { timeoutMs: options.shutdownTimeoutMs },
1092
+ plugins: options.plugins
1093
+ });
1094
+ process.once("SIGTERM", () => shutdown("SIGTERM"));
1095
+ process.once("SIGINT", () => shutdown("SIGINT"));
1096
+ }
1097
+ return server;
1041
1098
  }
1042
- function scheduleRevalidation(input) {
1043
- const normalized = input.pathname.length > 1 ? input.pathname.replace(/\/+$/, "") : input.pathname;
1044
- const seconds = readPrerenderManifest(input.clientDir).revalidate.get(normalized);
1045
- if (seconds === undefined || seconds < 0) {
1046
- return;
1099
+ // src/server/server-select.ts
1100
+ function startServer(handler, options = {}) {
1101
+ const forced = process.env["MOONS_SERVER"];
1102
+ const isBun = typeof Bun !== "undefined";
1103
+ if (forced === "node") {
1104
+ return startNodeServer(handler, options);
1047
1105
  }
1048
- if (!shouldRevalidate({ mtimeMs: input.mtimeMs, revalidateSeconds: seconds })) {
1049
- return;
1106
+ if (forced === "bun" && !isBun) {
1107
+ console.warn("[moons] MOONS_SERVER=bun requires the Bun runtime — falling back to node");
1108
+ return startNodeServer(handler, options);
1050
1109
  }
1051
- const regeneratedAt = performance.now();
1052
- const logger = input.logger ?? null;
1053
- revalidatePrerendered({
1054
- pathname: normalized,
1055
- ...input.file === undefined ? {} : { file: input.file },
1056
- ...input.storage === undefined ? {} : { storage: input.storage },
1057
- handler: input.handler,
1058
- ...input.options.canary === undefined ? {} : { canary: input.options.canary },
1059
- env: process.env
1060
- }).then(() => {
1061
- logger?.({
1062
- msg: "isr:regenerated",
1063
- path: normalized,
1064
- durationMs: Math.round(performance.now() - regeneratedAt)
1065
- });
1066
- }).catch((error) => {
1067
- const message = error instanceof Error ? error.message : String(error);
1068
- if (logger !== null) {
1069
- logger({ msg: "isr:revalidate-failed", level: "warn", path: normalized, error: message });
1070
- } else {
1071
- console.error(`[moons] isr regeneration failed (${normalized}): ${message}`);
1072
- }
1073
- });
1110
+ if (forced !== undefined && forced !== "bun" && forced !== "node") {
1111
+ console.warn(`[moons] unknown MOONS_SERVER="${forced}" — using ${isBun ? "bun" : "node"}`);
1112
+ }
1113
+ return isBun ? startBunServer(handler, options) : startNodeServer(handler, options);
1074
1114
  }
1075
1115
  // src/server/test-server.ts
1076
- import { existsSync as existsSync2 } from "node:fs";
1077
- import { join as join2, resolve as resolve2 } from "node:path";
1116
+ import { existsSync } from "node:fs";
1117
+ import { join as join3, resolve as resolve3 } from "node:path";
1078
1118
  import { pathToFileURL } from "node:url";
1079
1119
  async function loadRenderer(candidates) {
1080
1120
  for (const root of candidates) {
@@ -1083,8 +1123,8 @@ async function loadRenderer(candidates) {
1083
1123
  "node_modules/@moonsjs/react/dist/ssr.js",
1084
1124
  "node_modules/@moonsjs/react/dist/ssr.mjs"
1085
1125
  ]) {
1086
- const file = join2(root, relative);
1087
- if (!existsSync2(file)) {
1126
+ const file = join3(root, relative);
1127
+ if (!existsSync(file)) {
1088
1128
  continue;
1089
1129
  }
1090
1130
  const mod = await import(pathToFileURL(file).href);
@@ -1107,7 +1147,7 @@ function createTestServer(options, baseUrl = "http://localhost") {
1107
1147
  ...options.loadApi === undefined ? {} : { loadApi: options.loadApi },
1108
1148
  ...options.apiMiddlewares === undefined ? {} : { apiMiddlewares: options.apiMiddlewares },
1109
1149
  ...options.loadApiMiddleware === undefined ? {} : { loadApiMiddleware: options.loadApiMiddleware },
1110
- renderer: () => loadRenderer([process.cwd(), resolve2(options.routesDir, "..", "..")]),
1150
+ renderer: () => loadRenderer([process.cwd(), resolve3(options.routesDir, "..", "..")]),
1111
1151
  shell
1112
1152
  });
1113
1153
  return {
@@ -1172,9 +1212,9 @@ function createPluginRuntime(options) {
1172
1212
  };
1173
1213
  }
1174
1214
  // src/server/prerender.ts
1175
- import { MoonsError as MoonsError3 } from "@moonsjs/core";
1176
- import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "node:fs";
1177
- import { dirname as dirname2, join as join3, resolve as resolve3, sep as sep2 } from "node:path";
1215
+ import { MoonsError as MoonsError2 } from "@moonsjs/core";
1216
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
1217
+ import { dirname as dirname2, join as join4, resolve as resolve4, sep } from "node:path";
1178
1218
  var PRERENDER_MANIFEST_FILE = "_moons_prerender.json";
1179
1219
  var NOT_FOUND_SENTINEL = "/_moons/404";
1180
1220
  var NOT_FOUND_FILE = "404.html";
@@ -1196,7 +1236,7 @@ async function runPrerender(options) {
1196
1236
  const revalidate = module.revalidate;
1197
1237
  if (revalidate !== undefined) {
1198
1238
  if (typeof revalidate !== "number" || !Number.isFinite(revalidate) || revalidate < 0 || prerender !== true) {
1199
- throw new MoonsError3({
1239
+ throw new MoonsError2({
1200
1240
  code: "E_ISR_INVALID",
1201
1241
  file: route.file,
1202
1242
  message: "`revalidate` is invalid or used without `prerender = true`",
@@ -1235,7 +1275,7 @@ async function runPrerender(options) {
1235
1275
  assertNoLeak(html, pathname, route.file, options, env);
1236
1276
  const file = outputFile(options.clientDir, pathname);
1237
1277
  mkdirSync2(dirname2(file), { recursive: true });
1238
- writeFileSync3(file, html);
1278
+ writeFileSync2(file, html);
1239
1279
  entries.push(typeof revalidate === "number" && revalidate >= 0 ? { path: pathname, revalidate } : { path: pathname });
1240
1280
  written += 1;
1241
1281
  }
@@ -1243,10 +1283,10 @@ async function runPrerender(options) {
1243
1283
  const notFound = await renderStaticNotFound(options, env, log);
1244
1284
  let wrote404 = false;
1245
1285
  if (notFound !== null) {
1246
- writeFileSync3(join3(options.clientDir, NOT_FOUND_FILE), notFound);
1286
+ writeFileSync2(join4(options.clientDir, NOT_FOUND_FILE), notFound);
1247
1287
  wrote404 = true;
1248
1288
  }
1249
- writeFileSync3(join3(options.clientDir, PRERENDER_MANIFEST_FILE), `${JSON.stringify({ version: 2, entries }, null, 2)}
1289
+ writeFileSync2(join4(options.clientDir, PRERENDER_MANIFEST_FILE), `${JSON.stringify({ version: 2, entries }, null, 2)}
1250
1290
  `);
1251
1291
  const emitted = emitStaticFiles(options.clientDir, options.staticConfig, entries);
1252
1292
  const extras = [...wrote404 ? [NOT_FOUND_FILE] : [], ...emitted];
@@ -1261,7 +1301,7 @@ function emitStaticFiles(clientDir, staticConfig, entries) {
1261
1301
  const headers = Object.entries(staticConfig.headers ?? {});
1262
1302
  if (headers.length > 0) {
1263
1303
  const lines = ["/*", ...headers.map(([name, value]) => ` ${name}: ${value}`)];
1264
- writeFileSync3(join3(clientDir, "_headers"), `${lines.join(`
1304
+ writeFileSync2(join4(clientDir, "_headers"), `${lines.join(`
1265
1305
  `)}
1266
1306
  `);
1267
1307
  emitted.push("_headers");
@@ -1269,7 +1309,7 @@ function emitStaticFiles(clientDir, staticConfig, entries) {
1269
1309
  const redirects = staticConfig.redirects ?? [];
1270
1310
  if (redirects.length > 0) {
1271
1311
  const lines = redirects.map((entry) => `${entry.from} ${entry.to} ${entry.status ?? 301}`);
1272
- writeFileSync3(join3(clientDir, "_redirects"), `${lines.join(`
1312
+ writeFileSync2(join4(clientDir, "_redirects"), `${lines.join(`
1273
1313
  `)}
1274
1314
  `);
1275
1315
  emitted.push("_redirects");
@@ -1287,7 +1327,7 @@ function emitStaticFiles(clientDir, staticConfig, entries) {
1287
1327
  ""
1288
1328
  ].join(`
1289
1329
  `);
1290
- writeFileSync3(join3(clientDir, "sitemap.xml"), xml);
1330
+ writeFileSync2(join4(clientDir, "sitemap.xml"), xml);
1291
1331
  emitted.push("sitemap.xml");
1292
1332
  }
1293
1333
  const robots = staticConfig.robots;
@@ -1299,7 +1339,7 @@ function emitStaticFiles(clientDir, staticConfig, entries) {
1299
1339
  if ((robots.sitemap ?? true) && sitemap !== undefined) {
1300
1340
  lines.push("", `Sitemap: ${sitemap.site.replace(/\/$/, "")}/sitemap.xml`);
1301
1341
  }
1302
- writeFileSync3(join3(clientDir, "robots.txt"), `${lines.join(`
1342
+ writeFileSync2(join4(clientDir, "robots.txt"), `${lines.join(`
1303
1343
  `)}
1304
1344
  `);
1305
1345
  emitted.push("robots.txt");
@@ -1331,7 +1371,7 @@ function assertNoLeak(html, pathname, file, options, env) {
1331
1371
  return;
1332
1372
  }
1333
1373
  const labels = findings.map((finding) => finding.kind === "canary" ? "canary" : `env(${finding.label})`).join(", ");
1334
- throw new MoonsError3({
1374
+ throw new MoonsError2({
1335
1375
  code: "E_PRERENDER_LEAK",
1336
1376
  file,
1337
1377
  message: `prerender output \`${pathname}\` contains ${labels}`,
@@ -1353,7 +1393,7 @@ async function resolvePathnames(route, module) {
1353
1393
  }
1354
1394
  const paths = module.paths;
1355
1395
  if (typeof paths !== "function") {
1356
- throw new MoonsError3({
1396
+ throw new MoonsError2({
1357
1397
  code: "E_PRERENDER_NO_PATHS",
1358
1398
  file: route.file,
1359
1399
  message: "dynamic route with `prerender = true` without `export const paths`",
@@ -1423,10 +1463,10 @@ function encodeSegment(route, value) {
1423
1463
  }
1424
1464
  function outputFile(clientDir, pathname) {
1425
1465
  const parts = pathname.split("/").filter((part) => part.length > 0);
1426
- const file = resolve3(join3(clientDir, ...parts, "index.html"));
1427
- const base = resolve3(clientDir);
1428
- if (file !== base && !file.startsWith(base + sep2)) {
1429
- throw new MoonsError3({
1466
+ const file = resolve4(join4(clientDir, ...parts, "index.html"));
1467
+ const base = resolve4(clientDir);
1468
+ if (file !== base && !file.startsWith(base + sep)) {
1469
+ throw new MoonsError2({
1430
1470
  code: "E_PRERENDER_INVALID",
1431
1471
  message: `prerender path escapes the client directory (\`${pathname}\`)`,
1432
1472
  details: "The file name must stay inside `dist/client`.",
@@ -1436,7 +1476,7 @@ function outputFile(clientDir, pathname) {
1436
1476
  return file;
1437
1477
  }
1438
1478
  function prerenderError(route, message) {
1439
- return new MoonsError3({
1479
+ return new MoonsError2({
1440
1480
  code: "E_PRERENDER_INVALID",
1441
1481
  file: route.file,
1442
1482
  message,
@@ -1460,11 +1500,13 @@ export {
1460
1500
  injectShell2 as injectShell,
1461
1501
  installDevMiddleware2 as installDevMiddleware,
1462
1502
  installGracefulShutdown,
1463
- resolveStaticFile,
1503
+ resolveStaticFile2 as resolveStaticFile,
1464
1504
  runPrerender,
1465
1505
  sendNodeResponse2 as sendNodeResponse,
1506
+ startBunServer,
1466
1507
  startNodeServer,
1508
+ startServer,
1467
1509
  toWebRequest2 as toWebRequest
1468
1510
  };
1469
1511
 
1470
- //# debugId=85218B929BF6A0D664756E2164756E21
1512
+ //# debugId=BFE73455FAB6702C64756E2164756E21