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.
Files changed (56) hide show
  1. package/README.md +45 -0
  2. package/dist/commands/build.d.ts.map +1 -1
  3. package/dist/commands/build.js +104 -2
  4. package/dist/commands/build.js.map +1 -1
  5. package/dist/commands/dev.d.ts.map +1 -1
  6. package/dist/commands/dev.js +6 -0
  7. package/dist/commands/dev.js.map +1 -1
  8. package/dist/components/Dynamic.d.ts +88 -0
  9. package/dist/components/Dynamic.d.ts.map +1 -0
  10. package/dist/components/Dynamic.js +86 -0
  11. package/dist/components/Dynamic.js.map +1 -0
  12. package/dist/components/index.d.ts +2 -0
  13. package/dist/components/index.d.ts.map +1 -1
  14. package/dist/components/index.js +1 -0
  15. package/dist/components/index.js.map +1 -1
  16. package/dist/index.d.ts +1 -1
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js.map +1 -1
  19. package/dist/server/app.d.ts.map +1 -1
  20. package/dist/server/app.js +62 -8
  21. package/dist/server/app.js.map +1 -1
  22. package/dist/server/cdn.d.ts +72 -0
  23. package/dist/server/cdn.d.ts.map +1 -0
  24. package/dist/server/cdn.js +132 -0
  25. package/dist/server/cdn.js.map +1 -0
  26. package/dist/server/revalidate.d.ts.map +1 -1
  27. package/dist/server/revalidate.js +6 -1
  28. package/dist/server/revalidate.js.map +1 -1
  29. package/dist/ssr/html.d.ts +7 -0
  30. package/dist/ssr/html.d.ts.map +1 -1
  31. package/dist/ssr/html.js +2 -1
  32. package/dist/ssr/html.js.map +1 -1
  33. package/dist/ssr/ppr.d.ts +69 -0
  34. package/dist/ssr/ppr.d.ts.map +1 -0
  35. package/dist/ssr/ppr.js +132 -0
  36. package/dist/ssr/ppr.js.map +1 -0
  37. package/dist/ssr/render.d.ts +2 -0
  38. package/dist/ssr/render.d.ts.map +1 -1
  39. package/dist/ssr/render.js +2 -1
  40. package/dist/ssr/render.js.map +1 -1
  41. package/dist/types/index.d.ts +20 -1
  42. package/dist/types/index.d.ts.map +1 -1
  43. package/package.json +15 -2
  44. package/src/commands/build.ts +117 -2
  45. package/src/commands/dev.ts +7 -0
  46. package/src/components/Dynamic.tsx +124 -0
  47. package/src/components/index.ts +2 -0
  48. package/src/index.ts +1 -0
  49. package/src/server/app.ts +64 -9
  50. package/src/server/cdn.ts +187 -0
  51. package/src/server/revalidate.ts +7 -1
  52. package/src/ssr/html.ts +9 -0
  53. package/src/ssr/ppr.ts +167 -0
  54. package/src/ssr/render.ts +4 -0
  55. package/src/types/index.ts +23 -0
  56. package/tsconfig.tsbuildinfo +1 -1
@@ -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, "&quot;")}" />`;
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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"}
@@ -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
@@ -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;CAChB;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,aAAa,GAAG,IAAI,CA2F/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"}
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"}
@@ -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 });
@@ -1 +1 @@
1
- {"version":3,"file":"render.js","sourceRoot":"","sources":["../../src/ssr/render.ts"],"names":[],"mappings":"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"}
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;AA+BnF;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,GAAmB,EAAE,IAAmB;IACvE,MAAM,EACJ,IAAI,EACJ,OAAO,GAAG,EAAE,EACZ,MAAM,EACN,YAAY,EACZ,QAAQ,GAAG,EAAE,EACb,SAAS,EACT,WAAW,EACX,WAAW,EACX,GAAG,EACH,SAAS,EACT,KAAK,EACL,OAAO,GACR,GAAG,IAAI,CAAC;IAET,MAAM,SAAS,GAAqB;QAClC,QAAQ;QACR,UAAU,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC;QAClC,gBAAgB,EAAE,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC;QAC9C,SAAS;QACT,WAAW;QACX,WAAW;QACX,GAAG;QACH,SAAS;QACT,KAAK;QACL,OAAO;KACR,CAAC;IAEF,MAAM,MAAM,GAAG,eAAe,CAAC,SAAS,CAAC,CAAC;IAC1C,MAAM,KAAK,GAAG,cAAc,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;IAExC,kEAAkE;IAClE,8DAA8D;IAC9D,MAAM,MAAM,GAAG,aAAa,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,YAAY,EAAE,CAAQ,CAAC;IACpE,8DAA8D;IAC9D,MAAM,MAAM,GAAQ,OAAO,CAAC,MAAM;QAChC,CAAC,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,CAAC,aAAa,CAAC,MAAM,EAAE,EAAE,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAClF,CAAC,CAAC,MAAM,CAAC;IAEX,IAAI,QAAQ,GAAG,KAAK,CAAC;IACrB,IAAI,WAAW,GAAG,KAAK,CAAC;IAExB,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,sBAAsB,CAC5C,MAAM,EACN;QACE,YAAY;YACV,IAAI,WAAW;gBAAE,OAAO;YACxB,WAAW,GAAG,IAAI,CAAC;YAEnB,GAAG,CAAC,UAAU,GAAG,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC;YACtC,GAAG,CAAC,SAAS,CAAC,cAAc,EAAE,0BAA0B,CAAC,CAAC;YAC1D,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;YAElB,mEAAmE;YACnE,gDAAgD;YAChD,MAAM,QAAQ,GAAG,IAAI,QAAQ,CAAC;gBAC5B,KAAK,CAAC,KAAa,EAAE,IAAI,EAAE,EAAE;oBAC3B,GAAG,CAAC,KAAK,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;gBACvB,CAAC;gBACD,KAAK,CAAC,EAAE;oBACN,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;oBACjB,GAAG,CAAC,GAAG,EAAE,CAAC;oBACV,EAAE,EAAE,CAAC;gBACP,CAAC;aACF,CAAC,CAAC;YAEH,IAAI,CAAC,QAAQ,CAAC,CAAC;QACjB,CAAC;QAED,YAAY,CAAC,GAAG;YACd,2EAA2E;YAC3E,IAAI,CAAC,WAAW,EAAE,CAAC;gBACjB,WAAW,GAAG,IAAI,CAAC;gBACnB,GAAG,CAAC,UAAU,GAAG,GAAG,CAAC;gBACrB,GAAG,CAAC,SAAS,CAAC,cAAc,EAAE,2BAA2B,CAAC,CAAC;gBAC3D,MAAM,GAAG,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;gBAC7D,GAAG,CAAC,GAAG,CAAC,+BAA+B,SAAS,KAAK,GAAG,EAAE,CAAC,CAAC;YAC9D,CAAC;YACD,OAAO,CAAC,KAAK,CAAC,+BAA+B,SAAS,GAAG,EAAE,GAAG,CAAC,CAAC;QAClE,CAAC;QAED,OAAO,CAAC,GAAG;YACT,QAAQ,GAAG,IAAI,CAAC;YAChB,OAAO,CAAC,KAAK,CAAC,mCAAmC,SAAS,GAAG,EAAE,GAAG,CAAC,CAAC;QACtE,CAAC;KACF,CACF,CAAC;IAEF,oDAAoD;IACpD,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;QACnB,IAAI,CAAC,GAAG,CAAC,aAAa;YAAE,KAAK,EAAE,CAAC;IAClC,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,IAA6F,EAC7F,MAA8B,EAC9B,YAAoC;IAEpC,MAAM,EAAE,cAAc,EAAE,mBAAmB,EAAE,GAAG,MAAM,MAAM,CAAC,kBAAkB,CAAC,CAAC;IACjF,OAAO,mBAAmB,CAAC,aAAa,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC,CAAC,CAAC;AAC5E,CAAC"}
@@ -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
- export {};
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.1.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
  }
@@ -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;
@@ -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
+ }
@@ -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
@@ -6,5 +6,6 @@ export type {
6
6
  PageMetadata,
7
7
  GenerateMetadata,
8
8
  RouteParams,
9
+ CdnCache,
9
10
  } from "./types/index.js";
10
11
  export { createApp } from "./server/app.js";