@moonsjs/vite 0.1.5 → 0.1.6
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-e7qz0epf.js → index-zs92aaz4.js} +2 -2
- package/dist/{index-e7qz0epf.js.map → index-zs92aaz4.js.map} +3 -3
- package/dist/index.js +5 -5
- package/dist/index.js.map +3 -3
- package/dist/server/index.js +120 -12
- package/dist/server/index.js.map +6 -5
- 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 +1 -1
- package/src/plugin.ts +7 -3
- package/src/server/isr.ts +81 -0
- package/src/server/node-server.ts +85 -12
- package/src/server/prerender.ts +30 -4
- package/src/transform.ts +1 -1
|
@@ -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` (PRD Addendum E.1).",
|
|
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) {
|
|
@@ -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)`);
|
package/src/transform.ts
CHANGED
|
@@ -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);
|