@moonsjs/vite 0.1.4 → 0.1.6

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.
Files changed (52) hide show
  1. package/dist/api.d.ts +8 -0
  2. package/dist/api.d.ts.map +1 -0
  3. package/dist/{index-33p1r65s.js → index-1fnt137k.js} +50 -5
  4. package/dist/index-1fnt137k.js.map +14 -0
  5. package/dist/{index-3ec9vpek.js → index-zs92aaz4.js} +232 -24
  6. package/dist/index-zs92aaz4.js.map +11 -0
  7. package/dist/index.js +216 -19
  8. package/dist/index.js.map +5 -4
  9. package/dist/manifest.d.ts +6 -1
  10. package/dist/manifest.d.ts.map +1 -1
  11. package/dist/payload-scan.d.ts +18 -0
  12. package/dist/payload-scan.d.ts.map +1 -0
  13. package/dist/plugin.d.ts.map +1 -1
  14. package/dist/server/dev.d.ts +3 -0
  15. package/dist/server/dev.d.ts.map +1 -1
  16. package/dist/server/handler.d.ts +16 -1
  17. package/dist/server/handler.d.ts.map +1 -1
  18. package/dist/server/index.d.ts +4 -0
  19. package/dist/server/index.d.ts.map +1 -1
  20. package/dist/server/index.js +561 -17
  21. package/dist/server/index.js.map +9 -6
  22. package/dist/server/isr.d.ts +25 -0
  23. package/dist/server/isr.d.ts.map +1 -0
  24. package/dist/server/node-server.d.ts +7 -1
  25. package/dist/server/node-server.d.ts.map +1 -1
  26. package/dist/server/node.d.ts.map +1 -1
  27. package/dist/server/prerender.d.ts +28 -0
  28. package/dist/server/prerender.d.ts.map +1 -0
  29. package/dist/server-only.d.ts +57 -0
  30. package/dist/server-only.d.ts.map +1 -0
  31. package/dist/transform.d.ts +9 -1
  32. package/dist/transform.d.ts.map +1 -1
  33. package/dist/transform.js +1 -1
  34. package/dist/virtual-modules.d.ts +1 -0
  35. package/dist/virtual-modules.d.ts.map +1 -1
  36. package/package.json +1 -1
  37. package/src/api.ts +124 -0
  38. package/src/manifest.ts +42 -1
  39. package/src/payload-scan.ts +53 -0
  40. package/src/plugin.ts +137 -10
  41. package/src/server/dev.ts +14 -5
  42. package/src/server/handler.ts +317 -13
  43. package/src/server/index.ts +4 -0
  44. package/src/server/isr.ts +81 -0
  45. package/src/server/node-server.ts +120 -7
  46. package/src/server/node.ts +17 -0
  47. package/src/server/prerender.ts +256 -0
  48. package/src/server-only.ts +220 -0
  49. package/src/transform.ts +138 -8
  50. package/src/virtual-modules.ts +1 -0
  51. package/dist/index-33p1r65s.js.map +0 -14
  52. package/dist/index-3ec9vpek.js.map +0 -10
@@ -1,6 +1,5 @@
1
1
  import {
2
2
  NOT_FOUND_ROUTE_KEY,
3
- ERROR_ROUTE_KEY,
4
3
  ROOT_PLACEHOLDER2,
5
4
  DEFAULT_TEMPLATE2,
6
5
  injectShell2,
@@ -9,12 +8,15 @@ import {
9
8
  toWebRequest2,
10
9
  sendNodeResponse2,
11
10
  installDevMiddleware2
12
- } from "../index-33p1r65s.js";
11
+ } from "../index-1fnt137k.js";
13
12
 
14
13
  // src/server/handler.ts
15
14
  import {
16
15
  MoonsError,
17
16
  composeMiddleware,
17
+ findApiDirMiddlewares,
18
+ findDirMiddlewares,
19
+ findErrorBoundary,
18
20
  isNotFoundSignal,
19
21
  isRedirectSignal,
20
22
  resolveRoute,
@@ -22,8 +24,34 @@ import {
22
24
  runLoaders
23
25
  } from "@moonsjs/core";
24
26
  import { encodeSsrPayload, reviveError, serializeError } from "@moonsjs/core/serialize";
27
+ import { API_METHODS, resolveApiRoute } from "@moonsjs/core";
25
28
  import { renderToMarkup } from "@moonsjs/react/ssr";
26
- import { createRpcHandler, runWithContext, runWithRequestCache } from "@moonsjs/server";
29
+ import { createRpcHandler, runWithContext, runWithRequestCache, runWithResponseCollector, takeResponseMutations } from "@moonsjs/server";
30
+
31
+ // src/payload-scan.ts
32
+ var SECRET_KEY_PATTERN = /(SECRET|TOKEN|PASSWORD|PASSWD|PRIVATE|CREDENTIAL|AUTH|API_KEY|ACCESS_KEY|SIGNING)/i;
33
+ var MIN_SECRET_LENGTH = 12;
34
+ function scanPayloadForSecrets(payload, options = {}) {
35
+ const findings = [];
36
+ const canary = options.canary;
37
+ if (canary !== undefined && canary.length > 0 && payload.includes(canary)) {
38
+ findings.push({ kind: "canary", label: "canary" });
39
+ }
40
+ for (const [key, value] of Object.entries(options.env ?? {})) {
41
+ if (typeof value !== "string" || value.length < MIN_SECRET_LENGTH) {
42
+ continue;
43
+ }
44
+ if (!SECRET_KEY_PATTERN.test(key) || /PUBLIC/i.test(key)) {
45
+ continue;
46
+ }
47
+ if (payload.includes(value)) {
48
+ findings.push({ kind: "env", label: key });
49
+ }
50
+ }
51
+ return findings;
52
+ }
53
+
54
+ // src/server/handler.ts
27
55
  var RPC_PREFIX = "/_moons/fn/";
28
56
  var HEALTH_PATH = "/_moons/health";
29
57
  function createRequestHandler(options) {
@@ -31,6 +59,7 @@ function createRequestHandler(options) {
31
59
  const rpc = createRpcHandler(config.security === undefined ? {} : { security: config.security });
32
60
  const notFoundRoute = routes.find((route) => route.kind === "404");
33
61
  const errorRoute = routes.find((route) => route.kind === "error");
62
+ const warnedPayloadLabels = new Set;
34
63
  async function renderRoute(resolved, request, ctx) {
35
64
  const entries = [...resolved.layouts, resolved.route];
36
65
  const keys = entries.map((entry) => routeKeyFromFile(routesDir, entry.file));
@@ -49,8 +78,72 @@ function createRequestHandler(options) {
49
78
  data,
50
79
  ...meta === undefined ? {} : { meta }
51
80
  });
81
+ warnPayloadFindings(payload, options.payloadScan, warnedPayloadLabels);
52
82
  return htmlResponse(await shell(markup, payload, request.url, meta), 200);
53
83
  }
84
+ async function handleApiRequest(route, params, request, ctx) {
85
+ const load = options.loadApi?.[route.path];
86
+ if (load === undefined) {
87
+ return apiErrorResponse(new MoonsError({
88
+ code: "E_API_HANDLER_INVALID",
89
+ file: route.file,
90
+ message: `modul API \`${route.path}\` tidak bisa dimuat`,
91
+ details: "Peta `loadApi` tidak memuat route ini (build/dev tidak sinkron).",
92
+ suggestions: ["Jalankan ulang build / restart dev server"]
93
+ }), route);
94
+ }
95
+ let mod;
96
+ try {
97
+ mod = await load();
98
+ } catch (error) {
99
+ return apiErrorResponse(error, route);
100
+ }
101
+ if (mod === null || typeof mod !== "object") {
102
+ return apiErrorResponse(new MoonsError({
103
+ code: "E_API_HANDLER_INVALID",
104
+ file: route.file,
105
+ message: "modul API tidak mengekspor objek handler",
106
+ details: `Modul \`${route.path}\` harus mengekspor handler method (PRD Addendum D.1).`,
107
+ suggestions: ["Export `GET`/`POST`/… sebagai fungsi `(args) => Response`"]
108
+ }), route);
109
+ }
110
+ const record = mod;
111
+ const methods = API_METHODS.filter((method) => typeof record[method] === "function");
112
+ const requestMethod = request.method.toUpperCase();
113
+ let handler = record[requestMethod];
114
+ let stripBody = false;
115
+ if (typeof handler !== "function" && requestMethod === "HEAD" && typeof record["GET"] === "function") {
116
+ handler = record["GET"];
117
+ stripBody = true;
118
+ }
119
+ if (typeof handler !== "function") {
120
+ if (requestMethod === "OPTIONS") {
121
+ return new Response(null, { status: 204, headers: { allow: methods.join(", ") } });
122
+ }
123
+ return new Response("Method Not Allowed", {
124
+ status: 405,
125
+ headers: { allow: methods.join(", ") }
126
+ });
127
+ }
128
+ try {
129
+ const result = await handler({ req: request, params, ctx });
130
+ if (!(result instanceof Response)) {
131
+ return apiErrorResponse(new MoonsError({
132
+ code: "E_API_HANDLER_INVALID",
133
+ file: route.file,
134
+ message: `handler \`${requestMethod}\` tidak mengembalikan \`Response\``,
135
+ details: "Handler API wajib mengembalikan `new Response(...)` (PRD Addendum D.1).",
136
+ suggestions: ["Kembalikan `new Response(body, { status, headers })`"]
137
+ }), route);
138
+ }
139
+ if (stripBody) {
140
+ return new Response(null, { status: result.status, headers: result.headers });
141
+ }
142
+ return result;
143
+ } catch (error) {
144
+ return apiErrorResponse(error, route);
145
+ }
146
+ }
54
147
  async function notFoundResponse(request) {
55
148
  if (notFoundRoute !== undefined) {
56
149
  try {
@@ -67,16 +160,17 @@ function createRequestHandler(options) {
67
160
  const html = await shell("<h1>404</h1><p>Halaman tidak ditemukan.</p>", payload, request.url);
68
161
  return htmlResponse(html, 404);
69
162
  }
70
- async function routeErrorResponse(error, request) {
163
+ async function routeErrorResponse(error, request, resolved) {
71
164
  console.error("[moons] route error:", error);
72
- if (errorRoute !== undefined) {
165
+ const boundary = (resolved === undefined ? null : findErrorBoundary(resolved.route, routes)) ?? errorRoute ?? null;
166
+ if (boundary !== null) {
73
167
  try {
74
- const key = routeKeyFromFile(routesDir, errorRoute.file);
168
+ const key = routeKeyFromFile(routesDir, boundary.file);
75
169
  const [module] = await loadModules([key], loadRoute);
76
170
  const isProduction = process.env["NODE_ENV"] === "production";
77
171
  const serialized = isProduction ? undefined : serializeError(error);
78
172
  const payload = encodeSsrPayload({
79
- key: ERROR_ROUTE_KEY,
173
+ key,
80
174
  params: {},
81
175
  data: {},
82
176
  ...serialized === undefined ? {} : { error: serialized }
@@ -89,6 +183,62 @@ function createRequestHandler(options) {
89
183
  }
90
184
  return internalErrorResponse(error, { log: false });
91
185
  }
186
+ async function loadDirMiddlewares(chain) {
187
+ const result = [];
188
+ for (const definition of chain) {
189
+ const key = routeKeyFromFile(routesDir, definition.file);
190
+ const load = options.loadRoute?.[key];
191
+ if (load === undefined) {
192
+ throw new MoonsError({
193
+ code: "E_MIDDLEWARE_INVALID",
194
+ file: definition.file,
195
+ message: "middleware direktori tidak bisa dimuat",
196
+ details: "Peta `loadRoute` tidak memuat `_middleware.ts` ini.",
197
+ suggestions: ["Restart dev server / jalankan ulang build"]
198
+ });
199
+ }
200
+ const module = await load();
201
+ const handler = module.default;
202
+ if (typeof handler !== "function") {
203
+ throw new MoonsError({
204
+ code: "E_MIDDLEWARE_INVALID",
205
+ file: definition.file,
206
+ message: "`_middleware.ts` tidak meng-export default fungsi",
207
+ details: "Middleware direktori mengikuti bentuk `(args) => Promise<Response>` (Addendum D.3).",
208
+ suggestions: ["Export default `async ({ req, ctx, next }) => await next()`"]
209
+ });
210
+ }
211
+ result.push(handler);
212
+ }
213
+ return result;
214
+ }
215
+ async function loadApiDirMiddlewares(chain) {
216
+ const result = [];
217
+ for (const definition of chain) {
218
+ const load = options.loadApiMiddleware?.[definition.file];
219
+ if (load === undefined) {
220
+ throw new MoonsError({
221
+ code: "E_MIDDLEWARE_INVALID",
222
+ file: definition.file,
223
+ message: "middleware API tidak bisa dimuat",
224
+ details: "Peta `loadApiMiddleware` tidak memuat `_middleware.ts` ini.",
225
+ suggestions: ["Restart dev server / jalankan ulang build"]
226
+ });
227
+ }
228
+ const module = await load();
229
+ if (typeof module.default !== "function") {
230
+ throw new MoonsError({
231
+ code: "E_MIDDLEWARE_INVALID",
232
+ file: definition.file,
233
+ message: "`_middleware.ts` tidak meng-export default fungsi",
234
+ details: "Middleware direktori mengikuti bentuk `(args) => Promise<Response>` (Addendum D.3).",
235
+ suggestions: ["Export default `async ({ req, ctx, next }) => await next()`"]
236
+ });
237
+ }
238
+ result.push(module.default);
239
+ }
240
+ return result;
241
+ }
92
242
  return async function handler(request) {
93
243
  try {
94
244
  const url = new URL(request.url);
@@ -97,16 +247,35 @@ function createRequestHandler(options) {
97
247
  }
98
248
  const ctx = await config.context(request);
99
249
  options.onContext?.(ctx);
250
+ if (request.headers.get("x-moons-prerender") === "1") {
251
+ ctx["__prerender"] = true;
252
+ }
100
253
  const respond = composeMiddleware(config.middleware ?? [], ctx, async () => {
101
254
  if (url.pathname.startsWith(RPC_PREFIX)) {
102
255
  return await rpc(request, url.pathname.slice(RPC_PREFIX.length), ctx);
103
256
  }
257
+ if (options.apiRoutes !== undefined && options.apiRoutes.length > 0) {
258
+ const apiMatch = resolveApiRoute(url.pathname, options.apiRoutes);
259
+ if (apiMatch !== null) {
260
+ const apiChain = findApiDirMiddlewares(apiMatch.route.file, options.apiMiddlewares ?? []);
261
+ if (apiChain.length === 0) {
262
+ return await handleApiRequest(apiMatch.route, apiMatch.params, request, ctx);
263
+ }
264
+ const middlewares = await loadApiDirMiddlewares(apiChain);
265
+ return await composeMiddleware(middlewares, ctx, () => handleApiRequest(apiMatch.route, apiMatch.params, request, ctx), { code: "E_MIDDLEWARE_INVALID", label: "middleware API" })(request);
266
+ }
267
+ }
104
268
  const resolved = resolveRoute(url.pathname, routes);
105
269
  if (resolved === null) {
106
270
  return await notFoundResponse(request);
107
271
  }
108
272
  try {
109
- return await renderRoute(resolved, request, ctx);
273
+ const dirChain = findDirMiddlewares(resolved.route, routes);
274
+ if (dirChain.length === 0) {
275
+ return await renderRoute(resolved, request, ctx);
276
+ }
277
+ const middlewares = await loadDirMiddlewares(dirChain);
278
+ return await composeMiddleware(middlewares, ctx, () => renderRoute(resolved, request, ctx), { code: "E_MIDDLEWARE_INVALID", label: "middleware direktori" })(request);
110
279
  } catch (error) {
111
280
  if (isRedirectSignal(error)) {
112
281
  return new Response(null, {
@@ -117,15 +286,46 @@ function createRequestHandler(options) {
117
286
  if (isNotFoundSignal(error)) {
118
287
  return await notFoundResponse(request);
119
288
  }
120
- return await routeErrorResponse(error, request);
289
+ return await routeErrorResponse(error, request, resolved);
290
+ }
291
+ });
292
+ return await runWithResponseCollector(async () => {
293
+ let response;
294
+ try {
295
+ response = await runWithRequestCache(() => runWithContext(ctx, () => respond(request)));
296
+ } catch (error) {
297
+ response = internalErrorResponse(error);
121
298
  }
299
+ return applyResponseMutations(response);
122
300
  });
123
- return await runWithRequestCache(() => runWithContext(ctx, () => respond(request)));
124
301
  } catch (error) {
125
302
  return internalErrorResponse(error);
126
303
  }
127
304
  };
128
305
  }
306
+ function applyResponseMutations(response) {
307
+ const mutations = takeResponseMutations();
308
+ if (mutations.length === 0) {
309
+ return response;
310
+ }
311
+ const headers = new Headers(response.headers);
312
+ for (const mutation of mutations) {
313
+ if (mutation.kind === "header") {
314
+ if (mutation.append) {
315
+ headers.append(mutation.name, mutation.value);
316
+ } else {
317
+ headers.set(mutation.name, mutation.value);
318
+ }
319
+ } else {
320
+ headers.append("set-cookie", mutation.value);
321
+ }
322
+ }
323
+ return new Response(response.body, {
324
+ status: response.status,
325
+ statusText: response.statusText,
326
+ headers
327
+ });
328
+ }
129
329
  async function loadModules(keys, loadRoute) {
130
330
  return await Promise.all(keys.map(async (key) => {
131
331
  const load = loadRoute[key];
@@ -169,6 +369,21 @@ async function runRouteMeta(pageModule, pageData, file) {
169
369
  }
170
370
  return result;
171
371
  }
372
+ function apiErrorResponse(error, route) {
373
+ const isProduction = process.env["NODE_ENV"] === "production";
374
+ if (error instanceof MoonsError) {
375
+ console.error(`[moons] API ${route.path}: ${error.format()}`);
376
+ return new Response(isProduction ? "Terjadi kesalahan internal." : error.format(), {
377
+ status: 500,
378
+ headers: { "content-type": "text/plain; charset=utf-8" }
379
+ });
380
+ }
381
+ console.error(`[moons] API ${route.path}:`, error);
382
+ return new Response(isProduction ? "Terjadi kesalahan internal." : String(error), {
383
+ status: 500,
384
+ headers: { "content-type": "text/plain; charset=utf-8" }
385
+ });
386
+ }
172
387
  function internalErrorResponse(error, options = {}) {
173
388
  if (options.log !== false) {
174
389
  console.error("[moons] request error:", error);
@@ -198,11 +413,71 @@ function healthResponse(method) {
198
413
  function escapeHtml(text) {
199
414
  return text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
200
415
  }
416
+ function warnPayloadFindings(payload, scan, warned) {
417
+ if (scan === undefined) {
418
+ return;
419
+ }
420
+ for (const finding of scanPayloadForSecrets(payload, scan)) {
421
+ const label = finding.kind === "canary" ? "canary" : `env(${finding.label})`;
422
+ if (warned.has(label)) {
423
+ continue;
424
+ }
425
+ warned.add(label);
426
+ console.warn(`[moons] peringatan dev: payload SSR memuat ${label} — jangan mengembalikan rahasia dari loader (PRD §2/§16.6).`);
427
+ }
428
+ }
201
429
  // src/server/node-server.ts
202
430
  import { existsSync, readFileSync, statSync } from "node:fs";
203
431
  import { createServer } from "node:http";
204
432
  import { extname, join, resolve, sep } from "node:path";
205
433
 
434
+ // src/server/isr.ts
435
+ import { renameSync, writeFileSync } from "node:fs";
436
+ function shouldRevalidate(input) {
437
+ const now = input.now ?? Date.now();
438
+ return now - input.mtimeMs >= input.revalidateSeconds * 1000;
439
+ }
440
+ var inflight = new Map;
441
+ function revalidatePrerendered(options) {
442
+ const existing = inflight.get(options.pathname);
443
+ if (existing !== undefined) {
444
+ return existing;
445
+ }
446
+ const task = runRevalidate(options).finally(() => {
447
+ inflight.delete(options.pathname);
448
+ });
449
+ inflight.set(options.pathname, task);
450
+ return task;
451
+ }
452
+ async function runRevalidate(options) {
453
+ const log = options.log ?? ((message) => console.log(message));
454
+ try {
455
+ const response = await options.handler(new Request(`http://localhost${options.pathname}`, {
456
+ method: "GET",
457
+ headers: { "x-moons-prerender": "1" }
458
+ }));
459
+ if (response.status !== 200) {
460
+ log(`[moons] isr: lewati ${options.pathname} (status ${response.status})`);
461
+ return;
462
+ }
463
+ const html = await response.text();
464
+ const findings = scanPayloadForSecrets(html, {
465
+ ...options.canary === undefined ? {} : { canary: options.canary },
466
+ env: options.env ?? process.env
467
+ });
468
+ if (findings.length > 0) {
469
+ const labels = findings.map((finding) => finding.kind === "canary" ? "canary" : `env(${finding.label})`).join(", ");
470
+ log(`[moons] isr: TIDAK menulis ${options.pathname} — memuat ${labels}`);
471
+ return;
472
+ }
473
+ const tmp = `${options.file}.tmp-${process.pid}`;
474
+ writeFileSync(tmp, html);
475
+ renameSync(tmp, options.file);
476
+ } catch (error) {
477
+ log(`[moons] isr: gagal regenerasi ${options.pathname}: ${String(error)}`);
478
+ }
479
+ }
480
+
206
481
  // src/server/shutdown.ts
207
482
  function installGracefulShutdown(server, options = {}) {
208
483
  const timeoutMs = options.timeoutMs ?? 1e4;
@@ -281,6 +556,7 @@ function startNodeServer(handler, options = {}) {
281
556
  if (req.method === "GET" || req.method === "HEAD") {
282
557
  const staticFile = resolveStaticFile(clientDir, url.pathname);
283
558
  if (staticFile !== null) {
559
+ scheduleRevalidation(staticFile, url.pathname, handler, options, clientDir);
284
560
  const body = readFileSync(staticFile);
285
561
  res.statusCode = 200;
286
562
  res.setHeader("content-type", MIME_TYPES[extname(staticFile).toLowerCase()] ?? "application/octet-stream");
@@ -328,18 +604,92 @@ function resolveStaticFile(clientDir, pathname) {
328
604
  } catch {
329
605
  return null;
330
606
  }
331
- const candidate = resolve(clientDir, `.${decoded}`);
332
- if (candidate !== clientDir && !candidate.startsWith(clientDir + sep)) {
333
- return null;
607
+ const exact = safeWithin(clientDir, `.${decoded}`);
608
+ if (exact !== null && isFile(exact)) {
609
+ return exact;
334
610
  }
335
- if (!existsSync(candidate)) {
336
- return null;
611
+ const normalized = decoded.length > 1 ? decoded.replace(/\/+$/, "") : decoded;
612
+ const manifest = readPrerenderManifest(clientDir);
613
+ if (manifest.paths.has(normalized)) {
614
+ const target = normalized === "/" ? "index.html" : `${normalized.slice(1)}/index.html`;
615
+ const candidate = safeWithin(clientDir, `./${target}`);
616
+ if (candidate !== null && isFile(candidate)) {
617
+ return candidate;
618
+ }
337
619
  }
338
- if (!statSync(candidate).isFile()) {
620
+ return null;
621
+ }
622
+ function safeWithin(clientDir, relative) {
623
+ const candidate = resolve(clientDir, relative);
624
+ if (candidate !== clientDir && !candidate.startsWith(clientDir + sep)) {
339
625
  return null;
340
626
  }
341
627
  return candidate;
342
628
  }
629
+ function isFile(path) {
630
+ return existsSync(path) && statSync(path).isFile();
631
+ }
632
+ var prerenderManifestCache = null;
633
+ function readPrerenderManifest(clientDir) {
634
+ const file = join(clientDir, "_moons_prerender.json");
635
+ const empty = { paths: new Set, revalidate: new Map };
636
+ try {
637
+ const mtimeMs = statSync(file).mtimeMs;
638
+ if (prerenderManifestCache !== null && prerenderManifestCache.file === file && prerenderManifestCache.mtimeMs === mtimeMs) {
639
+ return prerenderManifestCache.info;
640
+ }
641
+ const json = JSON.parse(readFileSync(file, "utf8"));
642
+ const paths = new Set;
643
+ const revalidate = new Map;
644
+ if (Array.isArray(json.paths)) {
645
+ for (const path of json.paths) {
646
+ if (typeof path === "string") {
647
+ paths.add(path);
648
+ }
649
+ }
650
+ }
651
+ if (Array.isArray(json.entries)) {
652
+ for (const entry of json.entries) {
653
+ if (entry === null || typeof entry !== "object" || typeof entry["path"] !== "string") {
654
+ continue;
655
+ }
656
+ const path = entry["path"];
657
+ paths.add(path);
658
+ const seconds = entry["revalidate"];
659
+ if (typeof seconds === "number" && Number.isFinite(seconds) && seconds >= 0) {
660
+ revalidate.set(path, seconds);
661
+ }
662
+ }
663
+ }
664
+ const info = { paths, revalidate };
665
+ prerenderManifestCache = { file, mtimeMs, info };
666
+ return info;
667
+ } catch {
668
+ return empty;
669
+ }
670
+ }
671
+ function scheduleRevalidation(staticFile, pathname, handler, options, clientDir) {
672
+ const normalized = pathname.length > 1 ? pathname.replace(/\/+$/, "") : pathname;
673
+ const seconds = readPrerenderManifest(clientDir).revalidate.get(normalized);
674
+ if (seconds === undefined || seconds < 0) {
675
+ return;
676
+ }
677
+ let mtimeMs;
678
+ try {
679
+ mtimeMs = statSync(staticFile).mtimeMs;
680
+ } catch {
681
+ return;
682
+ }
683
+ if (!shouldRevalidate({ mtimeMs, revalidateSeconds: seconds })) {
684
+ return;
685
+ }
686
+ revalidatePrerendered({
687
+ pathname: normalized,
688
+ file: staticFile,
689
+ handler,
690
+ ...options.canary === undefined ? {} : { canary: options.canary }
691
+ });
692
+ }
343
693
  // src/server/test-server.ts
344
694
  function createTestServer(options, baseUrl = "http://localhost") {
345
695
  const shell = options.shell ?? ((markup, payload, _url, meta) => injectShell2(DEFAULT_TEMPLATE2, markup, payload, meta));
@@ -411,6 +761,199 @@ function createPluginRuntime(options) {
411
761
  }
412
762
  };
413
763
  }
764
+ // src/server/prerender.ts
765
+ import { MoonsError as MoonsError2 } from "@moonsjs/core";
766
+ import { mkdirSync, writeFileSync as writeFileSync2 } from "node:fs";
767
+ import { dirname, join as join2, resolve as resolve2, sep as sep2 } from "node:path";
768
+ var PRERENDER_MANIFEST_FILE = "_moons_prerender.json";
769
+ async function runPrerender(options) {
770
+ const log = options.log ?? ((message) => console.log(message));
771
+ const env = options.env ?? process.env;
772
+ let written = 0;
773
+ let skipped = 0;
774
+ const entries = [];
775
+ const pages = options.routes.filter((route) => route.kind === "page");
776
+ for (const route of pages) {
777
+ const key = routeKeyOf(options.routesDir, route.file);
778
+ const load = options.loadRoute[key];
779
+ if (load === undefined) {
780
+ continue;
781
+ }
782
+ const module = await load();
783
+ const prerender = module.prerender;
784
+ const revalidate = module.revalidate;
785
+ if (revalidate !== undefined) {
786
+ if (typeof revalidate !== "number" || !Number.isFinite(revalidate) || revalidate < 0 || prerender !== true) {
787
+ throw new MoonsError2({
788
+ code: "E_ISR_INVALID",
789
+ file: route.file,
790
+ message: "`revalidate` tidak valid atau dipakai tanpa `prerender = true`",
791
+ details: "ISR berbasis file: `revalidate` (detik, number >= 0) + `prerender` (PRD Addendum E.1).",
792
+ suggestions: [
793
+ "Gunakan angka detik, mis. `export const revalidate = 60`",
794
+ "Tambahkan `export const prerender = true` pada page yang sama"
795
+ ]
796
+ });
797
+ }
798
+ }
799
+ if (prerender !== true) {
800
+ if (prerender !== undefined && prerender !== false) {
801
+ throw prerenderError(route, "`prerender` harus boolean (`true`/`false`)");
802
+ }
803
+ continue;
804
+ }
805
+ const pathnames = await resolvePathnames(route, module);
806
+ for (const pathname of pathnames) {
807
+ const response = await options.handler(new Request(`http://localhost${pathname}`, {
808
+ method: "GET",
809
+ headers: { "x-moons-prerender": "1" }
810
+ }));
811
+ if (response.status === 404) {
812
+ log(`[moons] prerender: lewati ${pathname} (notFound)`);
813
+ skipped += 1;
814
+ continue;
815
+ }
816
+ if (response.status >= 300 && response.status < 400) {
817
+ throw prerenderError(route, `\`redirect()\` tidak didukung saat prerender (${pathname})`);
818
+ }
819
+ if (response.status !== 200) {
820
+ throw prerenderError(route, `status ${response.status} saat merender ${pathname}`);
821
+ }
822
+ const html = await response.text();
823
+ const findings = scanPayloadForSecrets(html, {
824
+ ...options.canary === undefined ? {} : { canary: options.canary },
825
+ env
826
+ });
827
+ if (findings.length > 0) {
828
+ const labels = findings.map((finding) => finding.kind === "canary" ? "canary" : `env(${finding.label})`).join(", ");
829
+ throw new MoonsError2({
830
+ code: "E_PRERENDER_LEAK",
831
+ file: route.file,
832
+ message: `hasil prerender \`${pathname}\` memuat ${labels}`,
833
+ details: "HTML statis permanen — rahasia yang ter-prerender tidak bisa ditarik kembali (PRD Addendum D.5).",
834
+ suggestions: [
835
+ "Jangan mengembalikan nilai env rahasia dari loader yang di-prerender",
836
+ "Matikan `prerender` untuk route ini atau pindahkan datanya ke serverFn request-time"
837
+ ]
838
+ });
839
+ }
840
+ const file = outputFile(options.clientDir, pathname);
841
+ mkdirSync(dirname(file), { recursive: true });
842
+ writeFileSync2(file, html);
843
+ entries.push(typeof revalidate === "number" && revalidate >= 0 ? { path: pathname, revalidate } : { path: pathname });
844
+ written += 1;
845
+ }
846
+ }
847
+ writeFileSync2(join2(options.clientDir, PRERENDER_MANIFEST_FILE), `${JSON.stringify({ version: 2, entries }, null, 2)}
848
+ `);
849
+ log(`[moons] prerender selesai: ${written} file (${skipped} dilewati)`);
850
+ return { written, skipped };
851
+ }
852
+ function routeKeyOf(routesDir, file) {
853
+ const relative = file.startsWith(routesDir) ? file.slice(routesDir.length + 1) : file;
854
+ return relative.split("\\").join("/").replace(/\.(tsx|ts)$/, "");
855
+ }
856
+ async function resolvePathnames(route, module) {
857
+ const isDynamic = route.params.length > 0;
858
+ if (!isDynamic) {
859
+ return [route.path];
860
+ }
861
+ const paths = module.paths;
862
+ if (typeof paths !== "function") {
863
+ throw new MoonsError2({
864
+ code: "E_PRERENDER_NO_PATHS",
865
+ file: route.file,
866
+ message: "route dynamic ber-`prerender = true` tanpa `export const paths`",
867
+ details: "Prerender butuh daftar param untuk merender tiap path (PRD Addendum D.5).",
868
+ suggestions: [
869
+ 'Tambahkan `export const paths = () => [{ id: "…" }]`',
870
+ "Atau hapus `prerender` agar dirender SSR saat request"
871
+ ]
872
+ });
873
+ }
874
+ const raw = await paths();
875
+ if (!Array.isArray(raw)) {
876
+ throw prerenderError(route, "`paths()` harus mengembalikan array");
877
+ }
878
+ const expected = new Set(route.params);
879
+ const result = [];
880
+ for (const item of raw) {
881
+ if (item === null || typeof item !== "object" || Array.isArray(item)) {
882
+ throw prerenderError(route, "item `paths()` harus objek param string");
883
+ }
884
+ const record = item;
885
+ const keys = Object.keys(record);
886
+ for (const name of expected) {
887
+ const value = record[name];
888
+ if (typeof value !== "string" || value.length === 0) {
889
+ throw prerenderError(route, `param \`${name}\` wajib string non-kosong`);
890
+ }
891
+ }
892
+ for (const key of keys) {
893
+ if (!expected.has(key)) {
894
+ throw prerenderError(route, `param \`${key}\` tidak ada di pola route`);
895
+ }
896
+ }
897
+ result.push(buildPathname(route, record));
898
+ }
899
+ return result;
900
+ }
901
+ function buildPathname(route, params) {
902
+ const segments = route.path.split("/").filter((segment) => segment.length > 0);
903
+ const out = [];
904
+ let catchallName = null;
905
+ for (const segment of segments) {
906
+ if (segment === "*") {
907
+ catchallName = route.params[route.params.length - 1] ?? null;
908
+ const value = catchallName === null ? "" : params[catchallName] ?? "";
909
+ for (const part of value.split("/")) {
910
+ if (part.length === 0) {
911
+ continue;
912
+ }
913
+ out.push(encodeSegment(route, part));
914
+ }
915
+ continue;
916
+ }
917
+ if (segment.startsWith(":")) {
918
+ out.push(encodeSegment(route, params[segment.slice(1)] ?? ""));
919
+ continue;
920
+ }
921
+ out.push(segment);
922
+ }
923
+ return `/${out.join("/")}`;
924
+ }
925
+ function encodeSegment(route, value) {
926
+ if (value.length === 0 || /^\.+$/.test(value)) {
927
+ throw prerenderError(route, `nilai param \`${value}\` tidak valid untuk path file`);
928
+ }
929
+ return encodeURIComponent(value);
930
+ }
931
+ function outputFile(clientDir, pathname) {
932
+ const parts = pathname.split("/").filter((part) => part.length > 0);
933
+ const file = resolve2(join2(clientDir, ...parts, "index.html"));
934
+ const base = resolve2(clientDir);
935
+ if (file !== base && !file.startsWith(base + sep2)) {
936
+ throw new MoonsError2({
937
+ code: "E_PRERENDER_INVALID",
938
+ message: `path prerender keluar dari direktori client (\`${pathname}\`)`,
939
+ details: "Nama file harus berada di dalam `dist/client`.",
940
+ suggestions: ["Periksa nilai param `paths` — jangan memakai `..`"]
941
+ });
942
+ }
943
+ return file;
944
+ }
945
+ function prerenderError(route, message) {
946
+ return new MoonsError2({
947
+ code: "E_PRERENDER_INVALID",
948
+ file: route.file,
949
+ message,
950
+ details: "Kontrak prerender mengikuti PRD Addendum D.5.",
951
+ suggestions: [
952
+ "Perbaiki bentuk `paths`/param agar cocok dengan pola route",
953
+ "Jangan memakai `redirect()` pada route yang di-prerender"
954
+ ]
955
+ });
956
+ }
414
957
  export {
415
958
  DEFAULT_TEMPLATE2 as DEFAULT_TEMPLATE,
416
959
  ROOT_PLACEHOLDER2 as ROOT_PLACEHOLDER,
@@ -423,9 +966,10 @@ export {
423
966
  installDevMiddleware2 as installDevMiddleware,
424
967
  installGracefulShutdown,
425
968
  resolveStaticFile,
969
+ runPrerender,
426
970
  sendNodeResponse2 as sendNodeResponse,
427
971
  startNodeServer,
428
972
  toWebRequest2 as toWebRequest
429
973
  };
430
974
 
431
- //# debugId=871C5ACF2A35737064756E2164756E21
975
+ //# debugId=122CDE526B35801364756E2164756E21