alabjs 0.1.1 → 0.2.0
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/commands/build.d.ts.map +1 -1
- package/dist/commands/build.js +104 -2
- package/dist/commands/build.js.map +1 -1
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +6 -0
- package/dist/commands/dev.js.map +1 -1
- package/dist/components/Dynamic.d.ts +88 -0
- package/dist/components/Dynamic.d.ts.map +1 -0
- package/dist/components/Dynamic.js +86 -0
- package/dist/components/Dynamic.js.map +1 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +1 -0
- package/dist/components/index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/server/app.d.ts.map +1 -1
- package/dist/server/app.js +62 -8
- package/dist/server/app.js.map +1 -1
- package/dist/server/cdn.d.ts +72 -0
- package/dist/server/cdn.d.ts.map +1 -0
- package/dist/server/cdn.js +132 -0
- package/dist/server/cdn.js.map +1 -0
- package/dist/server/revalidate.d.ts.map +1 -1
- package/dist/server/revalidate.js +6 -1
- package/dist/server/revalidate.js.map +1 -1
- package/dist/ssr/html.d.ts +7 -0
- package/dist/ssr/html.d.ts.map +1 -1
- package/dist/ssr/html.js +2 -1
- package/dist/ssr/html.js.map +1 -1
- package/dist/ssr/ppr.d.ts +69 -0
- package/dist/ssr/ppr.d.ts.map +1 -0
- package/dist/ssr/ppr.js +132 -0
- package/dist/ssr/ppr.js.map +1 -0
- package/dist/ssr/render.d.ts +2 -0
- package/dist/ssr/render.d.ts.map +1 -1
- package/dist/ssr/render.js +2 -1
- package/dist/ssr/render.js.map +1 -1
- package/dist/types/index.d.ts +20 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/commands/build.ts +117 -2
- package/src/commands/dev.ts +7 -0
- package/src/components/Dynamic.tsx +124 -0
- package/src/components/index.ts +2 -0
- package/src/index.ts +1 -0
- package/src/server/app.ts +64 -9
- package/src/server/cdn.ts +187 -0
- package/src/server/revalidate.ts +7 -1
- package/src/ssr/html.ts +9 -0
- package/src/ssr/ppr.ts +167 -0
- package/src/ssr/render.ts +4 -0
- package/src/types/index.ts +23 -0
- package/tsconfig.tsbuildinfo +1 -1
package/dist/ssr/render.js
CHANGED
|
@@ -9,7 +9,7 @@ import { htmlShellBefore, htmlShellAfter } from "./html.js";
|
|
|
9
9
|
* the closing fragment is appended when the stream finishes.
|
|
10
10
|
*/
|
|
11
11
|
export function renderToResponse(res, opts) {
|
|
12
|
-
const { Page, layouts = [], params, searchParams, metadata = {}, routeFile, layoutsJson, loadingFile, ssr, headExtra, nonce, } = opts;
|
|
12
|
+
const { Page, layouts = [], params, searchParams, metadata = {}, routeFile, layoutsJson, loadingFile, ssr, headExtra, nonce, buildId, } = opts;
|
|
13
13
|
const shellOpts = {
|
|
14
14
|
metadata,
|
|
15
15
|
paramsJson: JSON.stringify(params),
|
|
@@ -20,6 +20,7 @@ export function renderToResponse(res, opts) {
|
|
|
20
20
|
ssr,
|
|
21
21
|
headExtra,
|
|
22
22
|
nonce,
|
|
23
|
+
buildId,
|
|
23
24
|
};
|
|
24
25
|
const before = htmlShellBefore(shellOpts);
|
|
25
26
|
const after = htmlShellAfter({ nonce });
|
package/dist/ssr/render.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render.js","sourceRoot":"","sources":["../../src/ssr/render.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAsB,MAAM,OAAO,CAAC;AAC1D,OAAO,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAC1D,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEvC,OAAO,EAAE,eAAe,EAAE,cAAc,EAAyB,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"render.js","sourceRoot":"","sources":["../../src/ssr/render.ts"],"names":[],"mappings":"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"}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -113,5 +113,24 @@ export type GenerateMetadata<Path extends string = string> = (props: {
|
|
|
113
113
|
readonly params: RouteParams<Path>;
|
|
114
114
|
readonly searchParams: Readonly<Record<string, string | readonly string[]>>;
|
|
115
115
|
}) => Promise<PageMetadata> | PageMetadata;
|
|
116
|
-
|
|
116
|
+
/**
|
|
117
|
+
* Export `cdnCache` from a page to let any CDN or shared proxy cache it at
|
|
118
|
+
* the edge — no Vercel required.
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* // app/posts/[id]/page.tsx
|
|
122
|
+
* import type { CdnCache } from "alabjs";
|
|
123
|
+
*
|
|
124
|
+
* export const cdnCache: CdnCache = {
|
|
125
|
+
* maxAge: 60, // CDN keeps the page for 60 s
|
|
126
|
+
* swr: 30, // serve stale for 30 s while revalidating
|
|
127
|
+
* tags: ["posts", "post:42"], // invalidate via /_alabjs/revalidate
|
|
128
|
+
* };
|
|
129
|
+
*
|
|
130
|
+
* @remarks
|
|
131
|
+
* CDN-cached pages are **public pages** — Alab skips CSRF token injection for
|
|
132
|
+
* them because a shared cache would hand the same token to every visitor.
|
|
133
|
+
* Do not use `cdnCache` on pages that contain user-specific state.
|
|
134
|
+
*/
|
|
135
|
+
export type { CdnCache } from "../server/cdn.js";
|
|
117
136
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,OAAO,CAAC;AAI1C;;;;;GAKG;AACH,KAAK,aAAa,CAAC,IAAI,SAAS,MAAM,IACpC,IAAI,SAAS,GAAG,MAAM,IAAI,MAAM,KAAK,IAAI,MAAM,IAAI,EAAE,GACjD,KAAK,GAAG,aAAa,CAAC,IAAI,CAAC,GAC3B,KAAK,CAAC;AAEZ;;;;;GAKG;AACH,MAAM,MAAM,WAAW,CAAC,IAAI,SAAS,MAAM,GAAG,MAAM,IAClD;IAAC,aAAa,CAAC,IAAI,CAAC;CAAC,SAAS,CAAC,KAAK,CAAC,GACjC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACtB;IAAE,QAAQ,EAAE,CAAC,IAAI,aAAa,CAAC,IAAI,CAAC,GAAG,MAAM;CAAE,CAAC;AAItD;;;GAGG;AACH,MAAM,WAAW,eAAe,CAAC,IAAI,SAAS,MAAM,GAAG,MAAM;IAC3D,gEAAgE;IAChE,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC,IAAI,CAAC,CAAC;IACnC,sCAAsC;IACtC,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAAC,CAAC,CAAC;IACrE,4CAA4C;IAC5C,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAAC,CAAC,CAAC;IACvE,sCAAsC;IACtC,QAAQ,CAAC,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,CAAC;IACtE,oDAAoD;IACpD,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAID,OAAO,CAAC,MAAM,eAAe,EAAE,OAAO,MAAM,CAAC;AAE7C;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,QAAQ,CAClB,KAAK,GAAG,SAAS,EACjB,MAAM,GAAG,OAAO,EAChB,IAAI,SAAS,MAAM,GAAG,MAAM,IAC1B;IACF,qFAAqF;IACrF,QAAQ,CAAC,CAAC,eAAe,CAAC,EAAE,IAAI,CAAC;IACjC,CAAC,GAAG,EAAE,eAAe,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC7D,CAAC;AAEF,sDAAsD;AACtD,MAAM,MAAM,iBAAiB,CAAC,CAAC,SAAS,QAAQ,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,IAC7D,CAAC,SAAS,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC,EAAE,GAAG,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC;AAE7D,4CAA4C;AAC5C,MAAM,MAAM,gBAAgB,CAAC,CAAC,SAAS,QAAQ,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,IAC5D,CAAC,SAAS,QAAQ,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;AAEpD,iDAAiD;AACjD,MAAM,MAAM,eAAe,CAAC,CAAC,SAAS,QAAQ,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,IAC3D,CAAC,SAAS,QAAQ,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;AAIpD;;;;;;;;;GASG;AACH,MAAM,MAAM,QAAQ,CAAC,IAAI,SAAS,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,EAAE;IAC3D,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC,IAAI,CAAC,CAAC;IACnC,QAAQ,CAAC,YAAY,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAAC,CAAC,CAAC;CAC7E,KAAK,YAAY,GAAG,IAAI,CAAC;AAI1B,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,GAAG,SAAS,GAAG,SAAS,CAAC;IAClD,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,GAAG,qBAAqB,GAAG,KAAK,GAAG,QAAQ,CAAC;IACrE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,CAAC,EAAE,iBAAiB,CAAC;IAChC,QAAQ,CAAC,OAAO,CAAC,EAAE,eAAe,CAAC;IACnC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,wCAAwC;IACxC,QAAQ,CAAC,KAAK,CAAC,EAAE,aAAa,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;CAClE;AAED;;GAEG;AACH,MAAM,MAAM,gBAAgB,CAAC,IAAI,SAAS,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,EAAE;IACnE,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC,IAAI,CAAC,CAAC;IACnC,QAAQ,CAAC,YAAY,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAAC,CAAC,CAAC;CAC7E,KAAK,OAAO,CAAC,YAAY,CAAC,GAAG,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/commands/build.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { build as viteBuild, type PluginOption } from "vite";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
|
-
import { spawn } from "node:child_process";
|
|
4
|
-
import { existsSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { spawn, execSync } from "node:child_process";
|
|
4
|
+
import { existsSync, writeFileSync, readFileSync } from "node:fs";
|
|
5
|
+
import { preRenderPPRShell, findBuildLayoutFiles, PPR_CACHE_SUBDIR } from "../ssr/ppr.js";
|
|
6
|
+
import type { RouteManifest } from "../router/manifest.js";
|
|
5
7
|
|
|
6
8
|
interface BuildOptions {
|
|
7
9
|
cwd: string;
|
|
@@ -165,6 +167,12 @@ export async function build({ cwd, skipTypecheck = false, mode = "ssr", analyze
|
|
|
165
167
|
|
|
166
168
|
await Promise.all(tasks);
|
|
167
169
|
|
|
170
|
+
// Write a stable build ID for skew protection (must run after Vite so the
|
|
171
|
+
// route-manifest.json is in place for the content-hash fallback path).
|
|
172
|
+
const distDir = resolve(cwd, ".alabjs/dist");
|
|
173
|
+
await writeBuildId(distDir, cwd);
|
|
174
|
+
await buildPPRShells(distDir, cwd);
|
|
175
|
+
|
|
168
176
|
// Bundle the offline service worker as a separate iife chunk.
|
|
169
177
|
// Output: .alabjs/dist/client/_alabjs/offline-sw.js (served at /_alabjs/offline-sw.js)
|
|
170
178
|
await buildOfflineSw(cwd);
|
|
@@ -172,6 +180,113 @@ export async function build({ cwd, skipTypecheck = false, mode = "ssr", analyze
|
|
|
172
180
|
console.log("\n alab build complete → .alabjs/dist");
|
|
173
181
|
}
|
|
174
182
|
|
|
183
|
+
/**
|
|
184
|
+
* Generate a stable build ID and write it to `.alabjs/dist/BUILD_ID`.
|
|
185
|
+
*
|
|
186
|
+
* Strategy (in priority order):
|
|
187
|
+
* 1. Git short SHA — deterministic, human-readable, zero CPU cost.
|
|
188
|
+
* 2. Rust FNV-1a hash of the route-manifest JSON via `@alabjs/compiler`
|
|
189
|
+
* (napi binary) — content-addressed, no git required.
|
|
190
|
+
* 3. Base-36 millisecond timestamp — last resort when both git and napi
|
|
191
|
+
* are unavailable (e.g. first-time contributor without Rust toolchain).
|
|
192
|
+
*/
|
|
193
|
+
async function writeBuildId(distDir: string, cwd: string): Promise<void> {
|
|
194
|
+
let buildId: string;
|
|
195
|
+
|
|
196
|
+
// 1. Git SHA (preferred — zero cost, guaranteed unique per commit)
|
|
197
|
+
try {
|
|
198
|
+
buildId = execSync("git rev-parse --short HEAD", { cwd, encoding: "utf8" }).trim();
|
|
199
|
+
} catch {
|
|
200
|
+
// 2. Rust FNV-1a hash of the route manifest (content-addressed)
|
|
201
|
+
try {
|
|
202
|
+
const manifestPath = resolve(distDir, "route-manifest.json");
|
|
203
|
+
const manifestContent = readFileSync(manifestPath, "utf8");
|
|
204
|
+
type NapiWithHash = { hashBuildId(s: string): string };
|
|
205
|
+
const mod = await import("@alabjs/compiler") as unknown as { default?: NapiWithHash } & NapiWithHash;
|
|
206
|
+
const napi: NapiWithHash = (mod.default ?? mod) as NapiWithHash;
|
|
207
|
+
if (typeof napi.hashBuildId === "function") {
|
|
208
|
+
buildId = napi.hashBuildId(manifestContent);
|
|
209
|
+
} else {
|
|
210
|
+
throw new Error("hashBuildId not available");
|
|
211
|
+
}
|
|
212
|
+
} catch {
|
|
213
|
+
// 3. Timestamp fallback
|
|
214
|
+
buildId = Date.now().toString(36);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
writeFileSync(resolve(distDir, "BUILD_ID"), buildId, "utf8");
|
|
219
|
+
console.log(` alab build ID → ${buildId}`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Pre-render static HTML shells for every page that exports `ppr = true`.
|
|
224
|
+
*
|
|
225
|
+
* Runs AFTER the Vite SSR bundle so compiled page modules are available in
|
|
226
|
+
* `.alabjs/dist/server/`. Each shell is saved to `.alabjs/ppr-cache/`.
|
|
227
|
+
*/
|
|
228
|
+
async function buildPPRShells(distDir: string, cwd: string): Promise<void> {
|
|
229
|
+
const manifestPath = resolve(distDir, "route-manifest.json");
|
|
230
|
+
if (!existsSync(manifestPath)) return;
|
|
231
|
+
|
|
232
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf8")) as RouteManifest;
|
|
233
|
+
const pageRoutes = manifest.routes.filter((r) => r.kind === "page");
|
|
234
|
+
const pprCacheDir = resolve(cwd, PPR_CACHE_SUBDIR);
|
|
235
|
+
let count = 0;
|
|
236
|
+
|
|
237
|
+
for (const route of pageRoutes) {
|
|
238
|
+
const modulePath = resolve(distDir, "server", route.file);
|
|
239
|
+
if (!existsSync(modulePath)) continue;
|
|
240
|
+
|
|
241
|
+
// Dynamic import — module is compiled ESM, importable by Node directly.
|
|
242
|
+
const mod = await import(modulePath) as {
|
|
243
|
+
default?: unknown;
|
|
244
|
+
ppr?: unknown;
|
|
245
|
+
metadata?: Record<string, unknown>;
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
if (mod.ppr !== true) continue;
|
|
249
|
+
if (typeof mod.default !== "function") {
|
|
250
|
+
console.warn(` alab ppr: ${route.file} has no default export — skipping.`);
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Load layout modules (outermost → innermost).
|
|
255
|
+
const layoutPaths = findBuildLayoutFiles(route.file, distDir);
|
|
256
|
+
const layoutMods = await Promise.all(
|
|
257
|
+
layoutPaths.map((p) => import(resolve(distDir, "server", p))),
|
|
258
|
+
);
|
|
259
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
260
|
+
const layouts = layoutMods.map((m: any) => m.default).filter((c: unknown) => typeof c === "function");
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
await preRenderPPRShell({
|
|
264
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
265
|
+
Page: mod.default as any,
|
|
266
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
267
|
+
layouts: layouts as any[],
|
|
268
|
+
shellOpts: {
|
|
269
|
+
metadata: (mod.metadata as never) ?? {},
|
|
270
|
+
paramsJson: "{}",
|
|
271
|
+
searchParamsJson: "{}",
|
|
272
|
+
routeFile: route.file,
|
|
273
|
+
ssr: true,
|
|
274
|
+
},
|
|
275
|
+
pprCacheDir,
|
|
276
|
+
routePath: route.path,
|
|
277
|
+
});
|
|
278
|
+
count++;
|
|
279
|
+
} catch (err) {
|
|
280
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
281
|
+
console.warn(` alab ppr: failed to pre-render ${route.path}: ${msg}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (count > 0) {
|
|
286
|
+
console.log(` alab ppr → ${count} shell${count === 1 ? "" : "s"} written to ${PPR_CACHE_SUBDIR}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
175
290
|
/** Compile the offline service worker to a standalone iife bundle. */
|
|
176
291
|
async function buildOfflineSw(cwd: string): Promise<void> {
|
|
177
292
|
const swEntry = new URL("../client/offline-sw.js", import.meta.url).pathname;
|
package/src/commands/dev.ts
CHANGED
|
@@ -63,6 +63,12 @@ async function readJsonBody(req: IncomingMessage): Promise<unknown> {
|
|
|
63
63
|
export async function dev({ cwd, port = 3000, host = "localhost" }: DevOptions) {
|
|
64
64
|
console.log(" alab starting dev server...\n");
|
|
65
65
|
|
|
66
|
+
// Per-session build ID for skew protection in dev.
|
|
67
|
+
// A new ID is generated each time the dev server starts so that a browser
|
|
68
|
+
// tab left open across a restart will hard-reload on the next navigation
|
|
69
|
+
// rather than silently rendering with stale JS.
|
|
70
|
+
const devBuildId = `dev-${Date.now().toString(36)}`;
|
|
71
|
+
|
|
66
72
|
const appDir = resolve(cwd, "app");
|
|
67
73
|
|
|
68
74
|
const vite = await createServer({
|
|
@@ -459,6 +465,7 @@ export async function dev({ cwd, port = 3000, host = "localhost" }: DevOptions)
|
|
|
459
465
|
layoutsJson,
|
|
460
466
|
loadingFile,
|
|
461
467
|
ssr: ssrEnabled,
|
|
468
|
+
buildId: devBuildId,
|
|
462
469
|
});
|
|
463
470
|
const shellAfter = htmlShellAfter({});
|
|
464
471
|
const rawHtml = `${shellBefore}${ssrContent}${shellAfter}`;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Alab PPR — Partial Prerendering support.
|
|
3
|
+
*
|
|
4
|
+
* Pages that export `export const ppr = true` get their static HTML shell
|
|
5
|
+
* pre-rendered at build time and stored in `.alabjs/ppr-cache/`. At runtime,
|
|
6
|
+
* the shell is served instantly (CDN-cacheable) while `<Dynamic>` sections
|
|
7
|
+
* fill in per-request via React's Suspense streaming or client-side hydration.
|
|
8
|
+
*
|
|
9
|
+
* ## How it works
|
|
10
|
+
*
|
|
11
|
+
* During the **build-time static render** (pre-render pass):
|
|
12
|
+
* • `PPRShellProvider` sets the PPR context to `true`.
|
|
13
|
+
* • `<Dynamic>` sees the context and renders only its `fallback` inside a
|
|
14
|
+
* `data-ppr-hole` marker — children are omitted entirely.
|
|
15
|
+
* • The resulting HTML is the "static shell": complete page minus dynamic parts.
|
|
16
|
+
*
|
|
17
|
+
* At **runtime**:
|
|
18
|
+
* • `PPRShellProvider` is never rendered → context defaults to `false`.
|
|
19
|
+
* • `<Dynamic>` behaves as a plain `<Suspense>` boundary, streaming children
|
|
20
|
+
* as their async work resolves.
|
|
21
|
+
*
|
|
22
|
+
* ## Usage
|
|
23
|
+
*
|
|
24
|
+
* ```tsx
|
|
25
|
+
* // app/posts/[id]/page.tsx
|
|
26
|
+
* import { Dynamic } from "alabjs/components";
|
|
27
|
+
*
|
|
28
|
+
* export const ppr = true;
|
|
29
|
+
*
|
|
30
|
+
* export default function PostPage({ params }: { params: { id: string } }) {
|
|
31
|
+
* return (
|
|
32
|
+
* <article>
|
|
33
|
+
* <h1>Post {params.id}</h1>
|
|
34
|
+
* <Dynamic id="sidebar" fallback={<SidebarSkeleton />}>
|
|
35
|
+
* <PersonalisedSidebar userId={userId} />
|
|
36
|
+
* </Dynamic>
|
|
37
|
+
* </article>
|
|
38
|
+
* );
|
|
39
|
+
* }
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
import { Suspense, createContext, useContext, type ReactNode } from "react";
|
|
44
|
+
|
|
45
|
+
// ─── PPR shell context ─────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* When `true`, `<Dynamic>` renders only its `fallback` placeholder.
|
|
49
|
+
* Set exclusively by `PPRShellProvider` during build-time pre-renders.
|
|
50
|
+
*/
|
|
51
|
+
const PPRShellCtx = createContext(false);
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @internal
|
|
55
|
+
* Wrap the root element with this during build-time PPR pre-rendering so that
|
|
56
|
+
* every `<Dynamic>` in the tree emits a stable `data-ppr-hole` placeholder
|
|
57
|
+
* instead of its children.
|
|
58
|
+
*
|
|
59
|
+
* Do **not** use this at runtime — it is an implementation detail of
|
|
60
|
+
* `preRenderPPRShell` in `src/ssr/ppr.ts`.
|
|
61
|
+
*/
|
|
62
|
+
export function PPRShellProvider({ children }: { children: ReactNode }) {
|
|
63
|
+
return <PPRShellCtx.Provider value={true}>{children}</PPRShellCtx.Provider>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ─── Dynamic component ────────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
export interface DynamicProps {
|
|
69
|
+
/**
|
|
70
|
+
* Unique identifier for this dynamic section within the page.
|
|
71
|
+
*
|
|
72
|
+
* Used to correlate the placeholder emitted in the static shell with the
|
|
73
|
+
* live content streamed at runtime. **Must be stable across renders** —
|
|
74
|
+
* treat it like a React key: short, descriptive, no dynamic values.
|
|
75
|
+
*
|
|
76
|
+
* @example "sidebar", "user-nav", "related-posts"
|
|
77
|
+
*/
|
|
78
|
+
id: string;
|
|
79
|
+
/** Per-request dynamic content. Never rendered in the pre-built static shell. */
|
|
80
|
+
children: ReactNode;
|
|
81
|
+
/**
|
|
82
|
+
* Shown in the pre-built static shell **and** as the React Suspense fallback
|
|
83
|
+
* while the dynamic content is streaming in.
|
|
84
|
+
*
|
|
85
|
+
* Keep this lightweight — it is inlined into every CDN-cached response.
|
|
86
|
+
*/
|
|
87
|
+
fallback?: ReactNode;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Marks a subtree as **dynamic** (per-request) within a PPR page.
|
|
92
|
+
*
|
|
93
|
+
* - **Build time** (static shell pre-render): renders `fallback` inside a
|
|
94
|
+
* `<div data-ppr-hole="{id}">` marker. Children are not rendered.
|
|
95
|
+
* - **Runtime** (SSR + hydration): acts as a `<Suspense>` boundary. Children
|
|
96
|
+
* stream in as their async work resolves; `fallback` is shown meanwhile.
|
|
97
|
+
*
|
|
98
|
+
* The `display: contents` style on the wrapper div means it has no visual
|
|
99
|
+
* footprint — it exists only as a DOM anchor for Alab's PPR machinery.
|
|
100
|
+
*/
|
|
101
|
+
export function Dynamic({ id, children, fallback = null }: DynamicProps) {
|
|
102
|
+
const isShell = useContext(PPRShellCtx);
|
|
103
|
+
|
|
104
|
+
const holeWrapper = (content: ReactNode) => (
|
|
105
|
+
<div data-ppr-hole={id} style={{ display: "contents" }}>
|
|
106
|
+
{content}
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
if (isShell) {
|
|
111
|
+
// Build-time pre-render: emit only the placeholder + fallback.
|
|
112
|
+
// Children are intentionally omitted — they contain per-request logic.
|
|
113
|
+
return holeWrapper(fallback);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Runtime: standard Suspense boundary.
|
|
117
|
+
// The hole wrapper on the fallback preserves the DOM anchor so client-side
|
|
118
|
+
// hydration can match it to the pre-rendered shell.
|
|
119
|
+
return (
|
|
120
|
+
<Suspense fallback={holeWrapper(fallback)}>
|
|
121
|
+
{children}
|
|
122
|
+
</Suspense>
|
|
123
|
+
);
|
|
124
|
+
}
|
package/src/components/index.ts
CHANGED
|
@@ -7,3 +7,5 @@ export { Script } from "./Script.js";
|
|
|
7
7
|
export type { ScriptProps } from "./Script.js";
|
|
8
8
|
export { Font } from "./Font.js";
|
|
9
9
|
export type { FontProps } from "./Font.js";
|
|
10
|
+
export { Dynamic, PPRShellProvider } from "./Dynamic.js";
|
|
11
|
+
export type { DynamicProps } from "./Dynamic.js";
|
package/src/index.ts
CHANGED
package/src/server/app.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createApp as createH3App, createRouter, defineEventHandler, getQuery, readBody } from "h3";
|
|
2
2
|
import { createServer } from "node:http";
|
|
3
3
|
import { resolve, dirname, join, extname } from "node:path";
|
|
4
|
-
import { existsSync, createReadStream, statSync } from "node:fs";
|
|
4
|
+
import { existsSync, createReadStream, statSync, readFileSync } from "node:fs";
|
|
5
5
|
import { toNodeListener } from "h3";
|
|
6
6
|
import type { RouteManifest } from "../router/manifest.js";
|
|
7
7
|
import { renderToResponse } from "../ssr/render.js";
|
|
@@ -12,6 +12,8 @@ import type { MiddlewareModule } from "./middleware.js";
|
|
|
12
12
|
import { runMiddleware } from "./middleware.js";
|
|
13
13
|
import type { PageMetadata } from "../types/index.js";
|
|
14
14
|
import { checkRevalidateAuth, applyRevalidate } from "./revalidate.js";
|
|
15
|
+
import { applyCdnHeaders, type CdnCache } from "./cdn.js";
|
|
16
|
+
import { getPPRShell, injectBuildIdIntoPPRShell, PPR_CACHE_SUBDIR } from "../ssr/ppr.js";
|
|
15
17
|
|
|
16
18
|
/**
|
|
17
19
|
* Find layout file paths (relative to cwd root) for a given route.file, ordered outermost→innermost.
|
|
@@ -75,6 +77,17 @@ export function createApp(manifest: RouteManifest, distDir: string): AlabApp {
|
|
|
75
77
|
const router = createRouter();
|
|
76
78
|
const publicDir = resolve(distDir, "../../public");
|
|
77
79
|
|
|
80
|
+
// Load the build ID written by `alab build` for skew protection.
|
|
81
|
+
// If the file is absent (first-run / non-standard setup) skew detection
|
|
82
|
+
// is silently disabled — existing behaviour is unchanged.
|
|
83
|
+
let buildId: string | undefined;
|
|
84
|
+
try {
|
|
85
|
+
buildId = readFileSync(resolve(distDir, "BUILD_ID"), "utf8").trim() || undefined;
|
|
86
|
+
} catch { /* no BUILD_ID file — skew protection disabled */ }
|
|
87
|
+
|
|
88
|
+
// Absolute path to the PPR shell cache directory.
|
|
89
|
+
const pprCacheDir = resolve(distDir, "../../", PPR_CACHE_SUBDIR);
|
|
90
|
+
|
|
78
91
|
// ─── Global middleware ───────────────────────────────────────────────────────
|
|
79
92
|
app.use(
|
|
80
93
|
defineEventHandler((event) => {
|
|
@@ -319,12 +332,14 @@ export function createApp(manifest: RouteManifest, distDir: string): AlabApp {
|
|
|
319
332
|
defineEventHandler(async (event) => {
|
|
320
333
|
const res = event.node.res;
|
|
321
334
|
|
|
322
|
-
//
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
335
|
+
// Skew protection: tell the client which build this server is running.
|
|
336
|
+
if (buildId) {
|
|
337
|
+
res.setHeader("x-alab-build-id", buildId);
|
|
338
|
+
const clientBuildId = event.node.req.headers["x-alab-build-id"];
|
|
339
|
+
if (clientBuildId && clientBuildId !== buildId) {
|
|
340
|
+
res.setHeader("x-alab-revalidate", "1");
|
|
341
|
+
}
|
|
342
|
+
}
|
|
328
343
|
|
|
329
344
|
const rawParams = (event.context.params ?? {}) as Record<string, string>;
|
|
330
345
|
const params = rawParams;
|
|
@@ -342,6 +357,8 @@ export function createApp(manifest: RouteManifest, distDir: string): AlabApp {
|
|
|
342
357
|
metadata?: PageMetadata;
|
|
343
358
|
generateMetadata?: (params: Record<string, string>) => PageMetadata | Promise<PageMetadata>;
|
|
344
359
|
ssr?: boolean;
|
|
360
|
+
cdnCache?: CdnCache;
|
|
361
|
+
ppr?: boolean;
|
|
345
362
|
};
|
|
346
363
|
|
|
347
364
|
const Page = mod.default;
|
|
@@ -351,6 +368,31 @@ export function createApp(manifest: RouteManifest, distDir: string): AlabApp {
|
|
|
351
368
|
return;
|
|
352
369
|
}
|
|
353
370
|
|
|
371
|
+
// ── PPR: serve pre-rendered static shell ──────────────────────────────
|
|
372
|
+
// Pages with `export const ppr = true` get a static HTML shell built
|
|
373
|
+
// at `alab build` time. Serve it instantly with a long CDN TTL so the
|
|
374
|
+
// static portion is edge-cached. Dynamic sections (`<Dynamic>`) render
|
|
375
|
+
// their fallback in the shell and are filled in client-side via React
|
|
376
|
+
// hydration, or server-side via Suspense streaming on direct hits.
|
|
377
|
+
if (mod.ppr === true) {
|
|
378
|
+
let shell = getPPRShell(route.path, pprCacheDir);
|
|
379
|
+
if (shell !== null) {
|
|
380
|
+
// Inject the per-build skew-protection tag into the pre-rendered HTML.
|
|
381
|
+
if (buildId) shell = injectBuildIdIntoPPRShell(shell, buildId);
|
|
382
|
+
|
|
383
|
+
res.statusCode = 200;
|
|
384
|
+
res.setHeader("content-type", "text/html; charset=utf-8");
|
|
385
|
+
// Long CDN TTL: static shell doesn't change until the next build.
|
|
386
|
+
res.setHeader("cache-control", "public, s-maxage=3600, stale-while-revalidate=86400");
|
|
387
|
+
res.setHeader("x-alab-ppr", "shell");
|
|
388
|
+
if (buildId) res.setHeader("x-alab-build-id", buildId);
|
|
389
|
+
res.end(shell);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
// Shell not found — fall through to normal SSR and warn once.
|
|
393
|
+
console.warn(`[alabjs] ppr: no pre-rendered shell for ${route.path} — run \`alab build\` to generate it. Falling back to SSR.`);
|
|
394
|
+
}
|
|
395
|
+
|
|
354
396
|
// Support both static metadata and dynamic generateMetadata (production fix)
|
|
355
397
|
const metadata: PageMetadata =
|
|
356
398
|
typeof mod.generateMetadata === "function"
|
|
@@ -369,8 +411,20 @@ export function createApp(manifest: RouteManifest, distDir: string): AlabApp {
|
|
|
369
411
|
const layoutsJson = JSON.stringify(layoutRelPaths);
|
|
370
412
|
const loadingFile = findProdLoadingFile(route.file, distDir) ?? undefined;
|
|
371
413
|
|
|
372
|
-
//
|
|
373
|
-
|
|
414
|
+
// ── Cache-control + CSRF ──────────────────────────────────────────────
|
|
415
|
+
// Pages that export `cdnCache` are public, edge-cacheable pages.
|
|
416
|
+
// They get CDN headers instead of `no-store`, and CSRF tokens are
|
|
417
|
+
// omitted — a shared cache would deliver the same token to every
|
|
418
|
+
// visitor, defeating CSRF protection.
|
|
419
|
+
let headExtra = "";
|
|
420
|
+
if (mod.cdnCache) {
|
|
421
|
+
applyCdnHeaders(res, mod.cdnCache);
|
|
422
|
+
} else {
|
|
423
|
+
// Private page: must not be cached by intermediaries.
|
|
424
|
+
res.setHeader("cache-control", "no-store");
|
|
425
|
+
const csrfToken = setCsrfCookie(event);
|
|
426
|
+
headExtra = `<meta name="csrf-token" content="${csrfToken.replace(/"/g, """)}" />`;
|
|
427
|
+
}
|
|
374
428
|
|
|
375
429
|
try {
|
|
376
430
|
renderToResponse(res, {
|
|
@@ -384,6 +438,7 @@ export function createApp(manifest: RouteManifest, distDir: string): AlabApp {
|
|
|
384
438
|
loadingFile,
|
|
385
439
|
ssr: ssrEnabled,
|
|
386
440
|
headExtra,
|
|
441
|
+
...(buildId ? { buildId } : {}),
|
|
387
442
|
});
|
|
388
443
|
} catch (err) {
|
|
389
444
|
// ── error.tsx fallback ────────────────────────────────────────────
|