authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2025-06-11 00:17:58-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2025-10-14 02:40:44-07:00
log896eb7a6b0005ea9214fc7f59ee0a5ac5e5d5a6d
tree07077ed1f53173b9ca9122f69ba9fbf83f1598e0
parent69a5d0ab42a5b739a835fea2fe2735e93eba30fd
signature Commit is signed but in an unrecognized format.

fine grained incremental rebuilding


7 files changed, 288 insertions(+), 117 deletions(-)

framework/bundle.ts+11-11
...@@ -65,7 +65,7 @@ export async function bundleClientJavaScript(...@@ -65,7 +65,7 @@ export async function bundleClientJavaScript(
65 route = "/js/" + key + ".js";65 route = "/js/" + key + ".js";
66 incr.put({66 incr.put({
67 sources,67 sources,
68 type: "script",68 kind: "script",
69 key,69 key,
70 value: text,70 value: text,
71 });71 });
...@@ -91,16 +91,6 @@ export async function bundleServerJavaScript(...@@ -91,16 +91,6 @@ export async function bundleServerJavaScript(
91 platform: ServerPlatform = "node",91 platform: ServerPlatform = "node",
92) {92) {
93 const scriptMagic = "CLOVER_CLIENT_SCRIPTS_DEFINITION";93 const scriptMagic = "CLOVER_CLIENT_SCRIPTS_DEFINITION";
94 const viewModules = viewEntryPoints.map((view) => {
95 const module = require(view.file);
96 if (!module.meta) {
97 throw new Error(`${view.file} is missing 'export const meta'`);
98 }
99 if (!module.default) {
100 throw new Error(`${view.file} is missing a default export.`);
101 }
102 return { module, view };
103 });
104 const viewSource = [94 const viewSource = [
105 ...viewEntryPoints.map((view, i) =>95 ...viewEntryPoints.map((view, i) =>
106 `import * as view${i} from ${JSON.stringify(view.file)}`96 `import * as view${i} from ${JSON.stringify(view.file)}`
...@@ -171,6 +161,16 @@ export async function bundleServerJavaScript(...@@ -171,6 +161,16 @@ export async function bundleServerJavaScript(
171 write: false,161 write: false,
172 metafile: true,162 metafile: true,
173 });163 });
164 const viewModules = viewEntryPoints.map((view) => {
165 const module = require(view.file);
166 if (!module.meta) {
167 throw new Error(`${view.file} is missing 'export const meta'`);
168 }
169 if (!module.default) {
170 throw new Error(`${view.file} is missing a default export.`);
171 }
172 return { module, view };
173 });
174 const viewData = viewModules.map(({ module, view }) => {174 const viewData = viewModules.map(({ module, view }) => {
175 return {175 return {
176 id: view.id,176 id: view.id,
framework/css.ts+18
...@@ -5,6 +5,12 @@ export interface Theme {...@@ -5,6 +5,12 @@ export interface Theme {
5 h1?: string;5 h1?: string;
6}6}
77
8export const defaultTheme: Theme = {
9 bg: "#ffffff",
10 fg: "#050505",
11 primary: "#2e7dab",
12};
13
8export function stringifyTheme(theme: Theme) {14export function stringifyTheme(theme: Theme) {
9 return [15 return [
10 ":root {",16 ":root {",
...@@ -39,6 +45,18 @@ export interface Output {...@@ -39,6 +45,18 @@ export interface Output {
39 sources: string[];45 sources: string[];
40}46}
4147
48export function styleKey(
49 cssImports: string[],
50 theme: Theme,
51) {
52 cssImports = cssImports
53 .map((file) =>
54 path.isAbsolute(file) ? path.relative(hot.projectRoot, file) : file
55 )
56 .sort();
57 return cssImports.join(":") + JSON.stringify(theme);
58}
59
42export async function bundleCssFiles(60export async function bundleCssFiles(
43 cssImports: string[],61 cssImports: string[],
44 theme: Theme,62 theme: Theme,
framework/definitions.d.ts+1-1
...@@ -1,4 +1,4 @@...@@ -1,4 +1,4 @@
1declare function UNWRAP<T>(value: T | null | undefined): T; 1declare function UNWRAP<T>(value: T | null | undefined, ...log: unknown[]): T;
2declare function ASSERT(value: unknown, ...log: unknown[]): asserts value; 2declare function ASSERT(value: unknown, ...log: unknown[]): asserts value;
3 3
4type Timer = ReturnType<typeof setTimeout>; 4type Timer = ReturnType<typeof setTimeout>;
framework/generate.ts+187-81
...@@ -1,11 +1,16 @@...@@ -1,11 +1,16 @@
1// This file contains the main site generator build process.
2// By using `Incremental`'s ability to automatically purge stale
3// assets, the `sitegen` function performs partial rebuilds.
4
1export function main(incremental?: Incremental) {5export function main(incremental?: Incremental) {
2 return withSpinner<Record<string, unknown>, any>({6 return withSpinner<Record<string, unknown>, any>({
3 text: "Recovering State",7 text: "Recovering State",
4 successText,8 successText,
5 failureText: () => "sitegen FAIL",9 failureText: () => "sitegen FAIL",
6 }, async (spinner) => {10 }, async (spinner) => {
7 const incr = Incremental.fromDisk();11 // const incr = Incremental.fromDisk();
8 await incr.statAllFiles();12 // await incr.statAllFiles();
13 const incr = new Incremental();
9 const result = await sitegen(spinner, incr);14 const result = await sitegen(spinner, incr);
10 incr.toDisk(); // Allows picking up this state again15 incr.toDisk(); // Allows picking up this state again
11 return result;16 return result;
...@@ -94,26 +99,42 @@ export async function sitegen(...@@ -94,26 +99,42 @@ export async function sitegen(
94 scripts = scripts.filter(({ file }) => !file.match(/\.client\.[tj]sx?/));99 scripts = scripts.filter(({ file }) => !file.match(/\.client\.[tj]sx?/));
95 const globalCssPath = join("global.css");100 const globalCssPath = join("global.css");
96101
97 // TODO: invalidate incremental resources102 // TODO: make sure that `static` and `pages` does not overlap
98103
99 // -- server side render --104 // -- inline style sheets, used and shared by pages and views --
100 status.text = "Building";105 status.text = "Building";
101 const cssOnce = new OnceMap<css.Output>();106 const cssOnce = new OnceMap();
102 const cssQueue = new Queue<[string, string[], css.Theme], css.Output>({107 const cssQueue = new Queue({
103 name: "Bundle",108 name: "Bundle",
104 fn: ([, files, theme]) => css.bundleCssFiles(files, theme),109 async fn([, key, files, theme]: [string, string, string[], css.Theme]) {
110 const { text, sources } = await css.bundleCssFiles(files, theme);
111 incr.put({
112 kind: "style",
113 key,
114 sources,
115 value: text,
116 });
117 },
105 passive: true,118 passive: true,
106 getItemText: ([id]) => id,119 getItemText: ([id]) => id,
107 maxJobs: 2,120 maxJobs: 2,
108 });121 });
109 interface RenderResult {122 function ensureCssGetsBuilt(
110 body: string;123 cssImports: string[],
111 head: string;124 theme: css.Theme,
112 css: css.Output;125 referrer: string,
113 clientRefs: string[];126 ) {
114 item: FileItem;127 const key = css.styleKey(cssImports, theme);
128 cssOnce.get(
129 key,
130 async () => {
131 incr.getArtifact("style", key) ??
132 await cssQueue.add([referrer, key, cssImports, theme]);
133 },
134 );
115 }135 }
116 const renderResults: RenderResult[] = [];136
137 // -- server side render pages --
117 async function loadPageModule({ file }: FileItem) {138 async function loadPageModule({ file }: FileItem) {
118 require(file);139 require(file);
119 }140 }
...@@ -125,28 +146,27 @@ export async function sitegen(...@@ -125,28 +146,27 @@ export async function sitegen(
125 theme: pageTheme,146 theme: pageTheme,
126 layout,147 layout,
127 } = require(item.file);148 } = require(item.file);
128 if (!Page) throw new Error("Page is missing a 'default' export.");149 if (!Page) {
150 throw new Error("Page is missing a 'default' export.");
151 }
129 if (!metadata) {152 if (!metadata) {
130 throw new Error("Page is missing 'meta' export with a title.");153 throw new Error("Page is missing 'meta' export with a title.");
131 }154 }
155
156 // -- css --
132 if (layout?.theme) pageTheme = layout.theme;157 if (layout?.theme) pageTheme = layout.theme;
133 const theme = {158 const theme: css.Theme = {
134 bg: "#fff",159 ...css.defaultTheme,
135 fg: "#050505",
136 primary: "#2e7dab",
137 ...pageTheme,160 ...pageTheme,
138 };161 };
162 const cssImports = [globalCssPath, ...hot.getCssImports(item.file)];
163 ensureCssGetsBuilt(cssImports, theme, item.id);
139164
140 // -- metadata --165 // -- metadata --
141 const renderedMetaPromise = Promise.resolve(166 const renderedMetaPromise = Promise.resolve(
142 typeof metadata === "function" ? metadata({ ssr: true }) : metadata,167 typeof metadata === "function" ? metadata({ ssr: true }) : metadata,
143 ).then((m) => meta.renderMeta(m));168 ).then((m) => meta.renderMeta(m));
144 // -- css --169
145 const cssImports = [globalCssPath, ...hot.getCssImports(item.file)];
146 const cssPromise = cssOnce.get(
147 cssImports.join(":") + JSON.stringify(theme),
148 () => cssQueue.add([item.id, cssImports, theme]),
149 );
150 // -- html --170 // -- html --
151 let page = [engine.kElement, Page, {}];171 let page = [engine.kElement, Page, {}];
152 if (layout?.default) {172 if (layout?.default) {
...@@ -156,9 +176,8 @@ export async function sitegen(...@@ -156,9 +176,8 @@ export async function sitegen(
156 sitegen: sg.initRender(),176 sitegen: sg.initRender(),
157 });177 });
158178
159 const [{ text, addon }, cssBundle, renderedMeta] = await Promise.all([179 const [{ text, addon }, renderedMeta] = await Promise.all([
160 bodyPromise,180 bodyPromise,
161 cssPromise,
162 renderedMetaPromise,181 renderedMetaPromise,
163 ]);182 ]);
164 if (!renderedMeta.includes("<title>")) {183 if (!renderedMeta.includes("<title>")) {
...@@ -167,20 +186,72 @@ export async function sitegen(...@@ -167,20 +186,72 @@ export async function sitegen(
167 "All pages need a title tag.",186 "All pages need a title tag.",
168 );187 );
169 }188 }
170 // The script content is not ready, allow another page to Render. The page189 incr.put({
171 // contents will be rebuilt at the end. This is more convenient anyways190 kind: "pageMetadata",
172 // because it means client scripts don't re-render the page.191 key: item.id,
173 renderResults.push({192 // Incremental integrates with `hot.ts` + `require`
174 body: text,193 // to trace all the needed source files here.
175 head: renderedMeta,194 sources: [item.file],
176 css: cssBundle,195 value: {
177 clientRefs: Array.from(addon.sitegen.scripts),196 html: text,
178 item: item,197 meta: renderedMeta,
198 cssImports,
199 theme: theme ?? null,
200 clientRefs: Array.from(addon.sitegen.scripts),
201 },
202 });
203 }
204 async function prepareView(view: FileItem) {
205 const module = require(view.file);
206 if (!module.meta) {
207 throw new Error(`${view.file} is missing 'export const meta'`);
208 }
209 if (!module.default) {
210 throw new Error(`${view.file} is missing a default export.`);
211 }
212 const pageTheme = module.layout?.theme ?? module.theme;
213 const theme: css.Theme = {
214 ...css.defaultTheme,
215 ...pageTheme,
216 };
217 const cssImports = hot.getCssImports(view.file)
218 .concat("src/global.css")
219 .map((file) => path.relative(hot.projectRoot, path.resolve(file)));
220 incr.put({
221 kind: "viewMetadata",
222 key: view.id,
223 sources: [view.file],
224 value: {
225 file: path.relative(hot.projectRoot, view.file),
226 cssImports,
227 theme,
228 clientRefs: hot.getClientScriptRefs(view.file),
229 hasLayout: !!module.layout?.default,
230 },
179 });231 });
180 }232 }
233
234 // Of the pages that are already built, a call to 'ensureCssGetsBuilt' is
235 // required so that it's (1) re-built if needed, (2) not pruned from build.
236 const neededPages = pages.filter((page) => {
237 const existing = incr.getArtifact("pageMetadata", page.id);
238 if (existing) {
239 const { cssImports, theme } = existing;
240 ensureCssGetsBuilt(cssImports, theme, page.id);
241 }
242 return !existing;
243 });
244 const neededViews = views.filter((view) => {
245 const existing = incr.getArtifact("viewMetadata", view.id);
246 if (existing) {
247 const { cssImports, theme } = existing;
248 ensureCssGetsBuilt(cssImports, theme, view.id);
249 }
250 return !existing;
251 });
252
181 // This is done in two passes so that a page that throws during evaluation253 // This is done in two passes so that a page that throws during evaluation
182 // will report "Load Render Module" instead of "Render Static Page".254 // will report "Load Render Module" instead of "Render Static Page".
183 const neededPages = pages.filter((page) => incr.needsBuild("asset", page.id));
184 const spinnerFormat = status.format;255 const spinnerFormat = status.format;
185 status.format = () => "";256 status.format = () => "";
186 const moduleLoadQueue = new Queue({257 const moduleLoadQueue = new Queue({
...@@ -190,6 +261,7 @@ export async function sitegen(...@@ -190,6 +261,7 @@ export async function sitegen(
190 maxJobs: 1,261 maxJobs: 1,
191 });262 });
192 moduleLoadQueue.addMany(neededPages);263 moduleLoadQueue.addMany(neededPages);
264 moduleLoadQueue.addMany(neededViews);
193 await moduleLoadQueue.done({ method: "stop" });265 await moduleLoadQueue.done({ method: "stop" });
194 const pageQueue = new Queue({266 const pageQueue = new Queue({
195 name: "Render Static Page",267 name: "Render Static Page",
...@@ -198,32 +270,52 @@ export async function sitegen(...@@ -198,32 +270,52 @@ export async function sitegen(
198 maxJobs: 2,270 maxJobs: 2,
199 });271 });
200 pageQueue.addMany(neededPages);272 pageQueue.addMany(neededPages);
273 const viewQueue = new Queue({
274 name: "Build Dynamic View",
275 fn: prepareView,
276 getItemText,
277 maxJobs: 2,
278 });
279 viewQueue.addMany(neededViews);
201 await pageQueue.done({ method: "stop" });280 await pageQueue.done({ method: "stop" });
281 await viewQueue.done({ method: "stop" });
202 status.format = spinnerFormat;282 status.format = spinnerFormat;
203283
204 // -- bundle backend and views --284 // -- bundle backend and views --
205 status.text = "Bundle backend code";285 // status.text = "Bundle backend code";
206 const backend = await bundle.bundleServerJavaScript(286 // const backend = await bundle.bundleServerJavaScript(
207 join("backend.ts"),287 // join("backend.ts"),
208 views,288 // views,
209 );289 // );
210 const viewCssPromise = await Promise.all(290 // const viewCssPromise = await Promise.all(
211 backend.views.map((view) =>291 // backend.views.map((view) =>
212 cssOnce.get(292 // cssOnce.get(
213 view.cssImports.join(":") + JSON.stringify(view.theme),293 // view.cssImports.join(":") + JSON.stringify(view.theme),
214 () => cssQueue.add([view.id, view.cssImports, view.theme ?? {}]),294 // () => cssQueue.add([view.id, view.cssImports, view.theme ?? {}]),
215 )295 // )
216 ),296 // ),
217 );297 // );
218298
219 // -- bundle scripts --299 // -- bundle scripts --
220 const referencedScripts = Array.from(300 const referencedScripts = Array.from(
221 new Set([301 new Set(
222 ...renderResults.flatMap((r) => r.clientRefs),302 [
223 ...backend.views.flatMap((r) => r.clientRefs),303 ...pages.map((item) =>
224 ]),304 UNWRAP(
305 incr.getArtifact("pageMetadata", item.id),
306 `Missing pageMetadata ${item.id}`,
307 )
308 ),
309 ...views.map((item) =>
310 UNWRAP(
311 incr.getArtifact("viewMetadata", item.id),
312 `Missing viewMetadata ${item.id}`,
313 )
314 ),
315 ].flatMap((item) => item.clientRefs),
316 ),
225 (script) => path.resolve(hot.projectSrc, script),317 (script) => path.resolve(hot.projectSrc, script),
226 );318 ).filter((file) => !incr.hasArtifact("script", hot.getScriptId(file)));
227 const extraPublicScripts = scripts.map((entry) => entry.file);319 const extraPublicScripts = scripts.map((entry) => entry.file);
228 const uniqueCount = new Set([320 const uniqueCount = new Set([
229 ...referencedScripts,321 ...referencedScripts,
...@@ -237,7 +329,7 @@ export async function sitegen(...@@ -237,7 +329,7 @@ export async function sitegen(
237 );329 );
238330
239 // -- finalize backend bundle --331 // -- finalize backend bundle --
240 await bundle.finalizeServerJavaScript(backend, await viewCssPromise, incr);332 // await bundle.finalizeServerJavaScript(backend, await viewCssPromise, incr);
241333
242 // -- copy/compress static files --334 // -- copy/compress static files --
243 async function doStaticFile(item: FileItem) {335 async function doStaticFile(item: FileItem) {
...@@ -256,37 +348,51 @@ export async function sitegen(...@@ -256,37 +348,51 @@ export async function sitegen(
256 });348 });
257 status.format = () => "";349 status.format = () => "";
258 staticQueue.addMany(350 staticQueue.addMany(
259 staticFiles.filter((file) => incr.needsBuild("asset", file.id)),351 staticFiles.filter((file) => !incr.hasArtifact("asset", file.id)),
260 );352 );
261 await staticQueue.done({ method: "stop" });353 await staticQueue.done({ method: "stop" });
262 status.format = spinnerFormat;354 status.format = spinnerFormat;
263355
356 await cssQueue.done({ method: "stop" });
357
264 // -- concatenate static rendered pages --358 // -- concatenate static rendered pages --
265 status.text = `Concat ${renderResults.length} Pages`;359 status.text = `Concat Pages`;
266 await Promise.all(360 await Promise.all(pages.map(async (page) => {
267 renderResults.map(361 if (incr.hasArtifact("asset", page.id)) return;
268 async (362 const {
269 { item: page, body, head, css, clientRefs: scriptFiles },363 html,
270 ) => {364 meta,
271 const doc = wrapDocument({365 cssImports,
272 body,366 theme,
273 head,367 clientRefs,
274 inlineCss: css.text,368 } = UNWRAP(incr.out.pageMetadata.get(page.id));
275 scripts: scriptFiles.map(369 const scriptIds = clientRefs.map(hot.getScriptId);
276 (file) => UNWRAP(incr.out.script.get(hot.getScriptId(file))),370 const styleKey = css.styleKey(cssImports, theme);
277 ).map((x) => `{${x}}`).join("\n"),371 const style = UNWRAP(
278 });372 incr.out.style.get(styleKey),
279 await incr.putAsset({373 `Missing style ${styleKey}`,
280 sources: [page.file, ...css.sources],374 );
281 key: page.id,375 const doc = wrapDocument({
282 body: doc,376 body: html,
283 headers: {377 head: meta,
284 "Content-Type": "text/html",378 inlineCss: style,
285 },379 scripts: scriptIds.map(
286 });380 (ref) => UNWRAP(incr.out.script.get(ref), `Missing script ${ref}`),
381 ).map((x) => `{${x}}`).join("\n"),
382 });
383 await incr.putAsset({
384 sources: [
385 page.file,
386 ...incr.sourcesFor("style", styleKey),
387 ...scriptIds.flatMap((ref) => incr.sourcesFor("script", ref)),
388 ],
389 key: page.id,
390 body: doc,
391 headers: {
392 "Content-Type": "text/html",
287 },393 },
288 ),394 });
289 );395 }));
290 status.format = () => "";396 status.format = () => "";
291 status.text = ``;397 status.text = ``;
292 // This will wait for all compression jobs to finish, which up398 // This will wait for all compression jobs to finish, which up
framework/incremental.ts+68-19
...@@ -1,27 +1,63 @@...@@ -1,27 +1,63 @@
1// `Incremental` contains multiple maps for the different parts of a site1// Incremental contains multiple maps for the different kinds
2// build, and tracks reused items across builds. It also handles emitting and2// of Artifact, which contain a list of source files which
3// updating the built site. This structure is self contained and serializable.3// were used to produce it. When files change, Incremental sees
4// that the `mtime` is newer, and purges the referenced artifacts.
45
5type SourceId = string; // relative to project root, e.g. 'src/global.css'6type SourceId = string; // relative to project root, e.g. 'src/global.css'
6type ArtifactId = string; // `${ArtifactType}#${string}`7type ArtifactId = string; // `${ArtifactType}\0${string}`
7type Sha1Id = string; // Sha1 hex string8type Sha1Id = string; // Sha1 hex string
89
10// -- artifact types --
9interface ArtifactMap {11interface ArtifactMap {
12 /* An asset (serve with "#sitegen/asset" */
10 asset: Asset;13 asset: Asset;
14 /* The bundled text of a '.client.ts' script */
15 // TODO: track imports this has into `asset`
11 script: string;16 script: string;
17 /* The bundled style tag contents. Keyed by 'css.styleKey' */
18 style: string;
19 /* Metadata about a static page */
20 pageMetadata: PageMetadata;
21 /* Metadata about a dynamic view */
22 viewMetadata: ViewMetadata;
12}23}
13type ArtifactType = keyof ArtifactMap;24type ArtifactKind = keyof ArtifactMap;
14interface Asset {25export interface Asset {
15 buffer: Buffer;26 buffer: Buffer;
16 headers: Record<string, string | undefined>;27 headers: Record<string, string | undefined>;
17 hash: string;28 hash: string;
18}29}
30/**
31 * This interface intentionally omits the *contents*
32 * of its scripts and styles for fine-grained rebuilds.
33 */
34export interface PageMetadata {
35 html: string;
36 meta: string;
37 cssImports: string[];
38 theme: css.Theme;
39 clientRefs: string[];
40}
41/**
42 * Like a page, this intentionally omits resources,
43 * but additionally omits the bundled server code.
44 */
45export interface ViewMetadata {
46 file: string;
47 // staticMeta: string | null; TODO
48 cssImports: string[];
49 theme: css.Theme;
50 clientRefs: string[];
51 hasLayout: boolean;
52}
53
54// -- incremental support types --
19export interface PutBase {55export interface PutBase {
20 sources: SourceId[];56 sources: SourceId[];
21 key: string;57 key: string;
22}58}
23export interface Put<T extends ArtifactType> extends PutBase {59export interface Put<T extends ArtifactKind> extends PutBase {
24 type: T;60 kind: T;
25 value: ArtifactMap[T];61 value: ArtifactMap[T];
26}62}
27export interface Invalidations {63export interface Invalidations {
...@@ -37,6 +73,9 @@ export class Incremental {...@@ -37,6 +73,9 @@ export class Incremental {
37 } = {73 } = {
38 asset: new Map(),74 asset: new Map(),
39 script: new Map(),75 script: new Map(),
76 style: new Map(),
77 pageMetadata: new Map(),
78 viewMetadata: new Map(),
40 };79 };
41 /** Tracking filesystem entries to `srcId` */80 /** Tracking filesystem entries to `srcId` */
42 invals = new Map<SourceId, Invalidations>();81 invals = new Map<SourceId, Invalidations>();
...@@ -53,9 +92,16 @@ export class Incremental {...@@ -53,9 +92,16 @@ export class Incremental {
53 getItemText: (job) => `${job.algo.toUpperCase()} ${job.label}`,92 getItemText: (job) => `${job.algo.toUpperCase()} ${job.label}`,
54 });93 });
5594
56 /** Invalidation deletes build artifacts so the check is trivial. */95 getArtifact<T extends ArtifactKind>(kind: T, key: string) {
57 needsBuild(type: ArtifactType, key: string) {96 return this.out[kind].get(key);
58 return !this.out[type].has(key);97 }
98
99 hasArtifact<T extends ArtifactKind>(kind: T, key: string) {
100 return this.out[kind].has(key);
101 }
102
103 sourcesFor(kind: ArtifactKind, key: string) {
104 return UNWRAP(this.sources.get(kind + "\0" + key));
59 }105 }
60106
61 /*107 /*
...@@ -63,18 +109,19 @@ export class Incremental {...@@ -63,18 +109,19 @@ export class Incremental {
63 * used to build this must be provided. 'Incremental' will trace JS109 * used to build this must be provided. 'Incremental' will trace JS
64 * imports and file modification times tracked by 'hot.ts'.110 * imports and file modification times tracked by 'hot.ts'.
65 */111 */
66 put<T extends ArtifactType>({112 put<T extends ArtifactKind>({
67 sources,113 sources,
68 type,114 kind,
69 key,115 key,
70 value,116 value,
71 }: Put<T>) {117 }: Put<T>) {
72 this.out[type].set(key, value);118 console.log("put " + kind + ": " + key);
119 this.out[kind].set(key, value);
73120
74 // Update sources information121 // Update sources information
75 ASSERT(sources.length > 0, "Missing sources for " + type + " " + key);122 ASSERT(sources.length > 0, "Missing sources for " + kind + " " + key);
76 sources = sources.map((src) => path.normalize(src));123 sources = sources.map((src) => path.normalize(src));
77 const fullKey = `${type}#${key}`;124 const fullKey = `${kind}\0${key}`;
78 const prevSources = this.sources.get(fullKey);125 const prevSources = this.sources.get(fullKey);
79 const newSources = new Set(126 const newSources = new Set(
80 sources.map((file) =>127 sources.map((file) =>
...@@ -155,8 +202,9 @@ export class Incremental {...@@ -155,8 +202,9 @@ export class Incremental {
155 );202 );
156 const { files, outputs } = invalidations;203 const { files, outputs } = invalidations;
157 for (const out of outputs) {204 for (const out of outputs) {
158 const [type, artifactKey] = out.split("#", 2);205 const [kind, artifactKey] = out.split("\0");
159 this.out[type as ArtifactType].delete(artifactKey);206 this.out[kind as ArtifactKind].delete(artifactKey);
207 console.log("stale " + kind + ": " + artifactKey);
160 }208 }
161 invalidQueue.push(...files);209 invalidQueue.push(...files);
162 }210 }
...@@ -179,7 +227,7 @@ export class Incremental {...@@ -179,7 +227,7 @@ export class Incremental {
179 },227 },
180 hash,228 hash,
181 };229 };
182 const a = this.put({ ...info, type: "asset", value });230 const a = this.put({ ...info, kind: "asset", value });
183 if (!this.compress.has(hash)) {231 if (!this.compress.has(hash)) {
184 const label = info.key;232 const label = info.key;
185 this.compress.set(hash, {233 this.compress.set(hash, {
...@@ -412,3 +460,4 @@ import * as hot from "./hot.ts";...@@ -412,3 +460,4 @@ import * as hot from "./hot.ts";
412import * as mime from "#sitegen/mime";460import * as mime from "#sitegen/mime";
413import * as path from "node:path";461import * as path from "node:path";
414import { Buffer } from "node:buffer";462import { Buffer } from "node:buffer";
463import * as css from "./css.ts";
framework/lib/view.ts+1-3
...@@ -42,10 +42,8 @@ export async function renderView(...@@ -42,10 +42,8 @@ export async function renderView(
42 const { text: body, addon: { sitegen } } = await engine.ssrAsync(page, { 42 const { text: body, addon: { sitegen } } = await engine.ssrAsync(page, {
43 sitegen: sg.initRender(), 43 sitegen: sg.initRender(),
44 }); 44 });
45 console.log(sitegen);
46 45
47 // -- join document and send -- 46 // -- join document and send --
48 console.log(scripts);
49 return c.html(wrapDocument({ 47 return c.html(wrapDocument({
50 body, 48 body,
51 head: await renderedMetaPromise, 49 head: await renderedMetaPromise,
...@@ -56,7 +54,7 @@ export async function renderView(...@@ -56,7 +54,7 @@ export async function renderView(
56 })); 54 }));
57} 55}
58 56
59export function provideViews(v: typeof views, s: typeof scripts) { 57export function provideViewData(v: typeof views, s: typeof scripts) {
60 views = v; 58 views = v;
61 scripts = s; 59 scripts = s;
62} 60}
framework/watch.ts+2-2
...@@ -25,7 +25,7 @@ export async function main() {...@@ -25,7 +25,7 @@ export async function main() {
25 successText: generate.successText, 25 successText: generate.successText,
26 failureText: () => "sitegen FAIL", 26 failureText: () => "sitegen FAIL",
27 }, async (spinner) => { 27 }, async (spinner) => {
28 console.clear(); 28 console.log("---");
29 console.log( 29 console.log(
30 "Updated" + 30 "Updated" +
31 (changed.length === 1 31 (changed.length === 1
...@@ -36,7 +36,7 @@ export async function main() {...@@ -36,7 +36,7 @@ export async function main() {
36 incr.toDisk(); // Allows picking up this state again 36 incr.toDisk(); // Allows picking up this state again
37 for (const file of watch.files) { 37 for (const file of watch.files) {
38 const relative = path.relative(hot.projectRoot, file); 38 const relative = path.relative(hot.projectRoot, file);
39 if (!incr.invals.has(file)) watch.remove(file); 39 if (!incr.invals.has(relative)) watch.remove(file);
40 } 40 }
41 return result; 41 return result;
42 }).catch((err) => { 42 }).catch((err) => {