Implement repository-driven theme rendering

This commit is contained in:
root
2026-07-23 17:49:10 -07:00
parent 68d76a59df
commit 884d182c86
32 changed files with 3040 additions and 1417 deletions
+177 -164
View File
@@ -1,181 +1,194 @@
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import { BuildEngine, renderMarkdown } from "./buildEngine";
import { BuildEngineInput } from "./buildEngine";
import { ContentItem, SiteConfig, ThemeConfig } from "../types";
import { BuildInputLoader } from "./buildInputLoader";
import { BuildEngine, BUILDER_VERSION, renderMarkdown } from "./buildEngine";
const siteConfig: SiteConfig = {
protocol: "labyricorn-site/v1",
site: {
id: "test-site",
title: "Test Site",
baseUrl: "https://example.test",
language: "en-US",
},
navigation: [
{ id: "home", label: "Home", route: "/", iconName: "Home", contentModel: "page" },
{ id: "articles", label: "Articles", route: "/articles/", iconName: "FileText", contentModel: "article" },
],
contentModels: [],
sourcesFile: "./sources.yml",
styleInstancesPath: "./styles",
pagesPath: "./pages",
navigationFile: "./navigation.yml",
pushIntegrationsPath: "./push",
theme: { source: "site-definition", path: "/.theme" },
markdown: {
dialect: "commonmark",
rawHtmlPolicy: "disabled",
rawHtmlEnabled: false,
extensions: {
tables: true,
taskLists: true,
footnotes: true,
definitionLists: true,
headingAnchors: true,
fencedCode: true,
syntaxHighlighting: true,
callouts: true,
youtube: true,
wikipediaLinks: true,
},
},
hosting: {
engine: "nginx",
production: { enabled: true, hostname: "example.test", listen: 80 },
staging: { enabled: true, hostname: "preview.example.test", listen: 8080 },
releases: { retainCount: 5 },
},
buildPolicy: {
staging: { enabled: true, requireApproval: true },
localActivation: { automatic: false },
push: { automatic: false },
},
rawYaml: "",
const git = (root: string, args: string[]): string => execFileSync("git", ["-C", root, ...args], {
encoding: "utf8",
env: { ...process.env, GIT_AUTHOR_DATE: "2026-01-01T00:00:00Z", GIT_COMMITTER_DATE: "2026-01-01T00:00:00Z" },
}).trim();
const createRepository = (): string => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "labyricorn-git-fixture-"));
fs.mkdirSync(path.join(root, "packages"), { recursive: true });
fs.cpSync(path.resolve("packages/site-definition"), path.join(root, "packages/site-definition"), { recursive: true });
fs.cpSync(path.resolve("packages/content-models"), path.join(root, "packages/content-models"), { recursive: true });
git(root, ["init", "-b", "main"]);
git(root, ["config", "user.name", "Fixture"]);
git(root, ["config", "user.email", "[email protected]"]);
git(root, ["add", "."]);
git(root, ["commit", "-m", "fixture"]);
return root;
};
const themeConfig: ThemeConfig = {
id: "test-theme",
name: "Test Theme",
version: "1.0.0",
path: "/.theme",
templates: {},
styles: [],
scripts: [],
supportsPackages: [],
isValidated: true,
validationErrors: [],
const commit = (root: string, message: string): string => {
git(root, ["add", "-A"]);
git(root, ["commit", "-m", message]);
return git(root, ["rev-parse", "HEAD"]);
};
const content = (overrides: Partial<ContentItem> = {}): ContentItem => ({
id: "article-one",
title: "Article One",
slug: "article-one",
published: "2026-01-01T00:00:00.000Z",
status: "published",
artifactType: "article",
summary: "A deterministic article.",
tags: ["test"],
aliases: [],
sourceRepo: "content",
path: "articles/article-one.md",
contentMarkdown: "# Hello\n\nThis is **safe** markdown.",
mediaReferences: [],
youtubeDirectives: [],
wikipediaLinks: [],
validationStatus: "valid",
validationMessages: [],
route: "/articles/article-one/",
styleInstanceId: "default",
...overrides,
});
const load = (root: string, runId: string) => {
const manager = new BuildInputLoader({ repositoryRoot: root, ref: "HEAD", siteDefinitionPath: "packages/site-definition", builderVersion: BUILDER_VERSION });
return { manager, input: manager.load(runId) };
};
const input = (buildId: string, items: ContentItem[]): BuildEngineInput => ({
buildId,
siteConfig,
contentItems: items,
mediaAssets: [],
sourceCommits: { content: "abc123" },
themeConfig,
});
const withRepository = (callback: (root: string, output: string) => void): void => {
const root = createRepository();
const output = fs.mkdtempSync(path.join(os.tmpdir(), "labyricorn-output-"));
try { callback(root, output); }
finally { fs.rmSync(root, { recursive: true, force: true }); fs.rmSync(output, { recursive: true, force: true }); }
};
test("build output is deterministic for equivalent inputs", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "labyricorn-engine-test-"));
test("reference repository theme renders declared routes and only declared assets", () => withRepository((root, output) => {
const { manager, input } = load(root, "build-000001");
try {
const engine = new BuildEngine(root);
const firstItem = content();
const secondItem = content({
id: "article-two",
title: "Article Two",
slug: "article-two",
path: "articles/article-two.md",
route: "/articles/article-two/",
});
const first = engine.build(input("build-one", [firstItem, secondItem]));
const second = engine.build(input("build-two", [secondItem, firstItem]));
assert.equal(first.success, true);
assert.equal(second.success, true);
assert.equal(first.artifactChecksum, second.artifactChecksum);
assert.deepEqual(first.generatedFiles, second.generatedFiles);
assert.ok(fs.existsSync(path.join(first.outputDirectory, "build-manifest.json")));
assert.ok(fs.existsSync(path.join(first.outputDirectory, "checksums.json")));
assert.match(
fs.readFileSync(path.join(first.outputDirectory, "articles/article-one/index.html"), "utf8"),
/<h1>Hello<\/h1>/,
);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});
test("route collisions fail validation without writing a release", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "labyricorn-engine-test-"));
try {
const engine = new BuildEngine(root);
const result = engine.build(
input("collision", [content(), content({ id: "article-two", route: "/articles/article-one/" })]),
);
assert.equal(result.success, false);
assert.equal(result.validationReport.summary.passed, false);
assert.deepEqual(result.validationReport.routeCollisions, ["/articles/article-one/"]);
assert.equal(fs.existsSync(result.outputDirectory), false);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});
test("raw HTML is rejected when the site policy disables it", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "labyricorn-engine-test-"));
try {
const engine = new BuildEngine(root);
const result = engine.build(input("raw-html", [content({ contentMarkdown: "<script>alert(1)</script>" })]));
assert.equal(result.success, false);
assert.equal(result.validationReport.htmlPolicyViolations.length, 1);
assert.equal(fs.existsSync(result.outputDirectory), false);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});
test("raw HTML inside fenced code remains a valid code example", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "labyricorn-engine-test-"));
try {
const engine = new BuildEngine(root);
const result = engine.build(
input("html-code-example", [content({ contentMarkdown: "```html\n<div>example</div>\n```" })]),
);
const result = new BuildEngine(output).build(input);
assert.equal(result.success, true);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
assert.match(input.siteDefinition.commit, /^[0-9a-f]{40}$/);
assert.ok(fs.existsSync(path.join(result.outputDirectory, "index.html")));
assert.ok(fs.existsSync(path.join(result.outputDirectory, "projects/index.html")));
assert.ok(fs.existsSync(path.join(result.outputDirectory, "project/website-engine-control-plane/index.html")));
assert.ok(fs.existsSync(path.join(result.outputDirectory, "project/website-engine-control-plane/demo.html")));
assert.ok(fs.existsSync(path.join(result.outputDirectory, "404.html")));
assert.ok(fs.existsSync(path.join(result.outputDirectory, "assets/styles/theme.css")));
assert.ok(fs.existsSync(path.join(result.outputDirectory, "assets/scripts/theme.js")));
assert.ok(fs.existsSync(path.join(result.outputDirectory, "assets/images/logo.svg")));
assert.equal(fs.existsSync(path.join(result.outputDirectory, ".theme/theme.yml")), false);
const manifest = JSON.parse(fs.readFileSync(path.join(result.outputDirectory, "build-manifest.json"), "utf8"));
assert.equal(manifest.siteDefinition.commit, input.siteDefinition.commit);
assert.equal(manifest.theme.commit, input.theme.snapshot.commit);
assert.equal(manifest.artifactBuildId, input.artifactBuildId);
assert.doesNotMatch(fs.readFileSync(path.join(result.outputDirectory, "index.html"), "utf8"), /fonts\.googleapis|tailwindcss\.com/);
} finally { manager.dispose(); }
}));
test("equivalent resolved inputs are byte-identical despite different run IDs", () => withRepository((root, output) => {
const first = load(root, "build-000001");
const second = load(root, "build-999999");
try {
assert.equal(first.input.artifactBuildId, second.input.artifactBuildId);
const engine = new BuildEngine(output);
const one = engine.build(first.input);
const two = engine.build(second.input);
assert.equal(one.artifactChecksum, two.artifactChecksum);
assert.equal(one.outputDirectory, two.outputDirectory);
assert.deepEqual(one.generatedFiles, two.generatedFiles);
assert.equal(fs.readFileSync(path.join(one.outputDirectory, "checksums.json"), "utf8"), fs.readFileSync(path.join(two.outputDirectory, "checksums.json"), "utf8"));
} finally { first.manager.dispose(); second.manager.dispose(); }
}));
test("an already resolved snapshot is unchanged when the branch advances", () => withRepository((root, output) => {
const pinned = load(root, "build-pinned");
const css = path.join(root, "packages/site-definition/.theme/assets/styles/theme.css");
fs.appendFileSync(css, "\n.branch-advanced{display:block}\n");
const advancedCommit = commit(root, "advance theme css");
const advanced = load(root, "build-advanced");
try {
assert.notEqual(pinned.input.siteDefinition.commit, advancedCommit);
assert.equal(advanced.input.siteDefinition.commit, advancedCommit);
const engine = new BuildEngine(output);
const pinnedResult = engine.build(pinned.input);
const advancedResult = engine.build(advanced.input);
assert.notEqual(pinned.input.artifactBuildId, advanced.input.artifactBuildId);
assert.notEqual(pinnedResult.artifactChecksum, advancedResult.artifactChecksum);
assert.doesNotMatch(fs.readFileSync(path.join(pinnedResult.outputDirectory, "assets/styles/theme.css"), "utf8"), /branch-advanced/);
} finally { pinned.manager.dispose(); advanced.manager.dispose(); }
}));
test("a missing theme fails before any release directory exists", () => withRepository((root, output) => {
fs.rmSync(path.join(root, "packages/site-definition/.theme"), { recursive: true, force: true });
commit(root, "remove theme");
const manager = new BuildInputLoader({ repositoryRoot: root, builderVersion: BUILDER_VERSION });
try {
assert.throws(() => manager.load("missing-theme"), /E_THEME_ROOT_INVALID/);
assert.equal(fs.existsSync(path.join(output, "releases")), false);
} finally { manager.dispose(); }
}));
test("unknown manifest keys and escaping paths fail closed", () => withRepository((root) => {
const manifest = path.join(root, "packages/site-definition/.theme/theme.yml");
fs.appendFileSync(manifest, "unknownKey: rejected\n");
commit(root, "invalid manifest key");
let manager = new BuildInputLoader({ repositoryRoot: root, builderVersion: BUILDER_VERSION });
assert.throws(() => manager.load("invalid-key"), /E_THEME_MANIFEST_INVALID/);
manager.dispose();
git(root, ["reset", "--hard", "HEAD~1"]);
fs.appendFileSync(manifest, "\n");
const source = fs.readFileSync(manifest, "utf8").replace("templates/layout.liquid", "../outside.liquid");
fs.writeFileSync(manifest, source);
commit(root, "escaping template path");
manager = new BuildInputLoader({ repositoryRoot: root, builderVersion: BUILDER_VERSION });
assert.throws(() => manager.load("escape"), /E_THEME_MANIFEST_INVALID|E_THEME_PATH_ESCAPE/);
manager.dispose();
}));
test("strict Liquid rejects unknown variables without publishing a release", () => withRepository((root, output) => {
const template = path.join(root, "packages/site-definition/.theme/templates/home.liquid");
fs.appendFileSync(template, "\n{{ process.env.SECRET }}\n");
commit(root, "unsafe variable");
const { manager, input } = load(root, "strict-variable");
try {
const engine = new BuildEngine(output);
assert.throws(() => engine.build(input), /undefined variable|not defined|process/i);
assert.equal(fs.existsSync(engine.buildDirectory(input.artifactBuildId)), false);
} finally { manager.dispose(); }
}));
test("project declaration controls the project route and standalone publication", () => withRepository((root, output) => {
fs.writeFileSync(path.join(root, "packages/site-definition/projects.yml"), "projects: []\n");
commit(root, "remove project declaration");
const { manager, input } = load(root, "no-project");
try {
const result = new BuildEngine(output).build(input);
assert.equal(result.success, true);
assert.equal(input.projects.length, 0);
assert.equal(fs.existsSync(path.join(result.outputDirectory, "project/website-engine-control-plane/index.html")), false);
assert.equal(fs.existsSync(path.join(result.outputDirectory, "project/website-engine-control-plane/demo.html")), false);
} finally { manager.dispose(); }
}));
test("declared standalone files are copied byte-for-byte", () => withRepository((root, output) => {
const { manager, input } = load(root, "publication");
try {
const result = new BuildEngine(output).build(input);
const source = fs.readFileSync(path.join(root, "packages/site-definition/demo-project/demo.html"));
const published = fs.readFileSync(path.join(result.outputDirectory, "project/website-engine-control-plane/demo.html"));
assert.deepEqual(published, source);
const manifest = JSON.parse(fs.readFileSync(path.join(result.outputDirectory, "build-manifest.json"), "utf8"));
assert.equal(manifest.publishedFiles[0].sourceChecksum, `sha256-${input.publishedFiles[0].sourceChecksum}`);
} finally { manager.dispose(); }
}));
test("promotion verifies every artifact byte and rejects tampering", () => withRepository((root, output) => {
const { manager, input } = load(root, "tamper");
try {
const engine = new BuildEngine(output);
const result = engine.build(input);
fs.appendFileSync(path.join(result.outputDirectory, "index.html"), "tampered");
assert.throws(() => engine.activate(input.artifactBuildId), /E_RELEASE_CHECKSUM/);
assert.equal(fs.existsSync(engine.currentDirectory()), false);
} finally { manager.dispose(); }
}));
test("markdown output is escaped and heading IDs are deterministic", () => {
const first = renderMarkdown("# Repeat\n\n## Repeat\n\nText <img src=x> and [safe](/docs/).\n\n```html\n<script>x</script>\n```");
const second = renderMarkdown("# Repeat\n\n## Repeat\n\nText <img src=x> and [safe](/docs/).\n\n```html\n<script>x</script>\n```");
assert.equal(first, second);
assert.match(first, /id="repeat"/);
assert.match(first, /id="repeat-2"/);
assert.match(first, /&lt;img src=x&gt;/);
assert.doesNotMatch(first, /<script>x<\/script>/);
});
test("markdown renderer escapes source HTML", () => {
assert.equal(renderMarkdown("Text <img src=x>"), "<p>Text &lt;img src=x&gt;</p>");
test("production sources contain no embedded renderer or synthesized preview fallback", () => {
assert.equal(fs.readFileSync(path.resolve("src/backend/buildEngine.ts"), "utf8").trim(), 'export * from "./repositoryBuildEngine";');
const server = fs.readFileSync(path.resolve("server.ts"), "utf8");
assert.doesNotMatch(server, /cdn\.tailwindcss\.com|Simple HTML renderer|SITE HEADER/);
assert.match(server, /E_RELEASE_UNAVAILABLE/);
assert.match(server, /E_CONFIG_READ_ONLY/);
});