@tyndall/build 0.0.1

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.
@@ -0,0 +1,56 @@
1
+ export class BuildStageError extends Error {
2
+ constructor(stage, error) {
3
+ const detail = error instanceof Error ? error.message : String(error);
4
+ super(`Build pipeline failed at ${stage}: ${detail}`);
5
+ this.stage = stage;
6
+ if (error !== undefined) {
7
+ this.cause = error;
8
+ }
9
+ }
10
+ }
11
+ export const runBuildPipeline = async (ctx, handlers, options = {}) => {
12
+ const timings = [];
13
+ const state = {};
14
+ const runStage = async (stage, task, assign) => {
15
+ const startedAt = Date.now();
16
+ try {
17
+ const result = await task();
18
+ if (assign) {
19
+ assign(result);
20
+ }
21
+ return result;
22
+ }
23
+ catch (error) {
24
+ throw new BuildStageError(stage, error);
25
+ }
26
+ finally {
27
+ timings.push({ stage, startedAt, durationMs: Date.now() - startedAt });
28
+ }
29
+ };
30
+ // Important: stage ordering is fixed to keep pipeline behavior deterministic.
31
+ await runStage("scanRoutes", () => handlers.scanRoutes(ctx), (value) => {
32
+ state.routeGraph = value;
33
+ });
34
+ await runStage("analyzeGraph", () => handlers.analyzeGraph(ctx, state), (value) => {
35
+ state.moduleGraph = value;
36
+ });
37
+ await runStage("bundleClient", () => handlers.bundleClient(ctx, state), (value) => {
38
+ state.clientBundle = value;
39
+ });
40
+ if (options.ssr && handlers.bundleServer) {
41
+ await runStage("bundleServer", () => handlers.bundleServer?.(ctx, state), (value) => {
42
+ state.serverBundle = value;
43
+ });
44
+ }
45
+ await runStage("runStaticData", () => handlers.runStaticData(ctx, state), (value) => {
46
+ state.staticData = value;
47
+ });
48
+ await runStage("renderHtml", () => handlers.renderHtml(ctx, state), (value) => {
49
+ state.renderResult = value;
50
+ });
51
+ await runStage("emitManifest", () => handlers.emitManifest(ctx, state), (value) => {
52
+ state.manifest = value;
53
+ });
54
+ await runStage("copyPublic", () => handlers.copyPublic(ctx, state));
55
+ return { state, timings };
56
+ };
@@ -0,0 +1,50 @@
1
+ import type { HeadDescriptor, RouteParams } from "@tyndall/core";
2
+ export interface RenderContext {
3
+ routeId: string;
4
+ params?: RouteParams;
5
+ props: Record<string, unknown>;
6
+ }
7
+ export interface RenderResult {
8
+ html: string;
9
+ head?: HeadDescriptor;
10
+ status?: number;
11
+ headers?: Record<string, string>;
12
+ }
13
+ export interface UIAdapterLike {
14
+ renderToHtml: (ctx: RenderContext) => Promise<RenderResult> | RenderResult;
15
+ serializeProps: (props: Record<string, unknown>) => string;
16
+ }
17
+ export interface TemplateAssets {
18
+ scripts?: string[];
19
+ legacyScripts?: string[];
20
+ styles?: string[];
21
+ hydration?: "full" | "islands";
22
+ scriptType?: "module" | "classic";
23
+ buildVersion?: string;
24
+ }
25
+ export interface TemplateContext {
26
+ html: string;
27
+ head: HeadDescriptor;
28
+ propsPayload: string;
29
+ routeId: string;
30
+ hydration: "full" | "islands";
31
+ scripts: string[];
32
+ legacyScripts: string[];
33
+ styles: string[];
34
+ scriptType: "module" | "classic";
35
+ buildVersion: string;
36
+ }
37
+ export type HtmlTemplate = (ctx: TemplateContext) => string;
38
+ export declare const computeTemplateHash: (template: HtmlTemplate, assets?: TemplateAssets) => string;
39
+ export declare const defaultHtmlTemplate: HtmlTemplate;
40
+ export interface RenderHtmlOutput {
41
+ html: string;
42
+ head: HeadDescriptor;
43
+ propsPayload: string;
44
+ finalHtml: string;
45
+ templateHash: string;
46
+ status?: number;
47
+ headers?: Record<string, string>;
48
+ }
49
+ export declare const renderHtml: (adapter: UIAdapterLike, ctx: RenderContext, template?: HtmlTemplate, assets?: TemplateAssets) => Promise<RenderHtmlOutput>;
50
+ //# sourceMappingURL=renderer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../src/renderer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAGjE,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,cAAc,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAED,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,CAAC,GAAG,EAAE,aAAa,KAAK,OAAO,CAAC,YAAY,CAAC,GAAG,YAAY,CAAC;IAC3E,cAAc,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,MAAM,CAAC;CAC5D;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,UAAU,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAC;IAClC,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,cAAc,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,UAAU,EAAE,QAAQ,GAAG,SAAS,CAAC;IACjC,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,MAAM,YAAY,GAAG,CAAC,GAAG,EAAE,eAAe,KAAK,MAAM,CAAC;AAoB5D,eAAO,MAAM,mBAAmB,GAC9B,UAAU,YAAY,EACtB,SAAQ,cAAmB,KAC1B,MAMA,CAAC;AAkDJ,eAAO,MAAM,mBAAmB,EAAE,YAyCjC,CAAC;AAEF,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,cAAc,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAED,eAAO,MAAM,UAAU,GACrB,SAAS,aAAa,EACtB,KAAK,aAAa,EAClB,WAAU,YAAkC,EAC5C,SAAQ,cAAmB,KAC1B,OAAO,CAAC,gBAAgB,CA4B1B,CAAC"}
@@ -0,0 +1,109 @@
1
+ import { hash, stableStringify } from "@tyndall/shared";
2
+ const normalizeAssets = (assets = {}) => ({
3
+ scripts: assets.scripts ? [...assets.scripts] : [],
4
+ legacyScripts: assets.legacyScripts ? [...assets.legacyScripts] : [],
5
+ styles: assets.styles ? [...assets.styles] : [],
6
+ hydration: assets.hydration === "islands" ? "islands" : "full",
7
+ scriptType: assets.scriptType === "classic" ? "classic" : "module",
8
+ buildVersion: assets.buildVersion?.trim() ?? "",
9
+ });
10
+ export const computeTemplateHash = (template, assets = {}) => hash(stableStringify({
11
+ template: template.toString(),
12
+ assets: normalizeAssets(assets),
13
+ }));
14
+ const renderHeadDescriptor = (head) => {
15
+ const parts = [];
16
+ if (head.title) {
17
+ parts.push(`<title>${head.title}</title>`);
18
+ }
19
+ for (const meta of head.meta ?? []) {
20
+ const attrs = Object.entries(meta)
21
+ .map(([key, value]) => `${key}="${value}"`)
22
+ .join(" ");
23
+ parts.push(`<meta ${attrs}>`);
24
+ }
25
+ for (const link of head.link ?? []) {
26
+ const attrs = Object.entries(link)
27
+ .map(([key, value]) => `${key}="${value}"`)
28
+ .join(" ");
29
+ parts.push(`<link ${attrs}>`);
30
+ }
31
+ for (const script of head.script ?? []) {
32
+ const attrs = Object.entries(script)
33
+ .map(([key, value]) => `${key}="${value}"`)
34
+ .join(" ");
35
+ parts.push(`<script ${attrs}></script>`);
36
+ }
37
+ return parts.join("");
38
+ };
39
+ const renderStyleLinks = (styles) => styles.map((href) => `<link rel=\"stylesheet\" href=\"${href}\">`).join("");
40
+ const renderScriptTags = (scripts, scriptType) => scripts
41
+ .map((src) => scriptType === "classic"
42
+ ? `<script src=\"${src}\"></script>`
43
+ : `<script type=\"module\" src=\"${src}\"></script>`)
44
+ .join("");
45
+ const renderLegacyScriptTags = (scripts) => scripts.map((src) => `<script nomodule src=\"${src}\"></script>`).join("");
46
+ const escapeHtml = (value) => value
47
+ .replace(/&/g, "&amp;")
48
+ .replace(/</g, "&lt;")
49
+ .replace(/>/g, "&gt;")
50
+ .replace(/\"/g, "&quot;");
51
+ export const defaultHtmlTemplate = ({ html, head, propsPayload, routeId, hydration, scripts, legacyScripts = [], styles, scriptType, buildVersion, }) => {
52
+ const headHtml = renderHeadDescriptor(head);
53
+ const stylesHtml = renderStyleLinks(styles);
54
+ const scriptsHtml = renderScriptTags(scripts, scriptType);
55
+ const legacyScriptsHtml = renderLegacyScriptTags(legacyScripts);
56
+ const islandAttr = hydration === "islands" ? " data-hyper-island-root=\"router\"" : "";
57
+ const buildVersionMeta = buildVersion
58
+ ? `<meta name=\"hyper-build-version\" content=\"${escapeHtml(buildVersion)}\">`
59
+ : "";
60
+ const buildVersionAttr = buildVersion
61
+ ? ` data-hyper-build-version=\"${escapeHtml(buildVersion)}\"`
62
+ : "";
63
+ // Important: keep a single payload slot for deterministic HTML snapshots.
64
+ return [
65
+ "<!doctype html>",
66
+ `<html${buildVersionAttr}>`,
67
+ "<head>",
68
+ headHtml,
69
+ buildVersionMeta,
70
+ stylesHtml,
71
+ "</head>",
72
+ "<body>",
73
+ `<div id=\"app\" data-hyper-route=\"${routeId}\" data-hyper-hydration=\"${hydration}\"${islandAttr}>${html}</div>`,
74
+ `<script id=\"__HYPER_PROPS__\" type=\"application/json\">${propsPayload}</script>`,
75
+ `<script>window.__HYPER_ROUTE_ID__ = ${JSON.stringify(routeId)};</script>`,
76
+ scriptsHtml,
77
+ legacyScriptsHtml,
78
+ "</body>",
79
+ "</html>",
80
+ ].join("");
81
+ };
82
+ export const renderHtml = async (adapter, ctx, template = defaultHtmlTemplate, assets = {}) => {
83
+ const result = await adapter.renderToHtml(ctx);
84
+ const head = result.head ?? {};
85
+ const propsPayload = adapter.serializeProps(ctx.props);
86
+ const normalizedAssets = normalizeAssets(assets);
87
+ const templateHash = computeTemplateHash(template, normalizedAssets);
88
+ const finalHtml = template({
89
+ html: result.html,
90
+ head,
91
+ propsPayload,
92
+ routeId: ctx.routeId,
93
+ hydration: normalizedAssets.hydration,
94
+ scripts: normalizedAssets.scripts,
95
+ legacyScripts: normalizedAssets.legacyScripts,
96
+ styles: normalizedAssets.styles,
97
+ scriptType: normalizedAssets.scriptType,
98
+ buildVersion: normalizedAssets.buildVersion,
99
+ });
100
+ return {
101
+ html: result.html,
102
+ head,
103
+ propsPayload,
104
+ finalHtml,
105
+ templateHash,
106
+ status: result.status,
107
+ headers: result.headers,
108
+ };
109
+ };
@@ -0,0 +1,48 @@
1
+ import type { HeadDescriptor } from "@tyndall/core";
2
+ export type PropsCacheInput = {
3
+ routeId: string;
4
+ paramsKey: string;
5
+ depsHash: string;
6
+ envHash: string;
7
+ getStaticPropsHash?: string;
8
+ };
9
+ export type PropsCacheEntry = {
10
+ routeId: string;
11
+ paramsKey: string;
12
+ props: Record<string, unknown>;
13
+ revalidate?: number | false;
14
+ propsKey: string;
15
+ propsHash: string;
16
+ depsHash: string;
17
+ envHash: string;
18
+ generatedAt: number;
19
+ };
20
+ export type RenderCacheInput = {
21
+ routeId: string;
22
+ paramsKey: string;
23
+ templateHash: string;
24
+ depsHash: string;
25
+ propsHash: string;
26
+ };
27
+ export type RenderCacheEntry = {
28
+ routeId: string;
29
+ paramsKey: string;
30
+ renderKey: string;
31
+ templateHash: string;
32
+ propsHash: string;
33
+ depsHash: string;
34
+ generatedAt: number;
35
+ head: HeadDescriptor;
36
+ propsPayload: string;
37
+ status?: number;
38
+ headers?: Record<string, string>;
39
+ finalHtml: string;
40
+ };
41
+ export declare const computePropsKey: (input: PropsCacheInput) => string;
42
+ export declare const computePropsHash: (props: Record<string, unknown>) => string;
43
+ export declare const computeRenderKey: (input: RenderCacheInput) => string;
44
+ export declare const readPropsCache: (cacheRoot: string, input: PropsCacheInput, ttlSeconds: number | false) => Promise<PropsCacheEntry | null>;
45
+ export declare const writePropsCache: (cacheRoot: string, entry: PropsCacheEntry) => Promise<void>;
46
+ export declare const readRenderCache: (cacheRoot: string, input: RenderCacheInput) => Promise<RenderCacheEntry | null>;
47
+ export declare const writeRenderCache: (cacheRoot: string, entry: RenderCacheEntry) => Promise<void>;
48
+ //# sourceMappingURL=ssg-cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ssg-cache.d.ts","sourceRoot":"","sources":["../src/ssg-cache.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AA0BpD,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,UAAU,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,cAAc,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAoFF,eAAO,MAAM,eAAe,GAAI,OAAO,eAAe,KAAG,MAUxD,CAAC;AAEF,eAAO,MAAM,gBAAgB,GAAI,OAAO,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAG,MACpC,CAAC;AAE/B,eAAO,MAAM,gBAAgB,GAAI,OAAO,gBAAgB,KAAG,MASxD,CAAC;AAEJ,eAAO,MAAM,cAAc,GACzB,WAAW,MAAM,EACjB,OAAO,eAAe,EACtB,YAAY,MAAM,GAAG,KAAK,KACzB,OAAO,CAAC,eAAe,GAAG,IAAI,CAchC,CAAC;AAEF,eAAO,MAAM,eAAe,GAC1B,WAAW,MAAM,EACjB,OAAO,eAAe,KACrB,OAAO,CAAC,IAAI,CAGd,CAAC;AAEF,eAAO,MAAM,eAAe,GAC1B,WAAW,MAAM,EACjB,OAAO,gBAAgB,KACtB,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAgBjC,CAAC;AAEF,eAAO,MAAM,gBAAgB,GAC3B,WAAW,MAAM,EACjB,OAAO,gBAAgB,KACtB,OAAO,CAAC,IAAI,CAoBd,CAAC"}
@@ -0,0 +1,159 @@
1
+ import { join } from "node:path";
2
+ import { hash, readFileSafe, stableStringify, writeFileAtomic } from "@tyndall/shared";
3
+ const encodeSegment = (value) => encodeURIComponent(value);
4
+ const resolveRouteSegments = (routeId) => {
5
+ const segments = routeId.split("/").filter(Boolean);
6
+ return segments.length > 0 ? segments.map(encodeSegment) : ["_root"];
7
+ };
8
+ const resolveParamsSegment = (paramsKey) => encodeSegment(paramsKey.length > 0 ? paramsKey : "null");
9
+ const resolveCachePath = (cacheRoot, kind, routeId, paramsKey, extension) => join(cacheRoot, kind, ...resolveRouteSegments(routeId), `${resolveParamsSegment(paramsKey)}.${extension}`);
10
+ const parsePropsCacheEntry = (raw) => {
11
+ if (!raw) {
12
+ return null;
13
+ }
14
+ try {
15
+ const parsed = JSON.parse(raw.toString());
16
+ if (!parsed || typeof parsed !== "object") {
17
+ return null;
18
+ }
19
+ if (typeof parsed.routeId !== "string" ||
20
+ typeof parsed.paramsKey !== "string" ||
21
+ typeof parsed.propsKey !== "string" ||
22
+ typeof parsed.propsHash !== "string" ||
23
+ typeof parsed.depsHash !== "string" ||
24
+ typeof parsed.envHash !== "string" ||
25
+ typeof parsed.generatedAt !== "number") {
26
+ return null;
27
+ }
28
+ if (!parsed.props || typeof parsed.props !== "object") {
29
+ return null;
30
+ }
31
+ return {
32
+ routeId: parsed.routeId,
33
+ paramsKey: parsed.paramsKey,
34
+ props: parsed.props,
35
+ revalidate: parsed.revalidate,
36
+ propsKey: parsed.propsKey,
37
+ propsHash: parsed.propsHash,
38
+ depsHash: parsed.depsHash,
39
+ envHash: parsed.envHash,
40
+ generatedAt: parsed.generatedAt,
41
+ };
42
+ }
43
+ catch {
44
+ return null;
45
+ }
46
+ };
47
+ const parseRenderCacheMeta = (raw) => {
48
+ if (!raw) {
49
+ return null;
50
+ }
51
+ try {
52
+ const parsed = JSON.parse(raw.toString());
53
+ if (!parsed || typeof parsed !== "object") {
54
+ return null;
55
+ }
56
+ if (typeof parsed.routeId !== "string" ||
57
+ typeof parsed.paramsKey !== "string" ||
58
+ typeof parsed.renderKey !== "string" ||
59
+ typeof parsed.templateHash !== "string" ||
60
+ typeof parsed.propsHash !== "string" ||
61
+ typeof parsed.depsHash !== "string" ||
62
+ typeof parsed.generatedAt !== "number" ||
63
+ typeof parsed.propsPayload !== "string") {
64
+ return null;
65
+ }
66
+ return {
67
+ routeId: parsed.routeId,
68
+ paramsKey: parsed.paramsKey,
69
+ renderKey: parsed.renderKey,
70
+ templateHash: parsed.templateHash,
71
+ propsHash: parsed.propsHash,
72
+ depsHash: parsed.depsHash,
73
+ generatedAt: parsed.generatedAt,
74
+ head: (parsed.head ?? {}),
75
+ propsPayload: parsed.propsPayload,
76
+ status: typeof parsed.status === "number" ? parsed.status : undefined,
77
+ headers: parsed.headers && typeof parsed.headers === "object"
78
+ ? parsed.headers
79
+ : undefined,
80
+ };
81
+ }
82
+ catch {
83
+ return null;
84
+ }
85
+ };
86
+ export const computePropsKey = (input) => {
87
+ const getStaticPropsHash = input.getStaticPropsHash ?? input.depsHash;
88
+ return hash(stableStringify({
89
+ routeId: input.routeId,
90
+ paramsKey: input.paramsKey,
91
+ getStaticPropsHash,
92
+ envHash: input.envHash,
93
+ }));
94
+ };
95
+ export const computePropsHash = (props) => hash(stableStringify(props));
96
+ export const computeRenderKey = (input) => hash(stableStringify({
97
+ routeId: input.routeId,
98
+ paramsKey: input.paramsKey,
99
+ templateHash: input.templateHash,
100
+ depsHash: input.depsHash,
101
+ propsHash: input.propsHash,
102
+ }));
103
+ export const readPropsCache = async (cacheRoot, input, ttlSeconds) => {
104
+ const propsKey = computePropsKey(input);
105
+ const filePath = resolveCachePath(cacheRoot, "props", input.routeId, input.paramsKey, "json");
106
+ const parsed = parsePropsCacheEntry(await readFileSafe(filePath));
107
+ if (!parsed || parsed.propsKey !== propsKey) {
108
+ return null;
109
+ }
110
+ if (typeof ttlSeconds === "number" && ttlSeconds > 0) {
111
+ const ageMs = Date.now() - parsed.generatedAt;
112
+ if (ageMs > ttlSeconds * 1000) {
113
+ return null;
114
+ }
115
+ }
116
+ return parsed;
117
+ };
118
+ export const writePropsCache = async (cacheRoot, entry) => {
119
+ const filePath = resolveCachePath(cacheRoot, "props", entry.routeId, entry.paramsKey, "json");
120
+ await writeFileAtomic(filePath, JSON.stringify(entry, null, 2), { encoding: "utf-8" });
121
+ };
122
+ export const readRenderCache = async (cacheRoot, input) => {
123
+ const renderKey = computeRenderKey(input);
124
+ const metaPath = resolveCachePath(cacheRoot, "render", input.routeId, input.paramsKey, "json");
125
+ const htmlPath = resolveCachePath(cacheRoot, "render", input.routeId, input.paramsKey, "html");
126
+ const meta = parseRenderCacheMeta(await readFileSafe(metaPath));
127
+ if (!meta || meta.renderKey !== renderKey) {
128
+ return null;
129
+ }
130
+ const html = await readFileSafe(htmlPath, "utf-8");
131
+ if (!html) {
132
+ return null;
133
+ }
134
+ return {
135
+ ...meta,
136
+ finalHtml: html.toString(),
137
+ };
138
+ };
139
+ export const writeRenderCache = async (cacheRoot, entry) => {
140
+ const metaPath = resolveCachePath(cacheRoot, "render", entry.routeId, entry.paramsKey, "json");
141
+ const htmlPath = resolveCachePath(cacheRoot, "render", entry.routeId, entry.paramsKey, "html");
142
+ const meta = {
143
+ routeId: entry.routeId,
144
+ paramsKey: entry.paramsKey,
145
+ renderKey: entry.renderKey,
146
+ templateHash: entry.templateHash,
147
+ propsHash: entry.propsHash,
148
+ depsHash: entry.depsHash,
149
+ generatedAt: entry.generatedAt,
150
+ head: entry.head,
151
+ propsPayload: entry.propsPayload,
152
+ status: entry.status,
153
+ headers: entry.headers,
154
+ };
155
+ await Promise.all([
156
+ writeFileAtomic(metaPath, JSON.stringify(meta, null, 2), { encoding: "utf-8" }),
157
+ writeFileAtomic(htmlPath, entry.finalHtml, { encoding: "utf-8" }),
158
+ ]);
159
+ };
@@ -0,0 +1,21 @@
1
+ import type { PageModule, RouteGraph, RouteParams, StaticPropsResult } from "@tyndall/core";
2
+ export interface StaticDataEntry {
3
+ routeId: string;
4
+ params?: RouteParams;
5
+ paramsKey: string;
6
+ props: Record<string, unknown>;
7
+ revalidate?: StaticPropsResult["revalidate"];
8
+ }
9
+ export interface StaticDataResult {
10
+ entries: StaticDataEntry[];
11
+ }
12
+ export interface StaticDataCache {
13
+ read: (routeId: string, params: RouteParams | undefined, paramsKey: string) => Promise<StaticDataEntry | null>;
14
+ write: (entry: StaticDataEntry) => Promise<void>;
15
+ }
16
+ export declare class StaticDataError extends Error {
17
+ readonly routeId: string;
18
+ constructor(routeId: string, message: string);
19
+ }
20
+ export declare const collectStaticData: (routeGraph: RouteGraph, pageModules: Record<string, PageModule>, cache?: StaticDataCache) => Promise<StaticDataResult>;
21
+ //# sourceMappingURL=ssg-data.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ssg-data.d.ts","sourceRoot":"","sources":["../src/ssg-data.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAI5F,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,UAAU,CAAC,EAAE,iBAAiB,CAAC,YAAY,CAAC,CAAC;CAC9C;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,eAAe,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,CACJ,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,WAAW,GAAG,SAAS,EAC/B,SAAS,EAAE,MAAM,KACd,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAAC;IACrC,KAAK,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAClD;AAED,qBAAa,eAAgB,SAAQ,KAAK;IACxC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;gBAEb,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;CAI7C;AAKD,eAAO,MAAM,iBAAiB,GAC5B,YAAY,UAAU,EACtB,aAAa,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,EACvC,QAAQ,eAAe,KACtB,OAAO,CAAC,gBAAgB,CAiF1B,CAAC"}
@@ -0,0 +1,82 @@
1
+ import { runGetStaticPaths, runGetStaticProps } from "@tyndall/core";
2
+ import { paramsKey } from "@tyndall/shared";
3
+ export class StaticDataError extends Error {
4
+ constructor(routeId, message) {
5
+ super(message);
6
+ this.routeId = routeId;
7
+ }
8
+ }
9
+ const isDynamicRoute = (route) => route.segments.some((segment) => segment.type === "dynamic" || segment.type === "catchAll");
10
+ export const collectStaticData = async (routeGraph, pageModules, cache) => {
11
+ const entries = [];
12
+ const orderedRoutes = [...routeGraph.routes].sort((a, b) => a.id.localeCompare(b.id));
13
+ for (const route of orderedRoutes) {
14
+ const page = pageModules[route.id];
15
+ if (!page) {
16
+ continue;
17
+ }
18
+ if (isDynamicRoute(route)) {
19
+ if (!page.getStaticPaths) {
20
+ throw new StaticDataError(route.id, `Missing getStaticPaths for dynamic route: ${route.id}`);
21
+ }
22
+ const paths = await runGetStaticPaths(page.getStaticPaths);
23
+ for (const path of paths.paths) {
24
+ const params = path.params;
25
+ const key = paramsKey(params);
26
+ if (cache) {
27
+ const cached = await cache.read(route.id, params, key);
28
+ if (cached) {
29
+ entries.push(cached);
30
+ continue;
31
+ }
32
+ }
33
+ const result = page.getStaticProps
34
+ ? await runGetStaticProps(page.getStaticProps, { routeId: route.id, params })
35
+ : { props: {} };
36
+ const entry = {
37
+ routeId: route.id,
38
+ params,
39
+ paramsKey: key,
40
+ props: result.props ?? {},
41
+ revalidate: result.revalidate,
42
+ };
43
+ if (cache) {
44
+ await cache.write(entry);
45
+ }
46
+ entries.push(entry);
47
+ }
48
+ continue;
49
+ }
50
+ const key = paramsKey(undefined);
51
+ if (cache) {
52
+ const cached = await cache.read(route.id, undefined, key);
53
+ if (cached) {
54
+ entries.push(cached);
55
+ continue;
56
+ }
57
+ }
58
+ const result = page.getStaticProps
59
+ ? await runGetStaticProps(page.getStaticProps, { routeId: route.id })
60
+ : { props: {} };
61
+ const entry = {
62
+ routeId: route.id,
63
+ params: undefined,
64
+ paramsKey: key,
65
+ props: result.props ?? {},
66
+ revalidate: result.revalidate,
67
+ };
68
+ if (cache) {
69
+ await cache.write(entry);
70
+ }
71
+ entries.push(entry);
72
+ }
73
+ // Important: keep deterministic ordering for stable build outputs.
74
+ entries.sort((a, b) => {
75
+ const routeCompare = a.routeId.localeCompare(b.routeId);
76
+ if (routeCompare !== 0) {
77
+ return routeCompare;
78
+ }
79
+ return a.paramsKey.localeCompare(b.paramsKey);
80
+ });
81
+ return { entries };
82
+ };
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@tyndall/build",
3
+ "version": "0.0.1",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "type": "module",
8
+ "main": "dist/index.js",
9
+ "types": "dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "bun": "./src/index.ts",
14
+ "default": "./dist/index.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "scripts": {
21
+ "build": "tsc -p tsconfig.json"
22
+ },
23
+ "dependencies": {
24
+ "@swc/core": "^1.15.17",
25
+ "@tyndall/core": "workspace:*",
26
+ "@tyndall/shared": "workspace:*",
27
+ "esbuild": "0.21.5"
28
+ }
29
+ }