@moonsjs/vite 0.3.0 → 0.5.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,36 @@ 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
+ RequestTimeoutError,
23
+ requestTimeoutText,
24
+ runHandlerWithTimeout,
25
+ isAbortLike,
26
+ textResponseWithHeaders,
27
+ applyStaticHeaders,
28
+ readBodyWithLimit,
29
+ resolveStaticFile2,
30
+ staticFileTarget,
31
+ scheduleRevalidation,
10
32
  resolveRequestUrl,
11
33
  resolveClientIp,
12
34
  toWebRequest2,
13
- DEFAULT_SECURITY_HEADERS,
14
35
  sendNodeResponse2,
15
36
  installDevMiddleware2
16
- } from "../index-0agjty3z.js";
37
+ } from "../index-sdm38v1p.js";
17
38
 
18
39
  // src/server/handler.ts
19
40
  import {
@@ -32,31 +53,6 @@ import {
32
53
  import { encodeSsrPayload, reviveError, serializeError } from "@moonsjs/core/serialize";
33
54
  import { API_METHODS, resolveApiRoute } from "@moonsjs/core";
34
55
  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
56
  function loadDefaultRenderer() {
61
57
  return import("@moonsjs/react/ssr").then((mod) => mod.renderToMarkup).catch(() => null);
62
58
  }
@@ -519,78 +515,21 @@ function warnPayloadFindings(payload, scan, warned) {
519
515
  }
520
516
  }
521
517
  // src/server/node-server.ts
522
- import { existsSync, readFileSync as readFileSync2, statSync as statSync2 } from "node:fs";
523
- import { gzipSync } from "node:zlib";
518
+ import { readFileSync as readFileSync2, statSync as statSync2 } from "node:fs";
524
519
  import { createServer } from "node:http";
525
- import { extname as extname2, join, resolve, sep } from "node:path";
520
+ import { extname as extname2, join, resolve } from "node:path";
526
521
  import {
527
522
  DEFAULT_BODY_LIMIT_BYTES,
528
- MoonsError as MoonsError2,
529
523
  trailingSlashRedirect as trailingSlashRedirect2
530
524
  } from "@moonsjs/core";
531
525
 
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
526
  // src/server/storage.ts
588
- import { mkdirSync, readFileSync, renameSync as renameSync2, statSync, writeFileSync as writeFileSync2 } from "node:fs";
527
+ import { mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
589
528
  import { dirname, extname } from "node:path";
590
529
  function createFileStorage(clientDir) {
591
530
  return {
592
531
  async read(pathname) {
593
- const file = resolveStaticFile(clientDir, pathname);
532
+ const file = resolveStaticFile2(clientDir, pathname);
594
533
  if (file === null) {
595
534
  return null;
596
535
  }
@@ -604,8 +543,8 @@ function createFileStorage(clientDir) {
604
543
  }
605
544
  mkdirSync(dirname(target), { recursive: true });
606
545
  const tmp = `${target}.tmp-${process.pid}`;
607
- writeFileSync2(tmp, body);
608
- renameSync2(tmp, target);
546
+ writeFileSync(tmp, body);
547
+ renameSync(tmp, target);
609
548
  }
610
549
  };
611
550
  }
@@ -625,7 +564,17 @@ var MIME = {
625
564
  ".png": "image/png",
626
565
  ".webp": "image/webp",
627
566
  ".woff": "font/woff",
628
- ".woff2": "font/woff2"
567
+ ".woff2": "font/woff2",
568
+ ".xml": "application/xml; charset=utf-8",
569
+ ".csv": "text/csv; charset=utf-8",
570
+ ".md": "text/markdown; charset=utf-8",
571
+ ".pdf": "application/pdf",
572
+ ".mp4": "video/mp4",
573
+ ".webm": "video/webm",
574
+ ".mp3": "audio/mpeg",
575
+ ".wav": "audio/wav",
576
+ ".zip": "application/zip",
577
+ ".gz": "application/gzip"
629
578
  };
630
579
 
631
580
  // src/server/logger.ts
@@ -719,27 +668,6 @@ function installGracefulShutdown(server, options = {}) {
719
668
  }
720
669
 
721
670
  // 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
671
  function startNodeServer(handler, options = {}) {
744
672
  const clientDir = resolve(options.clientDir ?? join(process.cwd(), "dist/client"));
745
673
  const port = options.port ?? Number(process.env["PORT"] ?? 3000);
@@ -769,7 +697,7 @@ function startNodeServer(handler, options = {}) {
769
697
  } catch (error) {
770
698
  if (error instanceof InvalidRequestError) {
771
699
  console.warn(`[moons] ${error.message} — 400`);
772
- respondBadRequest(res);
700
+ respondText(res, 400, "Bad Request", options.headers);
773
701
  return;
774
702
  }
775
703
  throw error;
@@ -787,7 +715,17 @@ function startNodeServer(handler, options = {}) {
787
715
  }
788
716
  if (req.method === "GET" || req.method === "HEAD") {
789
717
  if (options.storage !== undefined) {
790
- const entry = await options.storage.read(url.pathname);
718
+ let entry = null;
719
+ try {
720
+ entry = await options.storage.read(url.pathname);
721
+ } catch (error) {
722
+ const message = error instanceof Error ? error.message : String(error);
723
+ if (accessLogger !== null) {
724
+ accessLogger({ msg: "isr:storage-read-failed", level: "warn", path: url.pathname, error: message });
725
+ } else {
726
+ console.warn(`[moons] isr storage read failed (${url.pathname}): ${message} — falling back to dist/client`);
727
+ }
728
+ }
791
729
  if (entry !== null) {
792
730
  scheduleRevalidation({
793
731
  pathname: url.pathname,
@@ -800,13 +738,13 @@ function startNodeServer(handler, options = {}) {
800
738
  });
801
739
  res.statusCode = 200;
802
740
  res.setHeader("content-type", contentTypeForPathname(url.pathname));
803
- applyStaticHeaders(res, url.pathname, options.headers);
741
+ applyStaticHeaders({ has: (name) => res.hasHeader(name), set: (name, value) => res.setHeader(name, value) }, url.pathname, options.headers);
804
742
  res.end(req.method === "HEAD" ? undefined : Buffer.from(entry.body));
805
743
  return;
806
744
  }
807
745
  }
808
746
  {
809
- const staticFile = resolveStaticFile(clientDir, url.pathname);
747
+ const staticFile = resolveStaticFile2(clientDir, url.pathname);
810
748
  if (staticFile !== null) {
811
749
  scheduleRevalidation({
812
750
  pathname: url.pathname,
@@ -822,7 +760,7 @@ function startNodeServer(handler, options = {}) {
822
760
  const stat = statSync2(staticFile);
823
761
  res.statusCode = 200;
824
762
  res.setHeader("content-type", MIME_TYPES[extname2(staticFile).toLowerCase()] ?? "application/octet-stream");
825
- applyStaticHeaders(res, url.pathname, options.headers);
763
+ applyStaticHeaders({ has: (name) => res.hasHeader(name), set: (name, value) => res.setHeader(name, value) }, url.pathname, options.headers);
826
764
  const etag = `W/"${stat.size}-${Math.floor(stat.mtimeMs)}"`;
827
765
  res.setHeader("etag", etag);
828
766
  const ifNoneMatch = req.headers["if-none-match"];
@@ -832,7 +770,9 @@ function startNodeServer(handler, options = {}) {
832
770
  return;
833
771
  }
834
772
  let payload = body;
835
- if (req.method === "GET" && COMPRESSIBLE_FILE.test(staticFile) && body.byteLength >= GZIP_MIN_BYTES && acceptsGzip(req)) {
773
+ const encodings = req.headers["accept-encoding"];
774
+ const acceptsGzip = acceptsGzipHeader(Array.isArray(encodings) ? encodings[0] ?? null : encodings ?? null);
775
+ if (req.method === "GET" && COMPRESSIBLE_FILE.test(staticFile) && body.byteLength >= GZIP_MIN_BYTES && acceptsGzip) {
836
776
  payload = gzipCached(staticFile, stat.mtimeMs, body);
837
777
  res.setHeader("content-encoding", "gzip");
838
778
  appendVary(res, "accept-encoding");
@@ -847,9 +787,7 @@ function startNodeServer(handler, options = {}) {
847
787
  if (isRpc) {
848
788
  const contentLength = Number(req.headers["content-length"] ?? "0");
849
789
  if (Number.isFinite(contentLength) && contentLength > rpcLimit) {
850
- res.statusCode = 413;
851
- res.setHeader("content-type", "text/plain; charset=utf-8");
852
- res.end(rpcBodyTooLargeText(rpcLimit, contentLength));
790
+ respondText(res, 413, rpcBodyTooLargeText(rpcLimit, contentLength), options.headers);
853
791
  res.once("finish", () => req.destroy());
854
792
  return;
855
793
  }
@@ -858,21 +796,48 @@ function startNodeServer(handler, options = {}) {
858
796
  trustProxy: options.trustProxy === true,
859
797
  ...isRpc ? { maxBodyBytes: rpcLimit } : {}
860
798
  });
861
- const response = await handler(request);
799
+ const response = await runHandlerWithTimeout(handler, request, options.requestTimeoutMs);
862
800
  await sendNodeResponse2(res, response);
863
801
  } catch (error) {
864
802
  if (error instanceof BodyLimitError) {
865
803
  const isRpc = url.pathname.startsWith("/_moons/fn/");
866
- res.statusCode = 413;
867
- res.setHeader("content-type", "text/plain; charset=utf-8");
868
- res.end(isRpc ? rpcBodyTooLargeText(error.limit, null) : "Payload Too Large");
804
+ respondText(res, 413, isRpc ? rpcBodyTooLargeText(error.limit, null) : "Payload Too Large", options.headers);
869
805
  res.once("finish", () => req.destroy());
870
806
  return;
871
807
  }
808
+ if (error instanceof RequestTimeoutError) {
809
+ if (accessLogger !== null) {
810
+ accessLogger({
811
+ msg: "request-timeout",
812
+ level: "warn",
813
+ method: req.method ?? "GET",
814
+ path: url.pathname + url.search,
815
+ limitMs: error.limit
816
+ });
817
+ } else {
818
+ console.warn(`[moons] request timed out after ${error.limit}ms — ${req.method ?? "GET"} ${url.pathname}`);
819
+ }
820
+ respondText(res, 504, requestTimeoutText(error.limit), options.headers);
821
+ return;
822
+ }
823
+ if (isAbortLike(error)) {
824
+ if (accessLogger !== null) {
825
+ accessLogger({
826
+ msg: "request-aborted",
827
+ level: "warn",
828
+ method: req.method ?? "GET",
829
+ path: url.pathname + url.search
830
+ });
831
+ } else {
832
+ console.warn(`[moons] request aborted by the client — ${req.method ?? "GET"} ${url.pathname}`);
833
+ }
834
+ if (!res.headersSent && !res.writableEnded) {
835
+ respondText(res, 499, "Client Closed Request", options.headers);
836
+ }
837
+ return;
838
+ }
872
839
  console.error("[moons] server error:", error);
873
- res.statusCode = 500;
874
- res.setHeader("content-type", "text/plain; charset=utf-8");
875
- res.end("Internal Server Error");
840
+ respondText(res, 500, "Internal Server Error", options.headers);
876
841
  }
877
842
  })();
878
843
  });
@@ -899,47 +864,6 @@ function startNodeServer(handler, options = {}) {
899
864
  }
900
865
  return server;
901
866
  }
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
867
  function appendVary(res, value) {
944
868
  const existing = res.getHeader("vary");
945
869
  if (typeof existing === "string" && existing.length > 0) {
@@ -950,131 +874,337 @@ function appendVary(res, value) {
950
874
  }
951
875
  res.setHeader("vary", value);
952
876
  }
953
- function respondBadRequest(res) {
954
- res.statusCode = 400;
877
+ function respondText(res, status, text, configured) {
878
+ res.statusCode = status;
955
879
  res.setHeader("content-type", "text/plain; charset=utf-8");
956
- res.end("Bad Request");
880
+ applyStaticHeaders({ has: (name) => res.hasHeader(name), set: (name, value) => res.setHeader(name, value) }, "/", configured);
881
+ res.end(text);
957
882
  }
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();
883
+ // src/server/bun-server.ts
884
+ import { readFileSync as readFileSync3, statSync as statSync3 } from "node:fs";
885
+ import { extname as extname3, join as join2, resolve as resolve2 } from "node:path";
886
+ import { CLIENT_IP_HEADER, DEFAULT_BODY_LIMIT_BYTES as DEFAULT_BODY_LIMIT_BYTES2, trailingSlashRedirect as trailingSlashRedirect3 } from "@moonsjs/core";
887
+ function appendVaryHeader(headers, value) {
888
+ const existing = headers.get("vary");
889
+ if (existing !== null && existing.length > 0) {
890
+ if (!existing.toLowerCase().includes(value)) {
891
+ headers.set("vary", `${existing}, ${value}`);
892
+ }
893
+ return;
894
+ }
895
+ headers.set("vary", value);
969
896
  }
970
- function resolveStaticFile(clientDir, pathname) {
971
- const target = staticFileTarget(clientDir, pathname);
972
- return target !== null && isFile(target) ? target : null;
897
+ function withDefaultSecurityHeaders(response) {
898
+ let missing = false;
899
+ for (const name of Object.keys(DEFAULT_SECURITY_HEADERS)) {
900
+ if (!response.headers.has(name)) {
901
+ missing = true;
902
+ break;
903
+ }
904
+ }
905
+ if (!missing) {
906
+ return response;
907
+ }
908
+ try {
909
+ for (const [name, value] of Object.entries(DEFAULT_SECURITY_HEADERS)) {
910
+ if (!response.headers.has(name)) {
911
+ response.headers.set(name, value);
912
+ }
913
+ }
914
+ return response;
915
+ } catch {
916
+ const headers = new Headers(response.headers);
917
+ for (const [name, value] of Object.entries(DEFAULT_SECURITY_HEADERS)) {
918
+ if (!headers.has(name)) {
919
+ headers.set(name, value);
920
+ }
921
+ }
922
+ return new Response(response.body, {
923
+ status: response.status,
924
+ statusText: response.statusText,
925
+ headers
926
+ });
927
+ }
973
928
  }
974
- function staticFileTarget(clientDir, pathname) {
975
- let decoded;
929
+ function resolveBunUrl(request, trustProxy) {
930
+ let url;
976
931
  try {
977
- decoded = decodeURIComponent(pathname);
932
+ url = new URL(request.url);
978
933
  } catch {
979
- return null;
934
+ throw new InvalidRequestError(`invalid request URL: ${request.url}`);
980
935
  }
981
- const exact = safeWithin(clientDir, `.${decoded}`);
982
- if (exact !== null && isFile(exact)) {
983
- return exact;
936
+ if (!trustProxy) {
937
+ return url;
984
938
  }
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}`);
939
+ const proto = forwardedProtocol(request.headers.get("x-forwarded-proto") ?? undefined);
940
+ const host = forwardedHost(request.headers.get("x-forwarded-host") ?? undefined);
941
+ if (proto === null && host === null) {
942
+ return url;
990
943
  }
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;
944
+ const effectiveProto = proto ?? (url.protocol === "https:" ? "https" : "http");
945
+ const effectiveHost = host ?? url.host;
946
+ try {
947
+ return new URL(`${effectiveProto}://${effectiveHost}${url.pathname}${url.search}${url.hash}`);
948
+ } catch {
949
+ throw new InvalidRequestError(`invalid forwarded host: ${JSON.stringify(effectiveHost)}`);
997
950
  }
998
- return candidate;
999
951
  }
1000
- function isFile(path) {
1001
- return existsSync(path) && statSync2(path).isFile();
952
+ async function toBoundedRequest(request, url, clientIp, maxBodyBytes) {
953
+ const headers = new Headers(request.headers);
954
+ headers.delete("content-length");
955
+ if (clientIp !== null) {
956
+ headers.set(CLIENT_IP_HEADER, clientIp);
957
+ }
958
+ if (request.method === "GET" || request.method === "HEAD") {
959
+ return new Request(url, { method: request.method, headers });
960
+ }
961
+ const body = await readBodyWithLimit(request.body, maxBodyBytes);
962
+ return new Request(url, { method: request.method, headers, body });
1002
963
  }
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;
964
+ function installBunShutdown(server, options) {
965
+ const timeoutMs = options.timeoutMs ?? 1e4;
966
+ let shuttingDown = false;
967
+ return (signal) => {
968
+ if (shuttingDown) {
969
+ return;
1011
970
  }
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);
971
+ shuttingDown = true;
972
+ console.log(`[moons] received ${signal} — no longer accepting connections, draining...`);
973
+ const timer = setTimeout(() => {
974
+ console.log(`[moons] drain exceeded ${timeoutMs}ms — forcing exit`);
975
+ server.stop(true).catch(() => {});
976
+ process.exit(1);
977
+ }, timeoutMs);
978
+ timer.unref?.();
979
+ (async () => {
980
+ try {
981
+ await server.stop(false);
982
+ } catch (error) {
983
+ clearTimeout(timer);
984
+ console.log(`[moons] server close failed: ${error instanceof Error ? error.message : String(error)}`);
985
+ process.exit(1);
986
+ return;
987
+ }
988
+ if (options.plugins !== undefined) {
989
+ timer.ref?.();
990
+ try {
991
+ await options.plugins.shutdown();
992
+ } catch (error) {
993
+ console.log(`[moons] onShutdown failed: ${String(error)}`);
994
+ }
995
+ }
996
+ clearTimeout(timer);
997
+ console.log("[moons] shutdown complete (full drain)");
998
+ process.exit(0);
999
+ })();
1000
+ };
1001
+ }
1002
+ function startBunServer(handler, options = {}) {
1003
+ if (typeof Bun === "undefined") {
1004
+ throw new Error("[moons] startBunServer requires the Bun runtime");
1005
+ }
1006
+ const clientDir = resolve2(options.clientDir ?? join2(process.cwd(), "dist/client"));
1007
+ const port = options.port ?? Number(process.env["PORT"] ?? 3000);
1008
+ const trustProxy = options.trustProxy === true;
1009
+ const requestTimeoutMs = options.requestTimeoutMs;
1010
+ const idleTimeout = requestTimeoutMs === undefined || requestTimeoutMs <= 0 ? 0 : Math.ceil(requestTimeoutMs / 1000) + 5;
1011
+ const accessLogger = options.logging?.accessLog === true ? createLogger({
1012
+ json: options.logging.json !== false,
1013
+ redact: options.logging.redact ?? []
1014
+ }) : null;
1015
+ const serveStatic = async (request, url) => {
1016
+ if (options.storage !== undefined) {
1017
+ let entry = null;
1018
+ try {
1019
+ entry = await options.storage.read(url.pathname);
1020
+ } catch (error) {
1021
+ const message = error instanceof Error ? error.message : String(error);
1022
+ if (accessLogger !== null) {
1023
+ accessLogger({ msg: "isr:storage-read-failed", level: "warn", path: url.pathname, error: message });
1024
+ } else {
1025
+ console.warn(`[moons] isr storage read failed (${url.pathname}): ${message} — falling back to dist/client`);
1019
1026
  }
1020
1027
  }
1028
+ if (entry !== null) {
1029
+ scheduleRevalidation({
1030
+ pathname: url.pathname,
1031
+ mtimeMs: entry.mtimeMs,
1032
+ storage: options.storage,
1033
+ handler,
1034
+ options,
1035
+ clientDir,
1036
+ logger: accessLogger
1037
+ });
1038
+ const headers = new Headers;
1039
+ headers.set("content-type", contentTypeForPathname(url.pathname));
1040
+ applyStaticHeaders(headers, url.pathname, options.headers);
1041
+ return new Response(request.method === "HEAD" ? null : entry.body, { status: 200, headers });
1042
+ }
1021
1043
  }
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;
1044
+ const staticFile = resolveStaticFile2(clientDir, url.pathname);
1045
+ if (staticFile === null) {
1046
+ return null;
1047
+ }
1048
+ const stat = statSync3(staticFile);
1049
+ scheduleRevalidation({
1050
+ pathname: url.pathname,
1051
+ mtimeMs: stat.mtimeMs,
1052
+ file: staticFile,
1053
+ ...options.storage === undefined ? {} : { storage: options.storage },
1054
+ handler,
1055
+ options,
1056
+ clientDir,
1057
+ logger: accessLogger
1058
+ });
1059
+ const headers = new Headers;
1060
+ headers.set("content-type", MIME_TYPES[extname3(staticFile).toLowerCase()] ?? "application/octet-stream");
1061
+ applyStaticHeaders(headers, url.pathname, options.headers);
1062
+ const etag = `W/"${stat.size}-${Math.floor(stat.mtimeMs)}"`;
1063
+ headers.set("etag", etag);
1064
+ const ifNoneMatch = request.headers.get("if-none-match");
1065
+ if (ifNoneMatch !== null && ifNoneMatch.split(",").some((value) => value.trim() === etag)) {
1066
+ return new Response(null, { status: 304, headers });
1067
+ }
1068
+ let payload = request.method === "HEAD" ? null : readFileSync3(staticFile);
1069
+ if (payload !== null && COMPRESSIBLE_FILE.test(staticFile) && payload.byteLength >= GZIP_MIN_BYTES && acceptsGzipHeader(request.headers.get("accept-encoding"))) {
1070
+ payload = gzipCached(staticFile, stat.mtimeMs, Buffer.from(payload));
1071
+ headers.set("content-encoding", "gzip");
1072
+ appendVaryHeader(headers, "accept-encoding");
1073
+ }
1074
+ return new Response(payload, { status: 200, headers });
1075
+ };
1076
+ const server = Bun.serve({
1077
+ port,
1078
+ idleTimeout,
1079
+ fetch: async (request, bunServer) => {
1080
+ const requestStartedAt = performance.now();
1081
+ let url;
1082
+ try {
1083
+ url = resolveBunUrl(request, trustProxy);
1084
+ } catch (error) {
1085
+ if (error instanceof InvalidRequestError) {
1086
+ console.warn(`[moons] ${error.message} — 400`);
1087
+ return textResponseWithHeaders(400, "Bad Request", options.headers);
1088
+ }
1089
+ throw error;
1090
+ }
1091
+ let clientIp = null;
1092
+ const finish = (response) => {
1093
+ if (accessLogger !== null) {
1094
+ accessLogger({
1095
+ msg: "request",
1096
+ method: request.method,
1097
+ path: url.pathname + url.search,
1098
+ status: response.status,
1099
+ durationMs: Math.round(performance.now() - requestStartedAt),
1100
+ ...clientIp === null ? {} : { ip: clientIp }
1101
+ });
1102
+ }
1103
+ return response;
1104
+ };
1105
+ try {
1106
+ if (options.trailingSlash !== undefined) {
1107
+ const canonical = trailingSlashRedirect3(url.pathname, options.trailingSlash);
1108
+ if (canonical !== null) {
1109
+ url.pathname = canonical;
1110
+ return new Response(null, { status: 308, headers: { location: url.toString() } });
1111
+ }
1112
+ }
1113
+ if (request.method === "GET" || request.method === "HEAD") {
1114
+ const staticResponse = await serveStatic(request, url);
1115
+ if (staticResponse !== null) {
1116
+ return finish(staticResponse);
1117
+ }
1118
+ }
1119
+ const isRpc = url.pathname.startsWith("/_moons/fn/");
1120
+ const rpcLimit = options.bodyLimitBytes ?? DEFAULT_BODY_LIMIT_BYTES2;
1121
+ if (isRpc) {
1122
+ const contentLength = Number(request.headers.get("content-length") ?? "0");
1123
+ if (Number.isFinite(contentLength) && contentLength > rpcLimit) {
1124
+ return finish(textResponseWithHeaders(413, rpcBodyTooLargeText(rpcLimit, contentLength), options.headers));
1125
+ }
1126
+ }
1127
+ const remoteAddress = bunServer.requestIP(request)?.address ?? null;
1128
+ clientIp = resolveClientIpValues(request.headers.get("x-forwarded-for") ?? undefined, remoteAddress, trustProxy);
1129
+ const needsRewrite = clientIp !== null || url.toString() !== request.url;
1130
+ const webRequest = (request.method === "GET" || request.method === "HEAD") && !needsRewrite ? request : await toBoundedRequest(request, url, clientIp, isRpc ? rpcLimit : MAX_TRANSPORT_BODY_BYTES);
1131
+ const response = await runHandlerWithTimeout(handler, webRequest, requestTimeoutMs);
1132
+ return finish(withDefaultSecurityHeaders(response));
1133
+ } catch (error) {
1134
+ if (error instanceof BodyLimitError) {
1135
+ const isRpc = url.pathname.startsWith("/_moons/fn/");
1136
+ return finish(textResponseWithHeaders(413, isRpc ? rpcBodyTooLargeText(error.limit, null) : "Payload Too Large", options.headers));
1137
+ }
1138
+ if (error instanceof RequestTimeoutError) {
1139
+ if (accessLogger !== null) {
1140
+ accessLogger({
1141
+ msg: "request-timeout",
1142
+ level: "warn",
1143
+ method: request.method,
1144
+ path: url.pathname + url.search,
1145
+ limitMs: error.limit
1146
+ });
1147
+ } else {
1148
+ console.warn(`[moons] request timed out after ${error.limit}ms — ${request.method} ${url.pathname}`);
1149
+ }
1150
+ return finish(textResponseWithHeaders(504, requestTimeoutText(error.limit), options.headers));
1026
1151
  }
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);
1152
+ if (isAbortLike(error)) {
1153
+ if (accessLogger !== null) {
1154
+ accessLogger({
1155
+ msg: "request-aborted",
1156
+ level: "warn",
1157
+ method: request.method,
1158
+ path: url.pathname + url.search
1159
+ });
1160
+ } else {
1161
+ console.warn(`[moons] request aborted by the client — ${request.method} ${url.pathname}`);
1162
+ }
1163
+ return new Response(null, { status: 499 });
1032
1164
  }
1165
+ console.error("[moons] server error:", error);
1166
+ return finish(textResponseWithHeaders(500, "Internal Server Error", options.headers));
1033
1167
  }
1034
1168
  }
1035
- const info = { paths, revalidate };
1036
- prerenderManifestCache = { file, mtimeMs, info };
1037
- return info;
1038
- } catch {
1039
- return empty;
1169
+ });
1170
+ const listenAfterStart = () => {
1171
+ console.log(`[moons] server listening on http://localhost:${server.port}`);
1172
+ options.plugins?.ready();
1173
+ };
1174
+ if (options.plugins !== undefined) {
1175
+ options.plugins.start().then(listenAfterStart);
1176
+ } else {
1177
+ listenAfterStart();
1178
+ }
1179
+ if (options.handleSignals !== false) {
1180
+ const shutdown = installBunShutdown(server, {
1181
+ ...options.shutdownTimeoutMs === undefined ? {} : { timeoutMs: options.shutdownTimeoutMs },
1182
+ plugins: options.plugins
1183
+ });
1184
+ process.once("SIGTERM", () => shutdown("SIGTERM"));
1185
+ process.once("SIGINT", () => shutdown("SIGINT"));
1040
1186
  }
1187
+ return server;
1041
1188
  }
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;
1189
+ // src/server/server-select.ts
1190
+ function startServer(handler, options = {}) {
1191
+ const forced = process.env["MOONS_SERVER"];
1192
+ const isBun = typeof Bun !== "undefined";
1193
+ if (forced === "node") {
1194
+ return startNodeServer(handler, options);
1047
1195
  }
1048
- if (!shouldRevalidate({ mtimeMs: input.mtimeMs, revalidateSeconds: seconds })) {
1049
- return;
1196
+ if (forced === "bun" && !isBun) {
1197
+ console.warn("[moons] MOONS_SERVER=bun requires the Bun runtime — falling back to node");
1198
+ return startNodeServer(handler, options);
1050
1199
  }
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
- });
1200
+ if (forced !== undefined && forced !== "bun" && forced !== "node") {
1201
+ console.warn(`[moons] unknown MOONS_SERVER="${forced}" — using ${isBun ? "bun" : "node"}`);
1202
+ }
1203
+ return isBun ? startBunServer(handler, options) : startNodeServer(handler, options);
1074
1204
  }
1075
1205
  // src/server/test-server.ts
1076
- import { existsSync as existsSync2 } from "node:fs";
1077
- import { join as join2, resolve as resolve2 } from "node:path";
1206
+ import { existsSync } from "node:fs";
1207
+ import { join as join3, resolve as resolve3 } from "node:path";
1078
1208
  import { pathToFileURL } from "node:url";
1079
1209
  async function loadRenderer(candidates) {
1080
1210
  for (const root of candidates) {
@@ -1083,8 +1213,8 @@ async function loadRenderer(candidates) {
1083
1213
  "node_modules/@moonsjs/react/dist/ssr.js",
1084
1214
  "node_modules/@moonsjs/react/dist/ssr.mjs"
1085
1215
  ]) {
1086
- const file = join2(root, relative);
1087
- if (!existsSync2(file)) {
1216
+ const file = join3(root, relative);
1217
+ if (!existsSync(file)) {
1088
1218
  continue;
1089
1219
  }
1090
1220
  const mod = await import(pathToFileURL(file).href);
@@ -1107,7 +1237,7 @@ function createTestServer(options, baseUrl = "http://localhost") {
1107
1237
  ...options.loadApi === undefined ? {} : { loadApi: options.loadApi },
1108
1238
  ...options.apiMiddlewares === undefined ? {} : { apiMiddlewares: options.apiMiddlewares },
1109
1239
  ...options.loadApiMiddleware === undefined ? {} : { loadApiMiddleware: options.loadApiMiddleware },
1110
- renderer: () => loadRenderer([process.cwd(), resolve2(options.routesDir, "..", "..")]),
1240
+ renderer: () => loadRenderer([process.cwd(), resolve3(options.routesDir, "..", "..")]),
1111
1241
  shell
1112
1242
  });
1113
1243
  return {
@@ -1172,9 +1302,9 @@ function createPluginRuntime(options) {
1172
1302
  };
1173
1303
  }
1174
1304
  // 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";
1305
+ import { MoonsError as MoonsError2 } from "@moonsjs/core";
1306
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "node:fs";
1307
+ import { dirname as dirname2, join as join4, resolve as resolve4, sep } from "node:path";
1178
1308
  var PRERENDER_MANIFEST_FILE = "_moons_prerender.json";
1179
1309
  var NOT_FOUND_SENTINEL = "/_moons/404";
1180
1310
  var NOT_FOUND_FILE = "404.html";
@@ -1196,7 +1326,7 @@ async function runPrerender(options) {
1196
1326
  const revalidate = module.revalidate;
1197
1327
  if (revalidate !== undefined) {
1198
1328
  if (typeof revalidate !== "number" || !Number.isFinite(revalidate) || revalidate < 0 || prerender !== true) {
1199
- throw new MoonsError3({
1329
+ throw new MoonsError2({
1200
1330
  code: "E_ISR_INVALID",
1201
1331
  file: route.file,
1202
1332
  message: "`revalidate` is invalid or used without `prerender = true`",
@@ -1235,7 +1365,7 @@ async function runPrerender(options) {
1235
1365
  assertNoLeak(html, pathname, route.file, options, env);
1236
1366
  const file = outputFile(options.clientDir, pathname);
1237
1367
  mkdirSync2(dirname2(file), { recursive: true });
1238
- writeFileSync3(file, html);
1368
+ writeFileSync2(file, html);
1239
1369
  entries.push(typeof revalidate === "number" && revalidate >= 0 ? { path: pathname, revalidate } : { path: pathname });
1240
1370
  written += 1;
1241
1371
  }
@@ -1243,10 +1373,10 @@ async function runPrerender(options) {
1243
1373
  const notFound = await renderStaticNotFound(options, env, log);
1244
1374
  let wrote404 = false;
1245
1375
  if (notFound !== null) {
1246
- writeFileSync3(join3(options.clientDir, NOT_FOUND_FILE), notFound);
1376
+ writeFileSync2(join4(options.clientDir, NOT_FOUND_FILE), notFound);
1247
1377
  wrote404 = true;
1248
1378
  }
1249
- writeFileSync3(join3(options.clientDir, PRERENDER_MANIFEST_FILE), `${JSON.stringify({ version: 2, entries }, null, 2)}
1379
+ writeFileSync2(join4(options.clientDir, PRERENDER_MANIFEST_FILE), `${JSON.stringify({ version: 2, entries }, null, 2)}
1250
1380
  `);
1251
1381
  const emitted = emitStaticFiles(options.clientDir, options.staticConfig, entries);
1252
1382
  const extras = [...wrote404 ? [NOT_FOUND_FILE] : [], ...emitted];
@@ -1261,7 +1391,7 @@ function emitStaticFiles(clientDir, staticConfig, entries) {
1261
1391
  const headers = Object.entries(staticConfig.headers ?? {});
1262
1392
  if (headers.length > 0) {
1263
1393
  const lines = ["/*", ...headers.map(([name, value]) => ` ${name}: ${value}`)];
1264
- writeFileSync3(join3(clientDir, "_headers"), `${lines.join(`
1394
+ writeFileSync2(join4(clientDir, "_headers"), `${lines.join(`
1265
1395
  `)}
1266
1396
  `);
1267
1397
  emitted.push("_headers");
@@ -1269,7 +1399,7 @@ function emitStaticFiles(clientDir, staticConfig, entries) {
1269
1399
  const redirects = staticConfig.redirects ?? [];
1270
1400
  if (redirects.length > 0) {
1271
1401
  const lines = redirects.map((entry) => `${entry.from} ${entry.to} ${entry.status ?? 301}`);
1272
- writeFileSync3(join3(clientDir, "_redirects"), `${lines.join(`
1402
+ writeFileSync2(join4(clientDir, "_redirects"), `${lines.join(`
1273
1403
  `)}
1274
1404
  `);
1275
1405
  emitted.push("_redirects");
@@ -1287,7 +1417,7 @@ function emitStaticFiles(clientDir, staticConfig, entries) {
1287
1417
  ""
1288
1418
  ].join(`
1289
1419
  `);
1290
- writeFileSync3(join3(clientDir, "sitemap.xml"), xml);
1420
+ writeFileSync2(join4(clientDir, "sitemap.xml"), xml);
1291
1421
  emitted.push("sitemap.xml");
1292
1422
  }
1293
1423
  const robots = staticConfig.robots;
@@ -1299,7 +1429,7 @@ function emitStaticFiles(clientDir, staticConfig, entries) {
1299
1429
  if ((robots.sitemap ?? true) && sitemap !== undefined) {
1300
1430
  lines.push("", `Sitemap: ${sitemap.site.replace(/\/$/, "")}/sitemap.xml`);
1301
1431
  }
1302
- writeFileSync3(join3(clientDir, "robots.txt"), `${lines.join(`
1432
+ writeFileSync2(join4(clientDir, "robots.txt"), `${lines.join(`
1303
1433
  `)}
1304
1434
  `);
1305
1435
  emitted.push("robots.txt");
@@ -1331,7 +1461,7 @@ function assertNoLeak(html, pathname, file, options, env) {
1331
1461
  return;
1332
1462
  }
1333
1463
  const labels = findings.map((finding) => finding.kind === "canary" ? "canary" : `env(${finding.label})`).join(", ");
1334
- throw new MoonsError3({
1464
+ throw new MoonsError2({
1335
1465
  code: "E_PRERENDER_LEAK",
1336
1466
  file,
1337
1467
  message: `prerender output \`${pathname}\` contains ${labels}`,
@@ -1353,7 +1483,7 @@ async function resolvePathnames(route, module) {
1353
1483
  }
1354
1484
  const paths = module.paths;
1355
1485
  if (typeof paths !== "function") {
1356
- throw new MoonsError3({
1486
+ throw new MoonsError2({
1357
1487
  code: "E_PRERENDER_NO_PATHS",
1358
1488
  file: route.file,
1359
1489
  message: "dynamic route with `prerender = true` without `export const paths`",
@@ -1423,10 +1553,10 @@ function encodeSegment(route, value) {
1423
1553
  }
1424
1554
  function outputFile(clientDir, pathname) {
1425
1555
  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({
1556
+ const file = resolve4(join4(clientDir, ...parts, "index.html"));
1557
+ const base = resolve4(clientDir);
1558
+ if (file !== base && !file.startsWith(base + sep)) {
1559
+ throw new MoonsError2({
1430
1560
  code: "E_PRERENDER_INVALID",
1431
1561
  message: `prerender path escapes the client directory (\`${pathname}\`)`,
1432
1562
  details: "The file name must stay inside `dist/client`.",
@@ -1436,7 +1566,7 @@ function outputFile(clientDir, pathname) {
1436
1566
  return file;
1437
1567
  }
1438
1568
  function prerenderError(route, message) {
1439
- return new MoonsError3({
1569
+ return new MoonsError2({
1440
1570
  code: "E_PRERENDER_INVALID",
1441
1571
  file: route.file,
1442
1572
  message,
@@ -1460,11 +1590,13 @@ export {
1460
1590
  injectShell2 as injectShell,
1461
1591
  installDevMiddleware2 as installDevMiddleware,
1462
1592
  installGracefulShutdown,
1463
- resolveStaticFile,
1593
+ resolveStaticFile2 as resolveStaticFile,
1464
1594
  runPrerender,
1465
1595
  sendNodeResponse2 as sendNodeResponse,
1596
+ startBunServer,
1466
1597
  startNodeServer,
1598
+ startServer,
1467
1599
  toWebRequest2 as toWebRequest
1468
1600
  };
1469
1601
 
1470
- //# debugId=85218B929BF6A0D664756E2164756E21
1602
+ //# debugId=C3DF08B69DE9E6D964756E2164756E21