mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-08-31 02:41:44 +00:00
* fix(catalog): make component previews answer their variables panel Every control on a component's catalog page did nothing. Asking caption-camera-follow for a violet accent rendered gold, and so did green and blue, on 166 of the 168 components that declare variables. A component ships a snippet, which is what the page hands you to paste and which carries the declaration plus the script that turns a chosen value into a CSS custom property, and a demo.html which stages and animates it. The preview is built from the demo, and the demo was authored as a copy of the snippet rather than a reference to it. The copies drifted until almost none of them carried the declaration or the reader, so the payload for that page never contained the word violet at all. Components come in two shapes, so the repair does too. 123 ship a snippet that registers its own paused timeline. That snippet is a whole piece, so their preview is now built from it and carries markup, variables and motion together. 45 are markup plus a commented recipe, where the demo owns the motion. Those demos now carry the snippet's declaration, reader and var-driven CSS in the registry itself, written by scripts/catalog/sync-demo-variables.ts. Nothing is patched in at build time. A test runs that tool in dry mode and fails when a demo has drifted again, naming the command that repairs it. It also asserts it inspected more than a hundred components, because a check that silently matches nothing is how this rotted in the first place. Measured by rendering every payload in a real player rather than by reading markup: payloads declaring their variables go from 2 of 168 to 168 of 168, previews that animate go from 166 to 167, and nothing that moved stopped moving. ascii-render-pass and star-rating-fill render a still frame when built from their snippet, so they keep the demo path as a recorded exception and stay in the state they were already in. * refactor(catalog): give the preview pipeline one lookup and one entrypoint guard Follow-up on the same branch, no behaviour change: 42 tests still pass and `sync-demo-variables --check` still reports all 168 components clean. The payload generator and the demo sync had each grown their own copy of "given a component directory, find the snippet and the demo". Both now call `componentFiles`, which is the same duplication-by-copying that broke the previews in the first place. Both catalog generators also carried a byte-identical 12-line guard for "only run main() when this file is the entrypoint". That clone was already in the tree, but nothing had touched both files at once before, so it had never surfaced. It is now `runAsCommand`, and the sync script's variant of the same condition is `isEntrypoint`. The rest is flattening: the layering guards read as a table of conditions instead of a chain, the reporting splits by what it reports, and the entry resolution comes out of `buildPayload` rather than being spliced into it. Also runs the formatter over the demos this branch rewrote. Whitespace only, and `notes-typing` is the only component demo that renders pre-formatted text, which this does not touch.
116 lines
4.7 KiB
TypeScript
116 lines
4.7 KiB
TypeScript
#!/usr/bin/env tsx
|
|
/**
|
|
* Keep a component's demo carrying the variables its snippet declares.
|
|
*
|
|
* A component ships a snippet, which is what the catalog hands you to paste,
|
|
* and a `demo.html`, which stages it and animates it. The demo was authored as
|
|
* a copy of the snippet rather than as a reference to it, and copies drift:
|
|
* almost every demo had lost the `data-composition-variables` block, the script
|
|
* that turns a chosen value into a CSS custom property, and the CSS written
|
|
* against those properties. The catalog preview is built from the demo, so the
|
|
* variables panel on those pages could not change anything.
|
|
*
|
|
* This writes the missing pieces back into the demo, once, in the registry,
|
|
* rather than patching them in every time a payload is built. `--check` reports
|
|
* drift without writing, which is what CI runs: a demo edited later that drops
|
|
* the declaration fails the build instead of silently producing a dead panel.
|
|
*
|
|
* Only components whose snippet does not own its motion are handled here. The
|
|
* ones that register their own timeline have their preview built from the
|
|
* snippet directly, so their demo is not in that path at all.
|
|
*
|
|
* Usage:
|
|
* npx tsx scripts/catalog/sync-demo-variables.ts # write
|
|
* npx tsx scripts/catalog/sync-demo-variables.ts --check # report only
|
|
*/
|
|
|
|
import { readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
import { dirname, join, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
import { isEntrypoint } from "../entrypoint.ts";
|
|
import { componentFiles } from "./component-files.ts";
|
|
import { layerVariablesOntoDemo, snippetOwnsItsMotion } from "./component-variables.ts";
|
|
|
|
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
|
const repoRoot = resolve(scriptDir, "../..");
|
|
const componentsDir = join(repoRoot, "registry/components");
|
|
|
|
export interface DemoSyncResult {
|
|
name: string;
|
|
status: "synced" | "already" | "not-applicable";
|
|
detail?: string;
|
|
}
|
|
|
|
/** A demo left alone: either it was already right, or it cannot be layered. */
|
|
function untouched(name: string, reason: string): DemoSyncResult {
|
|
const already = reason === "demo already declares its variables";
|
|
return { name, status: already ? "already" : "not-applicable", detail: reason };
|
|
}
|
|
|
|
function syncPair(name: string, demoPath: string, snippet: string, write: boolean): DemoSyncResult {
|
|
if (snippetOwnsItsMotion(snippet)) return untouched(name, "preview uses the snippet");
|
|
|
|
const layered = layerVariablesOntoDemo(readFileSync(demoPath, "utf-8"), snippet);
|
|
if (!layered.applied) return untouched(name, layered.reason);
|
|
|
|
if (write) writeFileSync(demoPath, layered.html, "utf-8");
|
|
return { name, status: "synced" };
|
|
}
|
|
|
|
function syncOne(dir: string, write: boolean): DemoSyncResult | null {
|
|
const files = componentFiles(dir);
|
|
if (!files?.demoPath) return null;
|
|
|
|
const snippet = readFileSync(files.snippetPath, "utf-8");
|
|
return syncPair(files.name, files.demoPath, snippet, write);
|
|
}
|
|
|
|
export function syncDemoVariables(write: boolean): DemoSyncResult[] {
|
|
return readdirSync(componentsDir, { withFileTypes: true })
|
|
.filter((entry) => entry.isDirectory())
|
|
.map((entry) => syncOne(join(componentsDir, entry.name), write))
|
|
.filter((result): result is DemoSyncResult => result !== null);
|
|
}
|
|
|
|
/** Name the components that could not be layered, so a refusal is never silent. */
|
|
function logRefusal(result: DemoSyncResult): void {
|
|
if (result.status === "not-applicable" && result.detail !== "preview uses the snippet") {
|
|
console.log(` · ${result.name}: ${result.detail}`);
|
|
}
|
|
}
|
|
|
|
function reportWritten(results: DemoSyncResult[]): void {
|
|
const synced = results.filter((r) => r.status === "synced").length;
|
|
const already = results.filter((r) => r.status === "already").length;
|
|
console.log(`Synced ${synced} demo(s); ${already} already carried their variables.`);
|
|
}
|
|
|
|
function reportDrift(results: DemoSyncResult[]): void {
|
|
const drifted = results.filter((r) => r.status === "synced");
|
|
if (drifted.length === 0) {
|
|
console.log(
|
|
`Every component demo carries its snippet's variables (${results.length} checked).`,
|
|
);
|
|
return;
|
|
}
|
|
|
|
console.error(
|
|
`\n${drifted.length} demo(s) have drifted from their snippet and would render a dead ` +
|
|
`variables panel:\n${drifted.map((r) => ` ${r.name}`).join("\n")}\n\n` +
|
|
`Run: npx tsx scripts/catalog/sync-demo-variables.ts`,
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
function report(results: DemoSyncResult[], check: boolean): void {
|
|
for (const result of results) logRefusal(result);
|
|
if (check) reportDrift(results);
|
|
else reportWritten(results);
|
|
}
|
|
|
|
if (isEntrypoint(import.meta.url)) {
|
|
const check = process.argv.includes("--check");
|
|
report(syncDemoVariables(!check), check);
|
|
}
|