@moonsjs/vite 0.1.17 → 0.2.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.
Files changed (43) hide show
  1. package/dist/{index-v5sbd3az.js → index-0agjty3z.js} +21 -18
  2. package/dist/index-0agjty3z.js.map +15 -0
  3. package/dist/index-f8w073cm.js +30 -0
  4. package/dist/index-f8w073cm.js.map +10 -0
  5. package/dist/{index-snbxks72.js → index-tbph0859.js} +80 -80
  6. package/dist/{index-snbxks72.js.map → index-tbph0859.js.map} +4 -4
  7. package/dist/index.js +33 -33
  8. package/dist/index.js.map +4 -4
  9. package/dist/server/handler.d.ts.map +1 -1
  10. package/dist/server/index.js +104 -93
  11. package/dist/server/index.js.map +10 -10
  12. package/dist/server/test-server.d.ts +6 -0
  13. package/dist/server/test-server.d.ts.map +1 -1
  14. package/dist/testing-preload.d.ts +2 -0
  15. package/dist/testing-preload.d.ts.map +1 -0
  16. package/dist/testing-preload.js +10 -0
  17. package/dist/testing-preload.js.map +10 -0
  18. package/dist/testing.d.ts +11 -0
  19. package/dist/testing.d.ts.map +1 -0
  20. package/dist/testing.js +13 -0
  21. package/dist/testing.js.map +9 -0
  22. package/dist/transform.js +1 -1
  23. package/package.json +15 -5
  24. package/src/api.ts +14 -14
  25. package/src/devtools.ts +5 -5
  26. package/src/manifest.ts +2 -2
  27. package/src/plugin.ts +21 -21
  28. package/src/server/dev.ts +6 -1
  29. package/src/server/handler.ts +69 -47
  30. package/src/server/isr.ts +5 -5
  31. package/src/server/node-server.ts +6 -6
  32. package/src/server/node.ts +2 -2
  33. package/src/server/plugins.ts +1 -1
  34. package/src/server/prerender.ts +29 -29
  35. package/src/server/shell.ts +8 -8
  36. package/src/server/shutdown.ts +5 -5
  37. package/src/server/storage.ts +1 -1
  38. package/src/server/test-server.ts +12 -1
  39. package/src/server-only.ts +7 -7
  40. package/src/testing-preload.ts +5 -0
  41. package/src/testing.ts +34 -0
  42. package/src/transform.ts +76 -76
  43. package/dist/index-v5sbd3az.js.map +0 -15
@@ -104,6 +104,8 @@ export function createRequestHandler(
104
104
  resolved.params,
105
105
  data,
106
106
  searchParams,
107
+ undefined,
108
+ config.trailingSlash,
107
109
  );
108
110
  const payload = encodeSsrPayload({
109
111
  key: pageKey,
@@ -116,7 +118,10 @@ export function createRequestHandler(
116
118
  // A1: pindai payload SSR (dev saja — `payloadScan` tidak diisi production).
117
119
  warnPayloadFindings(payload, options.payloadScan, warnedPayloadLabels);
118
120
 
119
- return htmlResponse(await shell(markup, payload, request.url, meta), 200);
121
+ return htmlResponse(
122
+ withTrailingSlashMeta(await shell(markup, payload, request.url, meta), config.trailingSlash),
123
+ 200,
124
+ );
120
125
  }
121
126
 
122
127
  /** Addendum D.1: dispatch endpoint `src/api` (405/HEAD/OPTIONS + error). */
@@ -132,9 +137,9 @@ export function createRequestHandler(
132
137
  new MoonsError({
133
138
  code: "E_API_HANDLER_INVALID",
134
139
  file: route.file,
135
- message: `modul API \`${route.path}\` tidak bisa dimuat`,
136
- details: "Peta `loadApi` tidak memuat route ini (build/dev tidak sinkron).",
137
- suggestions: ["Jalankan ulang build / restart dev server"],
140
+ message: `API module \`${route.path}\` could not be loaded`,
141
+ details: "The `loadApi` map does not contain this route (build/dev out of sync).",
142
+ suggestions: ["Rebuild / restart the dev server"],
138
143
  }),
139
144
  route,
140
145
  );
@@ -151,9 +156,9 @@ export function createRequestHandler(
151
156
  new MoonsError({
152
157
  code: "E_API_HANDLER_INVALID",
153
158
  file: route.file,
154
- message: "modul API tidak mengekspor objek handler",
155
- details: `Modul \`${route.path}\` harus mengekspor handler method.`,
156
- suggestions: ["Export `GET`/`POST`/… sebagai fungsi `(args) => Response`"],
159
+ message: "API module does not export a handler object",
160
+ details: `Module \`${route.path}\` must export method handlers.`,
161
+ suggestions: ["Export `GET`/`POST`/… as `(args) => Response` functions"],
157
162
  }),
158
163
  route,
159
164
  );
@@ -194,9 +199,9 @@ export function createRequestHandler(
194
199
  new MoonsError({
195
200
  code: "E_API_HANDLER_INVALID",
196
201
  file: route.file,
197
- message: `handler \`${requestMethod}\` tidak mengembalikan \`Response\``,
198
- details: "Handler API wajib mengembalikan `new Response(...)`.",
199
- suggestions: ["Kembalikan `new Response(body, { status, headers })`"],
202
+ message: `handler \`${requestMethod}\` did not return a \`Response\``,
203
+ details: "API handlers must return `new Response(...)`.",
204
+ suggestions: ["Return `new Response(body, { status, headers })`"],
200
205
  }),
201
206
  route,
202
207
  );
@@ -222,20 +227,22 @@ export function createRequestHandler(
222
227
  { page: module as RouteModuleShape, layouts: [] },
223
228
  {},
224
229
  {},
230
+ {},
231
+ undefined,
232
+ config.trailingSlash,
233
+ );
234
+ return htmlResponse(
235
+ withTrailingSlashMeta(await shell(markup, payload, request.url), config.trailingSlash),
236
+ 404,
225
237
  );
226
- return htmlResponse(await shell(markup, payload, request.url), 404);
227
238
  } catch (error) {
228
- console.error("[moons] gagal merender _404 kustom:", error);
239
+ console.error("[moons] failed to render custom _404:", error);
229
240
  }
230
241
  }
231
242
 
232
243
  const payload = encodeSsrPayload({ key: "", params: {}, data: {} });
233
- const html = await shell(
234
- "<h1>404</h1><p>Halaman tidak ditemukan.</p>",
235
- payload,
236
- request.url,
237
- );
238
- return htmlResponse(html, 404);
244
+ const html = await shell("<h1>404</h1><p>Page not found.</p>", payload, request.url);
245
+ return htmlResponse(withTrailingSlashMeta(html, config.trailingSlash), 404);
239
246
  }
240
247
 
241
248
  /**
@@ -272,10 +279,14 @@ export function createRequestHandler(
272
279
  {},
273
280
  {},
274
281
  serialized === undefined ? undefined : reviveError(serialized),
282
+ config.trailingSlash,
283
+ );
284
+ return htmlResponse(
285
+ withTrailingSlashMeta(await shell(markup, payload, request.url), config.trailingSlash),
286
+ 500,
275
287
  );
276
- return htmlResponse(await shell(markup, payload, request.url), 500);
277
288
  } catch (renderError) {
278
- console.error("[moons] gagal merender _error kustom:", renderError);
289
+ console.error("[moons] failed to render custom _error:", renderError);
279
290
  }
280
291
  }
281
292
 
@@ -292,9 +303,9 @@ export function createRequestHandler(
292
303
  throw new MoonsError({
293
304
  code: "E_MIDDLEWARE_INVALID",
294
305
  file: definition.file,
295
- message: "middleware direktori tidak bisa dimuat",
296
- details: "Peta `loadRoute` tidak memuat `_middleware.ts` ini.",
297
- suggestions: ["Restart dev server / jalankan ulang build"],
306
+ message: "directory middleware could not be loaded",
307
+ details: "The `loadRoute` map does not contain this `_middleware.ts`.",
308
+ suggestions: ["Restart the dev server / rebuild"],
298
309
  });
299
310
  }
300
311
  const module = (await load()) as { default?: unknown };
@@ -303,8 +314,8 @@ export function createRequestHandler(
303
314
  throw new MoonsError({
304
315
  code: "E_MIDDLEWARE_INVALID",
305
316
  file: definition.file,
306
- message: "`_middleware.ts` tidak meng-export default fungsi",
307
- details: "Middleware direktori mengikuti bentuk `(args) => Promise<Response>`.",
317
+ message: "`_middleware.ts` does not default-export a function",
318
+ details: "Directory middleware follows the `(args) => Promise<Response>` shape.",
308
319
  suggestions: ["Export default `async ({ req, ctx, next }) => await next()`"],
309
320
  });
310
321
  }
@@ -324,9 +335,9 @@ export function createRequestHandler(
324
335
  throw new MoonsError({
325
336
  code: "E_MIDDLEWARE_INVALID",
326
337
  file: definition.file,
327
- message: "middleware API tidak bisa dimuat",
328
- details: "Peta `loadApiMiddleware` tidak memuat `_middleware.ts` ini.",
329
- suggestions: ["Restart dev server / jalankan ulang build"],
338
+ message: "API middleware could not be loaded",
339
+ details: "The `loadApiMiddleware` map does not contain this `_middleware.ts`.",
340
+ suggestions: ["Restart the dev server / rebuild"],
330
341
  });
331
342
  }
332
343
  const module = (await load()) as { default?: unknown };
@@ -334,8 +345,8 @@ export function createRequestHandler(
334
345
  throw new MoonsError({
335
346
  code: "E_MIDDLEWARE_INVALID",
336
347
  file: definition.file,
337
- message: "`_middleware.ts` tidak meng-export default fungsi",
338
- details: "Middleware direktori mengikuti bentuk `(args) => Promise<Response>`.",
348
+ message: "`_middleware.ts` does not default-export a function",
349
+ details: "Directory middleware follows the `(args) => Promise<Response>` shape.",
339
350
  suggestions: ["Export default `async ({ req, ctx, next }) => await next()`"],
340
351
  });
341
352
  }
@@ -385,7 +396,7 @@ export function createRequestHandler(
385
396
  middlewares,
386
397
  ctx,
387
398
  () => handleApiRequest(apiMatch.route, apiMatch.params, request, ctx),
388
- { code: "E_MIDDLEWARE_INVALID", label: "middleware API" },
399
+ { code: "E_MIDDLEWARE_INVALID", label: "API middleware" },
389
400
  )(request);
390
401
  }
391
402
  }
@@ -404,7 +415,7 @@ export function createRequestHandler(
404
415
  middlewares,
405
416
  ctx,
406
417
  () => renderRoute(resolved, request, ctx),
407
- { code: "E_MIDDLEWARE_INVALID", label: "middleware direktori" },
418
+ { code: "E_MIDDLEWARE_INVALID", label: "directory middleware" },
408
419
  )(request);
409
420
  } catch (error) {
410
421
  // Addendum B.2: sinyal routing dari loader/meta.
@@ -538,9 +549,9 @@ async function loadModules(
538
549
  if (load === undefined) {
539
550
  throw new MoonsError({
540
551
  code: "E_INTERNAL",
541
- message: `modul route \`${key}\` tidak terdaftar`,
542
- details: "Registry route tidak sinkron dengan hasil scan.",
543
- suggestions: ["Jalankan ulang `bun run build`, atau restart dev server"],
552
+ message: `route module \`${key}\` is not registered`,
553
+ details: "The route registry is out of sync with the scan result.",
554
+ suggestions: ["Run `bun run build` again, or restart the dev server"],
544
555
  });
545
556
  }
546
557
  return await load();
@@ -561,11 +572,11 @@ async function runRouteMeta(
561
572
  throw new MoonsError({
562
573
  code: "E_INTERNAL",
563
574
  file,
564
- message: "export `meta` harus berupa fungsi `({ data }) => RouteMeta`",
565
- details: "Bentuk `meta` mengikuti ",
575
+ message: "export `meta` must be a function `({ data }) => RouteMeta`",
576
+ details: "The `meta` shape follows `RouteMeta`.",
566
577
  suggestions: [
567
- "Ubah menjadi `export const meta = ({ data }) => ({ title: \"...\" })`",
568
- "Atau hapus export `meta`",
578
+ "Change it to `export const meta = ({ data }) => ({ title: \"...\" })`",
579
+ "Or remove the `meta` export",
569
580
  ],
570
581
  });
571
582
  }
@@ -574,9 +585,9 @@ async function runRouteMeta(
574
585
  throw new MoonsError({
575
586
  code: "E_INTERNAL",
576
587
  file,
577
- message: "`meta()` harus mengembalikan objek RouteMeta",
578
- details: "Nilai yang dikembalikan dipakai untuk <title>/description/og.",
579
- suggestions: ["Kembalikan objek, mis. `({ title: \"Halaman\" })`"],
588
+ message: "`meta()` must return a RouteMeta object",
589
+ details: "The returned value is used for <title>/description/og.",
590
+ suggestions: ["Return an object, e.g. `({ title: \"Home\" })`"],
580
591
  });
581
592
  }
582
593
  return result as Record<string, unknown>;
@@ -589,13 +600,13 @@ function apiErrorResponse(error: unknown, route: ApiRouteDefinition): Response {
589
600
  const isProduction = process.env["NODE_ENV"] === "production";
590
601
  if (error instanceof MoonsError) {
591
602
  console.error(`[moons] API ${route.path}: ${error.format()}`);
592
- return new Response(isProduction ? "Terjadi kesalahan internal." : error.format(), {
603
+ return new Response(isProduction ? "Internal server error." : error.format(), {
593
604
  status: 500,
594
605
  headers: { "content-type": "text/plain; charset=utf-8" },
595
606
  });
596
607
  }
597
608
  console.error(`[moons] API ${route.path}:`, error);
598
- return new Response(isProduction ? "Terjadi kesalahan internal." : String(error), {
609
+ return new Response(isProduction ? "Internal server error." : String(error), {
599
610
  status: 500,
600
611
  headers: { "content-type": "text/plain; charset=utf-8" },
601
612
  });
@@ -609,14 +620,25 @@ function internalErrorResponse(error: unknown, options: { log?: boolean } = {}):
609
620
  const detail =
610
621
  process.env["NODE_ENV"] !== "production" && error instanceof MoonsError
611
622
  ? `<pre>${escapeHtml(error.format())}</pre>`
612
- : "<p>Terjadi kesalahan internal.</p>";
623
+ : "<p>Internal server error.</p>";
613
624
 
614
625
  return htmlResponse(
615
- `<!doctype html><html lang="id"><head><meta charset="utf-8"><title>500</title></head><body><h1>500</h1>${detail}</body></html>`,
626
+ `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>500</title></head><body><h1>500</h1>${detail}</body></html>`,
616
627
  500,
617
628
  );
618
629
  }
619
630
 
631
+ /** Addendum O: expose the trailing-slash policy to the client runtime. */
632
+ function withTrailingSlashMeta(html: string, policy?: "never" | "always"): string {
633
+ if (policy === undefined) {
634
+ return html;
635
+ }
636
+ return html.replace(
637
+ "</head>",
638
+ ` <meta name="moons:trailing-slash" content="${policy}" />\n</head>`,
639
+ );
640
+ }
641
+
620
642
  function htmlResponse(html: string, status: number): Response {
621
643
  return new Response(html, {
622
644
  status,
@@ -667,7 +689,7 @@ function warnPayloadFindings(
667
689
  }
668
690
  warned.add(label);
669
691
  console.warn(
670
- `[moons] peringatan dev: payload SSR memuat ${label} — jangan mengembalikan rahasia dari loader.`,
692
+ `[moons] dev warning: SSR payload contains ${label} — do not return secrets from loaders.`,
671
693
  );
672
694
  }
673
695
  }
package/src/server/isr.ts CHANGED
@@ -57,7 +57,7 @@ async function runRevalidate(options: RevalidateOptions): Promise<void> {
57
57
  }),
58
58
  );
59
59
  if (response.status !== 200) {
60
- log(`[moons] isr: lewati ${options.pathname} (status ${response.status})`);
60
+ log(`[moons] isr: skipped ${options.pathname} (status ${response.status})`);
61
61
  return;
62
62
  }
63
63
  const html = await response.text();
@@ -69,8 +69,8 @@ async function runRevalidate(options: RevalidateOptions): Promise<void> {
69
69
  const labels = findings
70
70
  .map((finding) => (finding.kind === "canary" ? "canary" : `env(${finding.label})`))
71
71
  .join(", ");
72
- // Fail-closed: jangan menimpa file statis dengan konten bocor.
73
- log(`[moons] isr: TIDAK menulis ${options.pathname} — memuat ${labels}`);
72
+ // Fail-closed: tidak menimpa file statis dengan konten bocor.
73
+ log(`[moons] isr: NOT writing ${options.pathname} — contains ${labels}`);
74
74
  return;
75
75
  }
76
76
  if (options.storage !== undefined) {
@@ -78,14 +78,14 @@ async function runRevalidate(options: RevalidateOptions): Promise<void> {
78
78
  return;
79
79
  }
80
80
  if (options.file === undefined) {
81
- log(`[moons] isr: lewati ${options.pathname} — tanpa file/storage tujuan`);
81
+ log(`[moons] isr: skipped ${options.pathname} — no destination file/storage`);
82
82
  return;
83
83
  }
84
84
  const tmp = `${options.file}.tmp-${process.pid}`;
85
85
  writeFileSync(tmp, html);
86
86
  renameSync(tmp, options.file);
87
87
  } catch (error) {
88
- log(`[moons] isr: gagal regenerasi ${options.pathname}: ${String(error)}`);
88
+ log(`[moons] isr: failed to regenerate ${options.pathname}: ${String(error)}`);
89
89
  }
90
90
  }
91
91
 
@@ -366,13 +366,13 @@ function rpcBodyTooLargeText(limit: number, size: number | null): string {
366
366
  code: "E_RPC_BODY_TOO_LARGE",
367
367
  message:
368
368
  size === null
369
- ? `body melebihi limit ${megabytes(limit)} MB`
370
- : `body ${megabytes(size)} MB melebihi limit ${megabytes(limit)} MB`,
369
+ ? `body exceeds the ${megabytes(limit)} MB limit`
370
+ : `body ${megabytes(size)} MB exceeds the ${megabytes(limit)} MB limit`,
371
371
  details:
372
- "Body melebihi `security.bodyLimitBytes`; pembacaan dihentikan di transport sebelum penuh.",
372
+ "Body exceeds `security.bodyLimitBytes`; reading stops at the transport before the body is fully buffered.",
373
373
  suggestions: [
374
- "Kurangi ukuran payload, atau",
375
- "Naikkan security.bodyLimitBytes jika memang perlu",
374
+ "Reduce the payload size, or",
375
+ "Increase security.bodyLimitBytes if you really need to",
376
376
  ],
377
377
  }).format();
378
378
  }
@@ -524,7 +524,7 @@ function scheduleRevalidation(input: {
524
524
  if (logger !== null) {
525
525
  logger({ msg: "isr:revalidate-failed", level: "warn", path: normalized, error: message });
526
526
  } else {
527
- console.error(`[moons] isr regenerasi gagal (${normalized}): ${message}`);
527
+ console.error(`[moons] isr regeneration failed (${normalized}): ${message}`);
528
528
  }
529
529
  });
530
530
  }
@@ -18,7 +18,7 @@ export class InvalidRequestError extends Error {
18
18
  export class BodyLimitError extends Error {
19
19
  readonly limit: number;
20
20
  constructor(limit: number) {
21
- super(`request body melebihi batas ${limit} byte`);
21
+ super(`request body exceeds the ${limit} byte limit`);
22
22
  this.name = "BodyLimitError";
23
23
  this.limit = limit;
24
24
  }
@@ -48,7 +48,7 @@ export function resolveRequestUrl(
48
48
  try {
49
49
  return new URL(`${proto ?? "http"}://${host}${req.url ?? "/"}`);
50
50
  } catch {
51
- throw new InvalidRequestError(`header Host tidak valid: ${JSON.stringify(hostHeader)}`);
51
+ throw new InvalidRequestError(`invalid Host header: ${JSON.stringify(hostHeader)}`);
52
52
  }
53
53
  }
54
54
 
@@ -69,7 +69,7 @@ export function createPluginRuntime(options: PluginRuntimeOptions): PluginRuntim
69
69
  try {
70
70
  await plugin.onShutdown?.(app);
71
71
  } catch (error) {
72
- log(`[moons] plugin \`${plugin.name}\` gagal saat onShutdown: ${String(error)}`);
72
+ log(`[moons] plugin \`${plugin.name}\` failed during onShutdown: ${String(error)}`);
73
73
  }
74
74
  }
75
75
  },
@@ -80,11 +80,11 @@ export async function runPrerender(options: PrerenderOptions): Promise<Prerender
80
80
  throw new MoonsError({
81
81
  code: "E_ISR_INVALID",
82
82
  file: route.file,
83
- message: "`revalidate` tidak valid atau dipakai tanpa `prerender = true`",
84
- details: "ISR berbasis file: `revalidate` (detik, number >= 0) + `prerender`.",
83
+ message: "`revalidate` is invalid or used without `prerender = true`",
84
+ details: "File-based ISR: `revalidate` (seconds, number >= 0) + `prerender`.",
85
85
  suggestions: [
86
- "Gunakan angka detik, mis. `export const revalidate = 60`",
87
- "Tambahkan `export const prerender = true` pada page yang sama",
86
+ "Use a number of seconds, e.g. `export const revalidate = 60`",
87
+ "Add `export const prerender = true` to the same page",
88
88
  ],
89
89
  });
90
90
  }
@@ -92,7 +92,7 @@ export async function runPrerender(options: PrerenderOptions): Promise<Prerender
92
92
 
93
93
  if (prerender !== true) {
94
94
  if (prerender !== undefined && prerender !== false) {
95
- throw prerenderError(route, "`prerender` harus boolean (`true`/`false`)");
95
+ throw prerenderError(route, "`prerender` must be a boolean (`true`/`false`)");
96
96
  }
97
97
  continue;
98
98
  }
@@ -107,15 +107,15 @@ export async function runPrerender(options: PrerenderOptions): Promise<Prerender
107
107
  );
108
108
 
109
109
  if (response.status === 404) {
110
- log(`[moons] prerender: lewati ${pathname} (notFound)`);
110
+ log(`[moons] prerender: skipped ${pathname} (notFound)`);
111
111
  skipped += 1;
112
112
  continue;
113
113
  }
114
114
  if (response.status >= 300 && response.status < 400) {
115
- throw prerenderError(route, `\`redirect()\` tidak didukung saat prerender (${pathname})`);
115
+ throw prerenderError(route, `\`redirect()\` is not supported during prerender (${pathname})`);
116
116
  }
117
117
  if (response.status !== 200) {
118
- throw prerenderError(route, `status ${response.status} saat merender ${pathname}`);
118
+ throw prerenderError(route, `status ${response.status} while rendering ${pathname}`);
119
119
  }
120
120
 
121
121
  const html = await response.text();
@@ -152,7 +152,7 @@ export async function runPrerender(options: PrerenderOptions): Promise<Prerender
152
152
 
153
153
  const extras = [...(wrote404 ? [NOT_FOUND_FILE] : []), ...emitted];
154
154
  log(
155
- `[moons] prerender selesai: ${written} file${extras.length > 0 ? ` + ${extras.join(" + ")}` : ""} (${skipped} dilewati)`,
155
+ `[moons] prerender done: ${written} file${extras.length > 0 ? ` + ${extras.join(" + ")}` : ""} (${skipped} skipped)`,
156
156
  );
157
157
  return { written, skipped };
158
158
  }
@@ -237,7 +237,7 @@ async function renderStaticNotFound(
237
237
  );
238
238
  if (response.status !== 404) {
239
239
  log(
240
- `[moons] prerender: ${NOT_FOUND_FILE} dilewati (sentinel \`${NOT_FOUND_SENTINEL}\` → ${response.status})`,
240
+ `[moons] prerender: ${NOT_FOUND_FILE} skipped (sentinel \`${NOT_FOUND_SENTINEL}\` → ${response.status})`,
241
241
  );
242
242
  return null;
243
243
  }
@@ -267,11 +267,11 @@ function assertNoLeak(
267
267
  throw new MoonsError({
268
268
  code: "E_PRERENDER_LEAK",
269
269
  file,
270
- message: `hasil prerender \`${pathname}\` memuat ${labels}`,
271
- details: "HTML statis permanen — rahasia yang ter-prerender tidak bisa ditarik kembali.",
270
+ message: `prerender output \`${pathname}\` contains ${labels}`,
271
+ details: "Static HTML is permanent — a prerendered secret cannot be retracted.",
272
272
  suggestions: [
273
- "Jangan mengembalikan nilai env rahasia dari loader yang di-prerender",
274
- "Matikan `prerender` untuk route ini atau pindahkan datanya ke serverFn request-time",
273
+ "Do not return secret env values from a loader that runs at prerender time",
274
+ "Turn off `prerender` for this route or move the data to a request-time serverFn",
275
275
  ],
276
276
  });
277
277
  }
@@ -291,37 +291,37 @@ async function resolvePathnames(route: RouteDefinition, module: RouteModule): Pr
291
291
  throw new MoonsError({
292
292
  code: "E_PRERENDER_NO_PATHS",
293
293
  file: route.file,
294
- message: "route dynamic ber-`prerender = true` tanpa `export const paths`",
295
- details: "Prerender butuh daftar param untuk merender tiap path.",
294
+ message: "dynamic route with `prerender = true` without `export const paths`",
295
+ details: "Prerendering needs a param list to render each path.",
296
296
  suggestions: [
297
- 'Tambahkan `export const paths = () => [{ id: "…" }]`',
298
- "Atau hapus `prerender` agar dirender SSR saat request",
297
+ 'Add `export const paths = () => [{ id: "…" }]`',
298
+ "Or remove `prerender` so the route renders at request time (SSR)",
299
299
  ],
300
300
  });
301
301
  }
302
302
 
303
303
  const raw = await (paths as () => unknown | Promise<unknown>)();
304
304
  if (!Array.isArray(raw)) {
305
- throw prerenderError(route, "`paths()` harus mengembalikan array");
305
+ throw prerenderError(route, "`paths()` must return an array");
306
306
  }
307
307
 
308
308
  const expected = new Set(route.params);
309
309
  const result: string[] = [];
310
310
  for (const item of raw as unknown[]) {
311
311
  if (item === null || typeof item !== "object" || Array.isArray(item)) {
312
- throw prerenderError(route, "item `paths()` harus objek param string");
312
+ throw prerenderError(route, "`paths()` items must be objects with string params");
313
313
  }
314
314
  const record = item as Record<string, unknown>;
315
315
  const keys = Object.keys(record);
316
316
  for (const name of expected) {
317
317
  const value = record[name];
318
318
  if (typeof value !== "string" || value.length === 0) {
319
- throw prerenderError(route, `param \`${name}\` wajib string non-kosong`);
319
+ throw prerenderError(route, `param \`${name}\` must be a non-empty string`);
320
320
  }
321
321
  }
322
322
  for (const key of keys) {
323
323
  if (!expected.has(key)) {
324
- throw prerenderError(route, `param \`${key}\` tidak ada di pola route`);
324
+ throw prerenderError(route, `param \`${key}\` is not in the route pattern`);
325
325
  }
326
326
  }
327
327
  result.push(buildPathname(route, record as Record<string, string>));
@@ -356,7 +356,7 @@ function buildPathname(route: RouteDefinition, params: Record<string, string>):
356
356
 
357
357
  function encodeSegment(route: RouteDefinition, value: string): string {
358
358
  if (value.length === 0 || /^\.+$/.test(value)) {
359
- throw prerenderError(route, `nilai param \`${value}\` tidak valid untuk path file`);
359
+ throw prerenderError(route, `param value \`${value}\` is not valid for a file path`);
360
360
  }
361
361
  return encodeURIComponent(value);
362
362
  }
@@ -368,9 +368,9 @@ function outputFile(clientDir: string, pathname: string): string {
368
368
  if (file !== base && !file.startsWith(base + sep)) {
369
369
  throw new MoonsError({
370
370
  code: "E_PRERENDER_INVALID",
371
- message: `path prerender keluar dari direktori client (\`${pathname}\`)`,
372
- details: "Nama file harus berada di dalam `dist/client`.",
373
- suggestions: ["Periksa nilai param `paths` — jangan memakai `..`"],
371
+ message: `prerender path escapes the client directory (\`${pathname}\`)`,
372
+ details: "The file name must stay inside `dist/client`.",
373
+ suggestions: ["Check the `paths` param values — do not use `..`"],
374
374
  });
375
375
  }
376
376
  return file;
@@ -381,10 +381,10 @@ function prerenderError(route: RouteDefinition, message: string): MoonsError {
381
381
  code: "E_PRERENDER_INVALID",
382
382
  file: route.file,
383
383
  message,
384
- details: "Kontrak prerender mengikuti.",
384
+ details: "The prerender contract is enforced.",
385
385
  suggestions: [
386
- "Perbaiki bentuk `paths`/param agar cocok dengan pola route",
387
- "Jangan memakai `redirect()` pada route yang di-prerender",
386
+ "Fix the `paths`/params shape so it matches the route pattern",
387
+ "Do not use `redirect()` on a prerendered route",
388
388
  ],
389
389
  });
390
390
  }
@@ -14,7 +14,7 @@ export const ROOT_PLACEHOLDER = '<div id="root"></div>';
14
14
 
15
15
  export const DEFAULT_TEMPLATE = [
16
16
  "<!doctype html>",
17
- '<html lang="id">',
17
+ '<html lang="en">',
18
18
  "<head>",
19
19
  ' <meta charset="utf-8" />',
20
20
  ' <meta name="viewport" content="width=device-width, initial-scale=1" />',
@@ -38,11 +38,11 @@ export function injectShell(
38
38
  if (!template.includes(ROOT_PLACEHOLDER)) {
39
39
  throw new MoonsError({
40
40
  code: "E_INTERNAL",
41
- message: `shell HTML tidak memuat \`${ROOT_PLACEHOLDER}\``,
42
- details: "index.html aplikasi wajib memuat elemen root tersebut agar SSR bisa meng-inject markup.",
41
+ message: `shell HTML does not contain \`${ROOT_PLACEHOLDER}\``,
42
+ details: "The app's index.html must contain that root element so SSR can inject markup.",
43
43
  suggestions: [
44
- `Tambahkan ${ROOT_PLACEHOLDER} ke index.html`,
45
- "Atau hapus index.html untuk memakai shell bawaan",
44
+ `Add ${ROOT_PLACEHOLDER} to index.html`,
45
+ "Or remove index.html to use the built-in shell",
46
46
  ],
47
47
  });
48
48
  }
@@ -119,9 +119,9 @@ export function createFileShell(options: { clientDir?: string } = {}): Shell {
119
119
  throw new MoonsError({
120
120
  code: "E_INTERNAL",
121
121
  file,
122
- message: "`dist/client/index.html` tidak ditemukan",
123
- details: "Build client harus dijalankan sebelum server produksi dinyalakan.",
124
- suggestions: ["Jalankan `bun run build` lebih dulu"],
122
+ message: "`dist/client/index.html` not found",
123
+ details: "The client build must run before starting the production server.",
124
+ suggestions: ["Run `bun run build` first"],
125
125
  });
126
126
  }
127
127
  cached = readFileSync(file, "utf8");
@@ -36,10 +36,10 @@ export function installGracefulShutdown(
36
36
  return;
37
37
  }
38
38
  shuttingDown = true;
39
- log(`[moons] menerima ${signal} — berhenti menerima koneksi, drain...`);
39
+ log(`[moons] received ${signal} — no longer accepting connections, draining...`);
40
40
 
41
41
  const timer = setTimeout(() => {
42
- log(`[moons] drain melewati ${timeoutMs}ms — keluar paksa`);
42
+ log(`[moons] drain exceeded ${timeoutMs}ms — forcing exit`);
43
43
  const closeAll = (server as { closeAllConnections?: () => void }).closeAllConnections;
44
44
  try {
45
45
  closeAll?.call(server);
@@ -59,7 +59,7 @@ export function installGracefulShutdown(
59
59
  server.close(async (error?: Error) => {
60
60
  if (error !== undefined) {
61
61
  clearTimeout(timer);
62
- log(`[moons] penutupan server gagal: ${error.message}`);
62
+ log(`[moons] server close failed: ${error.message}`);
63
63
  exit(1);
64
64
  return;
65
65
  }
@@ -70,11 +70,11 @@ export function installGracefulShutdown(
70
70
  try {
71
71
  await options.onShutdown();
72
72
  } catch (hookError) {
73
- log(`[moons] onShutdown gagal: ${String(hookError)}`);
73
+ log(`[moons] onShutdown failed: ${String(hookError)}`);
74
74
  }
75
75
  }
76
76
  clearTimeout(timer);
77
- log("[moons] shutdown selesai (drain penuh)");
77
+ log("[moons] shutdown complete (full drain)");
78
78
  exit(0);
79
79
  });
80
80
  };
@@ -21,7 +21,7 @@ export function createFileStorage(clientDir: string): PrerenderStorage {
21
21
  async write(pathname: string, body: Uint8Array): Promise<void> {
22
22
  const target = staticFileTarget(clientDir, pathname);
23
23
  if (target === null) {
24
- throw new Error(`[moons] isr: path tidak valid untuk ditulis: ${pathname}`);
24
+ throw new Error(`[moons] isr: invalid path to write: ${pathname}`);
25
25
  }
26
26
  mkdirSync(dirname(target), { recursive: true });
27
27
  const tmp = `${target}.tmp-${process.pid}`;
@@ -1,5 +1,5 @@
1
1
  import type { MoonsConfig, RouteDefinition, RouteModule } from "@moonsjs/core";
2
- import { createRequestHandler } from "./handler";
2
+ import { createRequestHandler, type RequestHandlerOptions } from "./handler";
3
3
  import { DEFAULT_TEMPLATE, injectShell, type Shell } from "./shell";
4
4
 
5
5
  export interface TestServerOptions {
@@ -7,6 +7,11 @@ export interface TestServerOptions {
7
7
  config: MoonsConfig;
8
8
  routesDir: string;
9
9
  loadRoute: Record<string, () => Promise<RouteModule>>;
10
+ /** Endpoint `src/api` (Addendum O) — hasil scan app. */
11
+ apiRoutes?: RequestHandlerOptions["apiRoutes"];
12
+ loadApi?: RequestHandlerOptions["loadApi"];
13
+ apiMiddlewares?: RequestHandlerOptions["apiMiddlewares"];
14
+ loadApiMiddleware?: RequestHandlerOptions["loadApiMiddleware"];
10
15
  /** Default: shell in-memory dari `DEFAULT_TEMPLATE` (tanpa file). */
11
16
  shell?: Shell;
12
17
  }
@@ -34,6 +39,12 @@ export function createTestServer(
34
39
  config: options.config,
35
40
  routesDir: options.routesDir,
36
41
  loadRoute: options.loadRoute,
42
+ ...(options.apiRoutes === undefined ? {} : { apiRoutes: options.apiRoutes }),
43
+ ...(options.loadApi === undefined ? {} : { loadApi: options.loadApi }),
44
+ ...(options.apiMiddlewares === undefined ? {} : { apiMiddlewares: options.apiMiddlewares }),
45
+ ...(options.loadApiMiddleware === undefined
46
+ ? {}
47
+ : { loadApiMiddleware: options.loadApiMiddleware }),
37
48
  shell,
38
49
  });
39
50