@moonsjs/vite 0.1.5 → 0.1.7
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-1fnt137k.js → index-qz9sc74p.js} +2 -2
- package/dist/{index-1fnt137k.js.map → index-qz9sc74p.js.map} +2 -2
- package/dist/{index-e7qz0epf.js → index-tysyk2qf.js} +13 -13
- package/dist/{index-e7qz0epf.js.map → index-tysyk2qf.js.map} +4 -4
- package/dist/index.js +16 -16
- package/dist/index.js.map +4 -4
- package/dist/server/index.js +131 -23
- package/dist/server/index.js.map +7 -6
- package/dist/server/isr.d.ts +25 -0
- package/dist/server/isr.d.ts.map +1 -0
- package/dist/server/node-server.d.ts +6 -0
- package/dist/server/node-server.d.ts.map +1 -1
- package/dist/server/prerender.d.ts.map +1 -1
- package/dist/transform.js +1 -1
- package/package.json +10 -3
- package/src/api.ts +4 -4
- package/src/plugin.ts +13 -9
- package/src/server/handler.ts +7 -7
- package/src/server/isr.ts +81 -0
- package/src/server/node-server.ts +85 -12
- package/src/server/prerender.ts +33 -7
- package/src/server/shell.ts +1 -1
- package/src/server-only.ts +1 -1
- package/src/transform.ts +11 -11
package/src/server/handler.ts
CHANGED
|
@@ -151,7 +151,7 @@ export function createRequestHandler(
|
|
|
151
151
|
code: "E_API_HANDLER_INVALID",
|
|
152
152
|
file: route.file,
|
|
153
153
|
message: "modul API tidak mengekspor objek handler",
|
|
154
|
-
details: `Modul \`${route.path}\` harus mengekspor handler method
|
|
154
|
+
details: `Modul \`${route.path}\` harus mengekspor handler method.`,
|
|
155
155
|
suggestions: ["Export `GET`/`POST`/… sebagai fungsi `(args) => Response`"],
|
|
156
156
|
}),
|
|
157
157
|
route,
|
|
@@ -194,7 +194,7 @@ export function createRequestHandler(
|
|
|
194
194
|
code: "E_API_HANDLER_INVALID",
|
|
195
195
|
file: route.file,
|
|
196
196
|
message: `handler \`${requestMethod}\` tidak mengembalikan \`Response\``,
|
|
197
|
-
details: "Handler API wajib mengembalikan `new Response(...)
|
|
197
|
+
details: "Handler API wajib mengembalikan `new Response(...)`.",
|
|
198
198
|
suggestions: ["Kembalikan `new Response(body, { status, headers })`"],
|
|
199
199
|
}),
|
|
200
200
|
route,
|
|
@@ -303,7 +303,7 @@ export function createRequestHandler(
|
|
|
303
303
|
code: "E_MIDDLEWARE_INVALID",
|
|
304
304
|
file: definition.file,
|
|
305
305
|
message: "`_middleware.ts` tidak meng-export default fungsi",
|
|
306
|
-
details: "Middleware direktori mengikuti bentuk `(args) => Promise<Response
|
|
306
|
+
details: "Middleware direktori mengikuti bentuk `(args) => Promise<Response>`.",
|
|
307
307
|
suggestions: ["Export default `async ({ req, ctx, next }) => await next()`"],
|
|
308
308
|
});
|
|
309
309
|
}
|
|
@@ -334,7 +334,7 @@ export function createRequestHandler(
|
|
|
334
334
|
code: "E_MIDDLEWARE_INVALID",
|
|
335
335
|
file: definition.file,
|
|
336
336
|
message: "`_middleware.ts` tidak meng-export default fungsi",
|
|
337
|
-
details: "Middleware direktori mengikuti bentuk `(args) => Promise<Response
|
|
337
|
+
details: "Middleware direktori mengikuti bentuk `(args) => Promise<Response>`.",
|
|
338
338
|
suggestions: ["Export default `async ({ req, ctx, next }) => await next()`"],
|
|
339
339
|
});
|
|
340
340
|
}
|
|
@@ -497,7 +497,7 @@ async function runRouteMeta(
|
|
|
497
497
|
code: "E_INTERNAL",
|
|
498
498
|
file,
|
|
499
499
|
message: "export `meta` harus berupa fungsi `({ data }) => RouteMeta`",
|
|
500
|
-
details: "Bentuk `meta` mengikuti
|
|
500
|
+
details: "Bentuk `meta` mengikuti ",
|
|
501
501
|
suggestions: [
|
|
502
502
|
"Ubah menjadi `export const meta = ({ data }) => ({ title: \"...\" })`",
|
|
503
503
|
"Atau hapus export `meta`",
|
|
@@ -510,7 +510,7 @@ async function runRouteMeta(
|
|
|
510
510
|
code: "E_INTERNAL",
|
|
511
511
|
file,
|
|
512
512
|
message: "`meta()` harus mengembalikan objek RouteMeta",
|
|
513
|
-
details: "Nilai yang dikembalikan dipakai untuk <title>/description/og
|
|
513
|
+
details: "Nilai yang dikembalikan dipakai untuk <title>/description/og.",
|
|
514
514
|
suggestions: ["Kembalikan objek, mis. `({ title: \"Halaman\" })`"],
|
|
515
515
|
});
|
|
516
516
|
}
|
|
@@ -602,7 +602,7 @@ function warnPayloadFindings(
|
|
|
602
602
|
}
|
|
603
603
|
warned.add(label);
|
|
604
604
|
console.warn(
|
|
605
|
-
`[moons] peringatan dev: payload SSR memuat ${label} — jangan mengembalikan rahasia dari loader
|
|
605
|
+
`[moons] peringatan dev: payload SSR memuat ${label} — jangan mengembalikan rahasia dari loader.`,
|
|
606
606
|
);
|
|
607
607
|
}
|
|
608
608
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { renameSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { scanPayloadForSecrets } from "../payload-scan";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ISR / revalidate on-demand (PRD Addendum E.1): file prerender yang kedaluwarsa
|
|
6
|
+
* disajikan apa adanya (stale-while-revalidate) sambil diregenerasi via handler
|
|
7
|
+
* SSR in-process. Tulis atomik + guard kebocoran fail-closed.
|
|
8
|
+
*/
|
|
9
|
+
export function shouldRevalidate(input: {
|
|
10
|
+
mtimeMs: number;
|
|
11
|
+
revalidateSeconds: number;
|
|
12
|
+
now?: number;
|
|
13
|
+
}): boolean {
|
|
14
|
+
const now = input.now ?? Date.now();
|
|
15
|
+
return now - input.mtimeMs >= input.revalidateSeconds * 1000;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface RevalidateOptions {
|
|
19
|
+
/** Path URL ternormalisasi (mis. "/blog/hello"). */
|
|
20
|
+
pathname: string;
|
|
21
|
+
/** File HTML yang akan ditimpa. */
|
|
22
|
+
file: string;
|
|
23
|
+
handler: (request: Request) => Promise<Response>;
|
|
24
|
+
canary?: string;
|
|
25
|
+
env?: Record<string, string | undefined>;
|
|
26
|
+
log?: (message: string) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const inflight = new Map<string, Promise<void>>();
|
|
30
|
+
|
|
31
|
+
/** Regenerasi ter-dedupe per path (panggilan paralel berbagi promise). */
|
|
32
|
+
export function revalidatePrerendered(options: RevalidateOptions): Promise<void> {
|
|
33
|
+
const existing = inflight.get(options.pathname);
|
|
34
|
+
if (existing !== undefined) {
|
|
35
|
+
return existing;
|
|
36
|
+
}
|
|
37
|
+
const task = runRevalidate(options).finally(() => {
|
|
38
|
+
inflight.delete(options.pathname);
|
|
39
|
+
});
|
|
40
|
+
inflight.set(options.pathname, task);
|
|
41
|
+
return task;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function runRevalidate(options: RevalidateOptions): Promise<void> {
|
|
45
|
+
const log = options.log ?? ((message: string) => console.log(message));
|
|
46
|
+
try {
|
|
47
|
+
const response = await options.handler(
|
|
48
|
+
new Request(`http://localhost${options.pathname}`, {
|
|
49
|
+
method: "GET",
|
|
50
|
+
headers: { "x-moons-prerender": "1" },
|
|
51
|
+
}),
|
|
52
|
+
);
|
|
53
|
+
if (response.status !== 200) {
|
|
54
|
+
log(`[moons] isr: lewati ${options.pathname} (status ${response.status})`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const html = await response.text();
|
|
58
|
+
const findings = scanPayloadForSecrets(html, {
|
|
59
|
+
...(options.canary === undefined ? {} : { canary: options.canary }),
|
|
60
|
+
env: options.env ?? process.env,
|
|
61
|
+
});
|
|
62
|
+
if (findings.length > 0) {
|
|
63
|
+
const labels = findings
|
|
64
|
+
.map((finding) => (finding.kind === "canary" ? "canary" : `env(${finding.label})`))
|
|
65
|
+
.join(", ");
|
|
66
|
+
// Fail-closed: jangan menimpa file statis dengan konten bocor.
|
|
67
|
+
log(`[moons] isr: TIDAK menulis ${options.pathname} — memuat ${labels}`);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const tmp = `${options.file}.tmp-${process.pid}`;
|
|
71
|
+
writeFileSync(tmp, html);
|
|
72
|
+
renameSync(tmp, options.file);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
log(`[moons] isr: gagal regenerasi ${options.pathname}: ${String(error)}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Test helper: bersihkan dedupe in-flight. */
|
|
79
|
+
export function __resetIsrInflight(): void {
|
|
80
|
+
inflight.clear();
|
|
81
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { createServer, type Server } from "node:http";
|
|
3
3
|
import { extname, join, resolve, sep } from "node:path";
|
|
4
|
+
import { revalidatePrerendered, shouldRevalidate } from "./isr";
|
|
4
5
|
import { sendNodeResponse, toWebRequest } from "./node";
|
|
5
6
|
import type { PluginRuntime } from "./plugins";
|
|
6
7
|
import { installGracefulShutdown } from "./shutdown";
|
|
@@ -36,6 +37,8 @@ export interface NodeServerOptions {
|
|
|
36
37
|
shutdownTimeoutMs?: number;
|
|
37
38
|
/** Lifecycle plugin (Addendum B.4) — onStart/onReady/onShutdown. */
|
|
38
39
|
plugins?: PluginRuntime;
|
|
40
|
+
/** Canary `leakTest.canary` untuk guard ISR (Addendum E.1). */
|
|
41
|
+
canary?: string;
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
/**
|
|
@@ -57,6 +60,7 @@ export function startNodeServer(
|
|
|
57
60
|
if (req.method === "GET" || req.method === "HEAD") {
|
|
58
61
|
const staticFile = resolveStaticFile(clientDir, url.pathname);
|
|
59
62
|
if (staticFile !== null) {
|
|
63
|
+
scheduleRevalidation(staticFile, url.pathname, handler, options, clientDir);
|
|
60
64
|
const body = readFileSync(staticFile);
|
|
61
65
|
res.statusCode = 200;
|
|
62
66
|
res.setHeader("content-type", MIME_TYPES[extname(staticFile).toLowerCase()] ?? "application/octet-stream");
|
|
@@ -125,7 +129,7 @@ export function resolveStaticFile(clientDir: string, pathname: string): string |
|
|
|
125
129
|
// statis — bukan shell `index.html` biasa (yang berarti bypass SSR).
|
|
126
130
|
const normalized = decoded.length > 1 ? decoded.replace(/\/+$/, "") : decoded;
|
|
127
131
|
const manifest = readPrerenderManifest(clientDir);
|
|
128
|
-
if (manifest.has(normalized)) {
|
|
132
|
+
if (manifest.paths.has(normalized)) {
|
|
129
133
|
const target = normalized === "/" ? "index.html" : `${normalized.slice(1)}/index.html`;
|
|
130
134
|
const candidate = safeWithin(clientDir, `./${target}`);
|
|
131
135
|
if (candidate !== null && isFile(candidate)) {
|
|
@@ -148,22 +152,91 @@ function isFile(path: string): boolean {
|
|
|
148
152
|
return existsSync(path) && statSync(path).isFile();
|
|
149
153
|
}
|
|
150
154
|
|
|
151
|
-
|
|
155
|
+
export interface PrerenderManifestInfo {
|
|
156
|
+
paths: Set<string>;
|
|
157
|
+
revalidate: Map<string, number>;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let prerenderManifestCache:
|
|
161
|
+
| { file: string; mtimeMs: number; info: PrerenderManifestInfo }
|
|
162
|
+
| null = null;
|
|
152
163
|
|
|
153
|
-
function readPrerenderManifest(clientDir: string):
|
|
164
|
+
function readPrerenderManifest(clientDir: string): PrerenderManifestInfo {
|
|
154
165
|
const file = join(clientDir, "_moons_prerender.json");
|
|
166
|
+
const empty: PrerenderManifestInfo = { paths: new Set(), revalidate: new Map() };
|
|
155
167
|
try {
|
|
156
168
|
const mtimeMs = statSync(file).mtimeMs;
|
|
157
|
-
if (
|
|
158
|
-
|
|
169
|
+
if (
|
|
170
|
+
prerenderManifestCache !== null &&
|
|
171
|
+
prerenderManifestCache.file === file &&
|
|
172
|
+
prerenderManifestCache.mtimeMs === mtimeMs
|
|
173
|
+
) {
|
|
174
|
+
return prerenderManifestCache.info;
|
|
175
|
+
}
|
|
176
|
+
const json = JSON.parse(readFileSync(file, "utf8")) as {
|
|
177
|
+
paths?: unknown;
|
|
178
|
+
version?: unknown;
|
|
179
|
+
entries?: unknown;
|
|
180
|
+
};
|
|
181
|
+
const paths = new Set<string>();
|
|
182
|
+
const revalidate = new Map<string, number>();
|
|
183
|
+
if (Array.isArray(json.paths)) {
|
|
184
|
+
// Manifest v1 (D.5) — tanpa revalidate.
|
|
185
|
+
for (const path of json.paths) {
|
|
186
|
+
if (typeof path === "string") {
|
|
187
|
+
paths.add(path);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
159
190
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
191
|
+
if (Array.isArray(json.entries)) {
|
|
192
|
+
// Manifest v2 (E.1).
|
|
193
|
+
for (const entry of json.entries as Array<Record<string, unknown>>) {
|
|
194
|
+
if (entry === null || typeof entry !== "object" || typeof entry["path"] !== "string") {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
const path = entry["path"] as string;
|
|
198
|
+
paths.add(path);
|
|
199
|
+
const seconds = entry["revalidate"];
|
|
200
|
+
if (typeof seconds === "number" && Number.isFinite(seconds) && seconds >= 0) {
|
|
201
|
+
revalidate.set(path, seconds);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const info = { paths, revalidate };
|
|
206
|
+
prerenderManifestCache = { file, mtimeMs, info };
|
|
207
|
+
return info;
|
|
166
208
|
} catch {
|
|
167
|
-
return
|
|
209
|
+
return empty;
|
|
168
210
|
}
|
|
169
211
|
}
|
|
212
|
+
|
|
213
|
+
/** ISR: jika file prerender kedaluwarsa, regenerasi background (SWR). */
|
|
214
|
+
function scheduleRevalidation(
|
|
215
|
+
staticFile: string,
|
|
216
|
+
pathname: string,
|
|
217
|
+
handler: (request: Request) => Promise<Response>,
|
|
218
|
+
options: NodeServerOptions,
|
|
219
|
+
clientDir: string,
|
|
220
|
+
): void {
|
|
221
|
+
const normalized = pathname.length > 1 ? pathname.replace(/\/+$/, "") : pathname;
|
|
222
|
+
const seconds = readPrerenderManifest(clientDir).revalidate.get(normalized);
|
|
223
|
+
if (seconds === undefined || seconds < 0) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
let mtimeMs: number;
|
|
227
|
+
try {
|
|
228
|
+
mtimeMs = statSync(staticFile).mtimeMs;
|
|
229
|
+
} catch {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (!shouldRevalidate({ mtimeMs, revalidateSeconds: seconds })) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
void revalidatePrerendered({
|
|
236
|
+
pathname: normalized,
|
|
237
|
+
file: staticFile,
|
|
238
|
+
handler,
|
|
239
|
+
...(options.canary === undefined ? {} : { canary: options.canary }),
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
package/src/server/prerender.ts
CHANGED
|
@@ -36,7 +36,7 @@ export async function runPrerender(options: PrerenderOptions): Promise<Prerender
|
|
|
36
36
|
const env = options.env ?? process.env;
|
|
37
37
|
let written = 0;
|
|
38
38
|
let skipped = 0;
|
|
39
|
-
const
|
|
39
|
+
const entries: Array<{ path: string; revalidate?: number }> = [];
|
|
40
40
|
|
|
41
41
|
const pages = options.routes.filter((route) => route.kind === "page");
|
|
42
42
|
for (const route of pages) {
|
|
@@ -47,6 +47,28 @@ export async function runPrerender(options: PrerenderOptions): Promise<Prerender
|
|
|
47
47
|
}
|
|
48
48
|
const module = (await load()) as RouteModule;
|
|
49
49
|
const prerender = module.prerender;
|
|
50
|
+
const revalidate = module.revalidate;
|
|
51
|
+
|
|
52
|
+
// Addendum E.1: `revalidate` hanya sah bersama `prerender`.
|
|
53
|
+
if (revalidate !== undefined) {
|
|
54
|
+
if (
|
|
55
|
+
typeof revalidate !== "number" ||
|
|
56
|
+
!Number.isFinite(revalidate) ||
|
|
57
|
+
revalidate < 0 ||
|
|
58
|
+
prerender !== true
|
|
59
|
+
) {
|
|
60
|
+
throw new MoonsError({
|
|
61
|
+
code: "E_ISR_INVALID",
|
|
62
|
+
file: route.file,
|
|
63
|
+
message: "`revalidate` tidak valid atau dipakai tanpa `prerender = true`",
|
|
64
|
+
details: "ISR berbasis file: `revalidate` (detik, number >= 0) + `prerender`.",
|
|
65
|
+
suggestions: [
|
|
66
|
+
"Gunakan angka detik, mis. `export const revalidate = 60`",
|
|
67
|
+
"Tambahkan `export const prerender = true` pada page yang sama",
|
|
68
|
+
],
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
50
72
|
|
|
51
73
|
if (prerender !== true) {
|
|
52
74
|
if (prerender !== undefined && prerender !== false) {
|
|
@@ -90,7 +112,7 @@ export async function runPrerender(options: PrerenderOptions): Promise<Prerender
|
|
|
90
112
|
file: route.file,
|
|
91
113
|
message: `hasil prerender \`${pathname}\` memuat ${labels}`,
|
|
92
114
|
details:
|
|
93
|
-
"HTML statis permanen — rahasia yang ter-prerender tidak bisa ditarik kembali
|
|
115
|
+
"HTML statis permanen — rahasia yang ter-prerender tidak bisa ditarik kembali.",
|
|
94
116
|
suggestions: [
|
|
95
117
|
"Jangan mengembalikan nilai env rahasia dari loader yang di-prerender",
|
|
96
118
|
"Matikan `prerender` untuk route ini atau pindahkan datanya ke serverFn request-time",
|
|
@@ -101,15 +123,19 @@ export async function runPrerender(options: PrerenderOptions): Promise<Prerender
|
|
|
101
123
|
const file = outputFile(options.clientDir, pathname);
|
|
102
124
|
mkdirSync(dirname(file), { recursive: true });
|
|
103
125
|
writeFileSync(file, html);
|
|
104
|
-
|
|
126
|
+
entries.push(
|
|
127
|
+
typeof revalidate === "number" && revalidate >= 0
|
|
128
|
+
? { path: pathname, revalidate }
|
|
129
|
+
: { path: pathname },
|
|
130
|
+
);
|
|
105
131
|
written += 1;
|
|
106
132
|
}
|
|
107
133
|
}
|
|
108
134
|
|
|
109
|
-
// Manifest
|
|
135
|
+
// Manifest v2 (Addendum E.1): entries + revalidate; reader tetap menerima v1.
|
|
110
136
|
writeFileSync(
|
|
111
137
|
join(options.clientDir, PRERENDER_MANIFEST_FILE),
|
|
112
|
-
`${JSON.stringify({
|
|
138
|
+
`${JSON.stringify({ version: 2, entries }, null, 2)}\n`,
|
|
113
139
|
);
|
|
114
140
|
|
|
115
141
|
log(`[moons] prerender selesai: ${written} file (${skipped} dilewati)`);
|
|
@@ -132,7 +158,7 @@ async function resolvePathnames(route: RouteDefinition, module: RouteModule): Pr
|
|
|
132
158
|
code: "E_PRERENDER_NO_PATHS",
|
|
133
159
|
file: route.file,
|
|
134
160
|
message: "route dynamic ber-`prerender = true` tanpa `export const paths`",
|
|
135
|
-
details: "Prerender butuh daftar param untuk merender tiap path
|
|
161
|
+
details: "Prerender butuh daftar param untuk merender tiap path.",
|
|
136
162
|
suggestions: [
|
|
137
163
|
'Tambahkan `export const paths = () => [{ id: "…" }]`',
|
|
138
164
|
"Atau hapus `prerender` agar dirender SSR saat request",
|
|
@@ -221,7 +247,7 @@ function prerenderError(route: RouteDefinition, message: string): MoonsError {
|
|
|
221
247
|
code: "E_PRERENDER_INVALID",
|
|
222
248
|
file: route.file,
|
|
223
249
|
message,
|
|
224
|
-
details: "Kontrak prerender mengikuti
|
|
250
|
+
details: "Kontrak prerender mengikuti.",
|
|
225
251
|
suggestions: [
|
|
226
252
|
"Perbaiki bentuk `paths`/param agar cocok dengan pola route",
|
|
227
253
|
"Jangan memakai `redirect()` pada route yang di-prerender",
|
package/src/server/shell.ts
CHANGED
|
@@ -120,7 +120,7 @@ export function createFileShell(options: { clientDir?: string } = {}): Shell {
|
|
|
120
120
|
code: "E_INTERNAL",
|
|
121
121
|
file,
|
|
122
122
|
message: "`dist/client/index.html` tidak ditemukan",
|
|
123
|
-
details: "Build client harus dijalankan sebelum server produksi dinyalakan
|
|
123
|
+
details: "Build client harus dijalankan sebelum server produksi dinyalakan.",
|
|
124
124
|
suggestions: ["Jalankan `bun run build` lebih dulu"],
|
|
125
125
|
});
|
|
126
126
|
}
|
package/src/server-only.ts
CHANGED
|
@@ -211,7 +211,7 @@ export function serverOnlyImportError(input: {
|
|
|
211
211
|
file: input.relativeFile,
|
|
212
212
|
line: input.line,
|
|
213
213
|
message: `mengimpor${input.dynamic === true ? " (dynamic)" : ""} \`${input.specifier}\` — paket server-only \`${input.packageName}\``,
|
|
214
|
-
details: `${sourceLabel}\nPaket server-only tidak boleh masuk bundle client
|
|
214
|
+
details: `${sourceLabel}\nPaket server-only tidak boleh masuk bundle client.`,
|
|
215
215
|
suggestions: [
|
|
216
216
|
"Pindahkan operasi tersebut ke `*.server.ts` dan panggil lewat serverFn",
|
|
217
217
|
"Atau ganti/hapus import tersebut di modul client",
|
package/src/transform.ts
CHANGED
|
@@ -542,7 +542,7 @@ export function renderServerFnStub(
|
|
|
542
542
|
annotation: fn.name,
|
|
543
543
|
message: `schema ${kind} serverFn \`${fn.name}\` merujuk deklarasi lokal \`${reference}\``,
|
|
544
544
|
details:
|
|
545
|
-
`\`${reference}\` dibuang dari stub client
|
|
545
|
+
`\`${reference}\` dibuang dari stub client, sehingga nilainya \`undefined\` di browser (crash hydration).\n\n` +
|
|
546
546
|
"Sebelum (ditolak):\n" +
|
|
547
547
|
` const ${reference} = z.object({ … }); // deklarasi lokal di file yang sama\n` +
|
|
548
548
|
` export const ${fn.name} = serverFn({ ${kind}: ${reference}, … });\n\n` +
|
|
@@ -572,7 +572,7 @@ export function renderServerFnStub(
|
|
|
572
572
|
code: "E_SERVER_IMPORT_IN_CLIENT",
|
|
573
573
|
file: filename,
|
|
574
574
|
message: `tidak bisa me-resolve \`${reExport.source}\` untuk re-export`,
|
|
575
|
-
details: "Re-export dari `*.server.ts` harus bisa di-resolve statis
|
|
575
|
+
details: "Re-export dari `*.server.ts` harus bisa di-resolve statis.",
|
|
576
576
|
suggestions: ["Perbaiki path re-export", "Hindari re-export dinamis dari file server"],
|
|
577
577
|
});
|
|
578
578
|
}
|
|
@@ -595,7 +595,7 @@ export function renderServerFnStub(
|
|
|
595
595
|
code: "E_SERVER_IMPORT_IN_CLIENT",
|
|
596
596
|
file: filename,
|
|
597
597
|
message: `\`export *\` dari \`${specifier}\` tidak bisa di-resolve`,
|
|
598
|
-
details: "`export *` dari `*.server.ts` wajib bisa di-expand statis
|
|
598
|
+
details: "`export *` dari `*.server.ts` wajib bisa di-expand statis.",
|
|
599
599
|
suggestions: ["Perbaiki path re-export", "Ganti menjadi named re-export"],
|
|
600
600
|
});
|
|
601
601
|
}
|
|
@@ -707,7 +707,7 @@ export function nonContractServerFileError(file: string, projectRoot: string): M
|
|
|
707
707
|
return new MoonsError({
|
|
708
708
|
code: "E_SERVER_IMPORT_IN_CLIENT",
|
|
709
709
|
file: toRelative(projectRoot, file),
|
|
710
|
-
message: "file server memakai ekstensi di luar kontrak
|
|
710
|
+
message: "file server memakai ekstensi di luar kontrak `*.server.ts`",
|
|
711
711
|
details:
|
|
712
712
|
"Transform zero-trust hanya menangani `*.server.ts`/`*.server.tsx`; file server dengan ekstensi lain tidak bisa di-stub dan tidak boleh masuk graph client.",
|
|
713
713
|
suggestions: [
|
|
@@ -747,7 +747,7 @@ export async function checkClientModuleImports(
|
|
|
747
747
|
line,
|
|
748
748
|
message: `mengimpor \`${specifier}\` — \`${relApi}\` adalah modul API server-only`,
|
|
749
749
|
details:
|
|
750
|
-
"`src/api/**` hanya berjalan di server (endpoint HTTP) dan tidak pernah masuk bundle client
|
|
750
|
+
"`src/api/**` hanya berjalan di server (endpoint HTTP) dan tidak pernah masuk bundle client.",
|
|
751
751
|
suggestions: [
|
|
752
752
|
"Pindahkan operasi tersebut ke `*.server.ts` dan panggil lewat serverFn",
|
|
753
753
|
"Jangan mengimpor file `src/api` dari route/komponen client",
|
|
@@ -892,7 +892,7 @@ export async function checkClientModuleImports(
|
|
|
892
892
|
|
|
893
893
|
if (statement.type === "ExportAllDeclaration") {
|
|
894
894
|
if (resolvedTarget === null) {
|
|
895
|
-
reject(line, `\`export *\` dari \`${targetRelative}\` tidak bisa di-resolve`, "Re-export dari `*.server.ts` wajib bisa di-resolve statis
|
|
895
|
+
reject(line, `\`export *\` dari \`${targetRelative}\` tidak bisa di-resolve`, "Re-export dari `*.server.ts` wajib bisa di-resolve statis.");
|
|
896
896
|
}
|
|
897
897
|
continue; // aman: target menjadi stub
|
|
898
898
|
}
|
|
@@ -908,7 +908,7 @@ export async function checkClientModuleImports(
|
|
|
908
908
|
const name = ((item["local"] as AstNode | undefined)?.["name"] as string) ?? "";
|
|
909
909
|
const kind = (item["importKind"] as string | undefined) ?? (statement["importKind"] as string | undefined);
|
|
910
910
|
if (kind === "type") {
|
|
911
|
-
reject(line, `mengimpor type \`${name}\` dari ${targetRelative}`, "Import dari file server tidak diizinkan di modul client
|
|
911
|
+
reject(line, `mengimpor type \`${name}\` dari ${targetRelative}`, "Import dari file server tidak diizinkan di modul client.");
|
|
912
912
|
}
|
|
913
913
|
if (name.length > 0 && !(await isAllowedServerFnImport(resolvedTarget, specifier, name))) {
|
|
914
914
|
reject(
|
|
@@ -1029,7 +1029,7 @@ export async function checkClientModuleImports(
|
|
|
1029
1029
|
|
|
1030
1030
|
const targetRelative =
|
|
1031
1031
|
resolvedTarget === null ? request.text : toRelative(projectRoot, resolvedTarget);
|
|
1032
|
-
reject(request.node.loc?.start.line ?? 1, `mengimpor (dynamic) \`${request.text}\` dari ${targetRelative}`, "Client tidak boleh melakukan dynamic import ke `*.server.ts
|
|
1032
|
+
reject(request.node.loc?.start.line ?? 1, `mengimpor (dynamic) \`${request.text}\` dari ${targetRelative}`, "Client tidak boleh melakukan dynamic import ke `*.server.ts`.");
|
|
1033
1033
|
}
|
|
1034
1034
|
|
|
1035
1035
|
// `import.meta.glob` yang mencakup `*.server.ts` (§12.7) — audit adversarial M6.
|
|
@@ -1060,7 +1060,7 @@ export async function checkClientModuleImports(
|
|
|
1060
1060
|
file: relativeFile,
|
|
1061
1061
|
line: node.loc?.start.line ?? 1,
|
|
1062
1062
|
message: `mengimpor (glob) \`${pattern}\` di modul client`,
|
|
1063
|
-
details: "`import.meta.glob` yang mencakup `*.server.ts` dilarang di client
|
|
1063
|
+
details: "`import.meta.glob` yang mencakup `*.server.ts` dilarang di client.",
|
|
1064
1064
|
suggestions: [
|
|
1065
1065
|
"Import statis `serverFn` dari `*.server.ts` (hanya itu yang boleh menyeberang), atau",
|
|
1066
1066
|
"Pindahkan kebutuhan data ke dalam server function",
|
|
@@ -1196,7 +1196,7 @@ export function checkRouteModuleExports(context: RouteExportCheckContext): void
|
|
|
1196
1196
|
? ["default", "loader"]
|
|
1197
1197
|
: base === "_error.tsx" || base === "_404.tsx"
|
|
1198
1198
|
? ["default"]
|
|
1199
|
-
: ["default", "loader", "meta", "prerender", "paths"];
|
|
1199
|
+
: ["default", "loader", "meta", "prerender", "paths", "revalidate"];
|
|
1200
1200
|
const allowedText = `Hanya ${allowed.map((name) => `\`${name}\``).join(", ")} yang boleh di-export.`;
|
|
1201
1201
|
const relativeFile = toRelative(projectRoot, file);
|
|
1202
1202
|
const ast = parseModule(code, file);
|
|
@@ -1270,7 +1270,7 @@ export function checkRouteModuleExports(context: RouteExportCheckContext): void
|
|
|
1270
1270
|
code: "E_ROUTE_EXPORT_INVALID",
|
|
1271
1271
|
file: relativeFile,
|
|
1272
1272
|
message: "modul route wajib mengekspor `default`",
|
|
1273
|
-
details: "Setiap file route harus punya `export default` berupa komponen React
|
|
1273
|
+
details: "Setiap file route harus punya `export default` berupa komponen React.",
|
|
1274
1274
|
suggestions: ["Tambahkan `export default function Page() { ... }`"],
|
|
1275
1275
|
});
|
|
1276
1276
|
}
|