@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.
- package/dist/{index-0agjty3z.js → index-df6b8rqt.js} +364 -62
- package/dist/index-df6b8rqt.js.map +18 -0
- package/dist/index.js +4 -4
- package/dist/index.js.map +3 -3
- package/dist/server/bun-server.d.ts +18 -0
- package/dist/server/bun-server.d.ts.map +1 -0
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +317 -275
- package/dist/server/index.js.map +6 -6
- package/dist/server/node-server.d.ts +2 -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 +82 -0
- package/dist/server/transport.d.ts.map +1 -0
- package/package.json +4 -4
- package/src/plugin.ts +2 -2
- package/src/server/bun-server.ts +385 -0
- package/src/server/index.ts +2 -0
- package/src/server/node-server.ts +31 -259
- package/src/server/node.ts +24 -92
- package/src/server/server-select.ts +27 -0
- package/src/server/transport.ts +414 -0
- package/dist/index-0agjty3z.js.map +0 -15
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { type PrerenderStorage } from "@moonsjs/core";
|
|
2
|
+
import type { Logger } from "./logger";
|
|
3
|
+
/**
|
|
4
|
+
* Addendum Q (3B): bagian transport yang tidak bergantung runtime — dipakai
|
|
5
|
+
* `node-server` dan `bun-server` agar paritas bukan duplikasi.
|
|
6
|
+
*/
|
|
7
|
+
/** Batas transport default (Addendum J.2); RPC punya batas lebih kecil dari config. */
|
|
8
|
+
export declare const MAX_TRANSPORT_BODY_BYTES: number;
|
|
9
|
+
/** Request tidak valid (Host/URL malformed) → dipetakan ke 400 oleh server. */
|
|
10
|
+
export declare class InvalidRequestError extends Error {
|
|
11
|
+
constructor(message: string);
|
|
12
|
+
}
|
|
13
|
+
/** Body melebihi batas → dipetakan ke 413; pembacaan dihentikan saat melewati. */
|
|
14
|
+
export declare class BodyLimitError extends Error {
|
|
15
|
+
readonly limit: number;
|
|
16
|
+
constructor(limit: number);
|
|
17
|
+
}
|
|
18
|
+
export interface ToWebRequestOptions {
|
|
19
|
+
/** Addendum J.1: percayai `x-forwarded-proto`/`x-forwarded-host` (default false). */
|
|
20
|
+
trustProxy?: boolean;
|
|
21
|
+
/** Batas pembacaan body (byte). Default: `MAX_TRANSPORT_BODY_BYTES`. */
|
|
22
|
+
maxBodyBytes?: number;
|
|
23
|
+
}
|
|
24
|
+
/** Addendum J.7: default aman runtime; `config.headers` tetap menang (cek `has`). */
|
|
25
|
+
export declare const DEFAULT_SECURITY_HEADERS: Record<string, string>;
|
|
26
|
+
export declare function firstHeaderValue(value: string | string[] | undefined): string | null;
|
|
27
|
+
export declare function forwardedProtocol(value: string | string[] | undefined): "http" | "https" | null;
|
|
28
|
+
export declare function forwardedHost(value: string | string[] | undefined): string | null;
|
|
29
|
+
/**
|
|
30
|
+
* IP klien tepercaya (Addendum J.1) dari nilai header mentah — inti yang sama
|
|
31
|
+
* untuk Node (`IncomingMessage`) dan Bun (`Request` + `server.requestIP`).
|
|
32
|
+
*/
|
|
33
|
+
export declare function resolveClientIpValues(forwardedFor: string | string[] | undefined, remoteAddress: string | null, trustProxy: boolean): string | null;
|
|
34
|
+
export declare const MIME_TYPES: Record<string, string>;
|
|
35
|
+
export declare const COMPRESSIBLE_FILE: RegExp;
|
|
36
|
+
export declare const GZIP_MIN_BYTES = 1024;
|
|
37
|
+
/** Varian gzip ter-cache per file+mtime (bounded FIFO) — Addendum K.5. */
|
|
38
|
+
export declare function gzipCached(file: string, mtimeMs: number, body: Buffer): Buffer;
|
|
39
|
+
export declare function acceptsGzipHeader(value: string | null): boolean;
|
|
40
|
+
/** 413 transport dengan format §15 yang sama seperti `rpc.ts` (Addendum J.2). */
|
|
41
|
+
export declare function rpcBodyTooLargeText(limit: number, size: number | null): string;
|
|
42
|
+
/** Target header minimal yang dipenuhi `ServerResponse` dan `Headers`. */
|
|
43
|
+
export interface HeaderTarget {
|
|
44
|
+
has(name: string): boolean;
|
|
45
|
+
set(name: string, value: string): void;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Addendum J.7: header konfigurasi (menang), default aman, dan
|
|
49
|
+
* `Cache-Control: immutable` untuk aset ber-hash di `/assets/*`.
|
|
50
|
+
*/
|
|
51
|
+
export declare function applyStaticHeaders(target: HeaderTarget, pathname: string, configured: Record<string, string> | undefined): void;
|
|
52
|
+
/**
|
|
53
|
+
* Baca body Web stream dengan batas tegas (Addendum J.2): begitu melewati
|
|
54
|
+
* `maxBytes`, pembacaan dihentikan dan `BodyLimitError` dilempar.
|
|
55
|
+
*/
|
|
56
|
+
export declare function readBodyWithLimit(stream: ReadableStream<Uint8Array> | null, maxBytes: number): Promise<Uint8Array>;
|
|
57
|
+
/** Resolve file static dengan proteksi path traversal (+ manifest prerender D.5). */
|
|
58
|
+
export declare function resolveStaticFile(clientDir: string, pathname: string): string | null;
|
|
59
|
+
/**
|
|
60
|
+
* Target file untuk `pathname` (tanpa cek keberadaan) — dipakai penyajian,
|
|
61
|
+
* `createFileStorage().write`, dan test. Proteksi path traversal tetap.
|
|
62
|
+
*/
|
|
63
|
+
export declare function staticFileTarget(clientDir: string, pathname: string): string | null;
|
|
64
|
+
export interface PrerenderManifestInfo {
|
|
65
|
+
paths: Set<string>;
|
|
66
|
+
revalidate: Map<string, number>;
|
|
67
|
+
}
|
|
68
|
+
export declare function readPrerenderManifest(clientDir: string): PrerenderManifestInfo;
|
|
69
|
+
/** ISR: jika file prerender kedaluwarsa, regenerasi background (SWR). */
|
|
70
|
+
export declare function scheduleRevalidation(input: {
|
|
71
|
+
pathname: string;
|
|
72
|
+
mtimeMs: number;
|
|
73
|
+
file?: string;
|
|
74
|
+
storage?: PrerenderStorage;
|
|
75
|
+
handler: (request: Request) => Promise<Response>;
|
|
76
|
+
options: {
|
|
77
|
+
canary?: string;
|
|
78
|
+
};
|
|
79
|
+
clientDir: string;
|
|
80
|
+
logger?: Logger | null;
|
|
81
|
+
}): void;
|
|
82
|
+
//# sourceMappingURL=transport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../src/server/transport.ts"],"names":[],"mappings":"AAGA,OAAO,EAAc,KAAK,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAElE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAEvC;;;GAGG;AAEH,uFAAuF;AACvF,eAAO,MAAM,wBAAwB,QAAmB,CAAC;AAEzD,+EAA+E;AAC/E,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,YAAY,OAAO,EAAE,MAAM,EAG1B;CACF;AAED,kFAAkF;AAClF,qBAAa,cAAe,SAAQ,KAAK;IACvC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,YAAY,KAAK,EAAE,MAAM,EAIxB;CACF;AAED,MAAM,WAAW,mBAAmB;IAClC,qFAAqF;IACrF,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,wEAAwE;IACxE,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,qFAAqF;AACrF,eAAO,MAAM,wBAAwB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAG3D,CAAC;AAEF,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAUpF;AAED,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,GACnC,MAAM,GAAG,OAAO,GAAG,IAAI,CASzB;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAUjF;AAiBD;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,YAAY,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,EAC3C,aAAa,EAAE,MAAM,GAAG,IAAI,EAC5B,UAAU,EAAE,OAAO,GAClB,MAAM,GAAG,IAAI,CAQf;AAED,eAAO,MAAM,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAoB7C,CAAC;AAEF,eAAO,MAAM,iBAAiB,QAA+C,CAAC;AAC9E,eAAO,MAAM,cAAc,OAAO,CAAC;AAInC,0EAA0E;AAC1E,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAc9E;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAS/D;AAED,iFAAiF;AACjF,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAe9E;AAED,0EAA0E;AAC1E,MAAM,WAAW,YAAY;IAC3B,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAC3B,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACxC;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,YAAY,EACpB,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,GAC7C,IAAI,CAcN;AAED;;;GAGG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,GAAG,IAAI,EACzC,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,UAAU,CAAC,CAmCrB;AAED,qFAAqF;AACrF,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGpF;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAwBnF;AAcD,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACnB,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAMD,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,GAAG,qBAAqB,CA+C9E;AAED,yEAAyE;AACzE,wBAAgB,oBAAoB,CAAC,KAAK,EAAE;IAC1C,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,gBAAgB,CAAC;IAC3B,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;IACjD,OAAO,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB,GAAG,IAAI,CAkCP"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moonsjs/vite",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -50,9 +50,9 @@
|
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@babel/parser": "^8.0.7",
|
|
53
|
-
"@moonsjs/core": "^0.
|
|
54
|
-
"@moonsjs/react": "^0.
|
|
55
|
-
"@moonsjs/server": "^0.
|
|
53
|
+
"@moonsjs/core": "^0.4.0",
|
|
54
|
+
"@moonsjs/react": "^0.4.0",
|
|
55
|
+
"@moonsjs/server": "^0.4.0"
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|
|
58
58
|
"vite": "^8.3.4"
|
package/src/plugin.ts
CHANGED
|
@@ -557,7 +557,7 @@ function renderServerEntryBuild(
|
|
|
557
557
|
): string {
|
|
558
558
|
return [
|
|
559
559
|
"// Generated by @moons. Do not edit manually.",
|
|
560
|
-
'import { createFileShell, createPluginRuntime, createRequestHandler,
|
|
560
|
+
'import { createFileShell, createPluginRuntime, createRequestHandler, startServer } from "@moonsjs/vite/server";',
|
|
561
561
|
'import { renderToMarkup } from "@moonsjs/react/ssr";',
|
|
562
562
|
'import config from "virtual:moons/config";',
|
|
563
563
|
'import { routes, loadRoute } from "virtual:moons/routes";',
|
|
@@ -579,7 +579,7 @@ function renderServerEntryBuild(
|
|
|
579
579
|
" onContext: (ctx) => plugins.applyExtensions(ctx),",
|
|
580
580
|
"});",
|
|
581
581
|
"",
|
|
582
|
-
`
|
|
582
|
+
`startServer(handler, { plugins, headers: config.headers, storage: config.isr?.storage, trustProxy: config.security?.trustProxy, bodyLimitBytes: config.security?.bodyLimitBytes, logging: config.logging, trailingSlash: config.trailingSlash${canary === undefined ? "" : `, canary: ${JSON.stringify(canary)}`} });`,
|
|
583
583
|
"",
|
|
584
584
|
].join("\n");
|
|
585
585
|
}
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import { readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { extname, join, resolve } from "node:path";
|
|
3
|
+
import { CLIENT_IP_HEADER, DEFAULT_BODY_LIMIT_BYTES, trailingSlashRedirect } from "@moonsjs/core";
|
|
4
|
+
import { contentTypeForPathname } from "./storage";
|
|
5
|
+
import {
|
|
6
|
+
BodyLimitError,
|
|
7
|
+
COMPRESSIBLE_FILE,
|
|
8
|
+
DEFAULT_SECURITY_HEADERS,
|
|
9
|
+
GZIP_MIN_BYTES,
|
|
10
|
+
InvalidRequestError,
|
|
11
|
+
MAX_TRANSPORT_BODY_BYTES,
|
|
12
|
+
MIME_TYPES,
|
|
13
|
+
acceptsGzipHeader,
|
|
14
|
+
applyStaticHeaders,
|
|
15
|
+
forwardedHost,
|
|
16
|
+
forwardedProtocol,
|
|
17
|
+
gzipCached,
|
|
18
|
+
readBodyWithLimit,
|
|
19
|
+
resolveClientIpValues,
|
|
20
|
+
resolveStaticFile,
|
|
21
|
+
rpcBodyTooLargeText,
|
|
22
|
+
scheduleRevalidation,
|
|
23
|
+
} from "./transport";
|
|
24
|
+
import { createLogger, type Logger } from "./logger";
|
|
25
|
+
import type { NodeServerOptions } from "./node-server";
|
|
26
|
+
import type { PluginRuntime } from "./plugins";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Addendum Q (3B): host `Bun.serve` dengan perilaku paritas `node-server`:
|
|
30
|
+
* static + storage/ISR + ETag/gzip + trailingSlash + body limit RPC +
|
|
31
|
+
* header default + access log + drain SIGTERM. Dipilih otomatis oleh
|
|
32
|
+
* `startServer` saat berjalan di Bun (override: `MOONS_SERVER=node|bun`).
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
interface BunServerHandle {
|
|
36
|
+
port: number;
|
|
37
|
+
stop(closeActiveConnections?: boolean): Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const textResponse = (status: number, text: string): Response =>
|
|
41
|
+
new Response(text, { status, headers: { "content-type": "text/plain; charset=utf-8" } });
|
|
42
|
+
|
|
43
|
+
function appendVaryHeader(headers: Headers, value: string): void {
|
|
44
|
+
const existing = headers.get("vary");
|
|
45
|
+
if (existing !== null && existing.length > 0) {
|
|
46
|
+
if (!existing.toLowerCase().includes(value)) {
|
|
47
|
+
headers.set("vary", `${existing}, ${value}`);
|
|
48
|
+
}
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
headers.set("vary", value);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** J.7: response handler tanpa header default aman — mutasi langsung bila mungkin. */
|
|
55
|
+
function withDefaultSecurityHeaders(response: Response): Response {
|
|
56
|
+
let missing = false;
|
|
57
|
+
for (const name of Object.keys(DEFAULT_SECURITY_HEADERS)) {
|
|
58
|
+
if (!response.headers.has(name)) {
|
|
59
|
+
missing = true;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (!missing) {
|
|
64
|
+
return response;
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
for (const [name, value] of Object.entries(DEFAULT_SECURITY_HEADERS)) {
|
|
68
|
+
if (!response.headers.has(name)) {
|
|
69
|
+
response.headers.set(name, value);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return response;
|
|
73
|
+
} catch {
|
|
74
|
+
// Header immutable (mis. dari fetch) → bungkus ulang.
|
|
75
|
+
const headers = new Headers(response.headers);
|
|
76
|
+
for (const [name, value] of Object.entries(DEFAULT_SECURITY_HEADERS)) {
|
|
77
|
+
if (!headers.has(name)) {
|
|
78
|
+
headers.set(name, value);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return new Response(response.body, {
|
|
82
|
+
status: response.status,
|
|
83
|
+
statusText: response.statusText,
|
|
84
|
+
headers,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** J.1: URL dari Request Bun, mengikuti `x-forwarded-*` bila trustProxy aktif. */
|
|
90
|
+
function resolveBunUrl(request: Request, trustProxy: boolean): URL {
|
|
91
|
+
let url: URL;
|
|
92
|
+
try {
|
|
93
|
+
url = new URL(request.url);
|
|
94
|
+
} catch {
|
|
95
|
+
throw new InvalidRequestError(`invalid request URL: ${request.url}`);
|
|
96
|
+
}
|
|
97
|
+
if (!trustProxy) {
|
|
98
|
+
return url;
|
|
99
|
+
}
|
|
100
|
+
const proto = forwardedProtocol(request.headers.get("x-forwarded-proto") ?? undefined);
|
|
101
|
+
const host = forwardedHost(request.headers.get("x-forwarded-host") ?? undefined);
|
|
102
|
+
if (proto === null && host === null) {
|
|
103
|
+
return url;
|
|
104
|
+
}
|
|
105
|
+
// Bangun URL baru (bukan mutasi `.host`): setter `host` mempertahankan port
|
|
106
|
+
// lama, sehingga `internal:3000` + `x-forwarded-host: app.example.com`
|
|
107
|
+
// menghasilkan origin `https://app.example.com:3000` — origin RPC meleset.
|
|
108
|
+
const effectiveProto = proto ?? (url.protocol === "https:" ? "https" : "http");
|
|
109
|
+
const effectiveHost = host ?? url.host;
|
|
110
|
+
try {
|
|
111
|
+
return new URL(`${effectiveProto}://${effectiveHost}${url.pathname}${url.search}${url.hash}`);
|
|
112
|
+
} catch {
|
|
113
|
+
throw new InvalidRequestError(`invalid forwarded host: ${JSON.stringify(effectiveHost)}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Request Web dengan body terbatas + `x-moons-client-ip` tepercaya (J.1/J.2). */
|
|
118
|
+
async function toBoundedRequest(
|
|
119
|
+
request: Request,
|
|
120
|
+
url: URL,
|
|
121
|
+
clientIp: string | null,
|
|
122
|
+
maxBodyBytes: number,
|
|
123
|
+
): Promise<Request> {
|
|
124
|
+
const headers = new Headers(request.headers);
|
|
125
|
+
headers.delete("content-length");
|
|
126
|
+
if (clientIp !== null) {
|
|
127
|
+
headers.set(CLIENT_IP_HEADER, clientIp);
|
|
128
|
+
}
|
|
129
|
+
if (request.method === "GET" || request.method === "HEAD") {
|
|
130
|
+
return new Request(url, { method: request.method, headers });
|
|
131
|
+
}
|
|
132
|
+
const body = await readBodyWithLimit(request.body, maxBodyBytes);
|
|
133
|
+
return new Request(url, { method: request.method, headers, body: body as unknown as BodyInit });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function installBunShutdown(
|
|
137
|
+
server: BunServerHandle,
|
|
138
|
+
options: { timeoutMs?: number; plugins?: PluginRuntime | undefined },
|
|
139
|
+
): (signal: string) => void {
|
|
140
|
+
const timeoutMs = options.timeoutMs ?? 10_000;
|
|
141
|
+
let shuttingDown = false;
|
|
142
|
+
return (signal: string): void => {
|
|
143
|
+
if (shuttingDown) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
shuttingDown = true;
|
|
147
|
+
console.log(`[moons] received ${signal} — no longer accepting connections, draining...`);
|
|
148
|
+
const timer = setTimeout(() => {
|
|
149
|
+
console.log(`[moons] drain exceeded ${timeoutMs}ms — forcing exit`);
|
|
150
|
+
void server.stop(true).catch(() => {});
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}, timeoutMs);
|
|
153
|
+
(timer as { unref?: () => void }).unref?.();
|
|
154
|
+
void (async () => {
|
|
155
|
+
try {
|
|
156
|
+
await server.stop(false);
|
|
157
|
+
} catch (error) {
|
|
158
|
+
clearTimeout(timer);
|
|
159
|
+
console.log(
|
|
160
|
+
`[moons] server close failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
161
|
+
);
|
|
162
|
+
process.exit(1);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (options.plugins !== undefined) {
|
|
166
|
+
// Berbagi batas waktu dengan drain: timer di-ref kembali.
|
|
167
|
+
(timer as { ref?: () => void }).ref?.();
|
|
168
|
+
try {
|
|
169
|
+
await options.plugins.shutdown();
|
|
170
|
+
} catch (error) {
|
|
171
|
+
console.log(`[moons] onShutdown failed: ${String(error)}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
clearTimeout(timer);
|
|
175
|
+
console.log("[moons] shutdown complete (full drain)");
|
|
176
|
+
process.exit(0);
|
|
177
|
+
})();
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Serve produksi dengan `Bun.serve` (Addendum Q). API yang sama dengan
|
|
183
|
+
* `startNodeServer`; hanya tersedia di runtime Bun.
|
|
184
|
+
*/
|
|
185
|
+
export function startBunServer(
|
|
186
|
+
handler: (request: Request) => Promise<Response>,
|
|
187
|
+
options: NodeServerOptions = {},
|
|
188
|
+
): BunServerHandle {
|
|
189
|
+
if (typeof Bun === "undefined") {
|
|
190
|
+
throw new Error("[moons] startBunServer requires the Bun runtime");
|
|
191
|
+
}
|
|
192
|
+
const clientDir = resolve(options.clientDir ?? join(process.cwd(), "dist/client"));
|
|
193
|
+
const port = options.port ?? Number(process.env["PORT"] ?? 3000);
|
|
194
|
+
const trustProxy = options.trustProxy === true;
|
|
195
|
+
const accessLogger: Logger | null =
|
|
196
|
+
options.logging?.accessLog === true
|
|
197
|
+
? createLogger({
|
|
198
|
+
json: options.logging.json !== false,
|
|
199
|
+
redact: options.logging.redact ?? [],
|
|
200
|
+
})
|
|
201
|
+
: null;
|
|
202
|
+
|
|
203
|
+
const serveStatic = async (
|
|
204
|
+
request: Request,
|
|
205
|
+
url: URL,
|
|
206
|
+
): Promise<Response | null> => {
|
|
207
|
+
if (options.storage !== undefined) {
|
|
208
|
+
const entry = await options.storage.read(url.pathname);
|
|
209
|
+
if (entry !== null) {
|
|
210
|
+
scheduleRevalidation({
|
|
211
|
+
pathname: url.pathname,
|
|
212
|
+
mtimeMs: entry.mtimeMs,
|
|
213
|
+
storage: options.storage,
|
|
214
|
+
handler,
|
|
215
|
+
options,
|
|
216
|
+
clientDir,
|
|
217
|
+
logger: accessLogger,
|
|
218
|
+
});
|
|
219
|
+
const headers = new Headers();
|
|
220
|
+
headers.set("content-type", contentTypeForPathname(url.pathname));
|
|
221
|
+
applyStaticHeaders(headers, url.pathname, options.headers);
|
|
222
|
+
return new Response(
|
|
223
|
+
request.method === "HEAD" ? null : (entry.body as unknown as BodyInit),
|
|
224
|
+
{ status: 200, headers },
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
// L.2 (temuan F1): storage MISS → fallback `dist/client`; storage tetap menang.
|
|
228
|
+
}
|
|
229
|
+
const staticFile = resolveStaticFile(clientDir, url.pathname);
|
|
230
|
+
if (staticFile === null) {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
const stat = statSync(staticFile);
|
|
234
|
+
scheduleRevalidation({
|
|
235
|
+
pathname: url.pathname,
|
|
236
|
+
mtimeMs: stat.mtimeMs,
|
|
237
|
+
file: staticFile,
|
|
238
|
+
...(options.storage === undefined ? {} : { storage: options.storage }),
|
|
239
|
+
handler,
|
|
240
|
+
options,
|
|
241
|
+
clientDir,
|
|
242
|
+
logger: accessLogger,
|
|
243
|
+
});
|
|
244
|
+
const headers = new Headers();
|
|
245
|
+
headers.set(
|
|
246
|
+
"content-type",
|
|
247
|
+
MIME_TYPES[extname(staticFile).toLowerCase()] ?? "application/octet-stream",
|
|
248
|
+
);
|
|
249
|
+
applyStaticHeaders(headers, url.pathname, options.headers);
|
|
250
|
+
|
|
251
|
+
// K.5: ETag + 304.
|
|
252
|
+
const etag = `W/"${stat.size}-${Math.floor(stat.mtimeMs)}"`;
|
|
253
|
+
headers.set("etag", etag);
|
|
254
|
+
const ifNoneMatch = request.headers.get("if-none-match");
|
|
255
|
+
if (ifNoneMatch !== null && ifNoneMatch.split(",").some((value) => value.trim() === etag)) {
|
|
256
|
+
return new Response(null, { status: 304, headers });
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// K.5: gzip untuk aset teks (cache per mtime).
|
|
260
|
+
let payload: Uint8Array | null =
|
|
261
|
+
request.method === "HEAD" ? null : readFileSync(staticFile);
|
|
262
|
+
if (
|
|
263
|
+
payload !== null &&
|
|
264
|
+
COMPRESSIBLE_FILE.test(staticFile) &&
|
|
265
|
+
payload.byteLength >= GZIP_MIN_BYTES &&
|
|
266
|
+
acceptsGzipHeader(request.headers.get("accept-encoding"))
|
|
267
|
+
) {
|
|
268
|
+
payload = gzipCached(staticFile, stat.mtimeMs, Buffer.from(payload));
|
|
269
|
+
headers.set("content-encoding", "gzip");
|
|
270
|
+
appendVaryHeader(headers, "accept-encoding");
|
|
271
|
+
}
|
|
272
|
+
return new Response(payload as unknown as BodyInit | null, { status: 200, headers });
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const server = Bun.serve({
|
|
276
|
+
port,
|
|
277
|
+
fetch: async (request, bunServer) => {
|
|
278
|
+
const requestStartedAt = performance.now();
|
|
279
|
+
let url: URL;
|
|
280
|
+
try {
|
|
281
|
+
url = resolveBunUrl(request, trustProxy);
|
|
282
|
+
} catch (error) {
|
|
283
|
+
if (error instanceof InvalidRequestError) {
|
|
284
|
+
console.warn(`[moons] ${error.message} — 400`);
|
|
285
|
+
return textResponse(400, "Bad Request");
|
|
286
|
+
}
|
|
287
|
+
throw error;
|
|
288
|
+
}
|
|
289
|
+
let clientIp: string | null = null;
|
|
290
|
+
const finish = (response: Response): Response => {
|
|
291
|
+
if (accessLogger !== null) {
|
|
292
|
+
accessLogger({
|
|
293
|
+
msg: "request",
|
|
294
|
+
method: request.method,
|
|
295
|
+
path: url.pathname + url.search,
|
|
296
|
+
status: response.status,
|
|
297
|
+
durationMs: Math.round(performance.now() - requestStartedAt),
|
|
298
|
+
...(clientIp === null ? {} : { ip: clientIp }),
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
return response;
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
try {
|
|
305
|
+
// M: kanonikalisasi trailing slash di transport (termasuk path prerender).
|
|
306
|
+
if (options.trailingSlash !== undefined) {
|
|
307
|
+
const canonical = trailingSlashRedirect(url.pathname, options.trailingSlash);
|
|
308
|
+
if (canonical !== null) {
|
|
309
|
+
url.pathname = canonical;
|
|
310
|
+
return new Response(null, { status: 308, headers: { location: url.toString() } });
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (request.method === "GET" || request.method === "HEAD") {
|
|
315
|
+
const staticResponse = await serveStatic(request, url);
|
|
316
|
+
if (staticResponse !== null) {
|
|
317
|
+
return finish(staticResponse);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// J.2: batas body RPC di transport — content-length diperiksa lebih dulu.
|
|
322
|
+
const isRpc = url.pathname.startsWith("/_moons/fn/");
|
|
323
|
+
const rpcLimit = options.bodyLimitBytes ?? DEFAULT_BODY_LIMIT_BYTES;
|
|
324
|
+
if (isRpc) {
|
|
325
|
+
const contentLength = Number(request.headers.get("content-length") ?? "0");
|
|
326
|
+
if (Number.isFinite(contentLength) && contentLength > rpcLimit) {
|
|
327
|
+
return finish(textResponse(413, rpcBodyTooLargeText(rpcLimit, contentLength)));
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const remoteAddress = bunServer.requestIP(request)?.address ?? null;
|
|
332
|
+
clientIp = resolveClientIpValues(
|
|
333
|
+
request.headers.get("x-forwarded-for") ?? undefined,
|
|
334
|
+
remoteAddress,
|
|
335
|
+
trustProxy,
|
|
336
|
+
);
|
|
337
|
+
// Fast path: GET/HEAD tanpa rewrite URL & tanpa IP tepercaya → pakai
|
|
338
|
+
// Request asli (menghindari rekonstruksi + salinan header per request).
|
|
339
|
+
const needsRewrite = clientIp !== null || url.toString() !== request.url;
|
|
340
|
+
const webRequest =
|
|
341
|
+
(request.method === "GET" || request.method === "HEAD") && !needsRewrite
|
|
342
|
+
? request
|
|
343
|
+
: await toBoundedRequest(
|
|
344
|
+
request,
|
|
345
|
+
url,
|
|
346
|
+
clientIp,
|
|
347
|
+
isRpc ? rpcLimit : MAX_TRANSPORT_BODY_BYTES,
|
|
348
|
+
);
|
|
349
|
+
const response = await handler(webRequest);
|
|
350
|
+
return finish(withDefaultSecurityHeaders(response));
|
|
351
|
+
} catch (error) {
|
|
352
|
+
if (error instanceof BodyLimitError) {
|
|
353
|
+
const isRpc = url.pathname.startsWith("/_moons/fn/");
|
|
354
|
+
return finish(
|
|
355
|
+
textResponse(413, isRpc ? rpcBodyTooLargeText(error.limit, null) : "Payload Too Large"),
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
console.error("[moons] server error:", error);
|
|
359
|
+
return finish(textResponse(500, "Internal Server Error"));
|
|
360
|
+
}
|
|
361
|
+
},
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// B.4: onStart sebelum log siap, onReady setelah server menerima koneksi.
|
|
365
|
+
const listenAfterStart = (): void => {
|
|
366
|
+
console.log(`[moons] server listening on http://localhost:${server.port}`);
|
|
367
|
+
void options.plugins?.ready();
|
|
368
|
+
};
|
|
369
|
+
if (options.plugins !== undefined) {
|
|
370
|
+
void options.plugins.start().then(listenAfterStart);
|
|
371
|
+
} else {
|
|
372
|
+
listenAfterStart();
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (options.handleSignals !== false) {
|
|
376
|
+
const shutdown = installBunShutdown(server as unknown as BunServerHandle, {
|
|
377
|
+
...(options.shutdownTimeoutMs === undefined ? {} : { timeoutMs: options.shutdownTimeoutMs }),
|
|
378
|
+
plugins: options.plugins,
|
|
379
|
+
});
|
|
380
|
+
process.once("SIGTERM", () => shutdown("SIGTERM"));
|
|
381
|
+
process.once("SIGINT", () => shutdown("SIGINT"));
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return server as unknown as BunServerHandle;
|
|
385
|
+
}
|
package/src/server/index.ts
CHANGED
|
@@ -3,6 +3,8 @@ export type { RequestHandlerOptions } from "./handler";
|
|
|
3
3
|
export { createDevShell, createFileShell, injectShell, DEFAULT_TEMPLATE, ROOT_PLACEHOLDER } from "./shell";
|
|
4
4
|
export type { Shell } from "./shell";
|
|
5
5
|
export { startNodeServer, resolveStaticFile } from "./node-server";
|
|
6
|
+
export { startBunServer } from "./bun-server";
|
|
7
|
+
export { startServer } from "./server-select";
|
|
6
8
|
export type { NodeServerOptions } from "./node-server";
|
|
7
9
|
export { installGracefulShutdown } from "./shutdown";
|
|
8
10
|
export type { ShutdownOptions } from "./shutdown";
|