@moonsjs/vite 0.1.16 → 0.1.18

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moonsjs/vite",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "sideEffects": false,
package/src/api.ts CHANGED
@@ -25,9 +25,9 @@ export function assertApiModuleExports(code: string, file: string): string[] {
25
25
  if (statement["type"] === "ExportDefaultDeclaration") {
26
26
  throw apiError(
27
27
  file,
28
- "export `default` tidak didukung di modul API",
29
- "Modul `src/api` hanya mengekspor handler method bernama.",
30
- ["Ganti menjadi `export const GET = (args) => …`", "Hapus export default"],
28
+ "export `default` is not supported in API modules",
29
+ "`src/api` modules only export named method handlers.",
30
+ ["Replace it with `export const GET = (args) => …`", "Remove the default export"],
31
31
  );
32
32
  }
33
33
  if (statement["type"] !== "ExportNamedDeclaration") {
@@ -65,20 +65,20 @@ export function assertApiModuleExports(code: string, file: string): string[] {
65
65
  if (illegal.length > 0) {
66
66
  throw apiError(
67
67
  file,
68
- `export \`${illegal[0]}\` bukan handler method yang dikenal`,
69
- `Handler yang legal: ${API_METHODS.join(", ")}.`,
68
+ `export \`${illegal[0]}\` is not a recognized method handler`,
69
+ `Legal handlers: ${API_METHODS.join(", ")}.`,
70
70
  [
71
- "Ganti nama export menjadi salah satu method HTTP",
72
- "Pindahkan helper ke modul lain (mis. `src/server/…`)",
71
+ "Rename the export to one of the HTTP methods",
72
+ "Move helpers to another module (e.g. `src/server/…`)",
73
73
  ],
74
74
  );
75
75
  }
76
76
  if (methods.length === 0) {
77
77
  throw apiError(
78
78
  file,
79
- "modul API tanpa handler method",
80
- `Wajib ada minimal satu dari ${API_METHODS.join(", ")}.`,
81
- [`Tambahkan \`export const GET = (args) => new Response(…)\``],
79
+ "API module has no method handler",
80
+ `At least one of ${API_METHODS.join(", ")} is required.`,
81
+ ["Add `export const GET = (args) => new Response(…)`"],
82
82
  );
83
83
  }
84
84
 
@@ -103,11 +103,11 @@ export function assertMiddlewareExports(code: string, file: string): void {
103
103
  throw new MoonsError({
104
104
  code: "E_MIDDLEWARE_INVALID",
105
105
  file,
106
- message: "`_middleware.ts` tanpa `export default`",
107
- details: "Middleware direktori mengikuti bentuk `Middleware`: `(args) => Promise<Response>`.",
106
+ message: "`_middleware.ts` without `export default`",
107
+ details: "Directory middleware follows the `Middleware` shape: `(args) => Promise<Response>`.",
108
108
  suggestions: [
109
- "Tambahkan `export default async ({ req, ctx, next }) => await next()`",
110
- "Hapus file bila tidak dipakai",
109
+ "Add `export default async ({ req, ctx, next }) => await next()`",
110
+ "Delete the file if unused",
111
111
  ],
112
112
  });
113
113
  }
package/src/devtools.ts CHANGED
@@ -36,7 +36,7 @@ export function emptyDevtoolsData(): DevtoolsData {
36
36
  export function renderDevtoolsHtml(data: DevtoolsData): string {
37
37
  const rows = (items: DevtoolsRoute[]): string =>
38
38
  items.length === 0
39
- ? '<tr><td colspan="3" class="muted">(kosong)</td></tr>'
39
+ ? '<tr><td colspan="3" class="muted">(empty)</td></tr>'
40
40
  : items
41
41
  .map(
42
42
  (item) =>
@@ -46,7 +46,7 @@ export function renderDevtoolsHtml(data: DevtoolsData): string {
46
46
 
47
47
  const fnRows =
48
48
  data.serverFns.length === 0
49
- ? '<tr><td colspan="3" class="muted">(tidak ada serverFn)</td></tr>'
49
+ ? '<tr><td colspan="3" class="muted">(no serverFn)</td></tr>'
50
50
  : data.serverFns
51
51
  .map(
52
52
  (fn) =>
@@ -55,11 +55,11 @@ export function renderDevtoolsHtml(data: DevtoolsData): string {
55
55
  .join("\n");
56
56
 
57
57
  const canary = data.canary
58
- ? '<span class="ok">aktif</span> (canary terpasang)'
59
- : '<span class="muted">nonaktif</span> (tanpa canary)';
58
+ ? '<span class="ok">enabled</span> (canary installed)'
59
+ : '<span class="muted">disabled</span> (no canary)';
60
60
 
61
61
  return `<!doctype html>
62
- <html lang="id">
62
+ <html lang="en">
63
63
  <head>
64
64
  <meta charset="utf-8">
65
65
  <meta name="viewport" content="width=device-width, initial-scale=1">
package/src/manifest.ts CHANGED
@@ -70,7 +70,7 @@ export function renderApiModule(
70
70
  )
71
71
  .join("\n");
72
72
  return [
73
- "// Generated by @moons. Jangan diubah manual.",
73
+ "// Generated by @moons. Do not edit manually.",
74
74
  `export const apiRoutes = ${JSON.stringify(apiRoutes, null, 2)};`,
75
75
  `export const apiMiddlewares = ${JSON.stringify(apiMiddlewares, null, 2)};`,
76
76
  "export const loadApi = {",
@@ -100,7 +100,7 @@ export function renderRoutesModule(
100
100
  });
101
101
 
102
102
  return [
103
- "// Generated by @moons. Jangan diubah manual.",
103
+ "// Generated by @moons. Do not edit manually.",
104
104
  `export const routes = ${JSON.stringify(routes, null, 2)};`,
105
105
  "",
106
106
  `export const clientManifest = ${JSON.stringify(manifest, null, 2)};`,
package/src/plugin.ts CHANGED
@@ -141,9 +141,9 @@ export default function moons(options: MoonsVitePluginOptions = {}): Plugin {
141
141
  throw new MoonsError({
142
142
  code: "E_INTERNAL",
143
143
  file: id,
144
- message: "gagal membaca file server untuk transform",
145
- details: "File `.server.ts` harus bisa dibaca saat transform.",
146
- suggestions: ["Periksa path dan izin file"],
144
+ message: "failed to read the server file for transform",
145
+ details: "The `.server.ts` file must be readable during transform.",
146
+ suggestions: ["Check the path and file permissions"],
147
147
  });
148
148
  }
149
149
 
@@ -217,10 +217,10 @@ export default function moons(options: MoonsVitePluginOptions = {}): Plugin {
217
217
  throw new MoonsError({
218
218
  code: "E_CONFIG_INVALID",
219
219
  file: configFilePath,
220
- message: "`moons.config.ts` tidak ditemukan",
221
- details: "Framework membutuhkan file config di root project.",
220
+ message: "`moons.config.ts` not found",
221
+ details: "The framework requires a config file at the project root.",
222
222
  suggestions: [
223
- "Buat `moons.config.ts` dengan `export default defineConfig({ context: async (req) => ({}) })`",
223
+ "Create `moons.config.ts` with `export default defineConfig({ context: async (req) => ({}) })`",
224
224
  ],
225
225
  });
226
226
  }
@@ -240,9 +240,9 @@ export default function moons(options: MoonsVitePluginOptions = {}): Plugin {
240
240
  throw new MoonsError({
241
241
  code: "E_CONFIG_INVALID",
242
242
  file: configFilePath,
243
- message: "gagal memuat `moons.config.ts`",
243
+ message: "failed to load `moons.config.ts`",
244
244
  details: error instanceof Error ? error.message : String(error),
245
- suggestions: ["Periksa sintaks dan import di `moons.config.ts`"],
245
+ suggestions: ["Check the syntax and imports in `moons.config.ts`"],
246
246
  });
247
247
  }
248
248
 
@@ -314,7 +314,7 @@ export default function moons(options: MoonsVitePluginOptions = {}): Plugin {
314
314
  case VIRTUAL_MODULES.api:
315
315
  return renderApiModule(apiRoutes, apiMiddlewares, root);
316
316
  case VIRTUAL_MODULES.clientEntry:
317
- return '// Generated by @moons. Jangan diubah manual.\nexport { clientManifest, loadRoute } from "virtual:moons/routes";\n';
317
+ return '// Generated by @moons. Do not edit manually.\nexport { clientManifest, loadRoute } from "virtual:moons/routes";\n';
318
318
  case VIRTUAL_MODULES.config:
319
319
  return renderConfigModule(root, configFilePath);
320
320
  case VIRTUAL_MODULES.serverEntry:
@@ -393,12 +393,12 @@ export default function moons(options: MoonsVitePluginOptions = {}): Plugin {
393
393
  if (leak !== null) {
394
394
  throw new MoonsError({
395
395
  code: "E_INTERNAL",
396
- message: `canary \`${canary}\` ditemukan di bundle client (${leak.fileName})`,
396
+ message: `canary \`${canary}\` found in the client bundle (${leak.fileName})`,
397
397
  details:
398
- "Verifikasi kebocoran gagal: kode server bocor ke output client. Build dihentikan.",
398
+ "Leak verification failed: server code leaked into the client output. Build stopped.",
399
399
  suggestions: [
400
- "Periksa transform `*.server.ts`",
401
- "Jalankan `./scripts/m2/test-leak.sh` untuk audit",
400
+ "Check the `*.server.ts` transform",
401
+ "Run `./scripts/m2/test-leak.sh` to audit",
402
402
  ],
403
403
  });
404
404
  }
@@ -416,12 +416,12 @@ export default function moons(options: MoonsVitePluginOptions = {}): Plugin {
416
416
  throw new MoonsError({
417
417
  code: "E_FORBIDDEN_SERVER_IMPORT",
418
418
  file: cleanId,
419
- message: "modul server ikut ter-bundle di client tanpa melewati transform",
419
+ message: "server module got bundled into the client without passing through the transform",
420
420
  details:
421
- "Verifikasi build: setiap `*.server.ts` di bundle client wajib berupa stub RPC hasil transform.",
421
+ "Build verification: every `*.server.ts` in the client bundle must be an RPC stub produced by the transform.",
422
422
  suggestions: [
423
- "Pastikan transform zero-trust berjalan untuk modul ini",
424
- "Laporkan ini sebagai bug framework jika berulang",
423
+ "Make sure the zero-trust transform runs for this module",
424
+ "If this repeats, report it as a framework bug",
425
425
  ],
426
426
  });
427
427
  }
@@ -523,7 +523,7 @@ export default function moons(options: MoonsVitePluginOptions = {}): Plugin {
523
523
 
524
524
  function renderConfigModule(root: string, configFilePath: string): string {
525
525
  return [
526
- "// Generated by @moons. Jangan diubah manual.",
526
+ "// Generated by @moons. Do not edit manually.",
527
527
  `import raw from ${JSON.stringify(rootSpecifier(root, configFilePath))};`,
528
528
  'import { validateConfig } from "@moonsjs/core";',
529
529
  `export default validateConfig(raw, { file: ${JSON.stringify(configFilePath)} });`,
@@ -533,7 +533,7 @@ function renderConfigModule(root: string, configFilePath: string): string {
533
533
 
534
534
  function renderServerEntryDev(routesDir: string, eagerImports: string[]): string {
535
535
  return [
536
- "// Generated by @moons. Jangan diubah manual.",
536
+ "// Generated by @moons. Do not edit manually.",
537
537
  'import { createPluginRuntime, createRequestHandler } from "@moonsjs/vite/server";',
538
538
  'import config from "virtual:moons/config";',
539
539
  'import { routes, loadRoute } from "virtual:moons/routes";',
@@ -555,7 +555,7 @@ function renderServerEntryBuild(
555
555
  canary: string | undefined,
556
556
  ): string {
557
557
  return [
558
- "// Generated by @moons. Jangan diubah manual.",
558
+ "// Generated by @moons. Do not edit manually.",
559
559
  'import { createFileShell, createPluginRuntime, createRequestHandler, startNodeServer } from "@moonsjs/vite/server";',
560
560
  'import config from "virtual:moons/config";',
561
561
  'import { routes, loadRoute } from "virtual:moons/routes";',
@@ -576,7 +576,7 @@ function renderServerEntryBuild(
576
576
  " onContext: (ctx) => plugins.applyExtensions(ctx),",
577
577
  "});",
578
578
  "",
579
- `startNodeServer(handler, { plugins, headers: config.headers, storage: config.isr?.storage, trustProxy: config.security?.trustProxy, bodyLimitBytes: config.security?.bodyLimitBytes, logging: config.logging${canary === undefined ? "" : `, canary: ${JSON.stringify(canary)}`} });`,
579
+ `startNodeServer(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)}`} });`,
580
580
  "",
581
581
  ].join("\n");
582
582
  }
@@ -593,7 +593,7 @@ function renderPrerenderEntry(
593
593
  canary: string | undefined,
594
594
  ): string {
595
595
  return [
596
- "// Generated by @moons. Jangan diubah manual.",
596
+ "// Generated by @moons. Do not edit manually.",
597
597
  'import { createFileShell, createRequestHandler, runPrerender } from "@moonsjs/vite/server";',
598
598
  'import config from "virtual:moons/config";',
599
599
  'import { routes, loadRoute } from "virtual:moons/routes";',
package/src/server/dev.ts CHANGED
@@ -78,7 +78,7 @@ export function installDevMiddleware(
78
78
  } catch (error) {
79
79
  res.statusCode = 500;
80
80
  res.setHeader("content-type", "text/plain; charset=utf-8");
81
- res.end(`[moons] devtools gagal dirender: ${String(error)}`);
81
+ res.end(`[moons] devtools failed to render: ${String(error)}`);
82
82
  }
83
83
  return;
84
84
  }
@@ -17,6 +17,7 @@ import {
17
17
  type ResolvedRoute,
18
18
  type RouteDefinition,
19
19
  type RouteModule,
20
+ trailingSlashRedirect,
20
21
  } from "@moonsjs/core";
21
22
  import { encodeSsrPayload, reviveError, serializeError } from "@moonsjs/core/serialize";
22
23
  import { API_METHODS, resolveApiRoute } from "@moonsjs/core";
@@ -131,9 +132,9 @@ export function createRequestHandler(
131
132
  new MoonsError({
132
133
  code: "E_API_HANDLER_INVALID",
133
134
  file: route.file,
134
- message: `modul API \`${route.path}\` tidak bisa dimuat`,
135
- details: "Peta `loadApi` tidak memuat route ini (build/dev tidak sinkron).",
136
- suggestions: ["Jalankan ulang build / restart dev server"],
135
+ message: `API module \`${route.path}\` could not be loaded`,
136
+ details: "The `loadApi` map does not contain this route (build/dev out of sync).",
137
+ suggestions: ["Rebuild / restart the dev server"],
137
138
  }),
138
139
  route,
139
140
  );
@@ -150,9 +151,9 @@ export function createRequestHandler(
150
151
  new MoonsError({
151
152
  code: "E_API_HANDLER_INVALID",
152
153
  file: route.file,
153
- message: "modul API tidak mengekspor objek handler",
154
- details: `Modul \`${route.path}\` harus mengekspor handler method.`,
155
- suggestions: ["Export `GET`/`POST`/… sebagai fungsi `(args) => Response`"],
154
+ message: "API module does not export a handler object",
155
+ details: `Module \`${route.path}\` must export method handlers.`,
156
+ suggestions: ["Export `GET`/`POST`/… as `(args) => Response` functions"],
156
157
  }),
157
158
  route,
158
159
  );
@@ -193,9 +194,9 @@ export function createRequestHandler(
193
194
  new MoonsError({
194
195
  code: "E_API_HANDLER_INVALID",
195
196
  file: route.file,
196
- message: `handler \`${requestMethod}\` tidak mengembalikan \`Response\``,
197
- details: "Handler API wajib mengembalikan `new Response(...)`.",
198
- suggestions: ["Kembalikan `new Response(body, { status, headers })`"],
197
+ message: `handler \`${requestMethod}\` did not return a \`Response\``,
198
+ details: "API handlers must return `new Response(...)`.",
199
+ suggestions: ["Return `new Response(body, { status, headers })`"],
199
200
  }),
200
201
  route,
201
202
  );
@@ -224,13 +225,13 @@ export function createRequestHandler(
224
225
  );
225
226
  return htmlResponse(await shell(markup, payload, request.url), 404);
226
227
  } catch (error) {
227
- console.error("[moons] gagal merender _404 kustom:", error);
228
+ console.error("[moons] failed to render custom _404:", error);
228
229
  }
229
230
  }
230
231
 
231
232
  const payload = encodeSsrPayload({ key: "", params: {}, data: {} });
232
233
  const html = await shell(
233
- "<h1>404</h1><p>Halaman tidak ditemukan.</p>",
234
+ "<h1>404</h1><p>Page not found.</p>",
234
235
  payload,
235
236
  request.url,
236
237
  );
@@ -274,7 +275,7 @@ export function createRequestHandler(
274
275
  );
275
276
  return htmlResponse(await shell(markup, payload, request.url), 500);
276
277
  } catch (renderError) {
277
- console.error("[moons] gagal merender _error kustom:", renderError);
278
+ console.error("[moons] failed to render custom _error:", renderError);
278
279
  }
279
280
  }
280
281
 
@@ -291,9 +292,9 @@ export function createRequestHandler(
291
292
  throw new MoonsError({
292
293
  code: "E_MIDDLEWARE_INVALID",
293
294
  file: definition.file,
294
- message: "middleware direktori tidak bisa dimuat",
295
- details: "Peta `loadRoute` tidak memuat `_middleware.ts` ini.",
296
- suggestions: ["Restart dev server / jalankan ulang build"],
295
+ message: "directory middleware could not be loaded",
296
+ details: "The `loadRoute` map does not contain this `_middleware.ts`.",
297
+ suggestions: ["Restart the dev server / rebuild"],
297
298
  });
298
299
  }
299
300
  const module = (await load()) as { default?: unknown };
@@ -302,8 +303,8 @@ export function createRequestHandler(
302
303
  throw new MoonsError({
303
304
  code: "E_MIDDLEWARE_INVALID",
304
305
  file: definition.file,
305
- message: "`_middleware.ts` tidak meng-export default fungsi",
306
- details: "Middleware direktori mengikuti bentuk `(args) => Promise<Response>`.",
306
+ message: "`_middleware.ts` does not default-export a function",
307
+ details: "Directory middleware follows the `(args) => Promise<Response>` shape.",
307
308
  suggestions: ["Export default `async ({ req, ctx, next }) => await next()`"],
308
309
  });
309
310
  }
@@ -323,9 +324,9 @@ export function createRequestHandler(
323
324
  throw new MoonsError({
324
325
  code: "E_MIDDLEWARE_INVALID",
325
326
  file: definition.file,
326
- message: "middleware API tidak bisa dimuat",
327
- details: "Peta `loadApiMiddleware` tidak memuat `_middleware.ts` ini.",
328
- suggestions: ["Restart dev server / jalankan ulang build"],
327
+ message: "API middleware could not be loaded",
328
+ details: "The `loadApiMiddleware` map does not contain this `_middleware.ts`.",
329
+ suggestions: ["Restart the dev server / rebuild"],
329
330
  });
330
331
  }
331
332
  const module = (await load()) as { default?: unknown };
@@ -333,8 +334,8 @@ export function createRequestHandler(
333
334
  throw new MoonsError({
334
335
  code: "E_MIDDLEWARE_INVALID",
335
336
  file: definition.file,
336
- message: "`_middleware.ts` tidak meng-export default fungsi",
337
- details: "Middleware direktori mengikuti bentuk `(args) => Promise<Response>`.",
337
+ message: "`_middleware.ts` does not default-export a function",
338
+ details: "Directory middleware follows the `(args) => Promise<Response>` shape.",
338
339
  suggestions: ["Export default `async ({ req, ctx, next }) => await next()`"],
339
340
  });
340
341
  }
@@ -384,7 +385,7 @@ export function createRequestHandler(
384
385
  middlewares,
385
386
  ctx,
386
387
  () => handleApiRequest(apiMatch.route, apiMatch.params, request, ctx),
387
- { code: "E_MIDDLEWARE_INVALID", label: "middleware API" },
388
+ { code: "E_MIDDLEWARE_INVALID", label: "API middleware" },
388
389
  )(request);
389
390
  }
390
391
  }
@@ -403,7 +404,7 @@ export function createRequestHandler(
403
404
  middlewares,
404
405
  ctx,
405
406
  () => renderRoute(resolved, request, ctx),
406
- { code: "E_MIDDLEWARE_INVALID", label: "middleware direktori" },
407
+ { code: "E_MIDDLEWARE_INVALID", label: "directory middleware" },
407
408
  )(request);
408
409
  } catch (error) {
409
410
  // Addendum B.2: sinyal routing dari loader/meta.
@@ -456,6 +457,19 @@ export function createRequestHandler(
456
457
  };
457
458
 
458
459
  try {
460
+ // Addendum M: kanonikalisasi trailing slash sebelum routing (308,
461
+ // query dipertahankan; `/_moons/**`, root, dan aset dikecualikan).
462
+ if (!isPrerender && config.trailingSlash !== undefined) {
463
+ const policyUrl = new URL(request.url);
464
+ const canonical = trailingSlashRedirect(policyUrl.pathname, config.trailingSlash);
465
+ if (canonical !== null) {
466
+ policyUrl.pathname = canonical;
467
+ return applyHeaders(
468
+ new Response(null, { status: 308, headers: { location: policyUrl.toString() } }),
469
+ );
470
+ }
471
+ }
472
+
459
473
  if (!isPrerender && redirects.length > 0) {
460
474
  const url = new URL(request.url);
461
475
  const path = url.pathname.replace(/\/$/, "");
@@ -524,9 +538,9 @@ async function loadModules(
524
538
  if (load === undefined) {
525
539
  throw new MoonsError({
526
540
  code: "E_INTERNAL",
527
- message: `modul route \`${key}\` tidak terdaftar`,
528
- details: "Registry route tidak sinkron dengan hasil scan.",
529
- suggestions: ["Jalankan ulang `bun run build`, atau restart dev server"],
541
+ message: `route module \`${key}\` is not registered`,
542
+ details: "The route registry is out of sync with the scan result.",
543
+ suggestions: ["Run `bun run build` again, or restart the dev server"],
530
544
  });
531
545
  }
532
546
  return await load();
@@ -547,11 +561,11 @@ async function runRouteMeta(
547
561
  throw new MoonsError({
548
562
  code: "E_INTERNAL",
549
563
  file,
550
- message: "export `meta` harus berupa fungsi `({ data }) => RouteMeta`",
551
- details: "Bentuk `meta` mengikuti ",
564
+ message: "export `meta` must be a function `({ data }) => RouteMeta`",
565
+ details: "The `meta` shape follows `RouteMeta`.",
552
566
  suggestions: [
553
- "Ubah menjadi `export const meta = ({ data }) => ({ title: \"...\" })`",
554
- "Atau hapus export `meta`",
567
+ "Change it to `export const meta = ({ data }) => ({ title: \"...\" })`",
568
+ "Or remove the `meta` export",
555
569
  ],
556
570
  });
557
571
  }
@@ -560,9 +574,9 @@ async function runRouteMeta(
560
574
  throw new MoonsError({
561
575
  code: "E_INTERNAL",
562
576
  file,
563
- message: "`meta()` harus mengembalikan objek RouteMeta",
564
- details: "Nilai yang dikembalikan dipakai untuk <title>/description/og.",
565
- suggestions: ["Kembalikan objek, mis. `({ title: \"Halaman\" })`"],
577
+ message: "`meta()` must return a RouteMeta object",
578
+ details: "The returned value is used for <title>/description/og.",
579
+ suggestions: ["Return an object, e.g. `({ title: \"Home\" })`"],
566
580
  });
567
581
  }
568
582
  return result as Record<string, unknown>;
@@ -575,13 +589,13 @@ function apiErrorResponse(error: unknown, route: ApiRouteDefinition): Response {
575
589
  const isProduction = process.env["NODE_ENV"] === "production";
576
590
  if (error instanceof MoonsError) {
577
591
  console.error(`[moons] API ${route.path}: ${error.format()}`);
578
- return new Response(isProduction ? "Terjadi kesalahan internal." : error.format(), {
592
+ return new Response(isProduction ? "Internal server error." : error.format(), {
579
593
  status: 500,
580
594
  headers: { "content-type": "text/plain; charset=utf-8" },
581
595
  });
582
596
  }
583
597
  console.error(`[moons] API ${route.path}:`, error);
584
- return new Response(isProduction ? "Terjadi kesalahan internal." : String(error), {
598
+ return new Response(isProduction ? "Internal server error." : String(error), {
585
599
  status: 500,
586
600
  headers: { "content-type": "text/plain; charset=utf-8" },
587
601
  });
@@ -595,10 +609,10 @@ function internalErrorResponse(error: unknown, options: { log?: boolean } = {}):
595
609
  const detail =
596
610
  process.env["NODE_ENV"] !== "production" && error instanceof MoonsError
597
611
  ? `<pre>${escapeHtml(error.format())}</pre>`
598
- : "<p>Terjadi kesalahan internal.</p>";
612
+ : "<p>Internal server error.</p>";
599
613
 
600
614
  return htmlResponse(
601
- `<!doctype html><html lang="id"><head><meta charset="utf-8"><title>500</title></head><body><h1>500</h1>${detail}</body></html>`,
615
+ `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>500</title></head><body><h1>500</h1>${detail}</body></html>`,
602
616
  500,
603
617
  );
604
618
  }
@@ -653,7 +667,7 @@ function warnPayloadFindings(
653
667
  }
654
668
  warned.add(label);
655
669
  console.warn(
656
- `[moons] peringatan dev: payload SSR memuat ${label} — jangan mengembalikan rahasia dari loader.`,
670
+ `[moons] dev warning: SSR payload contains ${label} — do not return secrets from loaders.`,
657
671
  );
658
672
  }
659
673
  }
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
 
@@ -2,7 +2,12 @@ import { existsSync, readFileSync, statSync } from "node:fs";
2
2
  import { gzipSync } from "node:zlib";
3
3
  import { createServer, type Server } from "node:http";
4
4
  import { extname, join, resolve, sep } from "node:path";
5
- import { DEFAULT_BODY_LIMIT_BYTES, MoonsError, type PrerenderStorage } from "@moonsjs/core";
5
+ import {
6
+ DEFAULT_BODY_LIMIT_BYTES,
7
+ MoonsError,
8
+ trailingSlashRedirect,
9
+ type PrerenderStorage,
10
+ } from "@moonsjs/core";
6
11
  import { revalidatePrerendered, shouldRevalidate } from "./isr";
7
12
  import { contentTypeForPathname } from "./storage";
8
13
  import {
@@ -68,6 +73,8 @@ export interface NodeServerOptions {
68
73
  bodyLimitBytes?: number;
69
74
  /** Addendum L.3: log akses terstruktur (default senyap). */
70
75
  logging?: { accessLog?: boolean; json?: boolean; redact?: string[] };
76
+ /** Addendum M: kebijakan trailing slash (juga untuk path prerender/static). */
77
+ trailingSlash?: "never" | "always";
71
78
  }
72
79
 
73
80
  /**
@@ -120,6 +127,19 @@ export function startNodeServer(
120
127
  }
121
128
 
122
129
  try {
130
+ // Addendum M: kanonikalisasi trailing slash di transport — mencakup
131
+ // path prerender yang dilayani langsung (tidak lewat handler).
132
+ if (options.trailingSlash !== undefined) {
133
+ const canonical = trailingSlashRedirect(url.pathname, options.trailingSlash);
134
+ if (canonical !== null) {
135
+ url.pathname = canonical;
136
+ res.statusCode = 308;
137
+ res.setHeader("location", url.toString());
138
+ res.end();
139
+ return;
140
+ }
141
+ }
142
+
123
143
  if (req.method === "GET" || req.method === "HEAD") {
124
144
  // Addendum I: storage bersama (bila dikonfigurasi) diutamakan.
125
145
  if (options.storage !== undefined) {
@@ -346,13 +366,13 @@ function rpcBodyTooLargeText(limit: number, size: number | null): string {
346
366
  code: "E_RPC_BODY_TOO_LARGE",
347
367
  message:
348
368
  size === null
349
- ? `body melebihi limit ${megabytes(limit)} MB`
350
- : `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`,
351
371
  details:
352
- "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.",
353
373
  suggestions: [
354
- "Kurangi ukuran payload, atau",
355
- "Naikkan security.bodyLimitBytes jika memang perlu",
374
+ "Reduce the payload size, or",
375
+ "Increase security.bodyLimitBytes if you really need to",
356
376
  ],
357
377
  }).format();
358
378
  }
@@ -504,7 +524,7 @@ function scheduleRevalidation(input: {
504
524
  if (logger !== null) {
505
525
  logger({ msg: "isr:revalidate-failed", level: "warn", path: normalized, error: message });
506
526
  } else {
507
- console.error(`[moons] isr regenerasi gagal (${normalized}): ${message}`);
527
+ console.error(`[moons] isr regeneration failed (${normalized}): ${message}`);
508
528
  }
509
529
  });
510
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
  },