@moonsjs/vite 0.1.16 → 0.1.18
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-1ksjfvnz.js} +18 -18
- package/dist/index-1ksjfvnz.js.map +15 -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 +34 -34
- package/dist/index.js.map +4 -4
- package/dist/server/handler.d.ts.map +1 -1
- package/dist/server/index.js +111 -88
- package/dist/server/index.js.map +9 -9
- package/dist/server/node-server.d.ts +2 -0
- package/dist/server/node-server.d.ts.map +1 -1
- package/dist/transform.js +1 -1
- package/package.json +1 -1
- package/src/api.ts +14 -14
- package/src/devtools.ts +5 -5
- package/src/manifest.ts +2 -2
- package/src/plugin.ts +22 -22
- package/src/server/dev.ts +1 -1
- package/src/server/handler.ts +53 -39
- package/src/server/isr.ts +5 -5
- package/src/server/node-server.ts +27 -7
- 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-only.ts +7 -7
- package/src/transform.ts +76 -76
- package/dist/index-v5sbd3az.js.map +0 -15
package/package.json
CHANGED
package/src/api.ts
CHANGED
|
@@ -25,9 +25,9 @@ export function assertApiModuleExports(code: string, file: string): string[] {
|
|
|
25
25
|
if (statement["type"] === "ExportDefaultDeclaration") {
|
|
26
26
|
throw apiError(
|
|
27
27
|
file,
|
|
28
|
-
"export `default`
|
|
29
|
-
"
|
|
30
|
-
["
|
|
28
|
+
"export `default` is not supported in API modules",
|
|
29
|
+
"`src/api` modules only export named method handlers.",
|
|
30
|
+
["Replace it with `export const GET = (args) => …`", "Remove the default export"],
|
|
31
31
|
);
|
|
32
32
|
}
|
|
33
33
|
if (statement["type"] !== "ExportNamedDeclaration") {
|
|
@@ -65,20 +65,20 @@ export function assertApiModuleExports(code: string, file: string): string[] {
|
|
|
65
65
|
if (illegal.length > 0) {
|
|
66
66
|
throw apiError(
|
|
67
67
|
file,
|
|
68
|
-
`export \`${illegal[0]}\`
|
|
69
|
-
`
|
|
68
|
+
`export \`${illegal[0]}\` is not a recognized method handler`,
|
|
69
|
+
`Legal handlers: ${API_METHODS.join(", ")}.`,
|
|
70
70
|
[
|
|
71
|
-
"
|
|
72
|
-
"
|
|
71
|
+
"Rename the export to one of the HTTP methods",
|
|
72
|
+
"Move helpers to another module (e.g. `src/server/…`)",
|
|
73
73
|
],
|
|
74
74
|
);
|
|
75
75
|
}
|
|
76
76
|
if (methods.length === 0) {
|
|
77
77
|
throw apiError(
|
|
78
78
|
file,
|
|
79
|
-
"
|
|
80
|
-
`
|
|
81
|
-
[`
|
|
79
|
+
"API module has no method handler",
|
|
80
|
+
`At least one of ${API_METHODS.join(", ")} is required.`,
|
|
81
|
+
["Add `export const GET = (args) => new Response(…)`"],
|
|
82
82
|
);
|
|
83
83
|
}
|
|
84
84
|
|
|
@@ -103,11 +103,11 @@ export function assertMiddlewareExports(code: string, file: string): void {
|
|
|
103
103
|
throw new MoonsError({
|
|
104
104
|
code: "E_MIDDLEWARE_INVALID",
|
|
105
105
|
file,
|
|
106
|
-
message: "`_middleware.ts`
|
|
107
|
-
details: "
|
|
106
|
+
message: "`_middleware.ts` without `export default`",
|
|
107
|
+
details: "Directory middleware follows the `Middleware` shape: `(args) => Promise<Response>`.",
|
|
108
108
|
suggestions: [
|
|
109
|
-
"
|
|
110
|
-
"
|
|
109
|
+
"Add `export default async ({ req, ctx, next }) => await next()`",
|
|
110
|
+
"Delete the file if unused",
|
|
111
111
|
],
|
|
112
112
|
});
|
|
113
113
|
}
|
package/src/devtools.ts
CHANGED
|
@@ -36,7 +36,7 @@ export function emptyDevtoolsData(): DevtoolsData {
|
|
|
36
36
|
export function renderDevtoolsHtml(data: DevtoolsData): string {
|
|
37
37
|
const rows = (items: DevtoolsRoute[]): string =>
|
|
38
38
|
items.length === 0
|
|
39
|
-
? '<tr><td colspan="3" class="muted">(
|
|
39
|
+
? '<tr><td colspan="3" class="muted">(empty)</td></tr>'
|
|
40
40
|
: items
|
|
41
41
|
.map(
|
|
42
42
|
(item) =>
|
|
@@ -46,7 +46,7 @@ export function renderDevtoolsHtml(data: DevtoolsData): string {
|
|
|
46
46
|
|
|
47
47
|
const fnRows =
|
|
48
48
|
data.serverFns.length === 0
|
|
49
|
-
? '<tr><td colspan="3" class="muted">(
|
|
49
|
+
? '<tr><td colspan="3" class="muted">(no serverFn)</td></tr>'
|
|
50
50
|
: data.serverFns
|
|
51
51
|
.map(
|
|
52
52
|
(fn) =>
|
|
@@ -55,11 +55,11 @@ export function renderDevtoolsHtml(data: DevtoolsData): string {
|
|
|
55
55
|
.join("\n");
|
|
56
56
|
|
|
57
57
|
const canary = data.canary
|
|
58
|
-
? '<span class="ok">
|
|
59
|
-
: '<span class="muted">
|
|
58
|
+
? '<span class="ok">enabled</span> (canary installed)'
|
|
59
|
+
: '<span class="muted">disabled</span> (no canary)';
|
|
60
60
|
|
|
61
61
|
return `<!doctype html>
|
|
62
|
-
<html lang="
|
|
62
|
+
<html lang="en">
|
|
63
63
|
<head>
|
|
64
64
|
<meta charset="utf-8">
|
|
65
65
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
package/src/manifest.ts
CHANGED
|
@@ -70,7 +70,7 @@ export function renderApiModule(
|
|
|
70
70
|
)
|
|
71
71
|
.join("\n");
|
|
72
72
|
return [
|
|
73
|
-
"// Generated by @moons.
|
|
73
|
+
"// Generated by @moons. Do not edit manually.",
|
|
74
74
|
`export const apiRoutes = ${JSON.stringify(apiRoutes, null, 2)};`,
|
|
75
75
|
`export const apiMiddlewares = ${JSON.stringify(apiMiddlewares, null, 2)};`,
|
|
76
76
|
"export const loadApi = {",
|
|
@@ -100,7 +100,7 @@ export function renderRoutesModule(
|
|
|
100
100
|
});
|
|
101
101
|
|
|
102
102
|
return [
|
|
103
|
-
"// Generated by @moons.
|
|
103
|
+
"// Generated by @moons. Do not edit manually.",
|
|
104
104
|
`export const routes = ${JSON.stringify(routes, null, 2)};`,
|
|
105
105
|
"",
|
|
106
106
|
`export const clientManifest = ${JSON.stringify(manifest, null, 2)};`,
|
package/src/plugin.ts
CHANGED
|
@@ -141,9 +141,9 @@ export default function moons(options: MoonsVitePluginOptions = {}): Plugin {
|
|
|
141
141
|
throw new MoonsError({
|
|
142
142
|
code: "E_INTERNAL",
|
|
143
143
|
file: id,
|
|
144
|
-
message: "
|
|
145
|
-
details: "
|
|
146
|
-
suggestions: ["
|
|
144
|
+
message: "failed to read the server file for transform",
|
|
145
|
+
details: "The `.server.ts` file must be readable during transform.",
|
|
146
|
+
suggestions: ["Check the path and file permissions"],
|
|
147
147
|
});
|
|
148
148
|
}
|
|
149
149
|
|
|
@@ -217,10 +217,10 @@ export default function moons(options: MoonsVitePluginOptions = {}): Plugin {
|
|
|
217
217
|
throw new MoonsError({
|
|
218
218
|
code: "E_CONFIG_INVALID",
|
|
219
219
|
file: configFilePath,
|
|
220
|
-
message: "`moons.config.ts`
|
|
221
|
-
details: "
|
|
220
|
+
message: "`moons.config.ts` not found",
|
|
221
|
+
details: "The framework requires a config file at the project root.",
|
|
222
222
|
suggestions: [
|
|
223
|
-
"
|
|
223
|
+
"Create `moons.config.ts` with `export default defineConfig({ context: async (req) => ({}) })`",
|
|
224
224
|
],
|
|
225
225
|
});
|
|
226
226
|
}
|
|
@@ -240,9 +240,9 @@ export default function moons(options: MoonsVitePluginOptions = {}): Plugin {
|
|
|
240
240
|
throw new MoonsError({
|
|
241
241
|
code: "E_CONFIG_INVALID",
|
|
242
242
|
file: configFilePath,
|
|
243
|
-
message: "
|
|
243
|
+
message: "failed to load `moons.config.ts`",
|
|
244
244
|
details: error instanceof Error ? error.message : String(error),
|
|
245
|
-
suggestions: ["
|
|
245
|
+
suggestions: ["Check the syntax and imports in `moons.config.ts`"],
|
|
246
246
|
});
|
|
247
247
|
}
|
|
248
248
|
|
|
@@ -314,7 +314,7 @@ export default function moons(options: MoonsVitePluginOptions = {}): Plugin {
|
|
|
314
314
|
case VIRTUAL_MODULES.api:
|
|
315
315
|
return renderApiModule(apiRoutes, apiMiddlewares, root);
|
|
316
316
|
case VIRTUAL_MODULES.clientEntry:
|
|
317
|
-
return '// Generated by @moons.
|
|
317
|
+
return '// Generated by @moons. Do not edit manually.\nexport { clientManifest, loadRoute } from "virtual:moons/routes";\n';
|
|
318
318
|
case VIRTUAL_MODULES.config:
|
|
319
319
|
return renderConfigModule(root, configFilePath);
|
|
320
320
|
case VIRTUAL_MODULES.serverEntry:
|
|
@@ -393,12 +393,12 @@ export default function moons(options: MoonsVitePluginOptions = {}): Plugin {
|
|
|
393
393
|
if (leak !== null) {
|
|
394
394
|
throw new MoonsError({
|
|
395
395
|
code: "E_INTERNAL",
|
|
396
|
-
message: `canary \`${canary}\`
|
|
396
|
+
message: `canary \`${canary}\` found in the client bundle (${leak.fileName})`,
|
|
397
397
|
details:
|
|
398
|
-
"
|
|
398
|
+
"Leak verification failed: server code leaked into the client output. Build stopped.",
|
|
399
399
|
suggestions: [
|
|
400
|
-
"
|
|
401
|
-
"
|
|
400
|
+
"Check the `*.server.ts` transform",
|
|
401
|
+
"Run `./scripts/m2/test-leak.sh` to audit",
|
|
402
402
|
],
|
|
403
403
|
});
|
|
404
404
|
}
|
|
@@ -416,12 +416,12 @@ export default function moons(options: MoonsVitePluginOptions = {}): Plugin {
|
|
|
416
416
|
throw new MoonsError({
|
|
417
417
|
code: "E_FORBIDDEN_SERVER_IMPORT",
|
|
418
418
|
file: cleanId,
|
|
419
|
-
message: "
|
|
419
|
+
message: "server module got bundled into the client without passing through the transform",
|
|
420
420
|
details:
|
|
421
|
-
"
|
|
421
|
+
"Build verification: every `*.server.ts` in the client bundle must be an RPC stub produced by the transform.",
|
|
422
422
|
suggestions: [
|
|
423
|
-
"
|
|
424
|
-
"
|
|
423
|
+
"Make sure the zero-trust transform runs for this module",
|
|
424
|
+
"If this repeats, report it as a framework bug",
|
|
425
425
|
],
|
|
426
426
|
});
|
|
427
427
|
}
|
|
@@ -523,7 +523,7 @@ export default function moons(options: MoonsVitePluginOptions = {}): Plugin {
|
|
|
523
523
|
|
|
524
524
|
function renderConfigModule(root: string, configFilePath: string): string {
|
|
525
525
|
return [
|
|
526
|
-
"// Generated by @moons.
|
|
526
|
+
"// Generated by @moons. Do not edit manually.",
|
|
527
527
|
`import raw from ${JSON.stringify(rootSpecifier(root, configFilePath))};`,
|
|
528
528
|
'import { validateConfig } from "@moonsjs/core";',
|
|
529
529
|
`export default validateConfig(raw, { file: ${JSON.stringify(configFilePath)} });`,
|
|
@@ -533,7 +533,7 @@ function renderConfigModule(root: string, configFilePath: string): string {
|
|
|
533
533
|
|
|
534
534
|
function renderServerEntryDev(routesDir: string, eagerImports: string[]): string {
|
|
535
535
|
return [
|
|
536
|
-
"// Generated by @moons.
|
|
536
|
+
"// Generated by @moons. Do not edit manually.",
|
|
537
537
|
'import { createPluginRuntime, createRequestHandler } from "@moonsjs/vite/server";',
|
|
538
538
|
'import config from "virtual:moons/config";',
|
|
539
539
|
'import { routes, loadRoute } from "virtual:moons/routes";',
|
|
@@ -555,7 +555,7 @@ function renderServerEntryBuild(
|
|
|
555
555
|
canary: string | undefined,
|
|
556
556
|
): string {
|
|
557
557
|
return [
|
|
558
|
-
"// Generated by @moons.
|
|
558
|
+
"// Generated by @moons. Do not edit manually.",
|
|
559
559
|
'import { createFileShell, createPluginRuntime, createRequestHandler, startNodeServer } from "@moonsjs/vite/server";',
|
|
560
560
|
'import config from "virtual:moons/config";',
|
|
561
561
|
'import { routes, loadRoute } from "virtual:moons/routes";',
|
|
@@ -576,7 +576,7 @@ function renderServerEntryBuild(
|
|
|
576
576
|
" onContext: (ctx) => plugins.applyExtensions(ctx),",
|
|
577
577
|
"});",
|
|
578
578
|
"",
|
|
579
|
-
`startNodeServer(handler, { plugins, headers: config.headers, storage: config.isr?.storage, trustProxy: config.security?.trustProxy, bodyLimitBytes: config.security?.bodyLimitBytes, logging: config.logging${canary === undefined ? "" : `, canary: ${JSON.stringify(canary)}`} });`,
|
|
579
|
+
`startNodeServer(handler, { plugins, headers: config.headers, storage: config.isr?.storage, trustProxy: config.security?.trustProxy, bodyLimitBytes: config.security?.bodyLimitBytes, logging: config.logging, trailingSlash: config.trailingSlash${canary === undefined ? "" : `, canary: ${JSON.stringify(canary)}`} });`,
|
|
580
580
|
"",
|
|
581
581
|
].join("\n");
|
|
582
582
|
}
|
|
@@ -593,7 +593,7 @@ function renderPrerenderEntry(
|
|
|
593
593
|
canary: string | undefined,
|
|
594
594
|
): string {
|
|
595
595
|
return [
|
|
596
|
-
"// Generated by @moons.
|
|
596
|
+
"// Generated by @moons. Do not edit manually.",
|
|
597
597
|
'import { createFileShell, createRequestHandler, runPrerender } from "@moonsjs/vite/server";',
|
|
598
598
|
'import config from "virtual:moons/config";',
|
|
599
599
|
'import { routes, loadRoute } from "virtual:moons/routes";',
|
package/src/server/dev.ts
CHANGED
|
@@ -78,7 +78,7 @@ export function installDevMiddleware(
|
|
|
78
78
|
} catch (error) {
|
|
79
79
|
res.statusCode = 500;
|
|
80
80
|
res.setHeader("content-type", "text/plain; charset=utf-8");
|
|
81
|
-
res.end(`[moons] devtools
|
|
81
|
+
res.end(`[moons] devtools failed to render: ${String(error)}`);
|
|
82
82
|
}
|
|
83
83
|
return;
|
|
84
84
|
}
|
package/src/server/handler.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
type ResolvedRoute,
|
|
18
18
|
type RouteDefinition,
|
|
19
19
|
type RouteModule,
|
|
20
|
+
trailingSlashRedirect,
|
|
20
21
|
} from "@moonsjs/core";
|
|
21
22
|
import { encodeSsrPayload, reviveError, serializeError } from "@moonsjs/core/serialize";
|
|
22
23
|
import { API_METHODS, resolveApiRoute } from "@moonsjs/core";
|
|
@@ -131,9 +132,9 @@ export function createRequestHandler(
|
|
|
131
132
|
new MoonsError({
|
|
132
133
|
code: "E_API_HANDLER_INVALID",
|
|
133
134
|
file: route.file,
|
|
134
|
-
message: `
|
|
135
|
-
details: "
|
|
136
|
-
suggestions: ["
|
|
135
|
+
message: `API module \`${route.path}\` could not be loaded`,
|
|
136
|
+
details: "The `loadApi` map does not contain this route (build/dev out of sync).",
|
|
137
|
+
suggestions: ["Rebuild / restart the dev server"],
|
|
137
138
|
}),
|
|
138
139
|
route,
|
|
139
140
|
);
|
|
@@ -150,9 +151,9 @@ export function createRequestHandler(
|
|
|
150
151
|
new MoonsError({
|
|
151
152
|
code: "E_API_HANDLER_INVALID",
|
|
152
153
|
file: route.file,
|
|
153
|
-
message: "
|
|
154
|
-
details: `
|
|
155
|
-
suggestions: ["Export `GET`/`POST`/…
|
|
154
|
+
message: "API module does not export a handler object",
|
|
155
|
+
details: `Module \`${route.path}\` must export method handlers.`,
|
|
156
|
+
suggestions: ["Export `GET`/`POST`/… as `(args) => Response` functions"],
|
|
156
157
|
}),
|
|
157
158
|
route,
|
|
158
159
|
);
|
|
@@ -193,9 +194,9 @@ export function createRequestHandler(
|
|
|
193
194
|
new MoonsError({
|
|
194
195
|
code: "E_API_HANDLER_INVALID",
|
|
195
196
|
file: route.file,
|
|
196
|
-
message: `handler \`${requestMethod}\`
|
|
197
|
-
details: "
|
|
198
|
-
suggestions: ["
|
|
197
|
+
message: `handler \`${requestMethod}\` did not return a \`Response\``,
|
|
198
|
+
details: "API handlers must return `new Response(...)`.",
|
|
199
|
+
suggestions: ["Return `new Response(body, { status, headers })`"],
|
|
199
200
|
}),
|
|
200
201
|
route,
|
|
201
202
|
);
|
|
@@ -224,13 +225,13 @@ export function createRequestHandler(
|
|
|
224
225
|
);
|
|
225
226
|
return htmlResponse(await shell(markup, payload, request.url), 404);
|
|
226
227
|
} catch (error) {
|
|
227
|
-
console.error("[moons]
|
|
228
|
+
console.error("[moons] failed to render custom _404:", error);
|
|
228
229
|
}
|
|
229
230
|
}
|
|
230
231
|
|
|
231
232
|
const payload = encodeSsrPayload({ key: "", params: {}, data: {} });
|
|
232
233
|
const html = await shell(
|
|
233
|
-
"<h1>404</h1><p>
|
|
234
|
+
"<h1>404</h1><p>Page not found.</p>",
|
|
234
235
|
payload,
|
|
235
236
|
request.url,
|
|
236
237
|
);
|
|
@@ -274,7 +275,7 @@ export function createRequestHandler(
|
|
|
274
275
|
);
|
|
275
276
|
return htmlResponse(await shell(markup, payload, request.url), 500);
|
|
276
277
|
} catch (renderError) {
|
|
277
|
-
console.error("[moons]
|
|
278
|
+
console.error("[moons] failed to render custom _error:", renderError);
|
|
278
279
|
}
|
|
279
280
|
}
|
|
280
281
|
|
|
@@ -291,9 +292,9 @@ export function createRequestHandler(
|
|
|
291
292
|
throw new MoonsError({
|
|
292
293
|
code: "E_MIDDLEWARE_INVALID",
|
|
293
294
|
file: definition.file,
|
|
294
|
-
message: "middleware
|
|
295
|
-
details: "
|
|
296
|
-
suggestions: ["Restart dev server /
|
|
295
|
+
message: "directory middleware could not be loaded",
|
|
296
|
+
details: "The `loadRoute` map does not contain this `_middleware.ts`.",
|
|
297
|
+
suggestions: ["Restart the dev server / rebuild"],
|
|
297
298
|
});
|
|
298
299
|
}
|
|
299
300
|
const module = (await load()) as { default?: unknown };
|
|
@@ -302,8 +303,8 @@ export function createRequestHandler(
|
|
|
302
303
|
throw new MoonsError({
|
|
303
304
|
code: "E_MIDDLEWARE_INVALID",
|
|
304
305
|
file: definition.file,
|
|
305
|
-
message: "`_middleware.ts`
|
|
306
|
-
details: "
|
|
306
|
+
message: "`_middleware.ts` does not default-export a function",
|
|
307
|
+
details: "Directory middleware follows the `(args) => Promise<Response>` shape.",
|
|
307
308
|
suggestions: ["Export default `async ({ req, ctx, next }) => await next()`"],
|
|
308
309
|
});
|
|
309
310
|
}
|
|
@@ -323,9 +324,9 @@ export function createRequestHandler(
|
|
|
323
324
|
throw new MoonsError({
|
|
324
325
|
code: "E_MIDDLEWARE_INVALID",
|
|
325
326
|
file: definition.file,
|
|
326
|
-
message: "middleware
|
|
327
|
-
details: "
|
|
328
|
-
suggestions: ["Restart dev server /
|
|
327
|
+
message: "API middleware could not be loaded",
|
|
328
|
+
details: "The `loadApiMiddleware` map does not contain this `_middleware.ts`.",
|
|
329
|
+
suggestions: ["Restart the dev server / rebuild"],
|
|
329
330
|
});
|
|
330
331
|
}
|
|
331
332
|
const module = (await load()) as { default?: unknown };
|
|
@@ -333,8 +334,8 @@ export function createRequestHandler(
|
|
|
333
334
|
throw new MoonsError({
|
|
334
335
|
code: "E_MIDDLEWARE_INVALID",
|
|
335
336
|
file: definition.file,
|
|
336
|
-
message: "`_middleware.ts`
|
|
337
|
-
details: "
|
|
337
|
+
message: "`_middleware.ts` does not default-export a function",
|
|
338
|
+
details: "Directory middleware follows the `(args) => Promise<Response>` shape.",
|
|
338
339
|
suggestions: ["Export default `async ({ req, ctx, next }) => await next()`"],
|
|
339
340
|
});
|
|
340
341
|
}
|
|
@@ -384,7 +385,7 @@ export function createRequestHandler(
|
|
|
384
385
|
middlewares,
|
|
385
386
|
ctx,
|
|
386
387
|
() => handleApiRequest(apiMatch.route, apiMatch.params, request, ctx),
|
|
387
|
-
{ code: "E_MIDDLEWARE_INVALID", label: "middleware
|
|
388
|
+
{ code: "E_MIDDLEWARE_INVALID", label: "API middleware" },
|
|
388
389
|
)(request);
|
|
389
390
|
}
|
|
390
391
|
}
|
|
@@ -403,7 +404,7 @@ export function createRequestHandler(
|
|
|
403
404
|
middlewares,
|
|
404
405
|
ctx,
|
|
405
406
|
() => renderRoute(resolved, request, ctx),
|
|
406
|
-
{ code: "E_MIDDLEWARE_INVALID", label: "middleware
|
|
407
|
+
{ code: "E_MIDDLEWARE_INVALID", label: "directory middleware" },
|
|
407
408
|
)(request);
|
|
408
409
|
} catch (error) {
|
|
409
410
|
// Addendum B.2: sinyal routing dari loader/meta.
|
|
@@ -456,6 +457,19 @@ export function createRequestHandler(
|
|
|
456
457
|
};
|
|
457
458
|
|
|
458
459
|
try {
|
|
460
|
+
// Addendum M: kanonikalisasi trailing slash sebelum routing (308,
|
|
461
|
+
// query dipertahankan; `/_moons/**`, root, dan aset dikecualikan).
|
|
462
|
+
if (!isPrerender && config.trailingSlash !== undefined) {
|
|
463
|
+
const policyUrl = new URL(request.url);
|
|
464
|
+
const canonical = trailingSlashRedirect(policyUrl.pathname, config.trailingSlash);
|
|
465
|
+
if (canonical !== null) {
|
|
466
|
+
policyUrl.pathname = canonical;
|
|
467
|
+
return applyHeaders(
|
|
468
|
+
new Response(null, { status: 308, headers: { location: policyUrl.toString() } }),
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
459
473
|
if (!isPrerender && redirects.length > 0) {
|
|
460
474
|
const url = new URL(request.url);
|
|
461
475
|
const path = url.pathname.replace(/\/$/, "");
|
|
@@ -524,9 +538,9 @@ async function loadModules(
|
|
|
524
538
|
if (load === undefined) {
|
|
525
539
|
throw new MoonsError({
|
|
526
540
|
code: "E_INTERNAL",
|
|
527
|
-
message: `
|
|
528
|
-
details: "
|
|
529
|
-
suggestions: ["
|
|
541
|
+
message: `route module \`${key}\` is not registered`,
|
|
542
|
+
details: "The route registry is out of sync with the scan result.",
|
|
543
|
+
suggestions: ["Run `bun run build` again, or restart the dev server"],
|
|
530
544
|
});
|
|
531
545
|
}
|
|
532
546
|
return await load();
|
|
@@ -547,11 +561,11 @@ async function runRouteMeta(
|
|
|
547
561
|
throw new MoonsError({
|
|
548
562
|
code: "E_INTERNAL",
|
|
549
563
|
file,
|
|
550
|
-
message: "export `meta`
|
|
551
|
-
details: "
|
|
564
|
+
message: "export `meta` must be a function `({ data }) => RouteMeta`",
|
|
565
|
+
details: "The `meta` shape follows `RouteMeta`.",
|
|
552
566
|
suggestions: [
|
|
553
|
-
"
|
|
554
|
-
"
|
|
567
|
+
"Change it to `export const meta = ({ data }) => ({ title: \"...\" })`",
|
|
568
|
+
"Or remove the `meta` export",
|
|
555
569
|
],
|
|
556
570
|
});
|
|
557
571
|
}
|
|
@@ -560,9 +574,9 @@ async function runRouteMeta(
|
|
|
560
574
|
throw new MoonsError({
|
|
561
575
|
code: "E_INTERNAL",
|
|
562
576
|
file,
|
|
563
|
-
message: "`meta()`
|
|
564
|
-
details: "
|
|
565
|
-
suggestions: ["
|
|
577
|
+
message: "`meta()` must return a RouteMeta object",
|
|
578
|
+
details: "The returned value is used for <title>/description/og.",
|
|
579
|
+
suggestions: ["Return an object, e.g. `({ title: \"Home\" })`"],
|
|
566
580
|
});
|
|
567
581
|
}
|
|
568
582
|
return result as Record<string, unknown>;
|
|
@@ -575,13 +589,13 @@ function apiErrorResponse(error: unknown, route: ApiRouteDefinition): Response {
|
|
|
575
589
|
const isProduction = process.env["NODE_ENV"] === "production";
|
|
576
590
|
if (error instanceof MoonsError) {
|
|
577
591
|
console.error(`[moons] API ${route.path}: ${error.format()}`);
|
|
578
|
-
return new Response(isProduction ? "
|
|
592
|
+
return new Response(isProduction ? "Internal server error." : error.format(), {
|
|
579
593
|
status: 500,
|
|
580
594
|
headers: { "content-type": "text/plain; charset=utf-8" },
|
|
581
595
|
});
|
|
582
596
|
}
|
|
583
597
|
console.error(`[moons] API ${route.path}:`, error);
|
|
584
|
-
return new Response(isProduction ? "
|
|
598
|
+
return new Response(isProduction ? "Internal server error." : String(error), {
|
|
585
599
|
status: 500,
|
|
586
600
|
headers: { "content-type": "text/plain; charset=utf-8" },
|
|
587
601
|
});
|
|
@@ -595,10 +609,10 @@ function internalErrorResponse(error: unknown, options: { log?: boolean } = {}):
|
|
|
595
609
|
const detail =
|
|
596
610
|
process.env["NODE_ENV"] !== "production" && error instanceof MoonsError
|
|
597
611
|
? `<pre>${escapeHtml(error.format())}</pre>`
|
|
598
|
-
: "<p>
|
|
612
|
+
: "<p>Internal server error.</p>";
|
|
599
613
|
|
|
600
614
|
return htmlResponse(
|
|
601
|
-
`<!doctype html><html lang="
|
|
615
|
+
`<!doctype html><html lang="en"><head><meta charset="utf-8"><title>500</title></head><body><h1>500</h1>${detail}</body></html>`,
|
|
602
616
|
500,
|
|
603
617
|
);
|
|
604
618
|
}
|
|
@@ -653,7 +667,7 @@ function warnPayloadFindings(
|
|
|
653
667
|
}
|
|
654
668
|
warned.add(label);
|
|
655
669
|
console.warn(
|
|
656
|
-
`[moons]
|
|
670
|
+
`[moons] dev warning: SSR payload contains ${label} — do not return secrets from loaders.`,
|
|
657
671
|
);
|
|
658
672
|
}
|
|
659
673
|
}
|
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
|
|
|
@@ -2,7 +2,12 @@ import { existsSync, readFileSync, statSync } from "node:fs";
|
|
|
2
2
|
import { gzipSync } from "node:zlib";
|
|
3
3
|
import { createServer, type Server } from "node:http";
|
|
4
4
|
import { extname, join, resolve, sep } from "node:path";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_BODY_LIMIT_BYTES,
|
|
7
|
+
MoonsError,
|
|
8
|
+
trailingSlashRedirect,
|
|
9
|
+
type PrerenderStorage,
|
|
10
|
+
} from "@moonsjs/core";
|
|
6
11
|
import { revalidatePrerendered, shouldRevalidate } from "./isr";
|
|
7
12
|
import { contentTypeForPathname } from "./storage";
|
|
8
13
|
import {
|
|
@@ -68,6 +73,8 @@ export interface NodeServerOptions {
|
|
|
68
73
|
bodyLimitBytes?: number;
|
|
69
74
|
/** Addendum L.3: log akses terstruktur (default senyap). */
|
|
70
75
|
logging?: { accessLog?: boolean; json?: boolean; redact?: string[] };
|
|
76
|
+
/** Addendum M: kebijakan trailing slash (juga untuk path prerender/static). */
|
|
77
|
+
trailingSlash?: "never" | "always";
|
|
71
78
|
}
|
|
72
79
|
|
|
73
80
|
/**
|
|
@@ -120,6 +127,19 @@ export function startNodeServer(
|
|
|
120
127
|
}
|
|
121
128
|
|
|
122
129
|
try {
|
|
130
|
+
// Addendum M: kanonikalisasi trailing slash di transport — mencakup
|
|
131
|
+
// path prerender yang dilayani langsung (tidak lewat handler).
|
|
132
|
+
if (options.trailingSlash !== undefined) {
|
|
133
|
+
const canonical = trailingSlashRedirect(url.pathname, options.trailingSlash);
|
|
134
|
+
if (canonical !== null) {
|
|
135
|
+
url.pathname = canonical;
|
|
136
|
+
res.statusCode = 308;
|
|
137
|
+
res.setHeader("location", url.toString());
|
|
138
|
+
res.end();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
123
143
|
if (req.method === "GET" || req.method === "HEAD") {
|
|
124
144
|
// Addendum I: storage bersama (bila dikonfigurasi) diutamakan.
|
|
125
145
|
if (options.storage !== undefined) {
|
|
@@ -346,13 +366,13 @@ function rpcBodyTooLargeText(limit: number, size: number | null): string {
|
|
|
346
366
|
code: "E_RPC_BODY_TOO_LARGE",
|
|
347
367
|
message:
|
|
348
368
|
size === null
|
|
349
|
-
? `body
|
|
350
|
-
: `body ${megabytes(size)} MB
|
|
369
|
+
? `body exceeds the ${megabytes(limit)} MB limit`
|
|
370
|
+
: `body ${megabytes(size)} MB exceeds the ${megabytes(limit)} MB limit`,
|
|
351
371
|
details:
|
|
352
|
-
"Body
|
|
372
|
+
"Body exceeds `security.bodyLimitBytes`; reading stops at the transport before the body is fully buffered.",
|
|
353
373
|
suggestions: [
|
|
354
|
-
"
|
|
355
|
-
"
|
|
374
|
+
"Reduce the payload size, or",
|
|
375
|
+
"Increase security.bodyLimitBytes if you really need to",
|
|
356
376
|
],
|
|
357
377
|
}).format();
|
|
358
378
|
}
|
|
@@ -504,7 +524,7 @@ function scheduleRevalidation(input: {
|
|
|
504
524
|
if (logger !== null) {
|
|
505
525
|
logger({ msg: "isr:revalidate-failed", level: "warn", path: normalized, error: message });
|
|
506
526
|
} else {
|
|
507
|
-
console.error(`[moons] isr
|
|
527
|
+
console.error(`[moons] isr regeneration failed (${normalized}): ${message}`);
|
|
508
528
|
}
|
|
509
529
|
});
|
|
510
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
|
},
|