@moonsjs/vite 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.d.ts +8 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/{index-33p1r65s.js → index-1fnt137k.js} +50 -5
- package/dist/index-1fnt137k.js.map +14 -0
- package/dist/{index-3ec9vpek.js → index-e7qz0epf.js} +232 -24
- package/dist/index-e7qz0epf.js.map +11 -0
- package/dist/index.js +213 -16
- package/dist/index.js.map +5 -4
- package/dist/manifest.d.ts +6 -1
- package/dist/manifest.d.ts.map +1 -1
- package/dist/payload-scan.d.ts +18 -0
- package/dist/payload-scan.d.ts.map +1 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/server/dev.d.ts +3 -0
- package/dist/server/dev.d.ts.map +1 -1
- package/dist/server/handler.d.ts +16 -1
- package/dist/server/handler.d.ts.map +1 -1
- package/dist/server/index.d.ts +4 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +453 -17
- package/dist/server/index.js.map +8 -6
- package/dist/server/node-server.d.ts +1 -1
- package/dist/server/node-server.d.ts.map +1 -1
- package/dist/server/node.d.ts.map +1 -1
- package/dist/server/prerender.d.ts +28 -0
- package/dist/server/prerender.d.ts.map +1 -0
- package/dist/server-only.d.ts +57 -0
- package/dist/server-only.d.ts.map +1 -0
- package/dist/transform.d.ts +9 -1
- package/dist/transform.d.ts.map +1 -1
- package/dist/transform.js +1 -1
- package/dist/virtual-modules.d.ts +1 -0
- package/dist/virtual-modules.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/api.ts +124 -0
- package/src/manifest.ts +42 -1
- package/src/payload-scan.ts +53 -0
- package/src/plugin.ts +130 -7
- package/src/server/dev.ts +14 -5
- package/src/server/handler.ts +317 -13
- package/src/server/index.ts +4 -0
- package/src/server/node-server.ts +47 -7
- package/src/server/node.ts +17 -0
- package/src/server/prerender.ts +230 -0
- package/src/server-only.ts +220 -0
- package/src/transform.ts +138 -8
- package/src/virtual-modules.ts +1 -0
- package/dist/index-33p1r65s.js.map +0 -14
- package/dist/index-3ec9vpek.js.map +0 -10
package/dist/server/index.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
2
|
NOT_FOUND_ROUTE_KEY,
|
|
3
|
-
ERROR_ROUTE_KEY,
|
|
4
3
|
ROOT_PLACEHOLDER2,
|
|
5
4
|
DEFAULT_TEMPLATE2,
|
|
6
5
|
injectShell2,
|
|
@@ -9,12 +8,15 @@ import {
|
|
|
9
8
|
toWebRequest2,
|
|
10
9
|
sendNodeResponse2,
|
|
11
10
|
installDevMiddleware2
|
|
12
|
-
} from "../index-
|
|
11
|
+
} from "../index-1fnt137k.js";
|
|
13
12
|
|
|
14
13
|
// src/server/handler.ts
|
|
15
14
|
import {
|
|
16
15
|
MoonsError,
|
|
17
16
|
composeMiddleware,
|
|
17
|
+
findApiDirMiddlewares,
|
|
18
|
+
findDirMiddlewares,
|
|
19
|
+
findErrorBoundary,
|
|
18
20
|
isNotFoundSignal,
|
|
19
21
|
isRedirectSignal,
|
|
20
22
|
resolveRoute,
|
|
@@ -22,8 +24,34 @@ import {
|
|
|
22
24
|
runLoaders
|
|
23
25
|
} from "@moonsjs/core";
|
|
24
26
|
import { encodeSsrPayload, reviveError, serializeError } from "@moonsjs/core/serialize";
|
|
27
|
+
import { API_METHODS, resolveApiRoute } from "@moonsjs/core";
|
|
25
28
|
import { renderToMarkup } from "@moonsjs/react/ssr";
|
|
26
|
-
import { createRpcHandler, runWithContext, runWithRequestCache } from "@moonsjs/server";
|
|
29
|
+
import { createRpcHandler, runWithContext, runWithRequestCache, runWithResponseCollector, takeResponseMutations } from "@moonsjs/server";
|
|
30
|
+
|
|
31
|
+
// src/payload-scan.ts
|
|
32
|
+
var SECRET_KEY_PATTERN = /(SECRET|TOKEN|PASSWORD|PASSWD|PRIVATE|CREDENTIAL|AUTH|API_KEY|ACCESS_KEY|SIGNING)/i;
|
|
33
|
+
var MIN_SECRET_LENGTH = 12;
|
|
34
|
+
function scanPayloadForSecrets(payload, options = {}) {
|
|
35
|
+
const findings = [];
|
|
36
|
+
const canary = options.canary;
|
|
37
|
+
if (canary !== undefined && canary.length > 0 && payload.includes(canary)) {
|
|
38
|
+
findings.push({ kind: "canary", label: "canary" });
|
|
39
|
+
}
|
|
40
|
+
for (const [key, value] of Object.entries(options.env ?? {})) {
|
|
41
|
+
if (typeof value !== "string" || value.length < MIN_SECRET_LENGTH) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (!SECRET_KEY_PATTERN.test(key) || /PUBLIC/i.test(key)) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (payload.includes(value)) {
|
|
48
|
+
findings.push({ kind: "env", label: key });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return findings;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// src/server/handler.ts
|
|
27
55
|
var RPC_PREFIX = "/_moons/fn/";
|
|
28
56
|
var HEALTH_PATH = "/_moons/health";
|
|
29
57
|
function createRequestHandler(options) {
|
|
@@ -31,6 +59,7 @@ function createRequestHandler(options) {
|
|
|
31
59
|
const rpc = createRpcHandler(config.security === undefined ? {} : { security: config.security });
|
|
32
60
|
const notFoundRoute = routes.find((route) => route.kind === "404");
|
|
33
61
|
const errorRoute = routes.find((route) => route.kind === "error");
|
|
62
|
+
const warnedPayloadLabels = new Set;
|
|
34
63
|
async function renderRoute(resolved, request, ctx) {
|
|
35
64
|
const entries = [...resolved.layouts, resolved.route];
|
|
36
65
|
const keys = entries.map((entry) => routeKeyFromFile(routesDir, entry.file));
|
|
@@ -49,8 +78,72 @@ function createRequestHandler(options) {
|
|
|
49
78
|
data,
|
|
50
79
|
...meta === undefined ? {} : { meta }
|
|
51
80
|
});
|
|
81
|
+
warnPayloadFindings(payload, options.payloadScan, warnedPayloadLabels);
|
|
52
82
|
return htmlResponse(await shell(markup, payload, request.url, meta), 200);
|
|
53
83
|
}
|
|
84
|
+
async function handleApiRequest(route, params, request, ctx) {
|
|
85
|
+
const load = options.loadApi?.[route.path];
|
|
86
|
+
if (load === undefined) {
|
|
87
|
+
return apiErrorResponse(new MoonsError({
|
|
88
|
+
code: "E_API_HANDLER_INVALID",
|
|
89
|
+
file: route.file,
|
|
90
|
+
message: `modul API \`${route.path}\` tidak bisa dimuat`,
|
|
91
|
+
details: "Peta `loadApi` tidak memuat route ini (build/dev tidak sinkron).",
|
|
92
|
+
suggestions: ["Jalankan ulang build / restart dev server"]
|
|
93
|
+
}), route);
|
|
94
|
+
}
|
|
95
|
+
let mod;
|
|
96
|
+
try {
|
|
97
|
+
mod = await load();
|
|
98
|
+
} catch (error) {
|
|
99
|
+
return apiErrorResponse(error, route);
|
|
100
|
+
}
|
|
101
|
+
if (mod === null || typeof mod !== "object") {
|
|
102
|
+
return apiErrorResponse(new MoonsError({
|
|
103
|
+
code: "E_API_HANDLER_INVALID",
|
|
104
|
+
file: route.file,
|
|
105
|
+
message: "modul API tidak mengekspor objek handler",
|
|
106
|
+
details: `Modul \`${route.path}\` harus mengekspor handler method (PRD Addendum D.1).`,
|
|
107
|
+
suggestions: ["Export `GET`/`POST`/… sebagai fungsi `(args) => Response`"]
|
|
108
|
+
}), route);
|
|
109
|
+
}
|
|
110
|
+
const record = mod;
|
|
111
|
+
const methods = API_METHODS.filter((method) => typeof record[method] === "function");
|
|
112
|
+
const requestMethod = request.method.toUpperCase();
|
|
113
|
+
let handler = record[requestMethod];
|
|
114
|
+
let stripBody = false;
|
|
115
|
+
if (typeof handler !== "function" && requestMethod === "HEAD" && typeof record["GET"] === "function") {
|
|
116
|
+
handler = record["GET"];
|
|
117
|
+
stripBody = true;
|
|
118
|
+
}
|
|
119
|
+
if (typeof handler !== "function") {
|
|
120
|
+
if (requestMethod === "OPTIONS") {
|
|
121
|
+
return new Response(null, { status: 204, headers: { allow: methods.join(", ") } });
|
|
122
|
+
}
|
|
123
|
+
return new Response("Method Not Allowed", {
|
|
124
|
+
status: 405,
|
|
125
|
+
headers: { allow: methods.join(", ") }
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
const result = await handler({ req: request, params, ctx });
|
|
130
|
+
if (!(result instanceof Response)) {
|
|
131
|
+
return apiErrorResponse(new MoonsError({
|
|
132
|
+
code: "E_API_HANDLER_INVALID",
|
|
133
|
+
file: route.file,
|
|
134
|
+
message: `handler \`${requestMethod}\` tidak mengembalikan \`Response\``,
|
|
135
|
+
details: "Handler API wajib mengembalikan `new Response(...)` (PRD Addendum D.1).",
|
|
136
|
+
suggestions: ["Kembalikan `new Response(body, { status, headers })`"]
|
|
137
|
+
}), route);
|
|
138
|
+
}
|
|
139
|
+
if (stripBody) {
|
|
140
|
+
return new Response(null, { status: result.status, headers: result.headers });
|
|
141
|
+
}
|
|
142
|
+
return result;
|
|
143
|
+
} catch (error) {
|
|
144
|
+
return apiErrorResponse(error, route);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
54
147
|
async function notFoundResponse(request) {
|
|
55
148
|
if (notFoundRoute !== undefined) {
|
|
56
149
|
try {
|
|
@@ -67,16 +160,17 @@ function createRequestHandler(options) {
|
|
|
67
160
|
const html = await shell("<h1>404</h1><p>Halaman tidak ditemukan.</p>", payload, request.url);
|
|
68
161
|
return htmlResponse(html, 404);
|
|
69
162
|
}
|
|
70
|
-
async function routeErrorResponse(error, request) {
|
|
163
|
+
async function routeErrorResponse(error, request, resolved) {
|
|
71
164
|
console.error("[moons] route error:", error);
|
|
72
|
-
|
|
165
|
+
const boundary = (resolved === undefined ? null : findErrorBoundary(resolved.route, routes)) ?? errorRoute ?? null;
|
|
166
|
+
if (boundary !== null) {
|
|
73
167
|
try {
|
|
74
|
-
const key = routeKeyFromFile(routesDir,
|
|
168
|
+
const key = routeKeyFromFile(routesDir, boundary.file);
|
|
75
169
|
const [module] = await loadModules([key], loadRoute);
|
|
76
170
|
const isProduction = process.env["NODE_ENV"] === "production";
|
|
77
171
|
const serialized = isProduction ? undefined : serializeError(error);
|
|
78
172
|
const payload = encodeSsrPayload({
|
|
79
|
-
key
|
|
173
|
+
key,
|
|
80
174
|
params: {},
|
|
81
175
|
data: {},
|
|
82
176
|
...serialized === undefined ? {} : { error: serialized }
|
|
@@ -89,6 +183,62 @@ function createRequestHandler(options) {
|
|
|
89
183
|
}
|
|
90
184
|
return internalErrorResponse(error, { log: false });
|
|
91
185
|
}
|
|
186
|
+
async function loadDirMiddlewares(chain) {
|
|
187
|
+
const result = [];
|
|
188
|
+
for (const definition of chain) {
|
|
189
|
+
const key = routeKeyFromFile(routesDir, definition.file);
|
|
190
|
+
const load = options.loadRoute?.[key];
|
|
191
|
+
if (load === undefined) {
|
|
192
|
+
throw new MoonsError({
|
|
193
|
+
code: "E_MIDDLEWARE_INVALID",
|
|
194
|
+
file: definition.file,
|
|
195
|
+
message: "middleware direktori tidak bisa dimuat",
|
|
196
|
+
details: "Peta `loadRoute` tidak memuat `_middleware.ts` ini.",
|
|
197
|
+
suggestions: ["Restart dev server / jalankan ulang build"]
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
const module = await load();
|
|
201
|
+
const handler = module.default;
|
|
202
|
+
if (typeof handler !== "function") {
|
|
203
|
+
throw new MoonsError({
|
|
204
|
+
code: "E_MIDDLEWARE_INVALID",
|
|
205
|
+
file: definition.file,
|
|
206
|
+
message: "`_middleware.ts` tidak meng-export default fungsi",
|
|
207
|
+
details: "Middleware direktori mengikuti bentuk `(args) => Promise<Response>` (Addendum D.3).",
|
|
208
|
+
suggestions: ["Export default `async ({ req, ctx, next }) => await next()`"]
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
result.push(handler);
|
|
212
|
+
}
|
|
213
|
+
return result;
|
|
214
|
+
}
|
|
215
|
+
async function loadApiDirMiddlewares(chain) {
|
|
216
|
+
const result = [];
|
|
217
|
+
for (const definition of chain) {
|
|
218
|
+
const load = options.loadApiMiddleware?.[definition.file];
|
|
219
|
+
if (load === undefined) {
|
|
220
|
+
throw new MoonsError({
|
|
221
|
+
code: "E_MIDDLEWARE_INVALID",
|
|
222
|
+
file: definition.file,
|
|
223
|
+
message: "middleware API tidak bisa dimuat",
|
|
224
|
+
details: "Peta `loadApiMiddleware` tidak memuat `_middleware.ts` ini.",
|
|
225
|
+
suggestions: ["Restart dev server / jalankan ulang build"]
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
const module = await load();
|
|
229
|
+
if (typeof module.default !== "function") {
|
|
230
|
+
throw new MoonsError({
|
|
231
|
+
code: "E_MIDDLEWARE_INVALID",
|
|
232
|
+
file: definition.file,
|
|
233
|
+
message: "`_middleware.ts` tidak meng-export default fungsi",
|
|
234
|
+
details: "Middleware direktori mengikuti bentuk `(args) => Promise<Response>` (Addendum D.3).",
|
|
235
|
+
suggestions: ["Export default `async ({ req, ctx, next }) => await next()`"]
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
result.push(module.default);
|
|
239
|
+
}
|
|
240
|
+
return result;
|
|
241
|
+
}
|
|
92
242
|
return async function handler(request) {
|
|
93
243
|
try {
|
|
94
244
|
const url = new URL(request.url);
|
|
@@ -97,16 +247,35 @@ function createRequestHandler(options) {
|
|
|
97
247
|
}
|
|
98
248
|
const ctx = await config.context(request);
|
|
99
249
|
options.onContext?.(ctx);
|
|
250
|
+
if (request.headers.get("x-moons-prerender") === "1") {
|
|
251
|
+
ctx["__prerender"] = true;
|
|
252
|
+
}
|
|
100
253
|
const respond = composeMiddleware(config.middleware ?? [], ctx, async () => {
|
|
101
254
|
if (url.pathname.startsWith(RPC_PREFIX)) {
|
|
102
255
|
return await rpc(request, url.pathname.slice(RPC_PREFIX.length), ctx);
|
|
103
256
|
}
|
|
257
|
+
if (options.apiRoutes !== undefined && options.apiRoutes.length > 0) {
|
|
258
|
+
const apiMatch = resolveApiRoute(url.pathname, options.apiRoutes);
|
|
259
|
+
if (apiMatch !== null) {
|
|
260
|
+
const apiChain = findApiDirMiddlewares(apiMatch.route.file, options.apiMiddlewares ?? []);
|
|
261
|
+
if (apiChain.length === 0) {
|
|
262
|
+
return await handleApiRequest(apiMatch.route, apiMatch.params, request, ctx);
|
|
263
|
+
}
|
|
264
|
+
const middlewares = await loadApiDirMiddlewares(apiChain);
|
|
265
|
+
return await composeMiddleware(middlewares, ctx, () => handleApiRequest(apiMatch.route, apiMatch.params, request, ctx), { code: "E_MIDDLEWARE_INVALID", label: "middleware API" })(request);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
104
268
|
const resolved = resolveRoute(url.pathname, routes);
|
|
105
269
|
if (resolved === null) {
|
|
106
270
|
return await notFoundResponse(request);
|
|
107
271
|
}
|
|
108
272
|
try {
|
|
109
|
-
|
|
273
|
+
const dirChain = findDirMiddlewares(resolved.route, routes);
|
|
274
|
+
if (dirChain.length === 0) {
|
|
275
|
+
return await renderRoute(resolved, request, ctx);
|
|
276
|
+
}
|
|
277
|
+
const middlewares = await loadDirMiddlewares(dirChain);
|
|
278
|
+
return await composeMiddleware(middlewares, ctx, () => renderRoute(resolved, request, ctx), { code: "E_MIDDLEWARE_INVALID", label: "middleware direktori" })(request);
|
|
110
279
|
} catch (error) {
|
|
111
280
|
if (isRedirectSignal(error)) {
|
|
112
281
|
return new Response(null, {
|
|
@@ -117,15 +286,46 @@ function createRequestHandler(options) {
|
|
|
117
286
|
if (isNotFoundSignal(error)) {
|
|
118
287
|
return await notFoundResponse(request);
|
|
119
288
|
}
|
|
120
|
-
return await routeErrorResponse(error, request);
|
|
289
|
+
return await routeErrorResponse(error, request, resolved);
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
return await runWithResponseCollector(async () => {
|
|
293
|
+
let response;
|
|
294
|
+
try {
|
|
295
|
+
response = await runWithRequestCache(() => runWithContext(ctx, () => respond(request)));
|
|
296
|
+
} catch (error) {
|
|
297
|
+
response = internalErrorResponse(error);
|
|
121
298
|
}
|
|
299
|
+
return applyResponseMutations(response);
|
|
122
300
|
});
|
|
123
|
-
return await runWithRequestCache(() => runWithContext(ctx, () => respond(request)));
|
|
124
301
|
} catch (error) {
|
|
125
302
|
return internalErrorResponse(error);
|
|
126
303
|
}
|
|
127
304
|
};
|
|
128
305
|
}
|
|
306
|
+
function applyResponseMutations(response) {
|
|
307
|
+
const mutations = takeResponseMutations();
|
|
308
|
+
if (mutations.length === 0) {
|
|
309
|
+
return response;
|
|
310
|
+
}
|
|
311
|
+
const headers = new Headers(response.headers);
|
|
312
|
+
for (const mutation of mutations) {
|
|
313
|
+
if (mutation.kind === "header") {
|
|
314
|
+
if (mutation.append) {
|
|
315
|
+
headers.append(mutation.name, mutation.value);
|
|
316
|
+
} else {
|
|
317
|
+
headers.set(mutation.name, mutation.value);
|
|
318
|
+
}
|
|
319
|
+
} else {
|
|
320
|
+
headers.append("set-cookie", mutation.value);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return new Response(response.body, {
|
|
324
|
+
status: response.status,
|
|
325
|
+
statusText: response.statusText,
|
|
326
|
+
headers
|
|
327
|
+
});
|
|
328
|
+
}
|
|
129
329
|
async function loadModules(keys, loadRoute) {
|
|
130
330
|
return await Promise.all(keys.map(async (key) => {
|
|
131
331
|
const load = loadRoute[key];
|
|
@@ -169,6 +369,21 @@ async function runRouteMeta(pageModule, pageData, file) {
|
|
|
169
369
|
}
|
|
170
370
|
return result;
|
|
171
371
|
}
|
|
372
|
+
function apiErrorResponse(error, route) {
|
|
373
|
+
const isProduction = process.env["NODE_ENV"] === "production";
|
|
374
|
+
if (error instanceof MoonsError) {
|
|
375
|
+
console.error(`[moons] API ${route.path}: ${error.format()}`);
|
|
376
|
+
return new Response(isProduction ? "Terjadi kesalahan internal." : error.format(), {
|
|
377
|
+
status: 500,
|
|
378
|
+
headers: { "content-type": "text/plain; charset=utf-8" }
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
console.error(`[moons] API ${route.path}:`, error);
|
|
382
|
+
return new Response(isProduction ? "Terjadi kesalahan internal." : String(error), {
|
|
383
|
+
status: 500,
|
|
384
|
+
headers: { "content-type": "text/plain; charset=utf-8" }
|
|
385
|
+
});
|
|
386
|
+
}
|
|
172
387
|
function internalErrorResponse(error, options = {}) {
|
|
173
388
|
if (options.log !== false) {
|
|
174
389
|
console.error("[moons] request error:", error);
|
|
@@ -198,6 +413,19 @@ function healthResponse(method) {
|
|
|
198
413
|
function escapeHtml(text) {
|
|
199
414
|
return text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
200
415
|
}
|
|
416
|
+
function warnPayloadFindings(payload, scan, warned) {
|
|
417
|
+
if (scan === undefined) {
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
for (const finding of scanPayloadForSecrets(payload, scan)) {
|
|
421
|
+
const label = finding.kind === "canary" ? "canary" : `env(${finding.label})`;
|
|
422
|
+
if (warned.has(label)) {
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
warned.add(label);
|
|
426
|
+
console.warn(`[moons] peringatan dev: payload SSR memuat ${label} — jangan mengembalikan rahasia dari loader (PRD §2/§16.6).`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
201
429
|
// src/server/node-server.ts
|
|
202
430
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
203
431
|
import { createServer } from "node:http";
|
|
@@ -328,18 +556,47 @@ function resolveStaticFile(clientDir, pathname) {
|
|
|
328
556
|
} catch {
|
|
329
557
|
return null;
|
|
330
558
|
}
|
|
331
|
-
const
|
|
332
|
-
if (
|
|
333
|
-
return
|
|
559
|
+
const exact = safeWithin(clientDir, `.${decoded}`);
|
|
560
|
+
if (exact !== null && isFile(exact)) {
|
|
561
|
+
return exact;
|
|
334
562
|
}
|
|
335
|
-
|
|
336
|
-
|
|
563
|
+
const normalized = decoded.length > 1 ? decoded.replace(/\/+$/, "") : decoded;
|
|
564
|
+
const manifest = readPrerenderManifest(clientDir);
|
|
565
|
+
if (manifest.has(normalized)) {
|
|
566
|
+
const target = normalized === "/" ? "index.html" : `${normalized.slice(1)}/index.html`;
|
|
567
|
+
const candidate = safeWithin(clientDir, `./${target}`);
|
|
568
|
+
if (candidate !== null && isFile(candidate)) {
|
|
569
|
+
return candidate;
|
|
570
|
+
}
|
|
337
571
|
}
|
|
338
|
-
|
|
572
|
+
return null;
|
|
573
|
+
}
|
|
574
|
+
function safeWithin(clientDir, relative) {
|
|
575
|
+
const candidate = resolve(clientDir, relative);
|
|
576
|
+
if (candidate !== clientDir && !candidate.startsWith(clientDir + sep)) {
|
|
339
577
|
return null;
|
|
340
578
|
}
|
|
341
579
|
return candidate;
|
|
342
580
|
}
|
|
581
|
+
function isFile(path) {
|
|
582
|
+
return existsSync(path) && statSync(path).isFile();
|
|
583
|
+
}
|
|
584
|
+
var prerenderManifestCache = null;
|
|
585
|
+
function readPrerenderManifest(clientDir) {
|
|
586
|
+
const file = join(clientDir, "_moons_prerender.json");
|
|
587
|
+
try {
|
|
588
|
+
const mtimeMs = statSync(file).mtimeMs;
|
|
589
|
+
if (prerenderManifestCache !== null && prerenderManifestCache.file === file && prerenderManifestCache.mtimeMs === mtimeMs) {
|
|
590
|
+
return prerenderManifestCache.paths;
|
|
591
|
+
}
|
|
592
|
+
const json = JSON.parse(readFileSync(file, "utf8"));
|
|
593
|
+
const paths = new Set(Array.isArray(json.paths) ? json.paths.filter((path) => typeof path === "string") : []);
|
|
594
|
+
prerenderManifestCache = { file, mtimeMs, paths };
|
|
595
|
+
return paths;
|
|
596
|
+
} catch {
|
|
597
|
+
return new Set;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
343
600
|
// src/server/test-server.ts
|
|
344
601
|
function createTestServer(options, baseUrl = "http://localhost") {
|
|
345
602
|
const shell = options.shell ?? ((markup, payload, _url, meta) => injectShell2(DEFAULT_TEMPLATE2, markup, payload, meta));
|
|
@@ -411,6 +668,184 @@ function createPluginRuntime(options) {
|
|
|
411
668
|
}
|
|
412
669
|
};
|
|
413
670
|
}
|
|
671
|
+
// src/server/prerender.ts
|
|
672
|
+
import { MoonsError as MoonsError2 } from "@moonsjs/core";
|
|
673
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
674
|
+
import { dirname, join as join2, resolve as resolve2, sep as sep2 } from "node:path";
|
|
675
|
+
var PRERENDER_MANIFEST_FILE = "_moons_prerender.json";
|
|
676
|
+
async function runPrerender(options) {
|
|
677
|
+
const log = options.log ?? ((message) => console.log(message));
|
|
678
|
+
const env = options.env ?? process.env;
|
|
679
|
+
let written = 0;
|
|
680
|
+
let skipped = 0;
|
|
681
|
+
const writtenPaths = [];
|
|
682
|
+
const pages = options.routes.filter((route) => route.kind === "page");
|
|
683
|
+
for (const route of pages) {
|
|
684
|
+
const key = routeKeyOf(options.routesDir, route.file);
|
|
685
|
+
const load = options.loadRoute[key];
|
|
686
|
+
if (load === undefined) {
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
const module = await load();
|
|
690
|
+
const prerender = module.prerender;
|
|
691
|
+
if (prerender !== true) {
|
|
692
|
+
if (prerender !== undefined && prerender !== false) {
|
|
693
|
+
throw prerenderError(route, "`prerender` harus boolean (`true`/`false`)");
|
|
694
|
+
}
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
const pathnames = await resolvePathnames(route, module);
|
|
698
|
+
for (const pathname of pathnames) {
|
|
699
|
+
const response = await options.handler(new Request(`http://localhost${pathname}`, {
|
|
700
|
+
method: "GET",
|
|
701
|
+
headers: { "x-moons-prerender": "1" }
|
|
702
|
+
}));
|
|
703
|
+
if (response.status === 404) {
|
|
704
|
+
log(`[moons] prerender: lewati ${pathname} (notFound)`);
|
|
705
|
+
skipped += 1;
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
708
|
+
if (response.status >= 300 && response.status < 400) {
|
|
709
|
+
throw prerenderError(route, `\`redirect()\` tidak didukung saat prerender (${pathname})`);
|
|
710
|
+
}
|
|
711
|
+
if (response.status !== 200) {
|
|
712
|
+
throw prerenderError(route, `status ${response.status} saat merender ${pathname}`);
|
|
713
|
+
}
|
|
714
|
+
const html = await response.text();
|
|
715
|
+
const findings = scanPayloadForSecrets(html, {
|
|
716
|
+
...options.canary === undefined ? {} : { canary: options.canary },
|
|
717
|
+
env
|
|
718
|
+
});
|
|
719
|
+
if (findings.length > 0) {
|
|
720
|
+
const labels = findings.map((finding) => finding.kind === "canary" ? "canary" : `env(${finding.label})`).join(", ");
|
|
721
|
+
throw new MoonsError2({
|
|
722
|
+
code: "E_PRERENDER_LEAK",
|
|
723
|
+
file: route.file,
|
|
724
|
+
message: `hasil prerender \`${pathname}\` memuat ${labels}`,
|
|
725
|
+
details: "HTML statis permanen — rahasia yang ter-prerender tidak bisa ditarik kembali (PRD Addendum D.5).",
|
|
726
|
+
suggestions: [
|
|
727
|
+
"Jangan mengembalikan nilai env rahasia dari loader yang di-prerender",
|
|
728
|
+
"Matikan `prerender` untuk route ini atau pindahkan datanya ke serverFn request-time"
|
|
729
|
+
]
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
const file = outputFile(options.clientDir, pathname);
|
|
733
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
734
|
+
writeFileSync(file, html);
|
|
735
|
+
writtenPaths.push(pathname);
|
|
736
|
+
written += 1;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
writeFileSync(join2(options.clientDir, PRERENDER_MANIFEST_FILE), `${JSON.stringify({ paths: writtenPaths }, null, 2)}
|
|
740
|
+
`);
|
|
741
|
+
log(`[moons] prerender selesai: ${written} file (${skipped} dilewati)`);
|
|
742
|
+
return { written, skipped };
|
|
743
|
+
}
|
|
744
|
+
function routeKeyOf(routesDir, file) {
|
|
745
|
+
const relative = file.startsWith(routesDir) ? file.slice(routesDir.length + 1) : file;
|
|
746
|
+
return relative.split("\\").join("/").replace(/\.(tsx|ts)$/, "");
|
|
747
|
+
}
|
|
748
|
+
async function resolvePathnames(route, module) {
|
|
749
|
+
const isDynamic = route.params.length > 0;
|
|
750
|
+
if (!isDynamic) {
|
|
751
|
+
return [route.path];
|
|
752
|
+
}
|
|
753
|
+
const paths = module.paths;
|
|
754
|
+
if (typeof paths !== "function") {
|
|
755
|
+
throw new MoonsError2({
|
|
756
|
+
code: "E_PRERENDER_NO_PATHS",
|
|
757
|
+
file: route.file,
|
|
758
|
+
message: "route dynamic ber-`prerender = true` tanpa `export const paths`",
|
|
759
|
+
details: "Prerender butuh daftar param untuk merender tiap path (PRD Addendum D.5).",
|
|
760
|
+
suggestions: [
|
|
761
|
+
'Tambahkan `export const paths = () => [{ id: "…" }]`',
|
|
762
|
+
"Atau hapus `prerender` agar dirender SSR saat request"
|
|
763
|
+
]
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
const raw = await paths();
|
|
767
|
+
if (!Array.isArray(raw)) {
|
|
768
|
+
throw prerenderError(route, "`paths()` harus mengembalikan array");
|
|
769
|
+
}
|
|
770
|
+
const expected = new Set(route.params);
|
|
771
|
+
const result = [];
|
|
772
|
+
for (const item of raw) {
|
|
773
|
+
if (item === null || typeof item !== "object" || Array.isArray(item)) {
|
|
774
|
+
throw prerenderError(route, "item `paths()` harus objek param string");
|
|
775
|
+
}
|
|
776
|
+
const record = item;
|
|
777
|
+
const keys = Object.keys(record);
|
|
778
|
+
for (const name of expected) {
|
|
779
|
+
const value = record[name];
|
|
780
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
781
|
+
throw prerenderError(route, `param \`${name}\` wajib string non-kosong`);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
for (const key of keys) {
|
|
785
|
+
if (!expected.has(key)) {
|
|
786
|
+
throw prerenderError(route, `param \`${key}\` tidak ada di pola route`);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
result.push(buildPathname(route, record));
|
|
790
|
+
}
|
|
791
|
+
return result;
|
|
792
|
+
}
|
|
793
|
+
function buildPathname(route, params) {
|
|
794
|
+
const segments = route.path.split("/").filter((segment) => segment.length > 0);
|
|
795
|
+
const out = [];
|
|
796
|
+
let catchallName = null;
|
|
797
|
+
for (const segment of segments) {
|
|
798
|
+
if (segment === "*") {
|
|
799
|
+
catchallName = route.params[route.params.length - 1] ?? null;
|
|
800
|
+
const value = catchallName === null ? "" : params[catchallName] ?? "";
|
|
801
|
+
for (const part of value.split("/")) {
|
|
802
|
+
if (part.length === 0) {
|
|
803
|
+
continue;
|
|
804
|
+
}
|
|
805
|
+
out.push(encodeSegment(route, part));
|
|
806
|
+
}
|
|
807
|
+
continue;
|
|
808
|
+
}
|
|
809
|
+
if (segment.startsWith(":")) {
|
|
810
|
+
out.push(encodeSegment(route, params[segment.slice(1)] ?? ""));
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
out.push(segment);
|
|
814
|
+
}
|
|
815
|
+
return `/${out.join("/")}`;
|
|
816
|
+
}
|
|
817
|
+
function encodeSegment(route, value) {
|
|
818
|
+
if (value.length === 0 || /^\.+$/.test(value)) {
|
|
819
|
+
throw prerenderError(route, `nilai param \`${value}\` tidak valid untuk path file`);
|
|
820
|
+
}
|
|
821
|
+
return encodeURIComponent(value);
|
|
822
|
+
}
|
|
823
|
+
function outputFile(clientDir, pathname) {
|
|
824
|
+
const parts = pathname.split("/").filter((part) => part.length > 0);
|
|
825
|
+
const file = resolve2(join2(clientDir, ...parts, "index.html"));
|
|
826
|
+
const base = resolve2(clientDir);
|
|
827
|
+
if (file !== base && !file.startsWith(base + sep2)) {
|
|
828
|
+
throw new MoonsError2({
|
|
829
|
+
code: "E_PRERENDER_INVALID",
|
|
830
|
+
message: `path prerender keluar dari direktori client (\`${pathname}\`)`,
|
|
831
|
+
details: "Nama file harus berada di dalam `dist/client`.",
|
|
832
|
+
suggestions: ["Periksa nilai param `paths` — jangan memakai `..`"]
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
return file;
|
|
836
|
+
}
|
|
837
|
+
function prerenderError(route, message) {
|
|
838
|
+
return new MoonsError2({
|
|
839
|
+
code: "E_PRERENDER_INVALID",
|
|
840
|
+
file: route.file,
|
|
841
|
+
message,
|
|
842
|
+
details: "Kontrak prerender mengikuti PRD Addendum D.5.",
|
|
843
|
+
suggestions: [
|
|
844
|
+
"Perbaiki bentuk `paths`/param agar cocok dengan pola route",
|
|
845
|
+
"Jangan memakai `redirect()` pada route yang di-prerender"
|
|
846
|
+
]
|
|
847
|
+
});
|
|
848
|
+
}
|
|
414
849
|
export {
|
|
415
850
|
DEFAULT_TEMPLATE2 as DEFAULT_TEMPLATE,
|
|
416
851
|
ROOT_PLACEHOLDER2 as ROOT_PLACEHOLDER,
|
|
@@ -423,9 +858,10 @@ export {
|
|
|
423
858
|
installDevMiddleware2 as installDevMiddleware,
|
|
424
859
|
installGracefulShutdown,
|
|
425
860
|
resolveStaticFile,
|
|
861
|
+
runPrerender,
|
|
426
862
|
sendNodeResponse2 as sendNodeResponse,
|
|
427
863
|
startNodeServer,
|
|
428
864
|
toWebRequest2 as toWebRequest
|
|
429
865
|
};
|
|
430
866
|
|
|
431
|
-
//# debugId=
|
|
867
|
+
//# debugId=C7C001D56BF6021C64756E2164756E21
|