@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
package/src/api.ts ADDED
@@ -0,0 +1,124 @@
1
+ import { parse } from "@babel/parser";
2
+ import { API_METHODS, MoonsError } from "@moonsjs/core";
3
+
4
+ /**
5
+ * Validasi statis export modul `src/api` (PRD Addendum D.1):
6
+ * hanya handler method `GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS` sebagai fungsi;
7
+ * `default`/export lain ditolak → `E_API_HANDLER_INVALID`.
8
+ */
9
+ export function assertApiModuleExports(code: string, file: string): string[] {
10
+ let ast: unknown;
11
+ try {
12
+ ast = parse(code, {
13
+ sourceType: "module",
14
+ plugins: file.endsWith(".tsx") ? ["typescript", "jsx"] : ["typescript"],
15
+ });
16
+ } catch {
17
+ return []; // biarkan bundler melaporkan syntax error
18
+ }
19
+
20
+ const program = (ast as { program?: { body?: Array<Record<string, unknown>> } }).program;
21
+ const body = program?.body ?? [];
22
+ const methods: string[] = [];
23
+
24
+ for (const statement of body) {
25
+ if (statement["type"] === "ExportDefaultDeclaration") {
26
+ throw apiError(
27
+ file,
28
+ "export `default` tidak didukung di modul API",
29
+ "Modul `src/api` hanya mengekspor handler method bernama (PRD Addendum D.1).",
30
+ ["Ganti menjadi `export const GET = (args) => …`", "Hapus export default"],
31
+ );
32
+ }
33
+ if (statement["type"] !== "ExportNamedDeclaration") {
34
+ continue;
35
+ }
36
+ if (statement["source"] !== undefined) {
37
+ // re-export `export { X } from "..."` — nama tetap dicek di bawah.
38
+ }
39
+ const declaration = statement["declaration"] as Record<string, unknown> | undefined;
40
+ if (declaration !== undefined) {
41
+ if (declaration["type"] === "VariableDeclaration") {
42
+ for (const declarator of (declaration["declarations"] ?? []) as Array<Record<string, unknown>>) {
43
+ const id = declarator["id"] as Record<string, unknown> | undefined;
44
+ if (id?.["type"] === "Identifier") {
45
+ methods.push(id["name"] as string);
46
+ }
47
+ }
48
+ } else if (declaration["type"] === "FunctionDeclaration") {
49
+ const id = declaration["id"] as Record<string, unknown> | undefined;
50
+ if (id?.["type"] === "Identifier") {
51
+ methods.push(id["name"] as string);
52
+ }
53
+ }
54
+ continue;
55
+ }
56
+ for (const specifier of (statement["specifiers"] ?? []) as Array<Record<string, unknown>>) {
57
+ const exported = specifier["exported"] as Record<string, unknown> | undefined;
58
+ if (exported?.["type"] === "Identifier") {
59
+ methods.push(exported["name"] as string);
60
+ }
61
+ }
62
+ }
63
+
64
+ const illegal = methods.filter((name) => !API_METHODS.includes(name as never));
65
+ if (illegal.length > 0) {
66
+ throw apiError(
67
+ file,
68
+ `export \`${illegal[0]}\` bukan handler method yang dikenal`,
69
+ `Handler yang legal: ${API_METHODS.join(", ")} (PRD Addendum D.1).`,
70
+ [
71
+ "Ganti nama export menjadi salah satu method HTTP",
72
+ "Pindahkan helper ke modul lain (mis. `src/server/…`)",
73
+ ],
74
+ );
75
+ }
76
+ if (methods.length === 0) {
77
+ throw apiError(
78
+ file,
79
+ "modul API tanpa handler method",
80
+ `Wajib ada minimal satu dari ${API_METHODS.join(", ")} (PRD Addendum D.1).`,
81
+ [`Tambahkan \`export const GET = (args) => new Response(…)\``],
82
+ );
83
+ }
84
+
85
+ return methods;
86
+ }
87
+
88
+ export function assertMiddlewareExports(code: string, file: string): void {
89
+ let ast: unknown;
90
+ try {
91
+ ast = parse(code, {
92
+ sourceType: "module",
93
+ plugins: file.endsWith(".tsx") ? ["typescript", "jsx"] : ["typescript"],
94
+ });
95
+ } catch {
96
+ return;
97
+ }
98
+ const program = (ast as { program?: { body?: Array<Record<string, unknown>> } }).program;
99
+ const hasDefault = (program?.body ?? []).some(
100
+ (statement) => statement["type"] === "ExportDefaultDeclaration",
101
+ );
102
+ if (!hasDefault) {
103
+ throw new MoonsError({
104
+ code: "E_MIDDLEWARE_INVALID",
105
+ file,
106
+ message: "`_middleware.ts` tanpa `export default`",
107
+ details: "Middleware direktori mengikuti bentuk `Middleware`: `(args) => Promise<Response>` (PRD Addendum D.3).",
108
+ suggestions: [
109
+ "Tambahkan `export default async ({ req, ctx, next }) => await next()`",
110
+ "Hapus file bila tidak dipakai",
111
+ ],
112
+ });
113
+ }
114
+ }
115
+
116
+ function apiError(file: string, message: string, details: string, suggestions: string[]): MoonsError {
117
+ return new MoonsError({
118
+ code: "E_API_HANDLER_INVALID",
119
+ file,
120
+ message,
121
+ details,
122
+ suggestions,
123
+ });
124
+ }
package/src/manifest.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { findLayouts, routeKeyFromFile, type RouteDefinition } from "@moonsjs/core";
1
+ import { findLayouts, routeKeyFromFile, type ApiRouteDefinition, type RouteDefinition } from "@moonsjs/core";
2
2
  import { relative, sep } from "node:path";
3
3
 
4
4
  export interface ClientManifestEntry {
@@ -38,10 +38,51 @@ export function buildClientManifest(
38
38
  if (errorRoute !== undefined) {
39
39
  manifest[ERROR_ROUTE_KEY] = { page: routeKeyFromFile(routesDir, errorRoute.file), layouts: [] };
40
40
  }
41
+ // D.3: semua boundary `_error` (termasuk bersarang) ikut manifest — payload
42
+ // memakai key file boundary yang dipilih runtime.
43
+ for (const route of routes) {
44
+ if (route.kind !== "error") {
45
+ continue;
46
+ }
47
+ const key = routeKeyFromFile(routesDir, route.file);
48
+ manifest[key] = { page: key, layouts: [] };
49
+ }
41
50
 
42
51
  return manifest;
43
52
  }
44
53
 
54
+ /** `virtual:moons/api` (Addendum D.1 + D.3): tabel API + middleware + peta import(). */
55
+ export function renderApiModule(
56
+ apiRoutes: ApiRouteDefinition[],
57
+ apiMiddlewares: ReadonlyArray<{ file: string; dir: string }>,
58
+ projectRoot: string,
59
+ ): string {
60
+ const loadEntries = apiRoutes
61
+ .map(
62
+ (route) =>
63
+ ` ${JSON.stringify(route.path)}: () => import(${JSON.stringify(rootSpecifier(projectRoot, route.file))}),`,
64
+ )
65
+ .join("\n");
66
+ const middlewareEntries = apiMiddlewares
67
+ .map(
68
+ (middleware) =>
69
+ ` ${JSON.stringify(middleware.file)}: () => import(${JSON.stringify(rootSpecifier(projectRoot, middleware.file))}),`,
70
+ )
71
+ .join("\n");
72
+ return [
73
+ "// Generated by @moons. Jangan diubah manual.",
74
+ `export const apiRoutes = ${JSON.stringify(apiRoutes, null, 2)};`,
75
+ `export const apiMiddlewares = ${JSON.stringify(apiMiddlewares, null, 2)};`,
76
+ "export const loadApi = {",
77
+ loadEntries,
78
+ "};",
79
+ "export const loadApiMiddleware = {",
80
+ middlewareEntries,
81
+ "};",
82
+ "",
83
+ ].join("\n");
84
+ }
85
+
45
86
  /**
46
87
  * Isi `virtual:moons/routes`: daftar RouteDefinition, manifest client, dan
47
88
  * peta import() dinamis per file route (specifier root-relative).
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Pemindaian payload SSR (nilai A1 / PRD §2): HANYA dipakai di dev sebagai
3
+ * peringatan — tidak ada biaya runtime di production (production tidak pernah
4
+ * memanggil fungsi ini; lihat `RequestHandlerOptions.payloadScan`).
5
+ */
6
+
7
+ export interface PayloadScanOptions {
8
+ /** Canary `leakTest.canary` (bila dikonfigurasi). */
9
+ canary?: string;
10
+ /** Lingkungan server (biasanya `process.env`). */
11
+ env?: Record<string, string | undefined>;
12
+ }
13
+
14
+ export interface PayloadScanFinding {
15
+ kind: "canary" | "env";
16
+ /** Label untuk pesan: `canary` atau nama env var. */
17
+ label: string;
18
+ }
19
+
20
+ /**
21
+ * Nama env yang "mirip rahasia" — dideteksi lewat pola nama + panjang nilai
22
+ * minimum 12 karakter untuk menghindari false positive (keputusan pemilik).
23
+ */
24
+ const SECRET_KEY_PATTERN =
25
+ /(SECRET|TOKEN|PASSWORD|PASSWD|PRIVATE|CREDENTIAL|AUTH|API_KEY|ACCESS_KEY|SIGNING)/i;
26
+
27
+ const MIN_SECRET_LENGTH = 12;
28
+
29
+ export function scanPayloadForSecrets(
30
+ payload: string,
31
+ options: PayloadScanOptions = {},
32
+ ): PayloadScanFinding[] {
33
+ const findings: PayloadScanFinding[] = [];
34
+
35
+ const canary = options.canary;
36
+ if (canary !== undefined && canary.length > 0 && payload.includes(canary)) {
37
+ findings.push({ kind: "canary", label: "canary" });
38
+ }
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
+
52
+ return findings;
53
+ }
package/src/plugin.ts CHANGED
@@ -5,13 +5,19 @@ import {
5
5
  MoonsError,
6
6
  fnRegistryKey,
7
7
  generateTypes,
8
+ scanApiMiddlewares,
9
+ scanApiRoutes,
8
10
  scanRoutes,
9
11
  validateConfig,
12
+ type ApiMiddlewareDefinition,
13
+ type ApiRouteDefinition,
10
14
  type RouteDefinition,
11
15
  } from "@moonsjs/core";
12
16
  import { loadConfigFromFile, type Plugin, type ViteDevServer } from "vite";
17
+ import { assertApiModuleExports, assertMiddlewareExports } from "./api";
13
18
  import { findLeak } from "./leak-check";
14
- import { renderRoutesModule, rootSpecifier } from "./manifest";
19
+ import type { PackageJsonInfo } from "./server-only";
20
+ import { renderApiModule, renderRoutesModule, rootSpecifier } from "./manifest";
15
21
  import { installDevMiddleware } from "./server/dev";
16
22
  import {
17
23
  analyzeServerFile,
@@ -49,7 +55,12 @@ export default function moons(options: MoonsVitePluginOptions = {}): Plugin {
49
55
  let isSsrBuild = false;
50
56
  let configFilePath = "";
51
57
  let routesDir = "";
52
- let routes: RouteDefinition[] = [];
58
+ let serverOnlyPackages: ReadonlyArray<string> = [];
59
+ let apiDir = "";
60
+ let apiRoutes: ApiRouteDefinition[] = [];
61
+ let apiMiddlewares: ApiMiddlewareDefinition[] = [];
62
+ /** Cache package.json untuk gate Addendum C (dibagi lintas modul). */
63
+ const packageCache = new Map<string, PackageJsonInfo | null>(); let routes: RouteDefinition[] = [];
53
64
  let analysisCache = new Map<string, ServerFileAnalysis>();
54
65
  const stubTransformed = new Set<string>();
55
66
 
@@ -163,8 +174,18 @@ export default function moons(options: MoonsVitePluginOptions = {}): Plugin {
163
174
  emptyOutDir: true,
164
175
  // Entry nyata yang di-generate saat buildStart (Rolldown butuh file,
165
176
  // entry virtual tidak di-resolve lewat plugin container).
166
- ssr: ".moons/server-entry.ts",
167
- rollupOptions: { output: { entryFileNames: "index.js", codeSplitting: false } },
177
+ // D.5: dua entry — server (index) + prerender runner.
178
+ ssr: true,
179
+ rollupOptions: {
180
+ input: {
181
+ index: ".moons/server-entry.ts",
182
+ prerender: ".moons/prerender-entry.ts",
183
+ },
184
+ output: {
185
+ entryFileNames: "[name].js",
186
+ chunkFileNames: "chunks/[name]-[hash].js",
187
+ },
188
+ },
168
189
  },
169
190
  ssr: { noExternal: true, target: "node" as const },
170
191
  };
@@ -223,10 +244,31 @@ export default function moons(options: MoonsVitePluginOptions = {}): Plugin {
223
244
 
224
245
  const config = validateConfig(raw, { file: configFilePath, projectRoot: root });
225
246
  routesDir = resolvePath(root, config.routesDir);
247
+ serverOnlyPackages = config.security?.serverOnlyPackages ?? [];
248
+
249
+ // Addendum D.1: scan + validasi statis modul `src/api`.
250
+ apiDir = resolvePath(root, config.apiDir ?? "src/api");
251
+ if (existsSync(apiDir)) {
252
+ apiRoutes = scanApiRoutes(apiDir);
253
+ for (const route of apiRoutes) {
254
+ assertApiModuleExports(readFileSync(route.file, "utf8"), route.file);
255
+ }
256
+ apiMiddlewares = scanApiMiddlewares(apiDir);
257
+ for (const middleware of apiMiddlewares) {
258
+ assertMiddlewareExports(readFileSync(middleware.file, "utf8"), middleware.file);
259
+ }
260
+ } else {
261
+ apiRoutes = [];
262
+ apiMiddlewares = [];
263
+ }
226
264
  },
227
265
 
228
266
  async buildStart() {
229
267
  routes = scanRoutes(routesDir);
268
+ // D.3: validasi statis `_middleware.ts` pohon routes.
269
+ for (const middleware of routes.filter((route) => route.kind === "middleware")) {
270
+ assertMiddlewareExports(readFileSync(middleware.file, "utf8"), middleware.file);
271
+ }
230
272
  await generateTypes({
231
273
  routesDir,
232
274
  configPath: configFilePath,
@@ -236,7 +278,12 @@ export default function moons(options: MoonsVitePluginOptions = {}): Plugin {
236
278
  if (isSsrBuild) {
237
279
  writeFileSync(
238
280
  resolvePath(root, ".moons/server-entry.ts"),
239
- renderServerEntryBuild(routesDir, serverFnFileSpecifiers()),
281
+ renderServerEntryBuild(routesDir, serverFnFileSpecifiers(), options.leakTest?.canary),
282
+ "utf8",
283
+ );
284
+ writeFileSync(
285
+ resolvePath(root, ".moons/prerender-entry.ts"),
286
+ renderPrerenderEntry(routesDir, root, options.leakTest?.canary),
240
287
  "utf8",
241
288
  );
242
289
  }
@@ -258,6 +305,8 @@ export default function moons(options: MoonsVitePluginOptions = {}): Plugin {
258
305
  switch (name) {
259
306
  case VIRTUAL_MODULES.routes:
260
307
  return renderRoutesModule(routes, routesDir, root);
308
+ case VIRTUAL_MODULES.api:
309
+ return renderApiModule(apiRoutes, apiMiddlewares, root);
261
310
  case VIRTUAL_MODULES.clientEntry:
262
311
  return '// Generated by @moons. Jangan diubah manual.\nexport { clientManifest, loadRoute } from "virtual:moons/routes";\n';
263
312
  case VIRTUAL_MODULES.config:
@@ -316,6 +365,9 @@ export default function moons(options: MoonsVitePluginOptions = {}): Plugin {
316
365
  }
317
366
  },
318
367
  analyze: (absoluteFile) => analysisCache.get(absoluteFile) ?? analyzeFile(absoluteFile),
368
+ apiDir,
369
+ serverOnlyPackages,
370
+ packageCache,
319
371
  });
320
372
  }
321
373
 
@@ -369,7 +421,18 @@ export default function moons(options: MoonsVitePluginOptions = {}): Plugin {
369
421
  },
370
422
 
371
423
  configureServer(server) {
372
- installDevMiddleware(server, { templatePath: resolvePath(root, "index.html") });
424
+ installDevMiddleware(server, {
425
+ templatePath: resolvePath(root, "index.html"),
426
+ // A1: pemindaian payload SSR hanya di dev (warning), tanpa biaya di production.
427
+ payloadScan: {
428
+ ...(options.leakTest?.canary === undefined ? {} : { canary: options.leakTest.canary }),
429
+ env: process.env,
430
+ },
431
+ // D.1: request ke prefix API (semua method) dirutekan ke runtime.
432
+ ...(apiRoutes.length === 0
433
+ ? {}
434
+ : { apiPrefix: `/${apiDir.split(/[\\/]/).filter((part) => part.length > 0).pop() ?? "api"}` }),
435
+ });
373
436
  },
374
437
 
375
438
  async handleHotUpdate(context) {
@@ -379,12 +442,34 @@ export default function moons(options: MoonsVitePluginOptions = {}): Plugin {
379
442
  return;
380
443
  }
381
444
 
445
+ if (context.file.startsWith(apiDir) && existsSync(apiDir)) {
446
+ try {
447
+ apiRoutes = scanApiRoutes(apiDir);
448
+ for (const route of apiRoutes) {
449
+ assertApiModuleExports(readFileSync(route.file, "utf8"), route.file);
450
+ }
451
+ apiMiddlewares = scanApiMiddlewares(apiDir);
452
+ for (const middleware of apiMiddlewares) {
453
+ assertMiddlewareExports(readFileSync(middleware.file, "utf8"), middleware.file);
454
+ }
455
+ } catch (error) {
456
+ context.server.config.logger.error(String(error));
457
+ return;
458
+ }
459
+ invalidateVirtualModules(context.server);
460
+ context.server.ws.send({ type: "full-reload" });
461
+ return;
462
+ }
463
+
382
464
  if (!context.file.startsWith(routesDir)) {
383
465
  return;
384
466
  }
385
467
 
386
468
  try {
387
469
  routes = scanRoutes(routesDir);
470
+ for (const middleware of routes.filter((route) => route.kind === "middleware")) {
471
+ assertMiddlewareExports(readFileSync(middleware.file, "utf8"), middleware.file);
472
+ }
388
473
  await generateTypes({
389
474
  routesDir,
390
475
  configPath: configFilePath,
@@ -417,28 +502,38 @@ function renderServerEntryDev(routesDir: string, eagerImports: string[]): string
417
502
  'import { createPluginRuntime, createRequestHandler } from "@moonsjs/vite/server";',
418
503
  'import config from "virtual:moons/config";',
419
504
  'import { routes, loadRoute } from "virtual:moons/routes";',
505
+ 'import { apiRoutes, apiMiddlewares, loadApi, loadApiMiddleware } from "virtual:moons/api";',
420
506
  ...eagerImports.map((specifier) => `import ${JSON.stringify(specifier)};`),
421
507
  "",
422
- "export function createDevApp(shell) {",
508
+ "export function createDevApp(shell, payloadScan) {",
423
509
  " const plugins = createPluginRuntime({ config, routes, middlewares: config.middleware ?? [] });",
424
- ` const handler = createRequestHandler({ routes, config, routesDir: ${JSON.stringify(routesDir)}, loadRoute, shell, onContext: (ctx) => plugins.applyExtensions(ctx) });`,
510
+ ` const handler = createRequestHandler({ routes, apiRoutes, apiMiddlewares, loadApi, loadApiMiddleware, config, routesDir: ${JSON.stringify(routesDir)}, loadRoute, shell, onContext: (ctx) => plugins.applyExtensions(ctx), ...(payloadScan === undefined ? {} : { payloadScan }) });`,
425
511
  " return { handler, plugins };",
426
512
  "}",
427
513
  "",
428
514
  ].join("\n");
429
515
  }
430
516
 
431
- function renderServerEntryBuild(routesDir: string, eagerImports: string[]): string {
517
+ function renderServerEntryBuild(
518
+ routesDir: string,
519
+ eagerImports: string[],
520
+ canary: string | undefined,
521
+ ): string {
432
522
  return [
433
523
  "// Generated by @moons. Jangan diubah manual.",
434
524
  'import { createFileShell, createPluginRuntime, createRequestHandler, startNodeServer } from "@moonsjs/vite/server";',
435
525
  'import config from "virtual:moons/config";',
436
526
  'import { routes, loadRoute } from "virtual:moons/routes";',
527
+ 'import { apiRoutes, apiMiddlewares, loadApi, loadApiMiddleware } from "virtual:moons/api";',
437
528
  ...eagerImports.map((specifier) => `import ${JSON.stringify(specifier)};`),
438
529
  "",
439
530
  "const plugins = createPluginRuntime({ config, routes, middlewares: config.middleware ?? [] });",
440
531
  "const handler = createRequestHandler({",
441
532
  " routes,",
533
+ " apiRoutes,",
534
+ " apiMiddlewares,",
535
+ " loadApi,",
536
+ " loadApiMiddleware,",
442
537
  " config,",
443
538
  ` routesDir: ${JSON.stringify(routesDir)},`,
444
539
  " loadRoute,",
@@ -446,7 +541,7 @@ function renderServerEntryBuild(routesDir: string, eagerImports: string[]): stri
446
541
  " onContext: (ctx) => plugins.applyExtensions(ctx),",
447
542
  "});",
448
543
  "",
449
- "startNodeServer(handler, { plugins });",
544
+ `startNodeServer(handler, { plugins${canary === undefined ? "" : `, canary: ${JSON.stringify(canary)}`} });`,
450
545
  "",
451
546
  ].join("\n");
452
547
  }
@@ -456,6 +551,38 @@ interface ModuleGraphLike {
456
551
  invalidateModule(module: unknown): void;
457
552
  }
458
553
 
554
+ /** Entry D.5: runner prerender (dijalankan CLI setelah build server). */
555
+ function renderPrerenderEntry(
556
+ routesDir: string,
557
+ projectRoot: string,
558
+ canary: string | undefined,
559
+ ): string {
560
+ return [
561
+ "// Generated by @moons. Jangan diubah manual.",
562
+ 'import { createFileShell, createRequestHandler, runPrerender } from "@moonsjs/vite/server";',
563
+ 'import config from "virtual:moons/config";',
564
+ 'import { routes, loadRoute } from "virtual:moons/routes";',
565
+ "",
566
+ "const handler = createRequestHandler({",
567
+ " routes,",
568
+ " config,",
569
+ ` routesDir: ${JSON.stringify(routesDir)},`,
570
+ " loadRoute,",
571
+ " shell: createFileShell(),",
572
+ "});",
573
+ "",
574
+ "await runPrerender({",
575
+ " routes,",
576
+ ` routesDir: ${JSON.stringify(routesDir)},`,
577
+ " loadRoute,",
578
+ " handler,",
579
+ ` clientDir: ${JSON.stringify(resolvePath(projectRoot, "dist/client"))},`,
580
+ ...(canary === undefined ? [] : [` canary: ${JSON.stringify(canary)},`]),
581
+ "});",
582
+ "",
583
+ ].join("\n");
584
+ }
585
+
459
586
  function environmentName(context: unknown): string {
460
587
  const environment = (context as { environment?: { name?: string } } | undefined)?.environment;
461
588
  return environment?.name ?? "client";
package/src/server/dev.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import type { ViteDevServer } from "vite";
3
3
  import { VIRTUAL_MODULES } from "../virtual-modules";
4
+ import type { PayloadScanOptions } from "../payload-scan";
4
5
  import type { PluginRuntime } from "./plugins";
5
6
  import type { Shell } from "./shell";
6
7
  import { createDevShell } from "./shell";
@@ -12,7 +13,10 @@ interface DevApp {
12
13
  }
13
14
 
14
15
  type DevServerEntry = {
15
- createDevApp: (shell: Shell) => DevApp;
16
+ createDevApp: (
17
+ shell: Shell,
18
+ payloadScan?: import("../payload-scan").PayloadScanOptions,
19
+ ) => DevApp;
16
20
  };
17
21
 
18
22
  /**
@@ -22,7 +26,7 @@ type DevServerEntry = {
22
26
  */
23
27
  export function installDevMiddleware(
24
28
  server: ViteDevServer,
25
- options: { templatePath: string },
29
+ options: { templatePath: string; payloadScan?: PayloadScanOptions; apiPrefix?: string },
26
30
  ): void {
27
31
  let appPromise: Promise<DevApp> | null = null;
28
32
 
@@ -30,7 +34,7 @@ export function installDevMiddleware(
30
34
  if (appPromise === null) {
31
35
  appPromise = (async () => {
32
36
  const entry = (await server.ssrLoadModule(VIRTUAL_MODULES.serverEntry)) as DevServerEntry;
33
- const app = entry.createDevApp(createDevShell(server, options));
37
+ const app = entry.createDevApp(createDevShell(server, options), options.payloadScan);
34
38
  await app.plugins.start();
35
39
  await app.plugins.ready();
36
40
  return app;
@@ -44,7 +48,7 @@ export function installDevMiddleware(
44
48
  });
45
49
 
46
50
  server.middlewares.use((req, res, next) => {
47
- if (!shouldHandle(req)) {
51
+ if (!shouldHandle(req, options.apiPrefix)) {
48
52
  next();
49
53
  return;
50
54
  }
@@ -64,7 +68,7 @@ async function handleRequest(
64
68
  await sendNodeResponse(res, response);
65
69
  }
66
70
 
67
- function shouldHandle(req: IncomingMessage): boolean {
71
+ function shouldHandle(req: IncomingMessage, apiPrefix: string | undefined): boolean {
68
72
  const path = new URL(req.url ?? "/", "http://localhost").pathname;
69
73
 
70
74
  // RPC serverFn (semua method): dulu hanya GET/HEAD HTML yang dirutekan,
@@ -74,6 +78,11 @@ function shouldHandle(req: IncomingMessage): boolean {
74
78
  return true;
75
79
  }
76
80
 
81
+ // Addendum D.1: endpoint `src/api` (semua method) dirutekan ke runtime.
82
+ if (apiPrefix !== undefined && (path === apiPrefix || path.startsWith(`${apiPrefix}/`))) {
83
+ return true;
84
+ }
85
+
77
86
  if (req.method !== "GET" && req.method !== "HEAD") {
78
87
  return false;
79
88
  }