@moonsjs/vite 0.1.10 → 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.
@@ -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";
@@ -44,6 +46,12 @@ export interface NodeServerOptions {
44
46
  * prerender yang disajikan langsung (static host memakai `_headers`).
45
47
  */
46
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;
47
55
  }
48
56
 
49
57
  /**
@@ -63,19 +71,50 @@ export function startNodeServer(
63
71
  const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
64
72
 
65
73
  if (req.method === "GET" || req.method === "HEAD") {
66
- const staticFile = resolveStaticFile(clientDir, url.pathname);
67
- if (staticFile !== null) {
68
- scheduleRevalidation(staticFile, url.pathname, handler, options, clientDir);
69
- const body = readFileSync(staticFile);
70
- res.statusCode = 200;
71
- res.setHeader("content-type", MIME_TYPES[extname(staticFile).toLowerCase()] ?? "application/octet-stream");
72
- for (const [name, value] of Object.entries(options.headers ?? {})) {
73
- if (!res.hasHeader(name)) {
74
- res.setHeader(name, value);
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
+ }
75
114
  }
115
+ res.end(req.method === "HEAD" ? undefined : body);
116
+ return;
76
117
  }
77
- res.end(req.method === "HEAD" ? undefined : body);
78
- return;
79
118
  }
80
119
  }
81
120
 
@@ -122,6 +161,15 @@ export function startNodeServer(
122
161
 
123
162
  /** Resolve file static dengan proteksi path traversal (+ manifest prerender D.5). */
124
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 {
125
173
  let decoded: string;
126
174
  try {
127
175
  decoded = decodeURIComponent(pathname);
@@ -141,10 +189,7 @@ export function resolveStaticFile(clientDir: string, pathname: string): string |
141
189
  const manifest = readPrerenderManifest(clientDir);
142
190
  if (manifest.paths.has(normalized)) {
143
191
  const target = normalized === "/" ? "index.html" : `${normalized.slice(1)}/index.html`;
144
- const candidate = safeWithin(clientDir, `./${target}`);
145
- if (candidate !== null && isFile(candidate)) {
146
- return candidate;
147
- }
192
+ return safeWithin(clientDir, `./${target}`);
148
193
  }
149
194
 
150
195
  return null;
@@ -221,32 +266,30 @@ function readPrerenderManifest(clientDir: string): PrerenderManifestInfo {
221
266
  }
222
267
 
223
268
  /** ISR: jika file prerender kedaluwarsa, regenerasi background (SWR). */
224
- function scheduleRevalidation(
225
- staticFile: string,
226
- pathname: string,
227
- handler: (request: Request) => Promise<Response>,
228
- options: NodeServerOptions,
229
- clientDir: string,
230
- ): void {
231
- const normalized = pathname.length > 1 ? pathname.replace(/\/+$/, "") : pathname;
232
- const seconds = readPrerenderManifest(clientDir).revalidate.get(normalized);
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);
233
280
  if (seconds === undefined || seconds < 0) {
234
281
  return;
235
282
  }
236
- let mtimeMs: number;
237
- try {
238
- mtimeMs = statSync(staticFile).mtimeMs;
239
- } catch {
240
- return;
241
- }
242
- if (!shouldRevalidate({ mtimeMs, revalidateSeconds: seconds })) {
283
+ if (!shouldRevalidate({ mtimeMs: input.mtimeMs, revalidateSeconds: seconds })) {
243
284
  return;
244
285
  }
245
286
  void revalidatePrerendered({
246
287
  pathname: normalized,
247
- file: staticFile,
248
- handler,
249
- ...(options.canary === undefined ? {} : { canary: options.canary }),
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,
250
293
  });
251
294
  }
252
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
+ };