fix(cli): emit valid JSON for filtered keyframes (#6228)

This commit is contained in:
Akshar Patel
2026-10-25 00:28:42 +00:00
committed by GitHub
parent f21c3e3e7e
commit 6a917780f5
3 changed files with 36 additions and 9 deletions
+4
View File
@@ -42,6 +42,10 @@ remains authoritative: run `npx hyperframes <command> --help`.
`hyperframes timeline` also accepts agent editing verbs. Use `move`, `trim`, `split`, or `delete` with a row ref; add `--plan` to inspect the before/after rows and diff without writing, or `--json` for a machine-readable receipt. Time values accept seconds, relative offsets, frames, and anchors such as `after:#intro`.
For scripts, `npx hyperframes keyframes --runtime css --json` writes one JSON
result to stdout. Use `--runtime anime` for Anime.js. The static authoring
notice appears only in human-readable output.
## Rendering and automation
The CLI also renders in batches and from variables, writes image sequences and
+23 -1
View File
@@ -9,7 +9,8 @@ import {
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { beforeAll, describe, expect, it, onTestFinished } from "vitest";
import { beforeAll, describe, expect, it, onTestFinished, vi } from "vitest";
import { runCommand } from "citty";
import type { ArgsDef } from "citty";
import { ensureDOMParser } from "../utils/dom.js";
import keyframesCommand from "./keyframes.js";
@@ -38,6 +39,27 @@ beforeAll(() => ensureDOMParser());
const wrap = (script: string) =>
`<!doctype html><html><body><div id="root" data-composition-id="main" data-duration="4"><div id="dot" class="clip"></div></div><script>${script}</script></body></html>`;
describe("keyframes JSON output", () => {
it.each(["css", "anime"])("emits parseable JSON with --runtime %s", async (runtime) => {
const dir = mkdtempSync(join(tmpdir(), "hf-keyframes-json-"));
onTestFinished(() => rmSync(dir, { recursive: true, force: true }));
const source = join(dir, "index.html");
writeFileSync(source, wrap(""));
const log = vi.spyOn(console, "log").mockImplementation(() => {});
onTestFinished(() => log.mockRestore());
await runCommand(keyframesCommand, { rawArgs: [source, "--runtime", runtime, "--json"] });
const output = log.mock.calls.map((args) => args.join(" ")).join("\n");
const result: unknown = JSON.parse(output);
expect(result).toMatchObject({
project: "index.html",
runtime,
compositions: [{ composition: "index.html", tweens: [], cssKeyframes: [], anime: [] }],
});
});
});
describe("keyframes direct composition scope", () => {
it("keeps the project root and passes the nested HTML entry to --shot", () => {
const projectDir = mkdtempSync(join(tmpdir(), "hf-keyframes-target-"));
+9 -8
View File
@@ -1103,14 +1103,6 @@ function createKeyframesCommand(options: Partial<KeyframesCommandOptions> = {})
async run({ args }) {
ensureDOMParser();
const runtime = normalizeRuntime(args.runtime, commandOptions.defaultRuntime);
if (runtime === "css" || runtime === "anime") {
console.log(
c.dim(
`${commandOptions.name}: ${runtime} output is a static authoring surface; use validate/render/snapshot to verify runtime adapter seekability.`,
),
);
console.log();
}
const { comps: rawComps, allComps, projectName, projectDir, entryFile } = resolveScope(args);
const comps = filterCompositionsByRuntime(rawComps, runtime);
@@ -1125,6 +1117,15 @@ function createKeyframesCommand(options: Partial<KeyframesCommandOptions> = {})
return;
}
if (runtime === "css" || runtime === "anime") {
console.log(
c.dim(
`${commandOptions.name}: ${runtime} output is a static authoring surface; use validate/render/snapshot to verify runtime adapter seekability.`,
),
);
console.log();
}
const total = comps.reduce(
(n, cmp) => n + cmp.tweens.length + cmp.cssKeyframes.length + cmp.anime.length,
0,