@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.
- package/dist/{index-0agjty3z.js → index-sdm38v1p.js} +445 -63
- package/dist/index-sdm38v1p.js.map +18 -0
- package/dist/index.js +8 -5
- package/dist/index.js.map +3 -3
- package/dist/plugin.d.ts.map +1 -1
- package/dist/server/bun-server.d.ts +18 -0
- package/dist/server/bun-server.d.ts.map +1 -0
- package/dist/server/dev.d.ts +2 -0
- package/dist/server/dev.d.ts.map +1 -1
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +423 -291
- package/dist/server/index.js.map +7 -7
- package/dist/server/node-server.d.ts +4 -11
- package/dist/server/node-server.d.ts.map +1 -1
- package/dist/server/node.d.ts +3 -19
- package/dist/server/node.d.ts.map +1 -1
- package/dist/server/server-select.d.ts +9 -0
- package/dist/server/server-select.d.ts.map +1 -0
- package/dist/server/transport.d.ts +98 -0
- package/dist/server/transport.d.ts.map +1 -0
- package/package.json +4 -4
- package/src/plugin.ts +5 -2
- package/src/server/bun-server.ts +443 -0
- package/src/server/dev.ts +27 -2
- package/src/server/index.ts +2 -0
- package/src/server/node-server.ts +108 -274
- package/src/server/node.ts +24 -92
- package/src/server/server-select.ts +27 -0
- package/src/server/storage.ts +10 -0
- package/src/server/transport.ts +504 -0
- package/dist/index-0agjty3z.js.map +0 -15
package/dist/server/index.js
CHANGED
|
@@ -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-
|
|
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 {
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
608
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
954
|
-
res.statusCode =
|
|
877
|
+
function respondText(res, status, text, configured) {
|
|
878
|
+
res.statusCode = status;
|
|
955
879
|
res.setHeader("content-type", "text/plain; charset=utf-8");
|
|
956
|
-
res.
|
|
880
|
+
applyStaticHeaders({ has: (name) => res.hasHeader(name), set: (name, value) => res.setHeader(name, value) }, "/", configured);
|
|
881
|
+
res.end(text);
|
|
957
882
|
}
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
"
|
|
967
|
-
|
|
968
|
-
|
|
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
|
|
971
|
-
|
|
972
|
-
|
|
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
|
|
975
|
-
let
|
|
929
|
+
function resolveBunUrl(request, trustProxy) {
|
|
930
|
+
let url;
|
|
976
931
|
try {
|
|
977
|
-
|
|
932
|
+
url = new URL(request.url);
|
|
978
933
|
} catch {
|
|
979
|
-
|
|
934
|
+
throw new InvalidRequestError(`invalid request URL: ${request.url}`);
|
|
980
935
|
}
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
return exact;
|
|
936
|
+
if (!trustProxy) {
|
|
937
|
+
return url;
|
|
984
938
|
}
|
|
985
|
-
const
|
|
986
|
-
const
|
|
987
|
-
if (
|
|
988
|
-
|
|
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
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
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
|
|
1001
|
-
|
|
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
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
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
|
-
|
|
1013
|
-
|
|
1014
|
-
const
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
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
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
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
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
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
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
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
|
-
|
|
1043
|
-
|
|
1044
|
-
const
|
|
1045
|
-
|
|
1046
|
-
|
|
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 (
|
|
1049
|
-
|
|
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
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
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
|
|
1077
|
-
import { join as
|
|
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 =
|
|
1087
|
-
if (!
|
|
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(),
|
|
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
|
|
1176
|
-
import { mkdirSync as mkdirSync2, writeFileSync as
|
|
1177
|
-
import { dirname as dirname2, join as
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1376
|
+
writeFileSync2(join4(options.clientDir, NOT_FOUND_FILE), notFound);
|
|
1247
1377
|
wrote404 = true;
|
|
1248
1378
|
}
|
|
1249
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
1427
|
-
const base =
|
|
1428
|
-
if (file !== base && !file.startsWith(base +
|
|
1429
|
-
throw new
|
|
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
|
|
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=
|
|
1602
|
+
//# debugId=C3DF08B69DE9E6D964756E2164756E21
|