@moonsjs/vite 0.1.3 → 0.1.4

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.
@@ -1,6 +1,8 @@
1
1
  import {
2
2
  MoonsError,
3
3
  composeMiddleware,
4
+ isNotFoundSignal,
5
+ isRedirectSignal,
4
6
  resolveRoute,
5
7
  routeKeyFromFile,
6
8
  runLoaders,
@@ -12,7 +14,7 @@ import {
12
14
  import { encodeSsrPayload, reviveError, serializeError } from "@moonsjs/core/serialize";
13
15
  import { renderToMarkup } from "@moonsjs/react/ssr";
14
16
  import type { ClientRouteEntry, RouteModuleShape } from "@moonsjs/react/internal";
15
- import { createRpcHandler, runWithContext } from "@moonsjs/server";
17
+ import { createRpcHandler, runWithContext, runWithRequestCache } from "@moonsjs/server";
16
18
  import { ERROR_ROUTE_KEY, NOT_FOUND_ROUTE_KEY } from "../manifest";
17
19
  import type { Shell } from "./shell";
18
20
 
@@ -28,6 +30,11 @@ export interface RequestHandlerOptions {
28
30
  /** Peta import() modul route, key = routeKeyFromFile. */
29
31
  loadRoute: Record<string, () => Promise<RouteModule>>;
30
32
  shell: Shell;
33
+ /**
34
+ * Dipanggil setelah `config.context` per request (Addendum B.4: extender
35
+ * `extendContext` plugin di-merge ke ctx).
36
+ */
37
+ onContext?: (ctx: Ctx) => void;
31
38
  }
32
39
 
33
40
  /**
@@ -63,6 +70,8 @@ export function createRequestHandler(
63
70
  const entry: ClientRouteEntry = { page: pageKey, layouts: keys.slice(0, -1) };
64
71
  // Addendum A.3: query string ikut ke payload agar server/client identik.
65
72
  const searchParams = Object.fromEntries(new URL(request.url).searchParams);
73
+ // Addendum B.1: eksekusi `meta()` page (data = hasil loader page itu sendiri).
74
+ const meta = await runRouteMeta(pageModule, data[pageKey], resolved.route.file);
66
75
 
67
76
  const markup = renderToMarkup(
68
77
  entry,
@@ -76,9 +85,10 @@ export function createRequestHandler(
76
85
  params: resolved.params,
77
86
  searchParams,
78
87
  data,
88
+ ...(meta === undefined ? {} : { meta }),
79
89
  });
80
90
 
81
- return htmlResponse(await shell(markup, payload, request.url), 200);
91
+ return htmlResponse(await shell(markup, payload, request.url, meta), 200);
82
92
  }
83
93
 
84
94
  /** §11.2: halaman `_404.tsx` kustom (standalone), fallback bawaan. */
@@ -159,6 +169,7 @@ export function createRequestHandler(
159
169
  // batas runtime ini. Tanpa cast, tsc app user yang memuat `@moonsjs/vite/server`
160
170
  // (mis. `createTestServer` §19.5) melihat `unknown` vs `Ctx` global app.
161
171
  const ctx = (await config.context(request)) as Ctx;
172
+ options.onContext?.(ctx);
162
173
 
163
174
  const respond = composeMiddleware(config.middleware ?? [], ctx, async () => {
164
175
  if (url.pathname.startsWith(RPC_PREFIX)) {
@@ -171,11 +182,21 @@ export function createRequestHandler(
171
182
  try {
172
183
  return await renderRoute(resolved, request, ctx);
173
184
  } catch (error) {
185
+ // Addendum B.2: sinyal routing dari loader/meta.
186
+ if (isRedirectSignal(error)) {
187
+ return new Response(null, {
188
+ status: error.status,
189
+ headers: { location: new URL(error.to, request.url).toString() },
190
+ });
191
+ }
192
+ if (isNotFoundSignal(error)) {
193
+ return await notFoundResponse(request);
194
+ }
174
195
  return await routeErrorResponse(error, request);
175
196
  }
176
197
  });
177
198
 
178
- return await runWithContext(ctx, () => respond(request));
199
+ return await runWithRequestCache(() => runWithContext(ctx, () => respond(request)));
179
200
  } catch (error) {
180
201
  return internalErrorResponse(error);
181
202
  }
@@ -202,6 +223,40 @@ async function loadModules(
202
223
  );
203
224
  }
204
225
 
226
+ async function runRouteMeta(
227
+ pageModule: RouteModule,
228
+ pageData: unknown,
229
+ file: string,
230
+ ): Promise<Record<string, unknown> | undefined> {
231
+ const meta = (pageModule as { meta?: unknown }).meta;
232
+ if (meta === undefined) {
233
+ return undefined;
234
+ }
235
+ if (typeof meta !== "function") {
236
+ throw new MoonsError({
237
+ code: "E_INTERNAL",
238
+ file,
239
+ message: "export `meta` harus berupa fungsi `({ data }) => RouteMeta`",
240
+ details: "Bentuk `meta` mengikuti PRD §11.2 (Addendum B.1).",
241
+ suggestions: [
242
+ "Ubah menjadi `export const meta = ({ data }) => ({ title: \"...\" })`",
243
+ "Atau hapus export `meta`",
244
+ ],
245
+ });
246
+ }
247
+ const result = await (meta as (args: { data: unknown }) => unknown)({ data: pageData });
248
+ if (result === null || typeof result !== "object" || Array.isArray(result)) {
249
+ throw new MoonsError({
250
+ code: "E_INTERNAL",
251
+ file,
252
+ message: "`meta()` harus mengembalikan objek RouteMeta",
253
+ details: "Nilai yang dikembalikan dipakai untuk <title>/description/og (Addendum B.1).",
254
+ suggestions: ["Kembalikan objek, mis. `({ title: \"Halaman\" })`"],
255
+ });
256
+ }
257
+ return result as Record<string, unknown>;
258
+ }
259
+
205
260
  function internalErrorResponse(error: unknown, options: { log?: boolean } = {}): Response {
206
261
  if (options.log !== false) {
207
262
  console.error("[moons] request error:", error);
@@ -10,3 +10,5 @@ export { createTestServer } from "./test-server";
10
10
  export type { TestServer, TestServerOptions } from "./test-server";
11
11
  export { sendNodeResponse, toWebRequest } from "./node";
12
12
  export { installDevMiddleware } from "./dev";
13
+ export { createPluginRuntime } from "./plugins";
14
+ export type { PluginRuntime, PluginRuntimeOptions } from "./plugins";
@@ -2,6 +2,7 @@ import { existsSync, readFileSync, statSync } from "node:fs";
2
2
  import { createServer, type Server } from "node:http";
3
3
  import { extname, join, resolve, sep } from "node:path";
4
4
  import { sendNodeResponse, toWebRequest } from "./node";
5
+ import type { PluginRuntime } from "./plugins";
5
6
  import { installGracefulShutdown } from "./shutdown";
6
7
 
7
8
  const MIME_TYPES: Record<string, string> = {
@@ -33,6 +34,8 @@ export interface NodeServerOptions {
33
34
  handleSignals?: boolean;
34
35
  /** Batas drain shutdown (default 10_000 ms). */
35
36
  shutdownTimeoutMs?: number;
37
+ /** Lifecycle plugin (Addendum B.4) — onStart/onReady/onShutdown. */
38
+ plugins?: PluginRuntime;
36
39
  }
37
40
 
38
41
  /**
@@ -73,17 +76,28 @@ export function startNodeServer(
73
76
  })();
74
77
  });
75
78
 
76
- server.listen(port, () => {
77
- const address = server.address();
78
- const boundPort = typeof address === "object" && address !== null ? address.port : port;
79
- console.log(`[moons] server listening on http://localhost:${boundPort}`);
80
- });
79
+ // Addendum B.4: onStart sebelum listen, onReady setelah listen.
80
+ const listenAfterStart = (): void => {
81
+ server.listen(port, () => {
82
+ const address = server.address();
83
+ const boundPort = typeof address === "object" && address !== null ? address.port : port;
84
+ console.log(`[moons] server listening on http://localhost:${boundPort}`);
85
+ void options.plugins?.ready();
86
+ });
87
+ };
88
+ if (options.plugins !== undefined) {
89
+ void options.plugins.start().then(listenAfterStart);
90
+ } else {
91
+ listenAfterStart();
92
+ }
81
93
 
82
94
  if (options.handleSignals !== false) {
83
- const shutdown = installGracefulShutdown(
84
- server,
85
- options.shutdownTimeoutMs === undefined ? {} : { timeoutMs: options.shutdownTimeoutMs },
86
- );
95
+ const shutdown = installGracefulShutdown(server, {
96
+ ...(options.shutdownTimeoutMs === undefined ? {} : { timeoutMs: options.shutdownTimeoutMs }),
97
+ ...(options.plugins === undefined
98
+ ? {}
99
+ : { onShutdown: () => options.plugins?.shutdown() }),
100
+ });
87
101
  // `once`: sinyal kedua memakai perilaku default (proses langsung berhenti).
88
102
  process.once("SIGTERM", () => shutdown("SIGTERM"));
89
103
  process.once("SIGINT", () => shutdown("SIGINT"));
@@ -0,0 +1,85 @@
1
+ import { MOONS_VERSION, type App, type Middleware, type MoonsConfig, type RouteDefinition } from "@moonsjs/core";
2
+
3
+ /**
4
+ * Eksekusi lifecycle plugin (PRD §14.4 / Addendum B.4).
5
+ * Urutan: registrasi (addRoute per route → addMiddleware per middleware →
6
+ * extendContext(register)) → onStart → ready/onReady → shutdown/onShutdown.
7
+ */
8
+ export interface PluginRuntime {
9
+ readonly app: App;
10
+ /** Registrasi + `onStart` (sebelum server siap). */
11
+ start(): Promise<void>;
12
+ /** Tandai `app.ready = true` lalu panggil `onReady` (server siap). */
13
+ ready(): Promise<void>;
14
+ /** Panggil `onShutdown` (selama drain; error dilaporkan, tidak menggagalkan). */
15
+ shutdown(): Promise<void>;
16
+ /** Terapkan extender `extendContext` ke ctx request (dipanggil handler). */
17
+ applyExtensions(ctx: unknown): void;
18
+ }
19
+
20
+ export interface PluginRuntimeOptions {
21
+ config: MoonsConfig;
22
+ routes: RouteDefinition[];
23
+ /** Rantai middleware final (config.middleware). */
24
+ middlewares: Middleware[];
25
+ log?: (message: string) => void;
26
+ }
27
+
28
+ export function createPluginRuntime(options: PluginRuntimeOptions): PluginRuntime {
29
+ const log = options.log ?? ((message: string) => console.warn(message));
30
+ const plugins = options.config.plugins ?? [];
31
+ const state = { ready: false, version: MOONS_VERSION };
32
+ const app = state as App;
33
+ const extensions: Array<(ctx: unknown) => unknown> = [];
34
+ const register = (fn: (ctx: unknown) => unknown): void => {
35
+ extensions.push(fn);
36
+ };
37
+
38
+ // Fase registrasi (sinkron): addRoute → addMiddleware → extendContext.
39
+ for (const plugin of plugins) {
40
+ for (const route of options.routes) {
41
+ // Objek beku: hook bersifat observasi di v1 (lihat docs/decisions/0022).
42
+ const frozen = Object.freeze({
43
+ ...route,
44
+ params: [...route.params],
45
+ }) as RouteDefinition;
46
+ plugin.addRoute?.(frozen);
47
+ }
48
+ for (const middleware of options.middlewares) {
49
+ plugin.addMiddleware?.(middleware);
50
+ }
51
+ plugin.extendContext?.(register as never);
52
+ }
53
+
54
+ return {
55
+ app,
56
+ async start(): Promise<void> {
57
+ for (const plugin of plugins) {
58
+ await plugin.onStart?.(app);
59
+ }
60
+ },
61
+ async ready(): Promise<void> {
62
+ state.ready = true;
63
+ for (const plugin of plugins) {
64
+ await plugin.onReady?.(app);
65
+ }
66
+ },
67
+ async shutdown(): Promise<void> {
68
+ for (const plugin of plugins) {
69
+ try {
70
+ await plugin.onShutdown?.(app);
71
+ } catch (error) {
72
+ log(`[moons] plugin \`${plugin.name}\` gagal saat onShutdown: ${String(error)}`);
73
+ }
74
+ }
75
+ },
76
+ applyExtensions(ctx: unknown): void {
77
+ for (const extend of extensions) {
78
+ const result = extend(ctx);
79
+ if (result !== null && typeof result === "object" && !Array.isArray(result)) {
80
+ Object.assign(ctx as Record<string, unknown>, result);
81
+ }
82
+ }
83
+ },
84
+ };
85
+ }
@@ -3,7 +3,12 @@ import { existsSync, readFileSync } from "node:fs";
3
3
  import { join, resolve } from "node:path";
4
4
  import type { ViteDevServer } from "vite";
5
5
 
6
- export type Shell = (markup: string, payload: string, url: string) => string | Promise<string>;
6
+ export type Shell = (
7
+ markup: string,
8
+ payload: string,
9
+ url: string,
10
+ meta?: Record<string, unknown>,
11
+ ) => string | Promise<string>;
7
12
 
8
13
  export const ROOT_PLACEHOLDER = '<div id="root"></div>';
9
14
 
@@ -23,8 +28,13 @@ export const DEFAULT_TEMPLATE = [
23
28
  "",
24
29
  ].join("\n");
25
30
 
26
- /** Inject markup SSR + payload devalue ke shell HTML (PRD §8.2, §17). */
27
- export function injectShell(template: string, markup: string, payload: string): string {
31
+ /** Inject markup SSR + payload devalue ke shell HTML (PRD §8.2, §17, Addendum B.1). */
32
+ export function injectShell(
33
+ template: string,
34
+ markup: string,
35
+ payload: string,
36
+ meta?: Record<string, unknown>,
37
+ ): string {
28
38
  if (!template.includes(ROOT_PLACEHOLDER)) {
29
39
  throw new MoonsError({
30
40
  code: "E_INTERNAL",
@@ -37,7 +47,51 @@ export function injectShell(template: string, markup: string, payload: string):
37
47
  });
38
48
  }
39
49
  const dataScript = `<script id="__moons_data" type="application/x-moons">${payload}</script>`;
40
- return template.replace(ROOT_PLACEHOLDER, `<div id="root">${markup}</div>${dataScript}`);
50
+ const withRoot = template.replace(ROOT_PLACEHOLDER, `<div id="root">${markup}</div>${dataScript}`);
51
+ return applyMeta(withRoot, meta);
52
+ }
53
+
54
+ function escapeHtml(value: string): string {
55
+ return value
56
+ .replaceAll("&", "&amp;")
57
+ .replaceAll("<", "&lt;")
58
+ .replaceAll(">", "&gt;")
59
+ .replaceAll('"', "&quot;");
60
+ }
61
+
62
+ /**
63
+ * Terapkan meta route ke `<head>` (Addendum B.1): `<title>` + description/og.
64
+ * Tag yang sudah ada (mis. di index.html) ditimpa; yang belum ada disisipkan
65
+ * sebelum `</head>`.
66
+ */
67
+ function applyMeta(html: string, meta: Record<string, unknown> | undefined): string {
68
+ if (meta === undefined) {
69
+ return html;
70
+ }
71
+ let out = html;
72
+ if (typeof meta.title === "string") {
73
+ const title = `<title>${escapeHtml(meta.title)}</title>`;
74
+ out = /<title>[\s\S]*?<\/title>/.test(out)
75
+ ? out.replace(/<title>[\s\S]*?<\/title>/, title)
76
+ : out.replace("</head>", ` ${title}\n</head>`);
77
+ }
78
+
79
+ const tags: Array<{ attr: "name" | "property"; key: string; value: unknown }> = [
80
+ { attr: "name", key: "description", value: meta.description },
81
+ { attr: "property", key: "og:title", value: meta.title },
82
+ { attr: "property", key: "og:description", value: meta.description },
83
+ ];
84
+ for (const tag of tags) {
85
+ if (typeof tag.value !== "string") {
86
+ continue;
87
+ }
88
+ const rendered = `<meta ${tag.attr}="${tag.key}" content="${escapeHtml(tag.value)}" />`;
89
+ const pattern = new RegExp(`<meta[^>]*${tag.attr}="${tag.key}"[^>]*>`);
90
+ out = pattern.test(out)
91
+ ? out.replace(pattern, rendered)
92
+ : out.replace("</head>", ` ${rendered}\n</head>`);
93
+ }
94
+ return out;
41
95
  }
42
96
 
43
97
  /**
@@ -45,11 +99,11 @@ export function injectShell(template: string, markup: string, payload: string):
45
99
  * lalu lewatkan ke Vite `transformIndexHtml` (inject `/@vite/client`, dsb).
46
100
  */
47
101
  export function createDevShell(server: ViteDevServer, options: { templatePath: string }): Shell {
48
- return async (markup, payload, url) => {
102
+ return async (markup, payload, url, meta) => {
49
103
  const template = existsSync(options.templatePath)
50
104
  ? readFileSync(options.templatePath, "utf8")
51
105
  : DEFAULT_TEMPLATE;
52
- const html = injectShell(template, markup, payload);
106
+ const html = injectShell(template, markup, payload, meta);
53
107
  return await server.transformIndexHtml(url, html);
54
108
  };
55
109
  }
@@ -57,7 +111,7 @@ export function createDevShell(server: ViteDevServer, options: { templatePath: s
57
111
  /** Shell untuk produksi: baca `dist/client/index.html` sekali dari cwd. */
58
112
  export function createFileShell(options: { clientDir?: string } = {}): Shell {
59
113
  let cached: string | null = null;
60
- return async (markup, payload) => {
114
+ return async (markup, payload, _url, meta) => {
61
115
  if (cached === null) {
62
116
  const clientDir = resolve(options.clientDir ?? process.cwd(), "dist/client");
63
117
  const file = join(clientDir, "index.html");
@@ -72,6 +126,6 @@ export function createFileShell(options: { clientDir?: string } = {}): Shell {
72
126
  }
73
127
  cached = readFileSync(file, "utf8");
74
128
  }
75
- return injectShell(cached, markup, payload);
129
+ return injectShell(cached, markup, payload, meta);
76
130
  };
77
131
  }
@@ -6,6 +6,12 @@ export interface ShutdownOptions {
6
6
  log?: (message: string) => void;
7
7
  /** Injectable untuk test; default `process.exit`. */
8
8
  exit?: (code: number) => void;
9
+ /**
10
+ * Dipanggil setelah drain penuh, sebelum exit (Addendum B.4: onShutdown
11
+ * plugin). Berbagi batas waktu dengan drain; error dilaporkan, tidak
12
+ * menggagalkan shutdown.
13
+ */
14
+ onShutdown?: () => void | Promise<void>;
9
15
  }
10
16
 
11
17
  /**
@@ -50,13 +56,24 @@ export function installGracefulShutdown(
50
56
  // abaikan: fallback timeout tetap ada
51
57
  }
52
58
 
53
- server.close((error?: Error) => {
54
- clearTimeout(timer);
59
+ server.close(async (error?: Error) => {
55
60
  if (error !== undefined) {
61
+ clearTimeout(timer);
56
62
  log(`[moons] penutupan server gagal: ${error.message}`);
57
63
  exit(1);
58
64
  return;
59
65
  }
66
+ if (options.onShutdown !== undefined) {
67
+ // Tetap berbagi batas waktu dengan drain: timer di-ref kembali agar
68
+ // proses tidak keluar sebelum hook selesai.
69
+ (timer as { ref?: () => void }).ref?.();
70
+ try {
71
+ await options.onShutdown();
72
+ } catch (hookError) {
73
+ log(`[moons] onShutdown gagal: ${String(hookError)}`);
74
+ }
75
+ }
76
+ clearTimeout(timer);
60
77
  log("[moons] shutdown selesai (drain penuh)");
61
78
  exit(0);
62
79
  });
@@ -26,7 +26,8 @@ export function createTestServer(
26
26
  baseUrl = "http://localhost",
27
27
  ): TestServer {
28
28
  const shell: Shell =
29
- options.shell ?? ((markup, payload) => injectShell(DEFAULT_TEMPLATE, markup, payload));
29
+ options.shell ??
30
+ ((markup, payload, _url, meta) => injectShell(DEFAULT_TEMPLATE, markup, payload, meta));
30
31
 
31
32
  const handler = createRequestHandler({
32
33
  routes: options.routes,
@@ -1,14 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../src/server/shell.ts", "../src/server/node.ts", "../src/virtual-modules.ts", "../src/server/dev.ts", "../src/manifest.ts"],
4
- "sourcesContent": [
5
- "import { MoonsError } from \"@moonsjs/core\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { join, resolve } from \"node:path\";\nimport type { ViteDevServer } from \"vite\";\n\nexport type Shell = (markup: string, payload: string, url: string) => string | Promise<string>;\n\nexport const ROOT_PLACEHOLDER = '<div id=\"root\"></div>';\n\nexport const DEFAULT_TEMPLATE = [\n \"<!doctype html>\",\n '<html lang=\"id\">',\n \"<head>\",\n ' <meta charset=\"utf-8\" />',\n ' <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />',\n \" <title>Moons</title>\",\n \"</head>\",\n \"<body>\",\n ` ${ROOT_PLACEHOLDER}`,\n ' <script type=\"module\" src=\"/src/client/main.tsx\"></script>',\n \"</body>\",\n \"</html>\",\n \"\",\n].join(\"\\n\");\n\n/** Inject markup SSR + payload devalue ke shell HTML (PRD §8.2, §17). */\nexport function injectShell(template: string, markup: string, payload: string): string {\n if (!template.includes(ROOT_PLACEHOLDER)) {\n throw new MoonsError({\n code: \"E_INTERNAL\",\n message: `shell HTML tidak memuat \\`${ROOT_PLACEHOLDER}\\``,\n details: \"index.html aplikasi wajib memuat elemen root tersebut agar SSR bisa meng-inject markup.\",\n suggestions: [\n `Tambahkan ${ROOT_PLACEHOLDER} ke index.html`,\n \"Atau hapus index.html untuk memakai shell bawaan\",\n ],\n });\n }\n const dataScript = `<script id=\"__moons_data\" type=\"application/x-moons\">${payload}</script>`;\n return template.replace(ROOT_PLACEHOLDER, `<div id=\"root\">${markup}</div>${dataScript}`);\n}\n\n/**\n * Shell untuk dev: baca `index.html` dari disk setiap request (selalu segar)\n * lalu lewatkan ke Vite `transformIndexHtml` (inject `/@vite/client`, dsb).\n */\nexport function createDevShell(server: ViteDevServer, options: { templatePath: string }): Shell {\n return async (markup, payload, url) => {\n const template = existsSync(options.templatePath)\n ? readFileSync(options.templatePath, \"utf8\")\n : DEFAULT_TEMPLATE;\n const html = injectShell(template, markup, payload);\n return await server.transformIndexHtml(url, html);\n };\n}\n\n/** Shell untuk produksi: baca `dist/client/index.html` sekali dari cwd. */\nexport function createFileShell(options: { clientDir?: string } = {}): Shell {\n let cached: string | null = null;\n return async (markup, payload) => {\n if (cached === null) {\n const clientDir = resolve(options.clientDir ?? process.cwd(), \"dist/client\");\n const file = join(clientDir, \"index.html\");\n if (!existsSync(file)) {\n throw new MoonsError({\n code: \"E_INTERNAL\",\n file,\n message: \"`dist/client/index.html` tidak ditemukan\",\n details: \"Build client harus dijalankan sebelum server produksi dinyalakan (PRD §18).\",\n suggestions: [\"Jalankan `bun run build` lebih dulu\"],\n });\n }\n cached = readFileSync(file, \"utf8\");\n }\n return injectShell(cached, markup, payload);\n };\n}\n",
6
- "import type { IncomingMessage, ServerResponse } from \"node:http\";\n\nconst MAX_BODY_BYTES = 32 * 1024 * 1024; // batas defensif transport; limit RPC §7.3 tetap berlaku\n\n/** Konversi request Node → Web `Request` (body diteruskan untuk non-GET/HEAD, §7.3). */\nexport async function toWebRequest(req: IncomingMessage): Promise<Request> {\n const host = req.headers.host ?? \"localhost\";\n const url = `http://${host}${req.url ?? \"/\"}`;\n const method = req.method ?? \"GET\";\n\n const headers = new Headers();\n for (const [key, value] of Object.entries(req.headers)) {\n if (value === undefined || key === \"content-length\") {\n // content-length tidak disalin; fetch menghitung ulang dari body.\n continue;\n }\n if (Array.isArray(value)) {\n for (const item of value) {\n headers.append(key, item);\n }\n } else {\n headers.set(key, value);\n }\n }\n\n if (method === \"GET\" || method === \"HEAD\") {\n return new Request(url, { method, headers });\n }\n\n const body = await readBody(req);\n return new Request(url, { method, headers, body: new Uint8Array(body) });\n}\n\nfunction readBody(req: IncomingMessage): Promise<Buffer> {\n return new Promise((resolvePromise, reject) => {\n const chunks: Buffer[] = [];\n let size = 0;\n\n req.on(\"data\", (chunk: Buffer) => {\n size += chunk.length;\n if (size > MAX_BODY_BYTES) {\n req.destroy();\n reject(new Error(\"request body melebihi batas transport\"));\n return;\n }\n chunks.push(chunk);\n });\n req.on(\"end\", () => resolvePromise(Buffer.concat(chunks)));\n req.on(\"error\", (error) => reject(error));\n });\n}\n\n/** Tulis Web `Response` ke `ServerResponse` Node. */\nexport async function sendNodeResponse(res: ServerResponse, response: Response): Promise<void> {\n res.statusCode = response.status;\n response.headers.forEach((value, key) => {\n res.setHeader(key, value);\n });\n const body = Buffer.from(await response.arrayBuffer());\n res.end(body);\n}\n",
7
- "/** Nama virtual module PRD §9.4. Semua di-resolve ke id `\\0`-prefix. */\nexport const VIRTUAL_MODULES = {\n routes: \"virtual:moons/routes\",\n config: \"virtual:moons/config\",\n clientEntry: \"virtual:moons/client-entry\",\n serverEntry: \"virtual:moons/server-entry\",\n} as const;\n\nexport type VirtualModuleName = (typeof VIRTUAL_MODULES)[keyof typeof VIRTUAL_MODULES];\n\nexport function isVirtualModuleId(id: string): id is VirtualModuleName {\n const clean = id.startsWith(\"\\0\") ? id.slice(1) : id;\n return (Object.values(VIRTUAL_MODULES) as string[]).includes(clean);\n}\n\nexport function toNullPrefixed(id: VirtualModuleName): `\\0${string}` {\n return `\\0${id}`;\n}\n\nexport function fromNullPrefixed(id: string): VirtualModuleName | null {\n const clean = id.startsWith(\"\\0\") ? id.slice(1) : id;\n return isVirtualModuleId(clean) ? (clean as VirtualModuleName) : null;\n}\n",
8
- "import type { IncomingMessage, ServerResponse } from \"node:http\";\nimport type { ViteDevServer } from \"vite\";\nimport { VIRTUAL_MODULES } from \"../virtual-modules\";\nimport type { Shell } from \"./shell\";\nimport { createDevShell } from \"./shell\";\nimport { sendNodeResponse, toWebRequest } from \"./node\";\n\ntype DevServerEntry = {\n createDevHandler: (shell: Shell) => (request: Request) => Promise<Response>;\n};\n\n/**\n * Middleware SSR dev: dipasang sebelum middleware internal Vite, menangani\n * request HTML dan RPC serverFn (`/_moons/fn/*`) — modul/aset tetap Vite.\n */\nexport function installDevMiddleware(\n server: ViteDevServer,\n options: { templatePath: string },\n): void {\n server.middlewares.use((req, res, next) => {\n if (!shouldHandle(req)) {\n next();\n return;\n }\n void handleRequest(server, options)(req, res).catch((error: unknown) => {\n next(error as Error);\n });\n });\n}\n\nfunction handleRequest(server: ViteDevServer, options: { templatePath: string }) {\n return async (req: IncomingMessage, res: ServerResponse): Promise<void> => {\n const entry = (await server.ssrLoadModule(VIRTUAL_MODULES.serverEntry)) as DevServerEntry;\n const handler = entry.createDevHandler(createDevShell(server, options));\n const response = await handler(await toWebRequest(req));\n await sendNodeResponse(res, response);\n };\n}\n\nfunction shouldHandle(req: IncomingMessage): boolean {\n const path = new URL(req.url ?? \"/\", \"http://localhost\").pathname;\n\n // RPC serverFn (semua method): dulu hanya GET/HEAD HTML yang dirutekan,\n // sehingga `POST /_moons/fn/<hash>` jatuh ke Vite (\"Cannot POST\", 404) dan\n // serverFn tidak bisa dipakai di dev (temuan eval T2 ronde 2; decisions/0016).\n if (path.startsWith(\"/_moons/fn/\")) {\n return true;\n }\n\n if (req.method !== \"GET\" && req.method !== \"HEAD\") {\n return false;\n }\n if (path.startsWith(\"/@\") || path.startsWith(\"/node_modules/\") || path.startsWith(\"/__\")) {\n return false;\n }\n const lastSegment = path.split(\"/\").pop() ?? \"\";\n if (lastSegment.includes(\".\")) {\n return false;\n }\n const accept = req.headers.accept ?? \"\";\n return accept.includes(\"text/html\") || accept.includes(\"*/*\");\n}\n",
9
- "import { findLayouts, routeKeyFromFile, type RouteDefinition } from \"@moonsjs/core\";\nimport { relative, sep } from \"node:path\";\n\nexport interface ClientManifestEntry {\n page: string;\n layouts: string[];\n}\n\n/** Key manifest untuk halaman kustom `_404` dan `_error` (§11.2). */\nexport const NOT_FOUND_ROUTE_KEY = \"__moons/404\";\nexport const ERROR_ROUTE_KEY = \"__moons/error\";\n\n/**\n * Manifest route untuk client (PRD §8.2): key = key route file\n * (routeKeyFromFile), nilai = page + layout chain dari root ke leaf.\n */\nexport function buildClientManifest(\n routes: RouteDefinition[],\n routesDir: string,\n): Record<string, ClientManifestEntry> {\n const manifest: Record<string, ClientManifestEntry> = {};\n for (const route of routes) {\n if (route.kind !== \"page\") {\n continue;\n }\n const key = routeKeyFromFile(routesDir, route.file);\n manifest[key] = {\n page: key,\n layouts: findLayouts(route, routes).map((layout) => routeKeyFromFile(routesDir, layout.file)),\n };\n }\n\n const notFound = routes.find((route) => route.kind === \"404\");\n if (notFound !== undefined) {\n manifest[NOT_FOUND_ROUTE_KEY] = { page: routeKeyFromFile(routesDir, notFound.file), layouts: [] };\n }\n const errorRoute = routes.find((route) => route.kind === \"error\");\n if (errorRoute !== undefined) {\n manifest[ERROR_ROUTE_KEY] = { page: routeKeyFromFile(routesDir, errorRoute.file), layouts: [] };\n }\n\n return manifest;\n}\n\n/**\n * Isi `virtual:moons/routes`: daftar RouteDefinition, manifest client, dan\n * peta import() dinamis per file route (specifier root-relative).\n */\nexport function renderRoutesModule(\n routes: RouteDefinition[],\n routesDir: string,\n projectRoot: string,\n): string {\n const manifest = buildClientManifest(routes, routesDir);\n\n const loadEntries = routes.map((route) => {\n const key = routeKeyFromFile(routesDir, route.file);\n return ` ${JSON.stringify(key)}: () => import(${JSON.stringify(rootSpecifier(projectRoot, route.file))}),`;\n });\n\n return [\n \"// Generated by @moons. Jangan diubah manual.\",\n `export const routes = ${JSON.stringify(routes, null, 2)};`,\n \"\",\n `export const clientManifest = ${JSON.stringify(manifest, null, 2)};`,\n \"\",\n \"export const loadRoute = {\",\n ...loadEntries,\n \"};\",\n \"\",\n ].join(\"\\n\");\n}\n\n/** Path file absolut project → specifier root-relative yang bisa di-resolve Vite. */\nexport function rootSpecifier(projectRoot: string, file: string): string {\n let rel = relative(projectRoot, file);\n rel = sep === \"/\" ? rel : rel.split(sep).join(\"/\");\n return rel.startsWith(\"/\") ? rel : `/${rel}`;\n}\n"
10
- ],
11
- "mappings": "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",
12
- "debugId": "6E6FBA2917FCE4BD64756E2164756E21",
13
- "names": []
14
- }