Files
heygen-com__hyperframes/scripts/theme-gate.ts
2026-07-15 22:26:56 -04:00

100 lines
3.4 KiB
TypeScript

import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
import { join, resolve } from "node:path";
import { parseCoverageMap } from "./build-qa-gallery";
const THEME_NAMES = ["neutral", "bold", "editorial"] as const;
type ThemeName = (typeof THEME_NAMES)[number];
interface Theme {
name: ThemeName;
css: string;
}
interface StagedTriple {
primitive: string;
neutral: string;
bold: string;
editorial: string;
}
export function injectTheme(html: string, theme: Theme): string {
const closingHead = html.search(/<\/head\s*>/i);
if (closingHead === -1) throw new Error("QA page has no closing head tag");
// Demos declare contract tokens on element scope (#root { --bg: ... }),
// which beats a plain :root injection for every descendant. Re-target the
// theme block at both scopes and mark each declaration !important so the
// injected theme actually wins, emulating a host that supplies the theme.
const forced = theme.css
.trim()
.replace(/^:root\s*\{/, ":root, #root, [data-composition-id] {")
.replace(/;(\s*\n)/g, " !important;$1");
const style = ` <style data-hyperframes-theme="${theme.name}">\n${forced}\n </style>\n`;
return `${html.slice(0, closingHead)}${style}${html.slice(closingHead)}`;
}
async function main(): Promise<void> {
const repoRoot = resolve(import.meta.dir, "..");
const coveragePath = resolve(repoRoot, "../../../..", "candidates-registry-coverage.md");
const outputDir = join(repoRoot, "docs/public/theme-gate");
const candidates = parseCoverageMap(await readFile(coveragePath, "utf8"));
// Explicit names win (bun scripts/theme-gate.ts name1 name2 ...); default: all BUILT.
const requested = process.argv.slice(2);
const primitiveNames =
requested.length > 0
? requested
: [
...new Set(
candidates
.filter((candidate) => candidate.status === "BUILT")
.map((candidate) => candidate.candidate),
),
];
if (primitiveNames.length === 0) throw new Error("coverage map contains no BUILT primitives");
const themes: Theme[] = await Promise.all(
THEME_NAMES.map(async (name) => ({
name,
css: await readFile(join(repoRoot, "themes", `${name}.css`), "utf8"),
})),
);
const pages = await Promise.all(
primitiveNames.map(async (primitive) => ({
primitive,
html: await readFile(join(repoRoot, "docs/public/qa", `${primitive}.html`), "utf8"),
})),
);
// Full runs rebuild from scratch; explicit-name runs only refresh their own
// files (wiping here 404s every other staged page still referenced by
// compare boards).
if (requested.length === 0) await rm(outputDir, { force: true, recursive: true });
await mkdir(outputDir, { recursive: true });
const index: StagedTriple[] = [];
for (const page of pages) {
const triple: StagedTriple = {
primitive: page.primitive,
neutral: `${page.primitive}.neutral.html`,
bold: `${page.primitive}.bold.html`,
editorial: `${page.primitive}.editorial.html`,
};
for (const theme of themes) {
await writeFile(join(outputDir, triple[theme.name]), injectTheme(page.html, theme));
}
index.push(triple);
}
await writeFile(join(outputDir, "index.json"), `${JSON.stringify(index, null, 2)}\n`);
console.log(
`Staged ${index.length * themes.length} theme variants for ${index.length} BUILT primitives.`,
);
}
if (import.meta.main) {
await main();
}