@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.
@@ -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` (PRD Addendum E.1).",
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
- const findings = scanPayloadForSecrets(html, {
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(`[moons] prerender selesai: ${written} file (${skipped} dilewati)`);
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 (PRD Addendum D.5).",
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 PRD Addendum D.5.",
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",
@@ -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 (PRD §18).",
123
+ details: "Build client harus dijalankan sebelum server produksi dinyalakan.",
124
124
  suggestions: ["Jalankan `bun run build` lebih dulu"],
125
125
  });
126
126
  }
@@ -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 (PRD Addendum C).`,
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 (PRD §12.2/§12.3), sehingga nilainya \`undefined\` di browser (crash hydration).\n\n` +
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 (PRD §12.6).",
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 (PRD §12.6).",
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 §12",
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 (PRD Addendum D.1).",
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 (PRD §12.6).");
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 (PRD §12.4).");
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` (PRD §12.7).");
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 (PRD §12.7).",
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 (PRD §11.2).",
1273
+ details: "Setiap file route harus punya `export default` berupa komponen React.",
1274
1274
  suggestions: ["Tambahkan `export default function Page() { ... }`"],
1275
1275
  });
1276
1276
  }