mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-05 10:14:30 +00:00
* fix(cli): default render fps to the composition's data-fps
hyperframes render hard-coded fps to 30 when --fps was omitted, ignoring a
data-fps declared on the composition root — so a composition authored at
data-fps="24" silently rendered at 30fps unless the user knew to pass --fps 24.
The runtime already honors data-fps; the CLI now matches it.
Precedence: explicit --fps > composition root data-fps > 30. New pure
readCompositionFps() extracts the root data-fps via linkedom (mirrors the
runtime's root resolution: [data-composition-id][data-root=true], else the
outermost [data-composition-id]); render validates it through parseFps and
falls back to 30 on an absent/invalid value. Unit-tested.
* fix(cli): honor composition data-fps on cloud renders and --composition targets
The local render command read data-fps from project.dir/index.html even when
--composition rendered a different file, and the lambda/cloudrun render paths
ignored data-fps entirely (hardcoded ?? 30). Both are the same silently-wrong-
fps bug on other render entry points:
- render.ts resolves the entry file first, then reads data-fps from the file
actually being rendered (falling back to index.html).
- lambda render/render-batch and cloudrun render/render-batch default fps from
the composition's data-fps, accepted only when it is one of the cloud-allowed
values {24,30,60}, else the existing 30 default. Explicit --fps still wins.
* fix(cli): drop citty fps default so data-fps resolution actually runs
The fps arg had default: "30", so citty set args.fps="30" on omission and
resolveDefaultFpsArg short-circuited (explicitFps never null) — reverting the
command to always-30 and making the whole data-fps feature a no-op (caught in
review). Remove the arg default; the "30" fallback already lives at
parseFps(fpsArg ?? "30"). Adds a regression guard asserting the arg has no
default.
* test(cli): read citty args through a plain record in the fps-default guard
The regression guard accessed cmd.args.fps directly, but citty types args as
Resolvable<ArgsDef> so .fps failed typecheck in CI. Read it through a plain
record cast.
108 lines
3.8 KiB
TypeScript
108 lines
3.8 KiB
TypeScript
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
import { readAllowedCompositionFpsFromDir, readCompositionFps } from "./compositionFps.js";
|
|
|
|
const wrap = (body: string) => `<!DOCTYPE html><html><body>${body}</body></html>`;
|
|
|
|
describe("readCompositionFps", () => {
|
|
it("reads data-fps from the explicit data-root composition element", () => {
|
|
const html = wrap('<div data-composition-id="root" data-root="true" data-fps="24">x</div>');
|
|
expect(readCompositionFps(html)).toBe("24");
|
|
});
|
|
|
|
it("reads data-fps from the outermost composition when no data-root is marked", () => {
|
|
const html = wrap(
|
|
'<div data-composition-id="root" data-fps="48"><div data-composition-id="child" data-fps="12">x</div></div>',
|
|
);
|
|
expect(readCompositionFps(html)).toBe("48");
|
|
});
|
|
|
|
it("preserves a fractional rate verbatim for parseFps to validate", () => {
|
|
const html = wrap(
|
|
'<div data-composition-id="root" data-root="true" data-fps="30000/1001">x</div>',
|
|
);
|
|
expect(readCompositionFps(html)).toBe("30000/1001");
|
|
});
|
|
|
|
it("returns null when the root has no data-fps", () => {
|
|
expect(readCompositionFps(wrap('<div data-composition-id="root">x</div>'))).toBeNull();
|
|
});
|
|
|
|
it("returns null when there is no composition root", () => {
|
|
expect(readCompositionFps(wrap("<div>plain</div>"))).toBeNull();
|
|
});
|
|
|
|
it("returns null for a blank data-fps", () => {
|
|
expect(
|
|
readCompositionFps(wrap('<div data-composition-id="root" data-fps=" ">x</div>')),
|
|
).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("readAllowedCompositionFpsFromDir", () => {
|
|
const projectDirs: string[] = [];
|
|
const allowedCloudFps = [24, 30, 60] as const;
|
|
|
|
afterEach(() => {
|
|
for (const dir of projectDirs.splice(0)) {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
function makeProject(indexBody: string): string {
|
|
const dir = mkdtempSync(join(tmpdir(), "hyperframes-composition-fps-"));
|
|
projectDirs.push(dir);
|
|
writeFileSync(join(dir, "index.html"), wrap(indexBody));
|
|
return dir;
|
|
}
|
|
|
|
it("uses cloud-allowed data-fps=60 as the default", () => {
|
|
const dir = makeProject(
|
|
'<div data-composition-id="root" data-root="true" data-fps="60">x</div>',
|
|
);
|
|
|
|
expect(readAllowedCompositionFpsFromDir(dir, allowedCloudFps)).toBe(60);
|
|
});
|
|
|
|
it("uses cloud-allowed data-fps=24 as the default", () => {
|
|
const dir = makeProject(
|
|
'<div data-composition-id="root" data-root="true" data-fps="24">x</div>',
|
|
);
|
|
|
|
expect(readAllowedCompositionFpsFromDir(dir, allowedCloudFps)).toBe(24);
|
|
});
|
|
|
|
it("returns null for data-fps=48 so cloud callers keep their ?? 30 fallback", () => {
|
|
const dir = makeProject(
|
|
'<div data-composition-id="root" data-root="true" data-fps="48">x</div>',
|
|
);
|
|
|
|
const declared = readAllowedCompositionFpsFromDir(dir, allowedCloudFps);
|
|
expect(declared).toBeNull();
|
|
expect(declared ?? 30).toBe(30);
|
|
});
|
|
|
|
it("returns null for fractional data-fps because cloud fps must be an integer", () => {
|
|
const dir = makeProject(
|
|
'<div data-composition-id="root" data-root="true" data-fps="30000/1001">x</div>',
|
|
);
|
|
|
|
expect(readAllowedCompositionFpsFromDir(dir, allowedCloudFps)).toBeNull();
|
|
});
|
|
|
|
it("returns null when index.html has no data-fps", () => {
|
|
const dir = makeProject('<div data-composition-id="root" data-root="true">x</div>');
|
|
|
|
expect(readAllowedCompositionFpsFromDir(dir, allowedCloudFps)).toBeNull();
|
|
});
|
|
|
|
it("returns null when index.html cannot be read", () => {
|
|
const dir = mkdtempSync(join(tmpdir(), "hyperframes-composition-fps-missing-"));
|
|
projectDirs.push(dir);
|
|
|
|
expect(readAllowedCompositionFpsFromDir(dir, allowedCloudFps)).toBeNull();
|
|
});
|
|
});
|