@moonsjs/vite 0.1.6 → 0.1.8
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-zs92aaz4.js → index-tysyk2qf.js} +12 -12
- package/dist/{index-zs92aaz4.js.map → index-tysyk2qf.js.map} +4 -4
- package/dist/index.js +13 -13
- package/dist/index.js.map +4 -4
- package/dist/server/index.js +58 -30
- package/dist/server/index.js.map +4 -4
- package/dist/server/prerender.d.ts +8 -0
- 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 +6 -6
- package/src/server/handler.ts +7 -7
- package/src/server/prerender.ts +85 -24
- package/src/server/shell.ts +1 -1
- package/src/server-only.ts +1 -1
- package/src/transform.ts +10 -10
package/src/server/prerender.ts
CHANGED
|
@@ -31,6 +31,16 @@ export interface PrerenderResult {
|
|
|
31
31
|
*/
|
|
32
32
|
export const PRERENDER_MANIFEST_FILE = "_moons_prerender.json";
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Addendum G: sentinel internal untuk merender halaman 404 statis. Path ini
|
|
36
|
+
* tidak pernah menjadi route publik; handler merendernya seperti request tak
|
|
37
|
+
* dikenal (route `_404` atau 404 bawaan).
|
|
38
|
+
*/
|
|
39
|
+
export const NOT_FOUND_SENTINEL = "/_moons/404";
|
|
40
|
+
|
|
41
|
+
/** Addendum G: nama file 404 untuk static host (konvensi Cloudflare Pages/Netlify). */
|
|
42
|
+
export const NOT_FOUND_FILE = "404.html";
|
|
43
|
+
|
|
34
44
|
export async function runPrerender(options: PrerenderOptions): Promise<PrerenderResult> {
|
|
35
45
|
const log = options.log ?? ((message: string) => console.log(message));
|
|
36
46
|
const env = options.env ?? process.env;
|
|
@@ -61,7 +71,7 @@ export async function runPrerender(options: PrerenderOptions): Promise<Prerender
|
|
|
61
71
|
code: "E_ISR_INVALID",
|
|
62
72
|
file: route.file,
|
|
63
73
|
message: "`revalidate` tidak valid atau dipakai tanpa `prerender = true`",
|
|
64
|
-
details: "ISR berbasis file: `revalidate` (detik, number >= 0) + `prerender
|
|
74
|
+
details: "ISR berbasis file: `revalidate` (detik, number >= 0) + `prerender`.",
|
|
65
75
|
suggestions: [
|
|
66
76
|
"Gunakan angka detik, mis. `export const revalidate = 60`",
|
|
67
77
|
"Tambahkan `export const prerender = true` pada page yang sama",
|
|
@@ -99,26 +109,7 @@ export async function runPrerender(options: PrerenderOptions): Promise<Prerender
|
|
|
99
109
|
}
|
|
100
110
|
|
|
101
111
|
const html = await response.text();
|
|
102
|
-
|
|
103
|
-
...(options.canary === undefined ? {} : { canary: options.canary }),
|
|
104
|
-
env,
|
|
105
|
-
});
|
|
106
|
-
if (findings.length > 0) {
|
|
107
|
-
const labels = findings
|
|
108
|
-
.map((finding) => (finding.kind === "canary" ? "canary" : `env(${finding.label})`))
|
|
109
|
-
.join(", ");
|
|
110
|
-
throw new MoonsError({
|
|
111
|
-
code: "E_PRERENDER_LEAK",
|
|
112
|
-
file: route.file,
|
|
113
|
-
message: `hasil prerender \`${pathname}\` memuat ${labels}`,
|
|
114
|
-
details:
|
|
115
|
-
"HTML statis permanen — rahasia yang ter-prerender tidak bisa ditarik kembali (PRD Addendum D.5).",
|
|
116
|
-
suggestions: [
|
|
117
|
-
"Jangan mengembalikan nilai env rahasia dari loader yang di-prerender",
|
|
118
|
-
"Matikan `prerender` untuk route ini atau pindahkan datanya ke serverFn request-time",
|
|
119
|
-
],
|
|
120
|
-
});
|
|
121
|
-
}
|
|
112
|
+
assertNoLeak(html, pathname, route.file, options, env);
|
|
122
113
|
|
|
123
114
|
const file = outputFile(options.clientDir, pathname);
|
|
124
115
|
mkdirSync(dirname(file), { recursive: true });
|
|
@@ -132,16 +123,86 @@ export async function runPrerender(options: PrerenderOptions): Promise<Prerender
|
|
|
132
123
|
}
|
|
133
124
|
}
|
|
134
125
|
|
|
126
|
+
// Addendum G: `404.html` untuk static host, bila app punya route `_404`.
|
|
127
|
+
const notFound = await renderStaticNotFound(options, env, log);
|
|
128
|
+
let wrote404 = false;
|
|
129
|
+
if (notFound !== null) {
|
|
130
|
+
writeFileSync(join(options.clientDir, NOT_FOUND_FILE), notFound);
|
|
131
|
+
wrote404 = true;
|
|
132
|
+
}
|
|
133
|
+
|
|
135
134
|
// Manifest v2 (Addendum E.1): entries + revalidate; reader tetap menerima v1.
|
|
136
135
|
writeFileSync(
|
|
137
136
|
join(options.clientDir, PRERENDER_MANIFEST_FILE),
|
|
138
137
|
`${JSON.stringify({ version: 2, entries }, null, 2)}\n`,
|
|
139
138
|
);
|
|
140
139
|
|
|
141
|
-
log(
|
|
140
|
+
log(
|
|
141
|
+
`[moons] prerender selesai: ${written} file${wrote404 ? ` + ${NOT_FOUND_FILE}` : ""} (${skipped} dilewati)`,
|
|
142
|
+
);
|
|
142
143
|
return { written, skipped };
|
|
143
144
|
}
|
|
144
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Addendum G: render halaman 404 ke `dist/client/404.html` (static export).
|
|
148
|
+
* Ditulis hanya bila app memiliki route `_404` dan sentinel menghasilkan 404;
|
|
149
|
+
* hasilnya tetap melewati guard kebocoran (fail-mode).
|
|
150
|
+
*/
|
|
151
|
+
async function renderStaticNotFound(
|
|
152
|
+
options: PrerenderOptions,
|
|
153
|
+
env: Record<string, string | undefined>,
|
|
154
|
+
log: (message: string) => void,
|
|
155
|
+
): Promise<string | null> {
|
|
156
|
+
if (!options.routes.some((route) => route.kind === "404")) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
const response = await options.handler(
|
|
160
|
+
new Request(`http://localhost${NOT_FOUND_SENTINEL}`, {
|
|
161
|
+
method: "GET",
|
|
162
|
+
headers: { "x-moons-prerender": "1" },
|
|
163
|
+
}),
|
|
164
|
+
);
|
|
165
|
+
if (response.status !== 404) {
|
|
166
|
+
log(
|
|
167
|
+
`[moons] prerender: ${NOT_FOUND_FILE} dilewati (sentinel \`${NOT_FOUND_SENTINEL}\` → ${response.status})`,
|
|
168
|
+
);
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
const html = await response.text();
|
|
172
|
+
assertNoLeak(html, NOT_FOUND_SENTINEL, NOT_FOUND_FILE, options, env);
|
|
173
|
+
return html;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Guard kebocoran fail-mode (Addendum D.5) — dipakai halaman & 404 statis. */
|
|
177
|
+
function assertNoLeak(
|
|
178
|
+
html: string,
|
|
179
|
+
pathname: string,
|
|
180
|
+
file: string,
|
|
181
|
+
options: PrerenderOptions,
|
|
182
|
+
env: Record<string, string | undefined>,
|
|
183
|
+
): void {
|
|
184
|
+
const findings = scanPayloadForSecrets(html, {
|
|
185
|
+
...(options.canary === undefined ? {} : { canary: options.canary }),
|
|
186
|
+
env,
|
|
187
|
+
});
|
|
188
|
+
if (findings.length === 0) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const labels = findings
|
|
192
|
+
.map((finding) => (finding.kind === "canary" ? "canary" : `env(${finding.label})`))
|
|
193
|
+
.join(", ");
|
|
194
|
+
throw new MoonsError({
|
|
195
|
+
code: "E_PRERENDER_LEAK",
|
|
196
|
+
file,
|
|
197
|
+
message: `hasil prerender \`${pathname}\` memuat ${labels}`,
|
|
198
|
+
details: "HTML statis permanen — rahasia yang ter-prerender tidak bisa ditarik kembali.",
|
|
199
|
+
suggestions: [
|
|
200
|
+
"Jangan mengembalikan nilai env rahasia dari loader yang di-prerender",
|
|
201
|
+
"Matikan `prerender` untuk route ini atau pindahkan datanya ke serverFn request-time",
|
|
202
|
+
],
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
145
206
|
function routeKeyOf(routesDir: string, file: string): string {
|
|
146
207
|
const relative = file.startsWith(routesDir) ? file.slice(routesDir.length + 1) : file;
|
|
147
208
|
return relative.split("\\").join("/").replace(/\.(tsx|ts)$/, "");
|
|
@@ -158,7 +219,7 @@ async function resolvePathnames(route: RouteDefinition, module: RouteModule): Pr
|
|
|
158
219
|
code: "E_PRERENDER_NO_PATHS",
|
|
159
220
|
file: route.file,
|
|
160
221
|
message: "route dynamic ber-`prerender = true` tanpa `export const paths`",
|
|
161
|
-
details: "Prerender butuh daftar param untuk merender tiap path
|
|
222
|
+
details: "Prerender butuh daftar param untuk merender tiap path.",
|
|
162
223
|
suggestions: [
|
|
163
224
|
'Tambahkan `export const paths = () => [{ id: "…" }]`',
|
|
164
225
|
"Atau hapus `prerender` agar dirender SSR saat request",
|
|
@@ -247,7 +308,7 @@ function prerenderError(route: RouteDefinition, message: string): MoonsError {
|
|
|
247
308
|
code: "E_PRERENDER_INVALID",
|
|
248
309
|
file: route.file,
|
|
249
310
|
message,
|
|
250
|
-
details: "Kontrak prerender mengikuti
|
|
311
|
+
details: "Kontrak prerender mengikuti.",
|
|
251
312
|
suggestions: [
|
|
252
313
|
"Perbaiki bentuk `paths`/param agar cocok dengan pola route",
|
|
253
314
|
"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",
|
|
@@ -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
|
}
|