fine grained incremental rebuilding
This commit is contained in:
parent
15a4600c48
commit
d5ef829f01
7 changed files with 288 additions and 117 deletions
|
@ -65,7 +65,7 @@ export async function bundleClientJavaScript(
|
||||||
route = "/js/" + key + ".js";
|
route = "/js/" + key + ".js";
|
||||||
incr.put({
|
incr.put({
|
||||||
sources,
|
sources,
|
||||||
type: "script",
|
kind: "script",
|
||||||
key,
|
key,
|
||||||
value: text,
|
value: text,
|
||||||
});
|
});
|
||||||
|
@ -91,16 +91,6 @@ export async function bundleServerJavaScript(
|
||||||
platform: ServerPlatform = "node",
|
platform: ServerPlatform = "node",
|
||||||
) {
|
) {
|
||||||
const scriptMagic = "CLOVER_CLIENT_SCRIPTS_DEFINITION";
|
const scriptMagic = "CLOVER_CLIENT_SCRIPTS_DEFINITION";
|
||||||
const viewModules = viewEntryPoints.map((view) => {
|
|
||||||
const module = require(view.file);
|
|
||||||
if (!module.meta) {
|
|
||||||
throw new Error(`${view.file} is missing 'export const meta'`);
|
|
||||||
}
|
|
||||||
if (!module.default) {
|
|
||||||
throw new Error(`${view.file} is missing a default export.`);
|
|
||||||
}
|
|
||||||
return { module, view };
|
|
||||||
});
|
|
||||||
const viewSource = [
|
const viewSource = [
|
||||||
...viewEntryPoints.map((view, i) =>
|
...viewEntryPoints.map((view, i) =>
|
||||||
`import * as view${i} from ${JSON.stringify(view.file)}`
|
`import * as view${i} from ${JSON.stringify(view.file)}`
|
||||||
|
@ -171,6 +161,16 @@ export async function bundleServerJavaScript(
|
||||||
write: false,
|
write: false,
|
||||||
metafile: true,
|
metafile: true,
|
||||||
});
|
});
|
||||||
|
const viewModules = viewEntryPoints.map((view) => {
|
||||||
|
const module = require(view.file);
|
||||||
|
if (!module.meta) {
|
||||||
|
throw new Error(`${view.file} is missing 'export const meta'`);
|
||||||
|
}
|
||||||
|
if (!module.default) {
|
||||||
|
throw new Error(`${view.file} is missing a default export.`);
|
||||||
|
}
|
||||||
|
return { module, view };
|
||||||
|
});
|
||||||
const viewData = viewModules.map(({ module, view }) => {
|
const viewData = viewModules.map(({ module, view }) => {
|
||||||
return {
|
return {
|
||||||
id: view.id,
|
id: view.id,
|
||||||
|
|
|
@ -5,6 +5,12 @@ export interface Theme {
|
||||||
h1?: string;
|
h1?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const defaultTheme: Theme = {
|
||||||
|
bg: "#ffffff",
|
||||||
|
fg: "#050505",
|
||||||
|
primary: "#2e7dab",
|
||||||
|
};
|
||||||
|
|
||||||
export function stringifyTheme(theme: Theme) {
|
export function stringifyTheme(theme: Theme) {
|
||||||
return [
|
return [
|
||||||
":root {",
|
":root {",
|
||||||
|
@ -39,6 +45,18 @@ export interface Output {
|
||||||
sources: string[];
|
sources: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function styleKey(
|
||||||
|
cssImports: string[],
|
||||||
|
theme: Theme,
|
||||||
|
) {
|
||||||
|
cssImports = cssImports
|
||||||
|
.map((file) =>
|
||||||
|
path.isAbsolute(file) ? path.relative(hot.projectRoot, file) : file
|
||||||
|
)
|
||||||
|
.sort();
|
||||||
|
return cssImports.join(":") + JSON.stringify(theme);
|
||||||
|
}
|
||||||
|
|
||||||
export async function bundleCssFiles(
|
export async function bundleCssFiles(
|
||||||
cssImports: string[],
|
cssImports: string[],
|
||||||
theme: Theme,
|
theme: Theme,
|
||||||
|
|
2
framework/definitions.d.ts
vendored
2
framework/definitions.d.ts
vendored
|
@ -1,4 +1,4 @@
|
||||||
declare function UNWRAP<T>(value: T | null | undefined): T;
|
declare function UNWRAP<T>(value: T | null | undefined, ...log: unknown[]): T;
|
||||||
declare function ASSERT(value: unknown, ...log: unknown[]): asserts value;
|
declare function ASSERT(value: unknown, ...log: unknown[]): asserts value;
|
||||||
|
|
||||||
type Timer = ReturnType<typeof setTimeout>;
|
type Timer = ReturnType<typeof setTimeout>;
|
||||||
|
|
|
@ -1,11 +1,16 @@
|
||||||
|
// This file contains the main site generator build process.
|
||||||
|
// By using `Incremental`'s ability to automatically purge stale
|
||||||
|
// assets, the `sitegen` function performs partial rebuilds.
|
||||||
|
|
||||||
export function main(incremental?: Incremental) {
|
export function main(incremental?: Incremental) {
|
||||||
return withSpinner<Record<string, unknown>, any>({
|
return withSpinner<Record<string, unknown>, any>({
|
||||||
text: "Recovering State",
|
text: "Recovering State",
|
||||||
successText,
|
successText,
|
||||||
failureText: () => "sitegen FAIL",
|
failureText: () => "sitegen FAIL",
|
||||||
}, async (spinner) => {
|
}, async (spinner) => {
|
||||||
const incr = Incremental.fromDisk();
|
// const incr = Incremental.fromDisk();
|
||||||
await incr.statAllFiles();
|
// await incr.statAllFiles();
|
||||||
|
const incr = new Incremental();
|
||||||
const result = await sitegen(spinner, incr);
|
const result = await sitegen(spinner, incr);
|
||||||
incr.toDisk(); // Allows picking up this state again
|
incr.toDisk(); // Allows picking up this state again
|
||||||
return result;
|
return result;
|
||||||
|
@ -94,26 +99,42 @@ export async function sitegen(
|
||||||
scripts = scripts.filter(({ file }) => !file.match(/\.client\.[tj]sx?/));
|
scripts = scripts.filter(({ file }) => !file.match(/\.client\.[tj]sx?/));
|
||||||
const globalCssPath = join("global.css");
|
const globalCssPath = join("global.css");
|
||||||
|
|
||||||
// TODO: invalidate incremental resources
|
// TODO: make sure that `static` and `pages` does not overlap
|
||||||
|
|
||||||
// -- server side render --
|
// -- inline style sheets, used and shared by pages and views --
|
||||||
status.text = "Building";
|
status.text = "Building";
|
||||||
const cssOnce = new OnceMap<css.Output>();
|
const cssOnce = new OnceMap();
|
||||||
const cssQueue = new Queue<[string, string[], css.Theme], css.Output>({
|
const cssQueue = new Queue({
|
||||||
name: "Bundle",
|
name: "Bundle",
|
||||||
fn: ([, files, theme]) => css.bundleCssFiles(files, theme),
|
async fn([, key, files, theme]: [string, string, string[], css.Theme]) {
|
||||||
|
const { text, sources } = await css.bundleCssFiles(files, theme);
|
||||||
|
incr.put({
|
||||||
|
kind: "style",
|
||||||
|
key,
|
||||||
|
sources,
|
||||||
|
value: text,
|
||||||
|
});
|
||||||
|
},
|
||||||
passive: true,
|
passive: true,
|
||||||
getItemText: ([id]) => id,
|
getItemText: ([id]) => id,
|
||||||
maxJobs: 2,
|
maxJobs: 2,
|
||||||
});
|
});
|
||||||
interface RenderResult {
|
function ensureCssGetsBuilt(
|
||||||
body: string;
|
cssImports: string[],
|
||||||
head: string;
|
theme: css.Theme,
|
||||||
css: css.Output;
|
referrer: string,
|
||||||
clientRefs: string[];
|
) {
|
||||||
item: FileItem;
|
const key = css.styleKey(cssImports, theme);
|
||||||
|
cssOnce.get(
|
||||||
|
key,
|
||||||
|
async () => {
|
||||||
|
incr.getArtifact("style", key) ??
|
||||||
|
await cssQueue.add([referrer, key, cssImports, theme]);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const renderResults: RenderResult[] = [];
|
|
||||||
|
// -- server side render pages --
|
||||||
async function loadPageModule({ file }: FileItem) {
|
async function loadPageModule({ file }: FileItem) {
|
||||||
require(file);
|
require(file);
|
||||||
}
|
}
|
||||||
|
@ -125,28 +146,27 @@ export async function sitegen(
|
||||||
theme: pageTheme,
|
theme: pageTheme,
|
||||||
layout,
|
layout,
|
||||||
} = require(item.file);
|
} = require(item.file);
|
||||||
if (!Page) throw new Error("Page is missing a 'default' export.");
|
if (!Page) {
|
||||||
|
throw new Error("Page is missing a 'default' export.");
|
||||||
|
}
|
||||||
if (!metadata) {
|
if (!metadata) {
|
||||||
throw new Error("Page is missing 'meta' export with a title.");
|
throw new Error("Page is missing 'meta' export with a title.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -- css --
|
||||||
if (layout?.theme) pageTheme = layout.theme;
|
if (layout?.theme) pageTheme = layout.theme;
|
||||||
const theme = {
|
const theme: css.Theme = {
|
||||||
bg: "#fff",
|
...css.defaultTheme,
|
||||||
fg: "#050505",
|
|
||||||
primary: "#2e7dab",
|
|
||||||
...pageTheme,
|
...pageTheme,
|
||||||
};
|
};
|
||||||
|
const cssImports = [globalCssPath, ...hot.getCssImports(item.file)];
|
||||||
|
ensureCssGetsBuilt(cssImports, theme, item.id);
|
||||||
|
|
||||||
// -- metadata --
|
// -- metadata --
|
||||||
const renderedMetaPromise = Promise.resolve(
|
const renderedMetaPromise = Promise.resolve(
|
||||||
typeof metadata === "function" ? metadata({ ssr: true }) : metadata,
|
typeof metadata === "function" ? metadata({ ssr: true }) : metadata,
|
||||||
).then((m) => meta.renderMeta(m));
|
).then((m) => meta.renderMeta(m));
|
||||||
// -- css --
|
|
||||||
const cssImports = [globalCssPath, ...hot.getCssImports(item.file)];
|
|
||||||
const cssPromise = cssOnce.get(
|
|
||||||
cssImports.join(":") + JSON.stringify(theme),
|
|
||||||
() => cssQueue.add([item.id, cssImports, theme]),
|
|
||||||
);
|
|
||||||
// -- html --
|
// -- html --
|
||||||
let page = [engine.kElement, Page, {}];
|
let page = [engine.kElement, Page, {}];
|
||||||
if (layout?.default) {
|
if (layout?.default) {
|
||||||
|
@ -156,9 +176,8 @@ export async function sitegen(
|
||||||
sitegen: sg.initRender(),
|
sitegen: sg.initRender(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const [{ text, addon }, cssBundle, renderedMeta] = await Promise.all([
|
const [{ text, addon }, renderedMeta] = await Promise.all([
|
||||||
bodyPromise,
|
bodyPromise,
|
||||||
cssPromise,
|
|
||||||
renderedMetaPromise,
|
renderedMetaPromise,
|
||||||
]);
|
]);
|
||||||
if (!renderedMeta.includes("<title>")) {
|
if (!renderedMeta.includes("<title>")) {
|
||||||
|
@ -167,20 +186,72 @@ export async function sitegen(
|
||||||
"All pages need a title tag.",
|
"All pages need a title tag.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// The script content is not ready, allow another page to Render. The page
|
incr.put({
|
||||||
// contents will be rebuilt at the end. This is more convenient anyways
|
kind: "pageMetadata",
|
||||||
// because it means client scripts don't re-render the page.
|
key: item.id,
|
||||||
renderResults.push({
|
// Incremental integrates with `hot.ts` + `require`
|
||||||
body: text,
|
// to trace all the needed source files here.
|
||||||
head: renderedMeta,
|
sources: [item.file],
|
||||||
css: cssBundle,
|
value: {
|
||||||
|
html: text,
|
||||||
|
meta: renderedMeta,
|
||||||
|
cssImports,
|
||||||
|
theme: theme ?? null,
|
||||||
clientRefs: Array.from(addon.sitegen.scripts),
|
clientRefs: Array.from(addon.sitegen.scripts),
|
||||||
item: item,
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
async function prepareView(view: FileItem) {
|
||||||
|
const module = require(view.file);
|
||||||
|
if (!module.meta) {
|
||||||
|
throw new Error(`${view.file} is missing 'export const meta'`);
|
||||||
|
}
|
||||||
|
if (!module.default) {
|
||||||
|
throw new Error(`${view.file} is missing a default export.`);
|
||||||
|
}
|
||||||
|
const pageTheme = module.layout?.theme ?? module.theme;
|
||||||
|
const theme: css.Theme = {
|
||||||
|
...css.defaultTheme,
|
||||||
|
...pageTheme,
|
||||||
|
};
|
||||||
|
const cssImports = hot.getCssImports(view.file)
|
||||||
|
.concat("src/global.css")
|
||||||
|
.map((file) => path.relative(hot.projectRoot, path.resolve(file)));
|
||||||
|
incr.put({
|
||||||
|
kind: "viewMetadata",
|
||||||
|
key: view.id,
|
||||||
|
sources: [view.file],
|
||||||
|
value: {
|
||||||
|
file: path.relative(hot.projectRoot, view.file),
|
||||||
|
cssImports,
|
||||||
|
theme,
|
||||||
|
clientRefs: hot.getClientScriptRefs(view.file),
|
||||||
|
hasLayout: !!module.layout?.default,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Of the pages that are already built, a call to 'ensureCssGetsBuilt' is
|
||||||
|
// required so that it's (1) re-built if needed, (2) not pruned from build.
|
||||||
|
const neededPages = pages.filter((page) => {
|
||||||
|
const existing = incr.getArtifact("pageMetadata", page.id);
|
||||||
|
if (existing) {
|
||||||
|
const { cssImports, theme } = existing;
|
||||||
|
ensureCssGetsBuilt(cssImports, theme, page.id);
|
||||||
|
}
|
||||||
|
return !existing;
|
||||||
|
});
|
||||||
|
const neededViews = views.filter((view) => {
|
||||||
|
const existing = incr.getArtifact("viewMetadata", view.id);
|
||||||
|
if (existing) {
|
||||||
|
const { cssImports, theme } = existing;
|
||||||
|
ensureCssGetsBuilt(cssImports, theme, view.id);
|
||||||
|
}
|
||||||
|
return !existing;
|
||||||
|
});
|
||||||
|
|
||||||
// This is done in two passes so that a page that throws during evaluation
|
// This is done in two passes so that a page that throws during evaluation
|
||||||
// will report "Load Render Module" instead of "Render Static Page".
|
// will report "Load Render Module" instead of "Render Static Page".
|
||||||
const neededPages = pages.filter((page) => incr.needsBuild("asset", page.id));
|
|
||||||
const spinnerFormat = status.format;
|
const spinnerFormat = status.format;
|
||||||
status.format = () => "";
|
status.format = () => "";
|
||||||
const moduleLoadQueue = new Queue({
|
const moduleLoadQueue = new Queue({
|
||||||
|
@ -190,6 +261,7 @@ export async function sitegen(
|
||||||
maxJobs: 1,
|
maxJobs: 1,
|
||||||
});
|
});
|
||||||
moduleLoadQueue.addMany(neededPages);
|
moduleLoadQueue.addMany(neededPages);
|
||||||
|
moduleLoadQueue.addMany(neededViews);
|
||||||
await moduleLoadQueue.done({ method: "stop" });
|
await moduleLoadQueue.done({ method: "stop" });
|
||||||
const pageQueue = new Queue({
|
const pageQueue = new Queue({
|
||||||
name: "Render Static Page",
|
name: "Render Static Page",
|
||||||
|
@ -198,32 +270,52 @@ export async function sitegen(
|
||||||
maxJobs: 2,
|
maxJobs: 2,
|
||||||
});
|
});
|
||||||
pageQueue.addMany(neededPages);
|
pageQueue.addMany(neededPages);
|
||||||
|
const viewQueue = new Queue({
|
||||||
|
name: "Build Dynamic View",
|
||||||
|
fn: prepareView,
|
||||||
|
getItemText,
|
||||||
|
maxJobs: 2,
|
||||||
|
});
|
||||||
|
viewQueue.addMany(neededViews);
|
||||||
await pageQueue.done({ method: "stop" });
|
await pageQueue.done({ method: "stop" });
|
||||||
|
await viewQueue.done({ method: "stop" });
|
||||||
status.format = spinnerFormat;
|
status.format = spinnerFormat;
|
||||||
|
|
||||||
// -- bundle backend and views --
|
// -- bundle backend and views --
|
||||||
status.text = "Bundle backend code";
|
// status.text = "Bundle backend code";
|
||||||
const backend = await bundle.bundleServerJavaScript(
|
// const backend = await bundle.bundleServerJavaScript(
|
||||||
join("backend.ts"),
|
// join("backend.ts"),
|
||||||
views,
|
// views,
|
||||||
);
|
// );
|
||||||
const viewCssPromise = await Promise.all(
|
// const viewCssPromise = await Promise.all(
|
||||||
backend.views.map((view) =>
|
// backend.views.map((view) =>
|
||||||
cssOnce.get(
|
// cssOnce.get(
|
||||||
view.cssImports.join(":") + JSON.stringify(view.theme),
|
// view.cssImports.join(":") + JSON.stringify(view.theme),
|
||||||
() => cssQueue.add([view.id, view.cssImports, view.theme ?? {}]),
|
// () => cssQueue.add([view.id, view.cssImports, view.theme ?? {}]),
|
||||||
)
|
// )
|
||||||
),
|
// ),
|
||||||
);
|
// );
|
||||||
|
|
||||||
// -- bundle scripts --
|
// -- bundle scripts --
|
||||||
const referencedScripts = Array.from(
|
const referencedScripts = Array.from(
|
||||||
new Set([
|
new Set(
|
||||||
...renderResults.flatMap((r) => r.clientRefs),
|
[
|
||||||
...backend.views.flatMap((r) => r.clientRefs),
|
...pages.map((item) =>
|
||||||
]),
|
UNWRAP(
|
||||||
|
incr.getArtifact("pageMetadata", item.id),
|
||||||
|
`Missing pageMetadata ${item.id}`,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
...views.map((item) =>
|
||||||
|
UNWRAP(
|
||||||
|
incr.getArtifact("viewMetadata", item.id),
|
||||||
|
`Missing viewMetadata ${item.id}`,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
].flatMap((item) => item.clientRefs),
|
||||||
|
),
|
||||||
(script) => path.resolve(hot.projectSrc, script),
|
(script) => path.resolve(hot.projectSrc, script),
|
||||||
);
|
).filter((file) => !incr.hasArtifact("script", hot.getScriptId(file)));
|
||||||
const extraPublicScripts = scripts.map((entry) => entry.file);
|
const extraPublicScripts = scripts.map((entry) => entry.file);
|
||||||
const uniqueCount = new Set([
|
const uniqueCount = new Set([
|
||||||
...referencedScripts,
|
...referencedScripts,
|
||||||
|
@ -237,7 +329,7 @@ export async function sitegen(
|
||||||
);
|
);
|
||||||
|
|
||||||
// -- finalize backend bundle --
|
// -- finalize backend bundle --
|
||||||
await bundle.finalizeServerJavaScript(backend, await viewCssPromise, incr);
|
// await bundle.finalizeServerJavaScript(backend, await viewCssPromise, incr);
|
||||||
|
|
||||||
// -- copy/compress static files --
|
// -- copy/compress static files --
|
||||||
async function doStaticFile(item: FileItem) {
|
async function doStaticFile(item: FileItem) {
|
||||||
|
@ -256,37 +348,51 @@ export async function sitegen(
|
||||||
});
|
});
|
||||||
status.format = () => "";
|
status.format = () => "";
|
||||||
staticQueue.addMany(
|
staticQueue.addMany(
|
||||||
staticFiles.filter((file) => incr.needsBuild("asset", file.id)),
|
staticFiles.filter((file) => !incr.hasArtifact("asset", file.id)),
|
||||||
);
|
);
|
||||||
await staticQueue.done({ method: "stop" });
|
await staticQueue.done({ method: "stop" });
|
||||||
status.format = spinnerFormat;
|
status.format = spinnerFormat;
|
||||||
|
|
||||||
|
await cssQueue.done({ method: "stop" });
|
||||||
|
|
||||||
// -- concatenate static rendered pages --
|
// -- concatenate static rendered pages --
|
||||||
status.text = `Concat ${renderResults.length} Pages`;
|
status.text = `Concat Pages`;
|
||||||
await Promise.all(
|
await Promise.all(pages.map(async (page) => {
|
||||||
renderResults.map(
|
if (incr.hasArtifact("asset", page.id)) return;
|
||||||
async (
|
const {
|
||||||
{ item: page, body, head, css, clientRefs: scriptFiles },
|
html,
|
||||||
) => {
|
meta,
|
||||||
|
cssImports,
|
||||||
|
theme,
|
||||||
|
clientRefs,
|
||||||
|
} = UNWRAP(incr.out.pageMetadata.get(page.id));
|
||||||
|
const scriptIds = clientRefs.map(hot.getScriptId);
|
||||||
|
const styleKey = css.styleKey(cssImports, theme);
|
||||||
|
const style = UNWRAP(
|
||||||
|
incr.out.style.get(styleKey),
|
||||||
|
`Missing style ${styleKey}`,
|
||||||
|
);
|
||||||
const doc = wrapDocument({
|
const doc = wrapDocument({
|
||||||
body,
|
body: html,
|
||||||
head,
|
head: meta,
|
||||||
inlineCss: css.text,
|
inlineCss: style,
|
||||||
scripts: scriptFiles.map(
|
scripts: scriptIds.map(
|
||||||
(file) => UNWRAP(incr.out.script.get(hot.getScriptId(file))),
|
(ref) => UNWRAP(incr.out.script.get(ref), `Missing script ${ref}`),
|
||||||
).map((x) => `{${x}}`).join("\n"),
|
).map((x) => `{${x}}`).join("\n"),
|
||||||
});
|
});
|
||||||
await incr.putAsset({
|
await incr.putAsset({
|
||||||
sources: [page.file, ...css.sources],
|
sources: [
|
||||||
|
page.file,
|
||||||
|
...incr.sourcesFor("style", styleKey),
|
||||||
|
...scriptIds.flatMap((ref) => incr.sourcesFor("script", ref)),
|
||||||
|
],
|
||||||
key: page.id,
|
key: page.id,
|
||||||
body: doc,
|
body: doc,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "text/html",
|
"Content-Type": "text/html",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
}));
|
||||||
),
|
|
||||||
);
|
|
||||||
status.format = () => "";
|
status.format = () => "";
|
||||||
status.text = ``;
|
status.text = ``;
|
||||||
// This will wait for all compression jobs to finish, which up
|
// This will wait for all compression jobs to finish, which up
|
||||||
|
|
|
@ -1,27 +1,63 @@
|
||||||
// `Incremental` contains multiple maps for the different parts of a site
|
// Incremental contains multiple maps for the different kinds
|
||||||
// build, and tracks reused items across builds. It also handles emitting and
|
// of Artifact, which contain a list of source files which
|
||||||
// updating the built site. This structure is self contained and serializable.
|
// were used to produce it. When files change, Incremental sees
|
||||||
|
// that the `mtime` is newer, and purges the referenced artifacts.
|
||||||
|
|
||||||
type SourceId = string; // relative to project root, e.g. 'src/global.css'
|
type SourceId = string; // relative to project root, e.g. 'src/global.css'
|
||||||
type ArtifactId = string; // `${ArtifactType}#${string}`
|
type ArtifactId = string; // `${ArtifactType}\0${string}`
|
||||||
type Sha1Id = string; // Sha1 hex string
|
type Sha1Id = string; // Sha1 hex string
|
||||||
|
|
||||||
|
// -- artifact types --
|
||||||
interface ArtifactMap {
|
interface ArtifactMap {
|
||||||
|
/* An asset (serve with "#sitegen/asset" */
|
||||||
asset: Asset;
|
asset: Asset;
|
||||||
|
/* The bundled text of a '.client.ts' script */
|
||||||
|
// TODO: track imports this has into `asset`
|
||||||
script: string;
|
script: string;
|
||||||
|
/* The bundled style tag contents. Keyed by 'css.styleKey' */
|
||||||
|
style: string;
|
||||||
|
/* Metadata about a static page */
|
||||||
|
pageMetadata: PageMetadata;
|
||||||
|
/* Metadata about a dynamic view */
|
||||||
|
viewMetadata: ViewMetadata;
|
||||||
}
|
}
|
||||||
type ArtifactType = keyof ArtifactMap;
|
type ArtifactKind = keyof ArtifactMap;
|
||||||
interface Asset {
|
export interface Asset {
|
||||||
buffer: Buffer;
|
buffer: Buffer;
|
||||||
headers: Record<string, string | undefined>;
|
headers: Record<string, string | undefined>;
|
||||||
hash: string;
|
hash: string;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* This interface intentionally omits the *contents*
|
||||||
|
* of its scripts and styles for fine-grained rebuilds.
|
||||||
|
*/
|
||||||
|
export interface PageMetadata {
|
||||||
|
html: string;
|
||||||
|
meta: string;
|
||||||
|
cssImports: string[];
|
||||||
|
theme: css.Theme;
|
||||||
|
clientRefs: string[];
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Like a page, this intentionally omits resources,
|
||||||
|
* but additionally omits the bundled server code.
|
||||||
|
*/
|
||||||
|
export interface ViewMetadata {
|
||||||
|
file: string;
|
||||||
|
// staticMeta: string | null; TODO
|
||||||
|
cssImports: string[];
|
||||||
|
theme: css.Theme;
|
||||||
|
clientRefs: string[];
|
||||||
|
hasLayout: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- incremental support types --
|
||||||
export interface PutBase {
|
export interface PutBase {
|
||||||
sources: SourceId[];
|
sources: SourceId[];
|
||||||
key: string;
|
key: string;
|
||||||
}
|
}
|
||||||
export interface Put<T extends ArtifactType> extends PutBase {
|
export interface Put<T extends ArtifactKind> extends PutBase {
|
||||||
type: T;
|
kind: T;
|
||||||
value: ArtifactMap[T];
|
value: ArtifactMap[T];
|
||||||
}
|
}
|
||||||
export interface Invalidations {
|
export interface Invalidations {
|
||||||
|
@ -37,6 +73,9 @@ export class Incremental {
|
||||||
} = {
|
} = {
|
||||||
asset: new Map(),
|
asset: new Map(),
|
||||||
script: new Map(),
|
script: new Map(),
|
||||||
|
style: new Map(),
|
||||||
|
pageMetadata: new Map(),
|
||||||
|
viewMetadata: new Map(),
|
||||||
};
|
};
|
||||||
/** Tracking filesystem entries to `srcId` */
|
/** Tracking filesystem entries to `srcId` */
|
||||||
invals = new Map<SourceId, Invalidations>();
|
invals = new Map<SourceId, Invalidations>();
|
||||||
|
@ -53,9 +92,16 @@ export class Incremental {
|
||||||
getItemText: (job) => `${job.algo.toUpperCase()} ${job.label}`,
|
getItemText: (job) => `${job.algo.toUpperCase()} ${job.label}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
/** Invalidation deletes build artifacts so the check is trivial. */
|
getArtifact<T extends ArtifactKind>(kind: T, key: string) {
|
||||||
needsBuild(type: ArtifactType, key: string) {
|
return this.out[kind].get(key);
|
||||||
return !this.out[type].has(key);
|
}
|
||||||
|
|
||||||
|
hasArtifact<T extends ArtifactKind>(kind: T, key: string) {
|
||||||
|
return this.out[kind].has(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
sourcesFor(kind: ArtifactKind, key: string) {
|
||||||
|
return UNWRAP(this.sources.get(kind + "\0" + key));
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -63,18 +109,19 @@ export class Incremental {
|
||||||
* used to build this must be provided. 'Incremental' will trace JS
|
* used to build this must be provided. 'Incremental' will trace JS
|
||||||
* imports and file modification times tracked by 'hot.ts'.
|
* imports and file modification times tracked by 'hot.ts'.
|
||||||
*/
|
*/
|
||||||
put<T extends ArtifactType>({
|
put<T extends ArtifactKind>({
|
||||||
sources,
|
sources,
|
||||||
type,
|
kind,
|
||||||
key,
|
key,
|
||||||
value,
|
value,
|
||||||
}: Put<T>) {
|
}: Put<T>) {
|
||||||
this.out[type].set(key, value);
|
console.log("put " + kind + ": " + key);
|
||||||
|
this.out[kind].set(key, value);
|
||||||
|
|
||||||
// Update sources information
|
// Update sources information
|
||||||
ASSERT(sources.length > 0, "Missing sources for " + type + " " + key);
|
ASSERT(sources.length > 0, "Missing sources for " + kind + " " + key);
|
||||||
sources = sources.map((src) => path.normalize(src));
|
sources = sources.map((src) => path.normalize(src));
|
||||||
const fullKey = `${type}#${key}`;
|
const fullKey = `${kind}\0${key}`;
|
||||||
const prevSources = this.sources.get(fullKey);
|
const prevSources = this.sources.get(fullKey);
|
||||||
const newSources = new Set(
|
const newSources = new Set(
|
||||||
sources.map((file) =>
|
sources.map((file) =>
|
||||||
|
@ -155,8 +202,9 @@ export class Incremental {
|
||||||
);
|
);
|
||||||
const { files, outputs } = invalidations;
|
const { files, outputs } = invalidations;
|
||||||
for (const out of outputs) {
|
for (const out of outputs) {
|
||||||
const [type, artifactKey] = out.split("#", 2);
|
const [kind, artifactKey] = out.split("\0");
|
||||||
this.out[type as ArtifactType].delete(artifactKey);
|
this.out[kind as ArtifactKind].delete(artifactKey);
|
||||||
|
console.log("stale " + kind + ": " + artifactKey);
|
||||||
}
|
}
|
||||||
invalidQueue.push(...files);
|
invalidQueue.push(...files);
|
||||||
}
|
}
|
||||||
|
@ -179,7 +227,7 @@ export class Incremental {
|
||||||
},
|
},
|
||||||
hash,
|
hash,
|
||||||
};
|
};
|
||||||
const a = this.put({ ...info, type: "asset", value });
|
const a = this.put({ ...info, kind: "asset", value });
|
||||||
if (!this.compress.has(hash)) {
|
if (!this.compress.has(hash)) {
|
||||||
const label = info.key;
|
const label = info.key;
|
||||||
this.compress.set(hash, {
|
this.compress.set(hash, {
|
||||||
|
@ -412,3 +460,4 @@ import * as hot from "./hot.ts";
|
||||||
import * as mime from "#sitegen/mime";
|
import * as mime from "#sitegen/mime";
|
||||||
import * as path from "node:path";
|
import * as path from "node:path";
|
||||||
import { Buffer } from "node:buffer";
|
import { Buffer } from "node:buffer";
|
||||||
|
import * as css from "./css.ts";
|
||||||
|
|
|
@ -42,10 +42,8 @@ export async function renderView(
|
||||||
const { text: body, addon: { sitegen } } = await engine.ssrAsync(page, {
|
const { text: body, addon: { sitegen } } = await engine.ssrAsync(page, {
|
||||||
sitegen: sg.initRender(),
|
sitegen: sg.initRender(),
|
||||||
});
|
});
|
||||||
console.log(sitegen);
|
|
||||||
|
|
||||||
// -- join document and send --
|
// -- join document and send --
|
||||||
console.log(scripts);
|
|
||||||
return c.html(wrapDocument({
|
return c.html(wrapDocument({
|
||||||
body,
|
body,
|
||||||
head: await renderedMetaPromise,
|
head: await renderedMetaPromise,
|
||||||
|
@ -56,7 +54,7 @@ export async function renderView(
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function provideViews(v: typeof views, s: typeof scripts) {
|
export function provideViewData(v: typeof views, s: typeof scripts) {
|
||||||
views = v;
|
views = v;
|
||||||
scripts = s;
|
scripts = s;
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,7 @@ export async function main() {
|
||||||
successText: generate.successText,
|
successText: generate.successText,
|
||||||
failureText: () => "sitegen FAIL",
|
failureText: () => "sitegen FAIL",
|
||||||
}, async (spinner) => {
|
}, async (spinner) => {
|
||||||
console.clear();
|
console.log("---");
|
||||||
console.log(
|
console.log(
|
||||||
"Updated" +
|
"Updated" +
|
||||||
(changed.length === 1
|
(changed.length === 1
|
||||||
|
@ -36,7 +36,7 @@ export async function main() {
|
||||||
incr.toDisk(); // Allows picking up this state again
|
incr.toDisk(); // Allows picking up this state again
|
||||||
for (const file of watch.files) {
|
for (const file of watch.files) {
|
||||||
const relative = path.relative(hot.projectRoot, file);
|
const relative = path.relative(hot.projectRoot, file);
|
||||||
if (!incr.invals.has(file)) watch.remove(file);
|
if (!incr.invals.has(relative)) watch.remove(file);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
|
|
Loading…
Reference in a new issue