@moonsjs/vite 0.1.4 → 0.1.5

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 (49) 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-e7qz0epf.js} +232 -24
  6. package/dist/index-e7qz0epf.js.map +11 -0
  7. package/dist/index.js +213 -16
  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 +453 -17
  21. package/dist/server/index.js.map +8 -6
  22. package/dist/server/node-server.d.ts +1 -1
  23. package/dist/server/node-server.d.ts.map +1 -1
  24. package/dist/server/node.d.ts.map +1 -1
  25. package/dist/server/prerender.d.ts +28 -0
  26. package/dist/server/prerender.d.ts.map +1 -0
  27. package/dist/server-only.d.ts +57 -0
  28. package/dist/server-only.d.ts.map +1 -0
  29. package/dist/transform.d.ts +9 -1
  30. package/dist/transform.d.ts.map +1 -1
  31. package/dist/transform.js +1 -1
  32. package/dist/virtual-modules.d.ts +1 -0
  33. package/dist/virtual-modules.d.ts.map +1 -1
  34. package/package.json +1 -1
  35. package/src/api.ts +124 -0
  36. package/src/manifest.ts +42 -1
  37. package/src/payload-scan.ts +53 -0
  38. package/src/plugin.ts +130 -7
  39. package/src/server/dev.ts +14 -5
  40. package/src/server/handler.ts +317 -13
  41. package/src/server/index.ts +4 -0
  42. package/src/server/node-server.ts +47 -7
  43. package/src/server/node.ts +17 -0
  44. package/src/server/prerender.ts +230 -0
  45. package/src/server-only.ts +220 -0
  46. package/src/transform.ts +138 -8
  47. package/src/virtual-modules.ts +1 -0
  48. package/dist/index-33p1r65s.js.map +0 -14
  49. package/dist/index-3ec9vpek.js.map +0 -10
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,
@@ -239,6 +281,11 @@ export default function moons(options: MoonsVitePluginOptions = {}): Plugin {
239
281
  renderServerEntryBuild(routesDir, serverFnFileSpecifiers()),
240
282
  "utf8",
241
283
  );
284
+ writeFileSync(
285
+ resolvePath(root, ".moons/prerender-entry.ts"),
286
+ renderPrerenderEntry(routesDir, root, options.leakTest?.canary),
287
+ "utf8",
288
+ );
242
289
  }
243
290
  },
244
291
 
@@ -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,11 +502,12 @@ 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
  "",
@@ -434,11 +520,16 @@ function renderServerEntryBuild(routesDir: string, eagerImports: string[]): stri
434
520
  'import { createFileShell, createPluginRuntime, createRequestHandler, startNodeServer } from "@moonsjs/vite/server";',
435
521
  'import config from "virtual:moons/config";',
436
522
  'import { routes, loadRoute } from "virtual:moons/routes";',
523
+ 'import { apiRoutes, apiMiddlewares, loadApi, loadApiMiddleware } from "virtual:moons/api";',
437
524
  ...eagerImports.map((specifier) => `import ${JSON.stringify(specifier)};`),
438
525
  "",
439
526
  "const plugins = createPluginRuntime({ config, routes, middlewares: config.middleware ?? [] });",
440
527
  "const handler = createRequestHandler({",
441
528
  " routes,",
529
+ " apiRoutes,",
530
+ " apiMiddlewares,",
531
+ " loadApi,",
532
+ " loadApiMiddleware,",
442
533
  " config,",
443
534
  ` routesDir: ${JSON.stringify(routesDir)},`,
444
535
  " loadRoute,",
@@ -456,6 +547,38 @@ interface ModuleGraphLike {
456
547
  invalidateModule(module: unknown): void;
457
548
  }
458
549
 
550
+ /** Entry D.5: runner prerender (dijalankan CLI setelah build server). */
551
+ function renderPrerenderEntry(
552
+ routesDir: string,
553
+ projectRoot: string,
554
+ canary: string | undefined,
555
+ ): string {
556
+ return [
557
+ "// Generated by @moons. Jangan diubah manual.",
558
+ 'import { createFileShell, createRequestHandler, runPrerender } from "@moonsjs/vite/server";',
559
+ 'import config from "virtual:moons/config";',
560
+ 'import { routes, loadRoute } from "virtual:moons/routes";',
561
+ "",
562
+ "const handler = createRequestHandler({",
563
+ " routes,",
564
+ " config,",
565
+ ` routesDir: ${JSON.stringify(routesDir)},`,
566
+ " loadRoute,",
567
+ " shell: createFileShell(),",
568
+ "});",
569
+ "",
570
+ "await runPrerender({",
571
+ " routes,",
572
+ ` routesDir: ${JSON.stringify(routesDir)},`,
573
+ " loadRoute,",
574
+ " handler,",
575
+ ` clientDir: ${JSON.stringify(resolvePath(projectRoot, "dist/client"))},`,
576
+ ...(canary === undefined ? [] : [` canary: ${JSON.stringify(canary)},`]),
577
+ "});",
578
+ "",
579
+ ].join("\n");
580
+ }
581
+
459
582
  function environmentName(context: unknown): string {
460
583
  const environment = (context as { environment?: { name?: string } } | undefined)?.environment;
461
584
  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
  }