@moonsjs/vite 0.1.17 → 0.2.0
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/dist/{index-v5sbd3az.js → index-0agjty3z.js} +21 -18
- package/dist/index-0agjty3z.js.map +15 -0
- package/dist/index-f8w073cm.js +30 -0
- package/dist/index-f8w073cm.js.map +10 -0
- package/dist/{index-snbxks72.js → index-tbph0859.js} +80 -80
- package/dist/{index-snbxks72.js.map → index-tbph0859.js.map} +4 -4
- package/dist/index.js +33 -33
- package/dist/index.js.map +4 -4
- package/dist/server/handler.d.ts.map +1 -1
- package/dist/server/index.js +104 -93
- package/dist/server/index.js.map +10 -10
- package/dist/server/test-server.d.ts +6 -0
- package/dist/server/test-server.d.ts.map +1 -1
- package/dist/testing-preload.d.ts +2 -0
- package/dist/testing-preload.d.ts.map +1 -0
- package/dist/testing-preload.js +10 -0
- package/dist/testing-preload.js.map +10 -0
- package/dist/testing.d.ts +11 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +13 -0
- package/dist/testing.js.map +9 -0
- package/dist/transform.js +1 -1
- package/package.json +15 -5
- package/src/api.ts +14 -14
- package/src/devtools.ts +5 -5
- package/src/manifest.ts +2 -2
- package/src/plugin.ts +21 -21
- package/src/server/dev.ts +6 -1
- package/src/server/handler.ts +69 -47
- package/src/server/isr.ts +5 -5
- package/src/server/node-server.ts +6 -6
- package/src/server/node.ts +2 -2
- package/src/server/plugins.ts +1 -1
- package/src/server/prerender.ts +29 -29
- package/src/server/shell.ts +8 -8
- package/src/server/shutdown.ts +5 -5
- package/src/server/storage.ts +1 -1
- package/src/server/test-server.ts +12 -1
- package/src/server-only.ts +7 -7
- package/src/testing-preload.ts +5 -0
- package/src/testing.ts +34 -0
- package/src/transform.ts +76 -76
- package/dist/index-v5sbd3az.js.map +0 -15
package/src/server/handler.ts
CHANGED
|
@@ -104,6 +104,8 @@ export function createRequestHandler(
|
|
|
104
104
|
resolved.params,
|
|
105
105
|
data,
|
|
106
106
|
searchParams,
|
|
107
|
+
undefined,
|
|
108
|
+
config.trailingSlash,
|
|
107
109
|
);
|
|
108
110
|
const payload = encodeSsrPayload({
|
|
109
111
|
key: pageKey,
|
|
@@ -116,7 +118,10 @@ export function createRequestHandler(
|
|
|
116
118
|
// A1: pindai payload SSR (dev saja — `payloadScan` tidak diisi production).
|
|
117
119
|
warnPayloadFindings(payload, options.payloadScan, warnedPayloadLabels);
|
|
118
120
|
|
|
119
|
-
return htmlResponse(
|
|
121
|
+
return htmlResponse(
|
|
122
|
+
withTrailingSlashMeta(await shell(markup, payload, request.url, meta), config.trailingSlash),
|
|
123
|
+
200,
|
|
124
|
+
);
|
|
120
125
|
}
|
|
121
126
|
|
|
122
127
|
/** Addendum D.1: dispatch endpoint `src/api` (405/HEAD/OPTIONS + error). */
|
|
@@ -132,9 +137,9 @@ export function createRequestHandler(
|
|
|
132
137
|
new MoonsError({
|
|
133
138
|
code: "E_API_HANDLER_INVALID",
|
|
134
139
|
file: route.file,
|
|
135
|
-
message: `
|
|
136
|
-
details: "
|
|
137
|
-
suggestions: ["
|
|
140
|
+
message: `API module \`${route.path}\` could not be loaded`,
|
|
141
|
+
details: "The `loadApi` map does not contain this route (build/dev out of sync).",
|
|
142
|
+
suggestions: ["Rebuild / restart the dev server"],
|
|
138
143
|
}),
|
|
139
144
|
route,
|
|
140
145
|
);
|
|
@@ -151,9 +156,9 @@ export function createRequestHandler(
|
|
|
151
156
|
new MoonsError({
|
|
152
157
|
code: "E_API_HANDLER_INVALID",
|
|
153
158
|
file: route.file,
|
|
154
|
-
message: "
|
|
155
|
-
details: `
|
|
156
|
-
suggestions: ["Export `GET`/`POST`/…
|
|
159
|
+
message: "API module does not export a handler object",
|
|
160
|
+
details: `Module \`${route.path}\` must export method handlers.`,
|
|
161
|
+
suggestions: ["Export `GET`/`POST`/… as `(args) => Response` functions"],
|
|
157
162
|
}),
|
|
158
163
|
route,
|
|
159
164
|
);
|
|
@@ -194,9 +199,9 @@ export function createRequestHandler(
|
|
|
194
199
|
new MoonsError({
|
|
195
200
|
code: "E_API_HANDLER_INVALID",
|
|
196
201
|
file: route.file,
|
|
197
|
-
message: `handler \`${requestMethod}\`
|
|
198
|
-
details: "
|
|
199
|
-
suggestions: ["
|
|
202
|
+
message: `handler \`${requestMethod}\` did not return a \`Response\``,
|
|
203
|
+
details: "API handlers must return `new Response(...)`.",
|
|
204
|
+
suggestions: ["Return `new Response(body, { status, headers })`"],
|
|
200
205
|
}),
|
|
201
206
|
route,
|
|
202
207
|
);
|
|
@@ -222,20 +227,22 @@ export function createRequestHandler(
|
|
|
222
227
|
{ page: module as RouteModuleShape, layouts: [] },
|
|
223
228
|
{},
|
|
224
229
|
{},
|
|
230
|
+
{},
|
|
231
|
+
undefined,
|
|
232
|
+
config.trailingSlash,
|
|
233
|
+
);
|
|
234
|
+
return htmlResponse(
|
|
235
|
+
withTrailingSlashMeta(await shell(markup, payload, request.url), config.trailingSlash),
|
|
236
|
+
404,
|
|
225
237
|
);
|
|
226
|
-
return htmlResponse(await shell(markup, payload, request.url), 404);
|
|
227
238
|
} catch (error) {
|
|
228
|
-
console.error("[moons]
|
|
239
|
+
console.error("[moons] failed to render custom _404:", error);
|
|
229
240
|
}
|
|
230
241
|
}
|
|
231
242
|
|
|
232
243
|
const payload = encodeSsrPayload({ key: "", params: {}, data: {} });
|
|
233
|
-
const html = await shell(
|
|
234
|
-
|
|
235
|
-
payload,
|
|
236
|
-
request.url,
|
|
237
|
-
);
|
|
238
|
-
return htmlResponse(html, 404);
|
|
244
|
+
const html = await shell("<h1>404</h1><p>Page not found.</p>", payload, request.url);
|
|
245
|
+
return htmlResponse(withTrailingSlashMeta(html, config.trailingSlash), 404);
|
|
239
246
|
}
|
|
240
247
|
|
|
241
248
|
/**
|
|
@@ -272,10 +279,14 @@ export function createRequestHandler(
|
|
|
272
279
|
{},
|
|
273
280
|
{},
|
|
274
281
|
serialized === undefined ? undefined : reviveError(serialized),
|
|
282
|
+
config.trailingSlash,
|
|
283
|
+
);
|
|
284
|
+
return htmlResponse(
|
|
285
|
+
withTrailingSlashMeta(await shell(markup, payload, request.url), config.trailingSlash),
|
|
286
|
+
500,
|
|
275
287
|
);
|
|
276
|
-
return htmlResponse(await shell(markup, payload, request.url), 500);
|
|
277
288
|
} catch (renderError) {
|
|
278
|
-
console.error("[moons]
|
|
289
|
+
console.error("[moons] failed to render custom _error:", renderError);
|
|
279
290
|
}
|
|
280
291
|
}
|
|
281
292
|
|
|
@@ -292,9 +303,9 @@ export function createRequestHandler(
|
|
|
292
303
|
throw new MoonsError({
|
|
293
304
|
code: "E_MIDDLEWARE_INVALID",
|
|
294
305
|
file: definition.file,
|
|
295
|
-
message: "middleware
|
|
296
|
-
details: "
|
|
297
|
-
suggestions: ["Restart dev server /
|
|
306
|
+
message: "directory middleware could not be loaded",
|
|
307
|
+
details: "The `loadRoute` map does not contain this `_middleware.ts`.",
|
|
308
|
+
suggestions: ["Restart the dev server / rebuild"],
|
|
298
309
|
});
|
|
299
310
|
}
|
|
300
311
|
const module = (await load()) as { default?: unknown };
|
|
@@ -303,8 +314,8 @@ export function createRequestHandler(
|
|
|
303
314
|
throw new MoonsError({
|
|
304
315
|
code: "E_MIDDLEWARE_INVALID",
|
|
305
316
|
file: definition.file,
|
|
306
|
-
message: "`_middleware.ts`
|
|
307
|
-
details: "
|
|
317
|
+
message: "`_middleware.ts` does not default-export a function",
|
|
318
|
+
details: "Directory middleware follows the `(args) => Promise<Response>` shape.",
|
|
308
319
|
suggestions: ["Export default `async ({ req, ctx, next }) => await next()`"],
|
|
309
320
|
});
|
|
310
321
|
}
|
|
@@ -324,9 +335,9 @@ export function createRequestHandler(
|
|
|
324
335
|
throw new MoonsError({
|
|
325
336
|
code: "E_MIDDLEWARE_INVALID",
|
|
326
337
|
file: definition.file,
|
|
327
|
-
message: "middleware
|
|
328
|
-
details: "
|
|
329
|
-
suggestions: ["Restart dev server /
|
|
338
|
+
message: "API middleware could not be loaded",
|
|
339
|
+
details: "The `loadApiMiddleware` map does not contain this `_middleware.ts`.",
|
|
340
|
+
suggestions: ["Restart the dev server / rebuild"],
|
|
330
341
|
});
|
|
331
342
|
}
|
|
332
343
|
const module = (await load()) as { default?: unknown };
|
|
@@ -334,8 +345,8 @@ export function createRequestHandler(
|
|
|
334
345
|
throw new MoonsError({
|
|
335
346
|
code: "E_MIDDLEWARE_INVALID",
|
|
336
347
|
file: definition.file,
|
|
337
|
-
message: "`_middleware.ts`
|
|
338
|
-
details: "
|
|
348
|
+
message: "`_middleware.ts` does not default-export a function",
|
|
349
|
+
details: "Directory middleware follows the `(args) => Promise<Response>` shape.",
|
|
339
350
|
suggestions: ["Export default `async ({ req, ctx, next }) => await next()`"],
|
|
340
351
|
});
|
|
341
352
|
}
|
|
@@ -385,7 +396,7 @@ export function createRequestHandler(
|
|
|
385
396
|
middlewares,
|
|
386
397
|
ctx,
|
|
387
398
|
() => handleApiRequest(apiMatch.route, apiMatch.params, request, ctx),
|
|
388
|
-
{ code: "E_MIDDLEWARE_INVALID", label: "middleware
|
|
399
|
+
{ code: "E_MIDDLEWARE_INVALID", label: "API middleware" },
|
|
389
400
|
)(request);
|
|
390
401
|
}
|
|
391
402
|
}
|
|
@@ -404,7 +415,7 @@ export function createRequestHandler(
|
|
|
404
415
|
middlewares,
|
|
405
416
|
ctx,
|
|
406
417
|
() => renderRoute(resolved, request, ctx),
|
|
407
|
-
{ code: "E_MIDDLEWARE_INVALID", label: "middleware
|
|
418
|
+
{ code: "E_MIDDLEWARE_INVALID", label: "directory middleware" },
|
|
408
419
|
)(request);
|
|
409
420
|
} catch (error) {
|
|
410
421
|
// Addendum B.2: sinyal routing dari loader/meta.
|
|
@@ -538,9 +549,9 @@ async function loadModules(
|
|
|
538
549
|
if (load === undefined) {
|
|
539
550
|
throw new MoonsError({
|
|
540
551
|
code: "E_INTERNAL",
|
|
541
|
-
message: `
|
|
542
|
-
details: "
|
|
543
|
-
suggestions: ["
|
|
552
|
+
message: `route module \`${key}\` is not registered`,
|
|
553
|
+
details: "The route registry is out of sync with the scan result.",
|
|
554
|
+
suggestions: ["Run `bun run build` again, or restart the dev server"],
|
|
544
555
|
});
|
|
545
556
|
}
|
|
546
557
|
return await load();
|
|
@@ -561,11 +572,11 @@ async function runRouteMeta(
|
|
|
561
572
|
throw new MoonsError({
|
|
562
573
|
code: "E_INTERNAL",
|
|
563
574
|
file,
|
|
564
|
-
message: "export `meta`
|
|
565
|
-
details: "
|
|
575
|
+
message: "export `meta` must be a function `({ data }) => RouteMeta`",
|
|
576
|
+
details: "The `meta` shape follows `RouteMeta`.",
|
|
566
577
|
suggestions: [
|
|
567
|
-
"
|
|
568
|
-
"
|
|
578
|
+
"Change it to `export const meta = ({ data }) => ({ title: \"...\" })`",
|
|
579
|
+
"Or remove the `meta` export",
|
|
569
580
|
],
|
|
570
581
|
});
|
|
571
582
|
}
|
|
@@ -574,9 +585,9 @@ async function runRouteMeta(
|
|
|
574
585
|
throw new MoonsError({
|
|
575
586
|
code: "E_INTERNAL",
|
|
576
587
|
file,
|
|
577
|
-
message: "`meta()`
|
|
578
|
-
details: "
|
|
579
|
-
suggestions: ["
|
|
588
|
+
message: "`meta()` must return a RouteMeta object",
|
|
589
|
+
details: "The returned value is used for <title>/description/og.",
|
|
590
|
+
suggestions: ["Return an object, e.g. `({ title: \"Home\" })`"],
|
|
580
591
|
});
|
|
581
592
|
}
|
|
582
593
|
return result as Record<string, unknown>;
|
|
@@ -589,13 +600,13 @@ function apiErrorResponse(error: unknown, route: ApiRouteDefinition): Response {
|
|
|
589
600
|
const isProduction = process.env["NODE_ENV"] === "production";
|
|
590
601
|
if (error instanceof MoonsError) {
|
|
591
602
|
console.error(`[moons] API ${route.path}: ${error.format()}`);
|
|
592
|
-
return new Response(isProduction ? "
|
|
603
|
+
return new Response(isProduction ? "Internal server error." : error.format(), {
|
|
593
604
|
status: 500,
|
|
594
605
|
headers: { "content-type": "text/plain; charset=utf-8" },
|
|
595
606
|
});
|
|
596
607
|
}
|
|
597
608
|
console.error(`[moons] API ${route.path}:`, error);
|
|
598
|
-
return new Response(isProduction ? "
|
|
609
|
+
return new Response(isProduction ? "Internal server error." : String(error), {
|
|
599
610
|
status: 500,
|
|
600
611
|
headers: { "content-type": "text/plain; charset=utf-8" },
|
|
601
612
|
});
|
|
@@ -609,14 +620,25 @@ function internalErrorResponse(error: unknown, options: { log?: boolean } = {}):
|
|
|
609
620
|
const detail =
|
|
610
621
|
process.env["NODE_ENV"] !== "production" && error instanceof MoonsError
|
|
611
622
|
? `<pre>${escapeHtml(error.format())}</pre>`
|
|
612
|
-
: "<p>
|
|
623
|
+
: "<p>Internal server error.</p>";
|
|
613
624
|
|
|
614
625
|
return htmlResponse(
|
|
615
|
-
`<!doctype html><html lang="
|
|
626
|
+
`<!doctype html><html lang="en"><head><meta charset="utf-8"><title>500</title></head><body><h1>500</h1>${detail}</body></html>`,
|
|
616
627
|
500,
|
|
617
628
|
);
|
|
618
629
|
}
|
|
619
630
|
|
|
631
|
+
/** Addendum O: expose the trailing-slash policy to the client runtime. */
|
|
632
|
+
function withTrailingSlashMeta(html: string, policy?: "never" | "always"): string {
|
|
633
|
+
if (policy === undefined) {
|
|
634
|
+
return html;
|
|
635
|
+
}
|
|
636
|
+
return html.replace(
|
|
637
|
+
"</head>",
|
|
638
|
+
` <meta name="moons:trailing-slash" content="${policy}" />\n</head>`,
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
|
|
620
642
|
function htmlResponse(html: string, status: number): Response {
|
|
621
643
|
return new Response(html, {
|
|
622
644
|
status,
|
|
@@ -667,7 +689,7 @@ function warnPayloadFindings(
|
|
|
667
689
|
}
|
|
668
690
|
warned.add(label);
|
|
669
691
|
console.warn(
|
|
670
|
-
`[moons]
|
|
692
|
+
`[moons] dev warning: SSR payload contains ${label} — do not return secrets from loaders.`,
|
|
671
693
|
);
|
|
672
694
|
}
|
|
673
695
|
}
|
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:
|
|
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:
|
|
73
|
-
log(`[moons] isr:
|
|
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:
|
|
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:
|
|
88
|
+
log(`[moons] isr: failed to regenerate ${options.pathname}: ${String(error)}`);
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
|
|
@@ -366,13 +366,13 @@ function rpcBodyTooLargeText(limit: number, size: number | null): string {
|
|
|
366
366
|
code: "E_RPC_BODY_TOO_LARGE",
|
|
367
367
|
message:
|
|
368
368
|
size === null
|
|
369
|
-
? `body
|
|
370
|
-
: `body ${megabytes(size)} MB
|
|
369
|
+
? `body exceeds the ${megabytes(limit)} MB limit`
|
|
370
|
+
: `body ${megabytes(size)} MB exceeds the ${megabytes(limit)} MB limit`,
|
|
371
371
|
details:
|
|
372
|
-
"Body
|
|
372
|
+
"Body exceeds `security.bodyLimitBytes`; reading stops at the transport before the body is fully buffered.",
|
|
373
373
|
suggestions: [
|
|
374
|
-
"
|
|
375
|
-
"
|
|
374
|
+
"Reduce the payload size, or",
|
|
375
|
+
"Increase security.bodyLimitBytes if you really need to",
|
|
376
376
|
],
|
|
377
377
|
}).format();
|
|
378
378
|
}
|
|
@@ -524,7 +524,7 @@ function scheduleRevalidation(input: {
|
|
|
524
524
|
if (logger !== null) {
|
|
525
525
|
logger({ msg: "isr:revalidate-failed", level: "warn", path: normalized, error: message });
|
|
526
526
|
} else {
|
|
527
|
-
console.error(`[moons] isr
|
|
527
|
+
console.error(`[moons] isr regeneration failed (${normalized}): ${message}`);
|
|
528
528
|
}
|
|
529
529
|
});
|
|
530
530
|
}
|
package/src/server/node.ts
CHANGED
|
@@ -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
|
|
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(`
|
|
51
|
+
throw new InvalidRequestError(`invalid Host header: ${JSON.stringify(hostHeader)}`);
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
|
package/src/server/plugins.ts
CHANGED
|
@@ -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}\`
|
|
72
|
+
log(`[moons] plugin \`${plugin.name}\` failed during onShutdown: ${String(error)}`);
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
},
|
package/src/server/prerender.ts
CHANGED
|
@@ -80,11 +80,11 @@ export async function runPrerender(options: PrerenderOptions): Promise<Prerender
|
|
|
80
80
|
throw new MoonsError({
|
|
81
81
|
code: "E_ISR_INVALID",
|
|
82
82
|
file: route.file,
|
|
83
|
-
message: "`revalidate`
|
|
84
|
-
details: "ISR
|
|
83
|
+
message: "`revalidate` is invalid or used without `prerender = true`",
|
|
84
|
+
details: "File-based ISR: `revalidate` (seconds, number >= 0) + `prerender`.",
|
|
85
85
|
suggestions: [
|
|
86
|
-
"
|
|
87
|
-
"
|
|
86
|
+
"Use a number of seconds, e.g. `export const revalidate = 60`",
|
|
87
|
+
"Add `export const prerender = true` to the same page",
|
|
88
88
|
],
|
|
89
89
|
});
|
|
90
90
|
}
|
|
@@ -92,7 +92,7 @@ export async function runPrerender(options: PrerenderOptions): Promise<Prerender
|
|
|
92
92
|
|
|
93
93
|
if (prerender !== true) {
|
|
94
94
|
if (prerender !== undefined && prerender !== false) {
|
|
95
|
-
throw prerenderError(route, "`prerender`
|
|
95
|
+
throw prerenderError(route, "`prerender` must be a boolean (`true`/`false`)");
|
|
96
96
|
}
|
|
97
97
|
continue;
|
|
98
98
|
}
|
|
@@ -107,15 +107,15 @@ export async function runPrerender(options: PrerenderOptions): Promise<Prerender
|
|
|
107
107
|
);
|
|
108
108
|
|
|
109
109
|
if (response.status === 404) {
|
|
110
|
-
log(`[moons] prerender:
|
|
110
|
+
log(`[moons] prerender: skipped ${pathname} (notFound)`);
|
|
111
111
|
skipped += 1;
|
|
112
112
|
continue;
|
|
113
113
|
}
|
|
114
114
|
if (response.status >= 300 && response.status < 400) {
|
|
115
|
-
throw prerenderError(route, `\`redirect()\`
|
|
115
|
+
throw prerenderError(route, `\`redirect()\` is not supported during prerender (${pathname})`);
|
|
116
116
|
}
|
|
117
117
|
if (response.status !== 200) {
|
|
118
|
-
throw prerenderError(route, `status ${response.status}
|
|
118
|
+
throw prerenderError(route, `status ${response.status} while rendering ${pathname}`);
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
const html = await response.text();
|
|
@@ -152,7 +152,7 @@ export async function runPrerender(options: PrerenderOptions): Promise<Prerender
|
|
|
152
152
|
|
|
153
153
|
const extras = [...(wrote404 ? [NOT_FOUND_FILE] : []), ...emitted];
|
|
154
154
|
log(
|
|
155
|
-
`[moons] prerender
|
|
155
|
+
`[moons] prerender done: ${written} file${extras.length > 0 ? ` + ${extras.join(" + ")}` : ""} (${skipped} skipped)`,
|
|
156
156
|
);
|
|
157
157
|
return { written, skipped };
|
|
158
158
|
}
|
|
@@ -237,7 +237,7 @@ async function renderStaticNotFound(
|
|
|
237
237
|
);
|
|
238
238
|
if (response.status !== 404) {
|
|
239
239
|
log(
|
|
240
|
-
`[moons] prerender: ${NOT_FOUND_FILE}
|
|
240
|
+
`[moons] prerender: ${NOT_FOUND_FILE} skipped (sentinel \`${NOT_FOUND_SENTINEL}\` → ${response.status})`,
|
|
241
241
|
);
|
|
242
242
|
return null;
|
|
243
243
|
}
|
|
@@ -267,11 +267,11 @@ function assertNoLeak(
|
|
|
267
267
|
throw new MoonsError({
|
|
268
268
|
code: "E_PRERENDER_LEAK",
|
|
269
269
|
file,
|
|
270
|
-
message: `
|
|
271
|
-
details: "HTML
|
|
270
|
+
message: `prerender output \`${pathname}\` contains ${labels}`,
|
|
271
|
+
details: "Static HTML is permanent — a prerendered secret cannot be retracted.",
|
|
272
272
|
suggestions: [
|
|
273
|
-
"
|
|
274
|
-
"
|
|
273
|
+
"Do not return secret env values from a loader that runs at prerender time",
|
|
274
|
+
"Turn off `prerender` for this route or move the data to a request-time serverFn",
|
|
275
275
|
],
|
|
276
276
|
});
|
|
277
277
|
}
|
|
@@ -291,37 +291,37 @@ async function resolvePathnames(route: RouteDefinition, module: RouteModule): Pr
|
|
|
291
291
|
throw new MoonsError({
|
|
292
292
|
code: "E_PRERENDER_NO_PATHS",
|
|
293
293
|
file: route.file,
|
|
294
|
-
message: "route
|
|
295
|
-
details: "
|
|
294
|
+
message: "dynamic route with `prerender = true` without `export const paths`",
|
|
295
|
+
details: "Prerendering needs a param list to render each path.",
|
|
296
296
|
suggestions: [
|
|
297
|
-
'
|
|
298
|
-
"
|
|
297
|
+
'Add `export const paths = () => [{ id: "…" }]`',
|
|
298
|
+
"Or remove `prerender` so the route renders at request time (SSR)",
|
|
299
299
|
],
|
|
300
300
|
});
|
|
301
301
|
}
|
|
302
302
|
|
|
303
303
|
const raw = await (paths as () => unknown | Promise<unknown>)();
|
|
304
304
|
if (!Array.isArray(raw)) {
|
|
305
|
-
throw prerenderError(route, "`paths()`
|
|
305
|
+
throw prerenderError(route, "`paths()` must return an array");
|
|
306
306
|
}
|
|
307
307
|
|
|
308
308
|
const expected = new Set(route.params);
|
|
309
309
|
const result: string[] = [];
|
|
310
310
|
for (const item of raw as unknown[]) {
|
|
311
311
|
if (item === null || typeof item !== "object" || Array.isArray(item)) {
|
|
312
|
-
throw prerenderError(route, "
|
|
312
|
+
throw prerenderError(route, "`paths()` items must be objects with string params");
|
|
313
313
|
}
|
|
314
314
|
const record = item as Record<string, unknown>;
|
|
315
315
|
const keys = Object.keys(record);
|
|
316
316
|
for (const name of expected) {
|
|
317
317
|
const value = record[name];
|
|
318
318
|
if (typeof value !== "string" || value.length === 0) {
|
|
319
|
-
throw prerenderError(route, `param \`${name}\`
|
|
319
|
+
throw prerenderError(route, `param \`${name}\` must be a non-empty string`);
|
|
320
320
|
}
|
|
321
321
|
}
|
|
322
322
|
for (const key of keys) {
|
|
323
323
|
if (!expected.has(key)) {
|
|
324
|
-
throw prerenderError(route, `param \`${key}\`
|
|
324
|
+
throw prerenderError(route, `param \`${key}\` is not in the route pattern`);
|
|
325
325
|
}
|
|
326
326
|
}
|
|
327
327
|
result.push(buildPathname(route, record as Record<string, string>));
|
|
@@ -356,7 +356,7 @@ function buildPathname(route: RouteDefinition, params: Record<string, string>):
|
|
|
356
356
|
|
|
357
357
|
function encodeSegment(route: RouteDefinition, value: string): string {
|
|
358
358
|
if (value.length === 0 || /^\.+$/.test(value)) {
|
|
359
|
-
throw prerenderError(route, `
|
|
359
|
+
throw prerenderError(route, `param value \`${value}\` is not valid for a file path`);
|
|
360
360
|
}
|
|
361
361
|
return encodeURIComponent(value);
|
|
362
362
|
}
|
|
@@ -368,9 +368,9 @@ function outputFile(clientDir: string, pathname: string): string {
|
|
|
368
368
|
if (file !== base && !file.startsWith(base + sep)) {
|
|
369
369
|
throw new MoonsError({
|
|
370
370
|
code: "E_PRERENDER_INVALID",
|
|
371
|
-
message: `
|
|
372
|
-
details: "
|
|
373
|
-
suggestions: ["
|
|
371
|
+
message: `prerender path escapes the client directory (\`${pathname}\`)`,
|
|
372
|
+
details: "The file name must stay inside `dist/client`.",
|
|
373
|
+
suggestions: ["Check the `paths` param values — do not use `..`"],
|
|
374
374
|
});
|
|
375
375
|
}
|
|
376
376
|
return file;
|
|
@@ -381,10 +381,10 @@ function prerenderError(route: RouteDefinition, message: string): MoonsError {
|
|
|
381
381
|
code: "E_PRERENDER_INVALID",
|
|
382
382
|
file: route.file,
|
|
383
383
|
message,
|
|
384
|
-
details: "
|
|
384
|
+
details: "The prerender contract is enforced.",
|
|
385
385
|
suggestions: [
|
|
386
|
-
"
|
|
387
|
-
"
|
|
386
|
+
"Fix the `paths`/params shape so it matches the route pattern",
|
|
387
|
+
"Do not use `redirect()` on a prerendered route",
|
|
388
388
|
],
|
|
389
389
|
});
|
|
390
390
|
}
|
package/src/server/shell.ts
CHANGED
|
@@ -14,7 +14,7 @@ export const ROOT_PLACEHOLDER = '<div id="root"></div>';
|
|
|
14
14
|
|
|
15
15
|
export const DEFAULT_TEMPLATE = [
|
|
16
16
|
"<!doctype html>",
|
|
17
|
-
'<html lang="
|
|
17
|
+
'<html lang="en">',
|
|
18
18
|
"<head>",
|
|
19
19
|
' <meta charset="utf-8" />',
|
|
20
20
|
' <meta name="viewport" content="width=device-width, initial-scale=1" />',
|
|
@@ -38,11 +38,11 @@ export function injectShell(
|
|
|
38
38
|
if (!template.includes(ROOT_PLACEHOLDER)) {
|
|
39
39
|
throw new MoonsError({
|
|
40
40
|
code: "E_INTERNAL",
|
|
41
|
-
message: `shell HTML
|
|
42
|
-
details: "index.html
|
|
41
|
+
message: `shell HTML does not contain \`${ROOT_PLACEHOLDER}\``,
|
|
42
|
+
details: "The app's index.html must contain that root element so SSR can inject markup.",
|
|
43
43
|
suggestions: [
|
|
44
|
-
`
|
|
45
|
-
"
|
|
44
|
+
`Add ${ROOT_PLACEHOLDER} to index.html`,
|
|
45
|
+
"Or remove index.html to use the built-in shell",
|
|
46
46
|
],
|
|
47
47
|
});
|
|
48
48
|
}
|
|
@@ -119,9 +119,9 @@ export function createFileShell(options: { clientDir?: string } = {}): Shell {
|
|
|
119
119
|
throw new MoonsError({
|
|
120
120
|
code: "E_INTERNAL",
|
|
121
121
|
file,
|
|
122
|
-
message: "`dist/client/index.html`
|
|
123
|
-
details: "
|
|
124
|
-
suggestions: ["
|
|
122
|
+
message: "`dist/client/index.html` not found",
|
|
123
|
+
details: "The client build must run before starting the production server.",
|
|
124
|
+
suggestions: ["Run `bun run build` first"],
|
|
125
125
|
});
|
|
126
126
|
}
|
|
127
127
|
cached = readFileSync(file, "utf8");
|
package/src/server/shutdown.ts
CHANGED
|
@@ -36,10 +36,10 @@ export function installGracefulShutdown(
|
|
|
36
36
|
return;
|
|
37
37
|
}
|
|
38
38
|
shuttingDown = true;
|
|
39
|
-
log(`[moons]
|
|
39
|
+
log(`[moons] received ${signal} — no longer accepting connections, draining...`);
|
|
40
40
|
|
|
41
41
|
const timer = setTimeout(() => {
|
|
42
|
-
log(`[moons] drain
|
|
42
|
+
log(`[moons] drain exceeded ${timeoutMs}ms — forcing exit`);
|
|
43
43
|
const closeAll = (server as { closeAllConnections?: () => void }).closeAllConnections;
|
|
44
44
|
try {
|
|
45
45
|
closeAll?.call(server);
|
|
@@ -59,7 +59,7 @@ export function installGracefulShutdown(
|
|
|
59
59
|
server.close(async (error?: Error) => {
|
|
60
60
|
if (error !== undefined) {
|
|
61
61
|
clearTimeout(timer);
|
|
62
|
-
log(`[moons]
|
|
62
|
+
log(`[moons] server close failed: ${error.message}`);
|
|
63
63
|
exit(1);
|
|
64
64
|
return;
|
|
65
65
|
}
|
|
@@ -70,11 +70,11 @@ export function installGracefulShutdown(
|
|
|
70
70
|
try {
|
|
71
71
|
await options.onShutdown();
|
|
72
72
|
} catch (hookError) {
|
|
73
|
-
log(`[moons] onShutdown
|
|
73
|
+
log(`[moons] onShutdown failed: ${String(hookError)}`);
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
clearTimeout(timer);
|
|
77
|
-
log("[moons] shutdown
|
|
77
|
+
log("[moons] shutdown complete (full drain)");
|
|
78
78
|
exit(0);
|
|
79
79
|
});
|
|
80
80
|
};
|
package/src/server/storage.ts
CHANGED
|
@@ -21,7 +21,7 @@ export function createFileStorage(clientDir: string): PrerenderStorage {
|
|
|
21
21
|
async write(pathname: string, body: Uint8Array): Promise<void> {
|
|
22
22
|
const target = staticFileTarget(clientDir, pathname);
|
|
23
23
|
if (target === null) {
|
|
24
|
-
throw new Error(`[moons] isr: path
|
|
24
|
+
throw new Error(`[moons] isr: invalid path to write: ${pathname}`);
|
|
25
25
|
}
|
|
26
26
|
mkdirSync(dirname(target), { recursive: true });
|
|
27
27
|
const tmp = `${target}.tmp-${process.pid}`;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { MoonsConfig, RouteDefinition, RouteModule } from "@moonsjs/core";
|
|
2
|
-
import { createRequestHandler } from "./handler";
|
|
2
|
+
import { createRequestHandler, type RequestHandlerOptions } from "./handler";
|
|
3
3
|
import { DEFAULT_TEMPLATE, injectShell, type Shell } from "./shell";
|
|
4
4
|
|
|
5
5
|
export interface TestServerOptions {
|
|
@@ -7,6 +7,11 @@ export interface TestServerOptions {
|
|
|
7
7
|
config: MoonsConfig;
|
|
8
8
|
routesDir: string;
|
|
9
9
|
loadRoute: Record<string, () => Promise<RouteModule>>;
|
|
10
|
+
/** Endpoint `src/api` (Addendum O) — hasil scan app. */
|
|
11
|
+
apiRoutes?: RequestHandlerOptions["apiRoutes"];
|
|
12
|
+
loadApi?: RequestHandlerOptions["loadApi"];
|
|
13
|
+
apiMiddlewares?: RequestHandlerOptions["apiMiddlewares"];
|
|
14
|
+
loadApiMiddleware?: RequestHandlerOptions["loadApiMiddleware"];
|
|
10
15
|
/** Default: shell in-memory dari `DEFAULT_TEMPLATE` (tanpa file). */
|
|
11
16
|
shell?: Shell;
|
|
12
17
|
}
|
|
@@ -34,6 +39,12 @@ export function createTestServer(
|
|
|
34
39
|
config: options.config,
|
|
35
40
|
routesDir: options.routesDir,
|
|
36
41
|
loadRoute: options.loadRoute,
|
|
42
|
+
...(options.apiRoutes === undefined ? {} : { apiRoutes: options.apiRoutes }),
|
|
43
|
+
...(options.loadApi === undefined ? {} : { loadApi: options.loadApi }),
|
|
44
|
+
...(options.apiMiddlewares === undefined ? {} : { apiMiddlewares: options.apiMiddlewares }),
|
|
45
|
+
...(options.loadApiMiddleware === undefined
|
|
46
|
+
? {}
|
|
47
|
+
: { loadApiMiddleware: options.loadApiMiddleware }),
|
|
37
48
|
shell,
|
|
38
49
|
});
|
|
39
50
|
|