alabjs 0.1.0 → 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/README.md +45 -0
- 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 +15 -2
- 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/ppr.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Alab PPR — build-time static shell pre-renderer and runtime shell reader.
|
|
3
|
+
*
|
|
4
|
+
* ## Build-time
|
|
5
|
+
* `preRenderPPRShell()` renders a page with `PPRShellProvider` so that every
|
|
6
|
+
* `<Dynamic>` emits a `data-ppr-hole` placeholder instead of its children.
|
|
7
|
+
* The resulting HTML is saved to `.alabjs/ppr-cache/<slug>.html`.
|
|
8
|
+
*
|
|
9
|
+
* ## Runtime
|
|
10
|
+
* `getPPRShell()` reads the pre-rendered HTML for a given route path, or
|
|
11
|
+
* returns `null` if the file doesn't exist (triggers SSR fallback).
|
|
12
|
+
*/
|
|
13
|
+
import { createElement, Suspense } from "react";
|
|
14
|
+
import { renderToPipeableStream } from "react-dom/server";
|
|
15
|
+
import { Writable } from "node:stream";
|
|
16
|
+
import { writeFileSync, mkdirSync, existsSync, readFileSync } from "node:fs";
|
|
17
|
+
import { join, dirname } from "node:path";
|
|
18
|
+
import { PPRShellProvider } from "../components/Dynamic.js";
|
|
19
|
+
import { htmlShellBefore, htmlShellAfter } from "./html.js";
|
|
20
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
21
|
+
/** Relative path (from cwd) where pre-rendered shells are written. */
|
|
22
|
+
export const PPR_CACHE_SUBDIR = ".alabjs/ppr-cache";
|
|
23
|
+
// ─── Filename helpers ─────────────────────────────────────────────────────────
|
|
24
|
+
/**
|
|
25
|
+
* Convert a route path to a safe filesystem filename (no leading slash, no
|
|
26
|
+
* dynamic segment brackets, slashes replaced with `__`).
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* routeToFilename("/") → "index"
|
|
30
|
+
* routeToFilename("/posts") → "posts"
|
|
31
|
+
* routeToFilename("/posts/[id]") → "posts___id_"
|
|
32
|
+
* routeToFilename("/a/[b]/c/[d]") → "a___b___c___d_"
|
|
33
|
+
*/
|
|
34
|
+
export function routeToFilename(routePath) {
|
|
35
|
+
const slug = routePath
|
|
36
|
+
.replace(/^\//, "") // strip leading /
|
|
37
|
+
.replace(/\[([^\]]+)\]/g, "__$1_") // [param] → __param_
|
|
38
|
+
.replace(/\//g, "__"); // / → __
|
|
39
|
+
return (slug || "index") + ".html";
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Pre-render the static HTML shell for a PPR page and persist it to disk.
|
|
43
|
+
*
|
|
44
|
+
* The render uses `PPRShellProvider` so every `<Dynamic>` in the tree emits
|
|
45
|
+
* a `data-ppr-hole` placeholder — children (per-request logic) are omitted.
|
|
46
|
+
*
|
|
47
|
+
* Called from `alab build` after the Vite bundle step completes.
|
|
48
|
+
*/
|
|
49
|
+
export async function preRenderPPRShell({ Page, layouts, shellOpts, pprCacheDir, routePath, }) {
|
|
50
|
+
// Build the element tree: layouts wrapping Page, all inside PPRShellProvider.
|
|
51
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
52
|
+
let pageEl = createElement(Page, { params: {}, searchParams: {} });
|
|
53
|
+
// Suspense wrapper guards against any accidental top-level suspension.
|
|
54
|
+
pageEl = createElement(Suspense, { fallback: null }, pageEl);
|
|
55
|
+
for (let i = layouts.length - 1; i >= 0; i--) {
|
|
56
|
+
const Layout = layouts[i];
|
|
57
|
+
if (Layout)
|
|
58
|
+
pageEl = createElement(Layout, {}, pageEl);
|
|
59
|
+
}
|
|
60
|
+
// PPRShellProvider switches Dynamic into placeholder mode.
|
|
61
|
+
// Pass children via props to satisfy strict TS prop checking.
|
|
62
|
+
const tree = createElement(PPRShellProvider, { children: pageEl });
|
|
63
|
+
// Wait for the full render (allReady) — we need a complete snapshot, not a
|
|
64
|
+
// partially streamed response, because the file is served as-is from disk.
|
|
65
|
+
const reactHtml = await new Promise((resolve, reject) => {
|
|
66
|
+
let result = "";
|
|
67
|
+
const sink = new Writable({
|
|
68
|
+
write(chunk, _enc, cb) { result += chunk.toString(); cb(); },
|
|
69
|
+
final(cb) { resolve(result); cb(); },
|
|
70
|
+
});
|
|
71
|
+
const { pipe } = renderToPipeableStream(tree, {
|
|
72
|
+
onAllReady() { pipe(sink); },
|
|
73
|
+
onError(err) { reject(err instanceof Error ? err : new Error(String(err))); },
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
const before = htmlShellBefore({ ...shellOpts, headExtra: "" });
|
|
77
|
+
const after = htmlShellAfter({});
|
|
78
|
+
const fullHtml = `${before}${reactHtml}${after}`;
|
|
79
|
+
mkdirSync(pprCacheDir, { recursive: true });
|
|
80
|
+
writeFileSync(join(pprCacheDir, routeToFilename(routePath)), fullHtml, "utf8");
|
|
81
|
+
}
|
|
82
|
+
// ─── Runtime ──────────────────────────────────────────────────────────────────
|
|
83
|
+
/**
|
|
84
|
+
* Return the pre-rendered static HTML shell for `routePath`, or `null` if the
|
|
85
|
+
* cache file doesn't exist.
|
|
86
|
+
*
|
|
87
|
+
* Callers should fall back to normal SSR when `null` is returned.
|
|
88
|
+
*/
|
|
89
|
+
export function getPPRShell(routePath, pprCacheDir) {
|
|
90
|
+
const filePath = join(pprCacheDir, routeToFilename(routePath));
|
|
91
|
+
try {
|
|
92
|
+
return existsSync(filePath) ? readFileSync(filePath, "utf8") : null;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Inject a `<meta name="alabjs-build-id">` tag into a pre-rendered PPR shell.
|
|
100
|
+
*
|
|
101
|
+
* The shell is pre-built and therefore doesn't include the per-build ID. We
|
|
102
|
+
* splice it in at serve time so skew protection still works for PPR pages.
|
|
103
|
+
*/
|
|
104
|
+
export function injectBuildIdIntoPPRShell(html, buildId) {
|
|
105
|
+
const tag = `<meta name="alabjs-build-id" content="${buildId.replace(/"/g, """)}" />`;
|
|
106
|
+
// Insert after <head> if present; otherwise prepend to <html>.
|
|
107
|
+
const headClose = html.indexOf("</head>");
|
|
108
|
+
if (headClose !== -1) {
|
|
109
|
+
return html.slice(0, headClose) + ` ${tag}\n` + html.slice(headClose);
|
|
110
|
+
}
|
|
111
|
+
return tag + html;
|
|
112
|
+
}
|
|
113
|
+
// ─── Layout discovery (prod) ──────────────────────────────────────────────────
|
|
114
|
+
/**
|
|
115
|
+
* Find layout file paths for a given page route file, ordered
|
|
116
|
+
* outermost → innermost. Mirrors the logic in `app.ts` but scoped to the
|
|
117
|
+
* build-time dist directory.
|
|
118
|
+
*/
|
|
119
|
+
export function findBuildLayoutFiles(routeFile, distDir) {
|
|
120
|
+
const pageDir = dirname(routeFile);
|
|
121
|
+
const parts = pageDir.split("/");
|
|
122
|
+
const layouts = [];
|
|
123
|
+
for (let i = 1; i <= parts.length; i++) {
|
|
124
|
+
const dir = parts.slice(0, i).join("/");
|
|
125
|
+
const candidate = `${dir}/layout.tsx`;
|
|
126
|
+
if (existsSync(join(distDir, "server", candidate))) {
|
|
127
|
+
layouts.push(candidate);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return layouts;
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=ppr.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ppr.js","sourceRoot":"","sources":["../../src/ssr/ppr.ts"],"names":[],"mappings":"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"}
|
package/dist/ssr/render.d.ts
CHANGED
|
@@ -27,6 +27,8 @@ export interface RenderOptions {
|
|
|
27
27
|
headExtra?: string;
|
|
28
28
|
/** CSP nonce (optional). */
|
|
29
29
|
nonce?: string;
|
|
30
|
+
/** Build ID for skew protection (see html.ts). */
|
|
31
|
+
buildId?: string;
|
|
30
32
|
}
|
|
31
33
|
/**
|
|
32
34
|
* Render a page component to a streaming HTTP response using React 19's
|
package/dist/ssr/render.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../src/ssr/render.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,KAAK,aAAa,EAAE,MAAM,OAAO,CAAC;AAG1D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEhD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEtD,MAAM,WAAW,aAAa;IAC5B,gCAAgC;IAChC,IAAI,EAAE,aAAa,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,CAAC,CAAC;IAC9F,yEAAyE;IAEzE,OAAO,CAAC,EAAE,aAAa,CAAC,GAAG,CAAC,EAAE,CAAC;IAC/B,kDAAkD;IAClD,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,2BAA2B;IAC3B,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,YAAY,CAAC;IACxB,mFAAmF;IACnF,SAAS,EAAE,MAAM,CAAC;IAClB,iEAAiE;IACjE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0EAA0E;IAC1E,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,6CAA6C;IAC7C,GAAG,EAAE,OAAO,CAAC;IACb,gFAAgF;IAChF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4BAA4B;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../src/ssr/render.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,KAAK,aAAa,EAAE,MAAM,OAAO,CAAC;AAG1D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEhD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEtD,MAAM,WAAW,aAAa;IAC5B,gCAAgC;IAChC,IAAI,EAAE,aAAa,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,CAAC,CAAC;IAC9F,yEAAyE;IAEzE,OAAO,CAAC,EAAE,aAAa,CAAC,GAAG,CAAC,EAAE,CAAC;IAC/B,kDAAkD;IAClD,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,2BAA2B;IAC3B,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,YAAY,CAAC;IACxB,mFAAmF;IACnF,SAAS,EAAE,MAAM,CAAC;IAClB,iEAAiE;IACjE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0EAA0E;IAC1E,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,6CAA6C;IAC7C,GAAG,EAAE,OAAO,CAAC;IACb,gFAAgF;IAChF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4BAA4B;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kDAAkD;IAClD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,aAAa,GAAG,IAAI,CA6F/E;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAClC,IAAI,EAAE,aAAa,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,CAAC,EAC7F,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACnC,OAAO,CAAC,MAAM,CAAC,CAGjB"}
|
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":"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"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "alabjs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "AlabJS — full-stack React framework powered by a Rust compiler",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"react",
|
|
7
|
+
"framework",
|
|
8
|
+
"ssr",
|
|
9
|
+
"fullstack",
|
|
10
|
+
"rust",
|
|
11
|
+
"vite",
|
|
12
|
+
"typescript",
|
|
13
|
+
"server-functions",
|
|
14
|
+
"signals",
|
|
15
|
+
"isr"
|
|
16
|
+
],
|
|
5
17
|
"license": "MIT",
|
|
6
18
|
"type": "module",
|
|
7
19
|
"exports": {
|
|
@@ -103,5 +115,6 @@
|
|
|
103
115
|
"repository": {
|
|
104
116
|
"type": "git",
|
|
105
117
|
"url": "https://github.com/thinkgrid-labs/alabjs.git"
|
|
106
|
-
}
|
|
118
|
+
},
|
|
119
|
+
"author": "Dennis Paler <dennis@thinkgrid.dev>"
|
|
107
120
|
}
|
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";
|