@moonsjs/vite 0.1.18 → 0.2.1

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/src/server/dev.ts CHANGED
@@ -133,6 +133,11 @@ function shouldHandle(req: IncomingMessage, apiPrefix: string | undefined): bool
133
133
  return true;
134
134
  }
135
135
 
136
+ // Addendum O: health check (semua method) — paritas 405 dengan produksi.
137
+ if (path === "/_moons/health") {
138
+ return true;
139
+ }
140
+
136
141
  // Addendum D.1: endpoint `src/api` (semua method) dirutekan ke runtime.
137
142
  if (apiPrefix !== undefined && (path === apiPrefix || path.startsWith(`${apiPrefix}/`))) {
138
143
  return true;
@@ -21,7 +21,15 @@ import {
21
21
  } from "@moonsjs/core";
22
22
  import { encodeSsrPayload, reviveError, serializeError } from "@moonsjs/core/serialize";
23
23
  import { API_METHODS, resolveApiRoute } from "@moonsjs/core";
24
- import { renderToMarkup } from "@moonsjs/react/ssr";
24
+ import type { renderToMarkup as RenderToMarkupFn } from "@moonsjs/react/ssr";
25
+
26
+ type RenderMarkup = typeof RenderToMarkupFn;
27
+
28
+ /** Default: bare dynamic import (bundlers keep this resolvable). */
29
+ async function defaultRenderer(): Promise<RenderMarkup> {
30
+ const mod = (await import("@moonsjs/react/ssr")) as { renderToMarkup: RenderMarkup };
31
+ return mod.renderToMarkup;
32
+ }
25
33
  import type { ClientRouteEntry, RouteModuleShape } from "@moonsjs/react/internal";
26
34
  import { createRpcHandler, runWithContext, runWithRequestCache, runWithResponseCollector, takeResponseMutations } from "@moonsjs/server";
27
35
  import { NOT_FOUND_ROUTE_KEY } from "../manifest";
@@ -59,6 +67,12 @@ export interface RequestHandlerOptions {
59
67
  apiMiddlewares?: ApiMiddlewareDefinition[];
60
68
  /** Peta import() `_middleware.ts` pohon api, key = file path. */
61
69
  loadApiMiddleware?: Record<string, () => Promise<unknown>>;
70
+ /**
71
+ * Addendum O: sumber `renderToMarkup`. Test server memakai loader ini untuk
72
+ * memuat renderer dari `node_modules` app (Bun meresolusi paket registry dari
73
+ * cache; peer `react-dom` tidak ditemukan dari sana).
74
+ */
75
+ renderer?: () => Promise<RenderMarkup>;
62
76
  }
63
77
 
64
78
  /**
@@ -67,7 +81,15 @@ export interface RequestHandlerOptions {
67
81
  */
68
82
  export function createRequestHandler(
69
83
  options: RequestHandlerOptions,
70
- ): (request: Request) => Promise<Response> { const { routes, config, routesDir, loadRoute, shell } = options;
84
+ ): (request: Request) => Promise<Response> {
85
+ let renderMarkup: RenderMarkup | null = null;
86
+ const getRenderMarkup = async (): Promise<RenderMarkup> => {
87
+ if (renderMarkup === null) {
88
+ renderMarkup = await (options.renderer ?? defaultRenderer)();
89
+ }
90
+ return renderMarkup;
91
+ };
92
+ const { routes, config, routesDir, loadRoute, shell } = options;
71
93
  const rpc = createRpcHandler(
72
94
  config.security === undefined ? {} : { security: config.security },
73
95
  );
@@ -98,12 +120,15 @@ export function createRequestHandler(
98
120
  // Addendum B.1: eksekusi `meta()` page (data = hasil loader page itu sendiri).
99
121
  const meta = await runRouteMeta(pageModule, data[pageKey], resolved.route.file);
100
122
 
101
- const markup = renderToMarkup(
123
+ const render = await getRenderMarkup();
124
+ const markup = render(
102
125
  entry,
103
126
  { page: pageModule as RouteModuleShape, layouts: modules.slice(0, -1) as RouteModuleShape[] },
104
127
  resolved.params,
105
128
  data,
106
129
  searchParams,
130
+ undefined,
131
+ config.trailingSlash,
107
132
  );
108
133
  const payload = encodeSsrPayload({
109
134
  key: pageKey,
@@ -116,7 +141,10 @@ export function createRequestHandler(
116
141
  // A1: pindai payload SSR (dev saja — `payloadScan` tidak diisi production).
117
142
  warnPayloadFindings(payload, options.payloadScan, warnedPayloadLabels);
118
143
 
119
- return htmlResponse(await shell(markup, payload, request.url, meta), 200);
144
+ return htmlResponse(
145
+ withTrailingSlashMeta(await shell(markup, payload, request.url, meta), config.trailingSlash),
146
+ 200,
147
+ );
120
148
  }
121
149
 
122
150
  /** Addendum D.1: dispatch endpoint `src/api` (405/HEAD/OPTIONS + error). */
@@ -217,25 +245,28 @@ export function createRequestHandler(
217
245
  const key = routeKeyFromFile(routesDir, notFoundRoute.file);
218
246
  const [module] = await loadModules([key], loadRoute);
219
247
  const payload = encodeSsrPayload({ key: NOT_FOUND_ROUTE_KEY, params: {}, data: {} });
220
- const markup = renderToMarkup(
248
+ const render = await getRenderMarkup();
249
+ const markup = render(
221
250
  { page: key, layouts: [] },
222
251
  { page: module as RouteModuleShape, layouts: [] },
223
252
  {},
224
253
  {},
254
+ {},
255
+ undefined,
256
+ config.trailingSlash,
257
+ );
258
+ return htmlResponse(
259
+ withTrailingSlashMeta(await shell(markup, payload, request.url), config.trailingSlash),
260
+ 404,
225
261
  );
226
- return htmlResponse(await shell(markup, payload, request.url), 404);
227
262
  } catch (error) {
228
263
  console.error("[moons] failed to render custom _404:", error);
229
264
  }
230
265
  }
231
266
 
232
267
  const payload = encodeSsrPayload({ key: "", params: {}, data: {} });
233
- const html = await shell(
234
- "<h1>404</h1><p>Page not found.</p>",
235
- payload,
236
- request.url,
237
- );
238
- return htmlResponse(html, 404);
268
+ const html = await shell("<h1>404</h1><p>Page not found.</p>", payload, request.url);
269
+ return htmlResponse(withTrailingSlashMeta(html, config.trailingSlash), 404);
239
270
  }
240
271
 
241
272
  /**
@@ -265,15 +296,20 @@ export function createRequestHandler(
265
296
  data: {},
266
297
  ...(serialized === undefined ? {} : { error: serialized }),
267
298
  });
268
- const markup = renderToMarkup(
299
+ const render = await getRenderMarkup();
300
+ const markup = render(
269
301
  { page: key, layouts: [] },
270
302
  { page: module as RouteModuleShape, layouts: [] },
271
303
  {},
272
304
  {},
273
305
  {},
274
306
  serialized === undefined ? undefined : reviveError(serialized),
307
+ config.trailingSlash,
308
+ );
309
+ return htmlResponse(
310
+ withTrailingSlashMeta(await shell(markup, payload, request.url), config.trailingSlash),
311
+ 500,
275
312
  );
276
- return htmlResponse(await shell(markup, payload, request.url), 500);
277
313
  } catch (renderError) {
278
314
  console.error("[moons] failed to render custom _error:", renderError);
279
315
  }
@@ -617,6 +653,17 @@ function internalErrorResponse(error: unknown, options: { log?: boolean } = {}):
617
653
  );
618
654
  }
619
655
 
656
+ /** Addendum O: expose the trailing-slash policy to the client runtime. */
657
+ function withTrailingSlashMeta(html: string, policy?: "never" | "always"): string {
658
+ if (policy === undefined) {
659
+ return html;
660
+ }
661
+ return html.replace(
662
+ "</head>",
663
+ ` <meta name="moons:trailing-slash" content="${policy}" />\n</head>`,
664
+ );
665
+ }
666
+
620
667
  function htmlResponse(html: string, status: number): Response {
621
668
  return new Response(html, {
622
669
  status,
@@ -1,12 +1,51 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { pathToFileURL } from "node:url";
1
4
  import type { MoonsConfig, RouteDefinition, RouteModule } from "@moonsjs/core";
2
- import { createRequestHandler } from "./handler";
5
+ import type { renderToMarkup as RenderToMarkupFn } from "@moonsjs/react/ssr";
6
+ import { createRequestHandler, type RequestHandlerOptions } from "./handler";
3
7
  import { DEFAULT_TEMPLATE, injectShell, type Shell } from "./shell";
4
8
 
9
+ type RenderMarkup = typeof RenderToMarkupFn;
10
+
11
+ /**
12
+ * Addendum O: resolve the SSR renderer from the app's own `node_modules` first.
13
+ * Bun resolves registry packages through its install cache; peer imports
14
+ * (`react-dom/server`) are not reachable from there, but they are from the app.
15
+ */
16
+ async function loadRenderer(candidates: ReadonlyArray<string>): Promise<RenderMarkup> {
17
+ for (const root of candidates) {
18
+ for (const relative of [
19
+ "node_modules/@moonsjs/react/dist/ssr.js",
20
+ "node_modules/@moonsjs/react/dist/ssr.mjs",
21
+ "node_modules/@moonsjs/react/src/ssr.ts",
22
+ ]) {
23
+ const file = join(root, relative);
24
+ if (!existsSync(file)) {
25
+ continue;
26
+ }
27
+ const mod = (await import(pathToFileURL(file).href)) as {
28
+ renderToMarkup?: RenderMarkup;
29
+ };
30
+ if (typeof mod.renderToMarkup === "function") {
31
+ return mod.renderToMarkup;
32
+ }
33
+ }
34
+ }
35
+ const fallback = (await import("@moonsjs/react/ssr")) as { renderToMarkup: RenderMarkup };
36
+ return fallback.renderToMarkup;
37
+ }
38
+
5
39
  export interface TestServerOptions {
6
40
  routes: RouteDefinition[];
7
41
  config: MoonsConfig;
8
42
  routesDir: string;
9
43
  loadRoute: Record<string, () => Promise<RouteModule>>;
44
+ /** Endpoint `src/api` (Addendum O) — hasil scan app. */
45
+ apiRoutes?: RequestHandlerOptions["apiRoutes"];
46
+ loadApi?: RequestHandlerOptions["loadApi"];
47
+ apiMiddlewares?: RequestHandlerOptions["apiMiddlewares"];
48
+ loadApiMiddleware?: RequestHandlerOptions["loadApiMiddleware"];
10
49
  /** Default: shell in-memory dari `DEFAULT_TEMPLATE` (tanpa file). */
11
50
  shell?: Shell;
12
51
  }
@@ -34,6 +73,14 @@ export function createTestServer(
34
73
  config: options.config,
35
74
  routesDir: options.routesDir,
36
75
  loadRoute: options.loadRoute,
76
+ ...(options.apiRoutes === undefined ? {} : { apiRoutes: options.apiRoutes }),
77
+ ...(options.loadApi === undefined ? {} : { loadApi: options.loadApi }),
78
+ ...(options.apiMiddlewares === undefined ? {} : { apiMiddlewares: options.apiMiddlewares }),
79
+ ...(options.loadApiMiddleware === undefined
80
+ ? {}
81
+ : { loadApiMiddleware: options.loadApiMiddleware }),
82
+ renderer: () =>
83
+ loadRenderer([process.cwd(), resolve(options.routesDir, "..", "..")]),
37
84
  shell,
38
85
  });
39
86
 
@@ -0,0 +1,5 @@
1
+ import { registerMoonsTestPlugin } from "./testing";
2
+
3
+ // Marker so fixture tests can skip cleanly under a plain `bun test` run.
4
+ (globalThis as { __moonsTestPreload?: boolean }).__moonsTestPreload = true;
5
+ registerMoonsTestPlugin();
package/src/testing.ts ADDED
@@ -0,0 +1,34 @@
1
+ import { readFileSync } from "node:fs";
2
+ import type { BunPlugin } from "bun";
3
+ import { analyzeServerFile, injectServerFnMeta } from "./transform";
4
+
5
+ /**
6
+ * Annotate a `*.server.ts` module with the transform-generated serverFn meta
7
+ * (`__hash`, `__file`, `__line`, `__exportName`) — the same injection the
8
+ * build performs. Used by `moons test` so tests can import server modules.
9
+ */
10
+ export function transformServerModuleForTest(
11
+ file: string,
12
+ projectRoot = process.cwd(),
13
+ ): string {
14
+ const source = readFileSync(file, "utf8");
15
+ const analysis = analyzeServerFile(source, file, projectRoot);
16
+ return injectServerFnMeta(source, analysis, projectRoot);
17
+ }
18
+
19
+ /** Bun runtime plugin that applies the serverFn meta before tests load code. */
20
+ export function moonsTestPlugin(projectRoot = process.cwd()): BunPlugin {
21
+ return {
22
+ name: "moons-test-server-meta",
23
+ setup(build) {
24
+ build.onLoad({ filter: /\.server\.(ts|tsx)$/ }, (args) => ({
25
+ contents: transformServerModuleForTest(args.path, projectRoot),
26
+ loader: args.path.endsWith(".tsx") ? "tsx" : "ts",
27
+ }));
28
+ },
29
+ };
30
+ }
31
+
32
+ export function registerMoonsTestPlugin(projectRoot = process.cwd()): void {
33
+ Bun.plugin(moonsTestPlugin(projectRoot));
34
+ }