@moonsjs/vite 0.1.9 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +2 -2
- package/dist/index.js.map +3 -3
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +127 -41
- package/dist/server/index.js.map +6 -5
- package/dist/server/isr.d.ts +8 -2
- package/dist/server/isr.d.ts.map +1 -1
- package/dist/server/node-server.d.ts +17 -0
- package/dist/server/node-server.d.ts.map +1 -1
- package/dist/server/storage.d.ts +10 -0
- package/dist/server/storage.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/plugin.ts +1 -1
- package/src/server/index.ts +1 -0
- package/src/server/isr.ts +16 -2
- package/src/server/node-server.ts +84 -31
- package/src/server/storage.ts +53 -0
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { createServer, type Server } from "node:http";
|
|
3
3
|
import { extname, join, resolve, sep } from "node:path";
|
|
4
|
+
import type { PrerenderStorage } from "@moonsjs/core";
|
|
4
5
|
import { revalidatePrerendered, shouldRevalidate } from "./isr";
|
|
6
|
+
import { contentTypeForPathname } from "./storage";
|
|
5
7
|
import { sendNodeResponse, toWebRequest } from "./node";
|
|
6
8
|
import type { PluginRuntime } from "./plugins";
|
|
7
9
|
import { installGracefulShutdown } from "./shutdown";
|
|
@@ -39,6 +41,17 @@ export interface NodeServerOptions {
|
|
|
39
41
|
plugins?: PluginRuntime;
|
|
40
42
|
/** Canary `leakTest.canary` untuk guard ISR (Addendum E.1). */
|
|
41
43
|
canary?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Header statis dari config (Addendum H). Diterapkan juga pada file
|
|
46
|
+
* prerender yang disajikan langsung (static host memakai `_headers`).
|
|
47
|
+
*/
|
|
48
|
+
headers?: Record<string, string>;
|
|
49
|
+
/**
|
|
50
|
+
* Storage bersama untuk file prerender (Addendum I, ISR multi-instance).
|
|
51
|
+
* Bila diisi, penyajian file prerender dan penulisan hasil regenerasi
|
|
52
|
+
* memakai storage ini; default: file sistem lokal.
|
|
53
|
+
*/
|
|
54
|
+
storage?: PrerenderStorage;
|
|
42
55
|
}
|
|
43
56
|
|
|
44
57
|
/**
|
|
@@ -58,14 +71,50 @@ export function startNodeServer(
|
|
|
58
71
|
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
59
72
|
|
|
60
73
|
if (req.method === "GET" || req.method === "HEAD") {
|
|
61
|
-
|
|
62
|
-
if (
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
74
|
+
// Addendum I: storage bersama (bila dikonfigurasi) menggantikan file lokal.
|
|
75
|
+
if (options.storage !== undefined) {
|
|
76
|
+
const entry = await options.storage.read(url.pathname);
|
|
77
|
+
if (entry !== null) {
|
|
78
|
+
scheduleRevalidation({
|
|
79
|
+
pathname: url.pathname,
|
|
80
|
+
mtimeMs: entry.mtimeMs,
|
|
81
|
+
storage: options.storage,
|
|
82
|
+
handler,
|
|
83
|
+
options,
|
|
84
|
+
clientDir,
|
|
85
|
+
});
|
|
86
|
+
res.statusCode = 200;
|
|
87
|
+
res.setHeader("content-type", contentTypeForPathname(url.pathname));
|
|
88
|
+
for (const [name, value] of Object.entries(options.headers ?? {})) {
|
|
89
|
+
if (!res.hasHeader(name)) {
|
|
90
|
+
res.setHeader(name, value);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
res.end(req.method === "HEAD" ? undefined : Buffer.from(entry.body));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
const staticFile = resolveStaticFile(clientDir, url.pathname);
|
|
98
|
+
if (staticFile !== null) {
|
|
99
|
+
scheduleRevalidation({
|
|
100
|
+
pathname: url.pathname,
|
|
101
|
+
mtimeMs: statSync(staticFile).mtimeMs,
|
|
102
|
+
file: staticFile,
|
|
103
|
+
handler,
|
|
104
|
+
options,
|
|
105
|
+
clientDir,
|
|
106
|
+
});
|
|
107
|
+
const body = readFileSync(staticFile);
|
|
108
|
+
res.statusCode = 200;
|
|
109
|
+
res.setHeader("content-type", MIME_TYPES[extname(staticFile).toLowerCase()] ?? "application/octet-stream");
|
|
110
|
+
for (const [name, value] of Object.entries(options.headers ?? {})) {
|
|
111
|
+
if (!res.hasHeader(name)) {
|
|
112
|
+
res.setHeader(name, value);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
res.end(req.method === "HEAD" ? undefined : body);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
69
118
|
}
|
|
70
119
|
}
|
|
71
120
|
|
|
@@ -112,6 +161,15 @@ export function startNodeServer(
|
|
|
112
161
|
|
|
113
162
|
/** Resolve file static dengan proteksi path traversal (+ manifest prerender D.5). */
|
|
114
163
|
export function resolveStaticFile(clientDir: string, pathname: string): string | null {
|
|
164
|
+
const target = staticFileTarget(clientDir, pathname);
|
|
165
|
+
return target !== null && isFile(target) ? target : null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Target file untuk `pathname` (tanpa cek keberadaan) — dipakai penyajian,
|
|
170
|
+
* `createFileStorage().write`, dan test. Proteksi path traversal tetap.
|
|
171
|
+
*/
|
|
172
|
+
export function staticFileTarget(clientDir: string, pathname: string): string | null {
|
|
115
173
|
let decoded: string;
|
|
116
174
|
try {
|
|
117
175
|
decoded = decodeURIComponent(pathname);
|
|
@@ -131,10 +189,7 @@ export function resolveStaticFile(clientDir: string, pathname: string): string |
|
|
|
131
189
|
const manifest = readPrerenderManifest(clientDir);
|
|
132
190
|
if (manifest.paths.has(normalized)) {
|
|
133
191
|
const target = normalized === "/" ? "index.html" : `${normalized.slice(1)}/index.html`;
|
|
134
|
-
|
|
135
|
-
if (candidate !== null && isFile(candidate)) {
|
|
136
|
-
return candidate;
|
|
137
|
-
}
|
|
192
|
+
return safeWithin(clientDir, `./${target}`);
|
|
138
193
|
}
|
|
139
194
|
|
|
140
195
|
return null;
|
|
@@ -211,32 +266,30 @@ function readPrerenderManifest(clientDir: string): PrerenderManifestInfo {
|
|
|
211
266
|
}
|
|
212
267
|
|
|
213
268
|
/** ISR: jika file prerender kedaluwarsa, regenerasi background (SWR). */
|
|
214
|
-
function scheduleRevalidation(
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
269
|
+
function scheduleRevalidation(input: {
|
|
270
|
+
pathname: string;
|
|
271
|
+
mtimeMs: number;
|
|
272
|
+
file?: string;
|
|
273
|
+
storage?: PrerenderStorage;
|
|
274
|
+
handler: (request: Request) => Promise<Response>;
|
|
275
|
+
options: NodeServerOptions;
|
|
276
|
+
clientDir: string;
|
|
277
|
+
}): void {
|
|
278
|
+
const normalized = input.pathname.length > 1 ? input.pathname.replace(/\/+$/, "") : input.pathname;
|
|
279
|
+
const seconds = readPrerenderManifest(input.clientDir).revalidate.get(normalized);
|
|
223
280
|
if (seconds === undefined || seconds < 0) {
|
|
224
281
|
return;
|
|
225
282
|
}
|
|
226
|
-
|
|
227
|
-
try {
|
|
228
|
-
mtimeMs = statSync(staticFile).mtimeMs;
|
|
229
|
-
} catch {
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
if (!shouldRevalidate({ mtimeMs, revalidateSeconds: seconds })) {
|
|
283
|
+
if (!shouldRevalidate({ mtimeMs: input.mtimeMs, revalidateSeconds: seconds })) {
|
|
233
284
|
return;
|
|
234
285
|
}
|
|
235
286
|
void revalidatePrerendered({
|
|
236
287
|
pathname: normalized,
|
|
237
|
-
file:
|
|
238
|
-
|
|
239
|
-
|
|
288
|
+
...(input.file === undefined ? {} : { file: input.file }),
|
|
289
|
+
...(input.storage === undefined ? {} : { storage: input.storage }),
|
|
290
|
+
handler: input.handler,
|
|
291
|
+
...(input.options.canary === undefined ? {} : { canary: input.options.canary }),
|
|
292
|
+
env: process.env,
|
|
240
293
|
});
|
|
241
294
|
}
|
|
242
295
|
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, extname } from "node:path";
|
|
3
|
+
import type { PrerenderStorage, PrerenderStorageEntry } from "@moonsjs/core";
|
|
4
|
+
import { resolveStaticFile, staticFileTarget } from "./node-server";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Addendum I: storage file sistem (default) untuk file prerender/ISR.
|
|
8
|
+
* Read hanya melayani path yang benar-benar ada (manifest-aware);
|
|
9
|
+
* write atomik (tmp + rename) dan membuat direktori bila perlu.
|
|
10
|
+
*/
|
|
11
|
+
export function createFileStorage(clientDir: string): PrerenderStorage {
|
|
12
|
+
return {
|
|
13
|
+
async read(pathname: string): Promise<PrerenderStorageEntry | null> {
|
|
14
|
+
const file = resolveStaticFile(clientDir, pathname);
|
|
15
|
+
if (file === null) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
const stat = statSync(file);
|
|
19
|
+
return { body: readFileSync(file), mtimeMs: stat.mtimeMs };
|
|
20
|
+
},
|
|
21
|
+
async write(pathname: string, body: Uint8Array): Promise<void> {
|
|
22
|
+
const target = staticFileTarget(clientDir, pathname);
|
|
23
|
+
if (target === null) {
|
|
24
|
+
throw new Error(`[moons] isr: path tidak valid untuk ditulis: ${pathname}`);
|
|
25
|
+
}
|
|
26
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
27
|
+
const tmp = `${target}.tmp-${process.pid}`;
|
|
28
|
+
writeFileSync(tmp, body);
|
|
29
|
+
renameSync(tmp, target);
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Ekstensi pathname (untuk `content-type`) tanpa membaca storage. */
|
|
35
|
+
export function contentTypeForPathname(pathname: string): string {
|
|
36
|
+
const ext = extname(pathname.split("?")[0] ?? "").toLowerCase();
|
|
37
|
+
return MIME[ext] ?? MIME[".html"] ?? "text/html; charset=utf-8";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const MIME: Record<string, string> = {
|
|
41
|
+
".html": "text/html; charset=utf-8",
|
|
42
|
+
".js": "text/javascript; charset=utf-8",
|
|
43
|
+
".mjs": "text/javascript; charset=utf-8",
|
|
44
|
+
".css": "text/css; charset=utf-8",
|
|
45
|
+
".json": "application/json; charset=utf-8",
|
|
46
|
+
".txt": "text/plain; charset=utf-8",
|
|
47
|
+
".svg": "image/svg+xml",
|
|
48
|
+
".ico": "image/x-icon",
|
|
49
|
+
".png": "image/png",
|
|
50
|
+
".webp": "image/webp",
|
|
51
|
+
".woff": "font/woff",
|
|
52
|
+
".woff2": "font/woff2",
|
|
53
|
+
};
|