fix(cli): read info metadata from the root composition (#5299)

This commit is contained in:
Abdullah M. Nazif
2026-10-24 04:11:08 +00:00
committed by GitHub
parent 1001a79419
commit 8a84bedd38
2 changed files with 98 additions and 18 deletions
+78 -2
View File
@@ -1,5 +1,8 @@
import { describe, expect, it } from "vitest";
import { orientation, durationFromHtml } from "./info.js";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, it, vi } from "vitest";
import info, { orientation, durationFromHtml } from "./info.js";
describe("orientation", () => {
it("is landscape when width > height", () => {
@@ -31,3 +34,76 @@ describe("durationFromHtml", () => {
expect(durationFromHtml(html, 5)).toBe(5);
});
});
describe("info", () => {
it.each([
{
name: "quoted attributes",
html: '<div data-composition-id="main" data-width="64" data-height="96" data-duration=".2"></div>',
},
{
name: "whitespace around attribute assignments",
html: '<div data-composition-id = "main" data-width = "64" data-height\n= "96" data-duration = ".2"></div>',
},
{
name: "unquoted attributes",
html: "<div data-composition-id=main data-width=64 data-height=96 data-duration=.2></div>",
},
{
name: "commented composition before the root",
html: `<!-- <div data-composition-id="example" data-width="1920" data-height="1080" data-duration="8"></div> -->
<div data-composition-id="main" data-width="64" data-height="96" data-duration=".2"></div>`,
},
{
name: "composition markup inside a script string",
html: `<script>const example = '<div data-composition-id="example" data-width="1920" data-height="1080" data-duration="8"></div>';</script>
<div data-composition-id="main" data-width="64" data-height="96" data-duration=".2"></div>`,
},
{
name: "explicit root after another composition",
html: `<div data-composition-id="other" data-width="1920" data-height="1080" data-duration="8"></div>
<div data-composition-id="main" data-root="true" data-width="64" data-height="96" data-duration=".2"></div>`,
},
])("reports the root metadata with $name", async ({ html }) => {
const dir = mkdtempSync(join(tmpdir(), "hf-info-"));
const log = vi.spyOn(console, "log").mockImplementation(() => {});
try {
writeFileSync(join(dir, "index.html"), html);
await info.run?.({ args: { _: [], dir, json: true }, rawArgs: [], cmd: info });
expect(JSON.parse(String(log.mock.calls[0]?.[0]))).toMatchObject({
resolution: "portrait",
width: 64,
height: 96,
duration: 0.2,
});
} finally {
log.mockRestore();
rmSync(dir, { recursive: true, force: true });
}
});
it("keeps the inferred resolution and timeline duration when root metadata is absent", async () => {
const dir = mkdtempSync(join(tmpdir(), "hf-info-"));
const log = vi.spyOn(console, "log").mockImplementation(() => {});
try {
writeFileSync(
join(dir, "index.html"),
`<html data-resolution="landscape"><body><div data-composition-id="main">
<div class="clip" data-start="1" data-duration="2"></div>
</div></body></html>`,
);
await info.run?.({ args: { _: [], dir, json: true }, rawArgs: [], cmd: info });
expect(JSON.parse(String(log.mock.calls[0]?.[0]))).toMatchObject({
resolution: "landscape",
width: 1920,
height: 1080,
duration: 3,
});
} finally {
log.mockRestore();
rmSync(dir, { recursive: true, force: true });
}
});
});
+20 -16
View File
@@ -8,6 +8,11 @@ export const examples: Example[] = [
];
import { join } from "node:path";
import { parseHtml, CANVAS_DIMENSIONS } from "@hyperframes/core";
import {
findRootCompositionElement,
readCompositionSize,
} from "@hyperframes/core/runtime/composition-length";
import { readDataDurationSeconds } from "@hyperframes/parsers/media-duration";
import { c } from "../ui/colors.js";
import { formatBytes, label } from "../ui/format.js";
import { ensureDOMParser } from "../utils/dom.js";
@@ -26,11 +31,16 @@ export function orientation(width: number, height: number): "landscape" | "portr
* fall back to the computed timeline end.
*/
export function durationFromHtml(html: string, fallback: number): number {
const match =
html.match(/data-composition-id[^>]*data-duration=["']([\d.]+)["']/) ||
html.match(/data-duration=["']([\d.]+)["'][^>]*data-composition-id/);
const value = match?.[1] ? parseFloat(match[1]) : NaN;
return Number.isFinite(value) ? value : fallback;
return durationFromRoot(rootFromHtml(html), fallback);
}
function rootFromHtml(html: string): HTMLElement | null {
ensureDOMParser();
return findRootCompositionElement(new DOMParser().parseFromString(html, "text/html"));
}
function durationFromRoot(root: Element | null, fallback: number): number {
return readDataDurationSeconds((name) => root?.getAttribute(name)) ?? fallback;
}
function totalSize(dir: string): number {
@@ -56,7 +66,7 @@ export default defineCommand({
const project = resolveProject(args.dir);
const html = readFileSync(project.indexPath, "utf-8");
ensureDOMParser();
const root = rootFromHtml(html);
const parsed = parseHtml(html);
const tracks = new Set(parsed.elements.map((el) => el.zIndex));
@@ -64,18 +74,12 @@ export default defineCommand({
(max, el) => Math.max(max, el.startTime + el.duration),
0,
);
// Read actual dimensions from root composition element
const widthMatch =
html.match(/data-composition-id[^>]*data-width=["'](\d+)["']/) ||
html.match(/data-width=["'](\d+)["'][^>]*data-composition-id/);
const heightMatch =
html.match(/data-composition-id[^>]*data-height=["'](\d+)["']/) ||
html.match(/data-height=["'](\d+)["'][^>]*data-composition-id/);
const dimensions = root ? readCompositionSize(root) : null;
const fallback = CANVAS_DIMENSIONS[parsed.resolution];
const width = widthMatch?.[1] ? parseInt(widthMatch[1], 10) : fallback.width;
const height = heightMatch?.[1] ? parseInt(heightMatch[1], 10) : fallback.height;
const width = dimensions?.width ?? fallback.width;
const height = dimensions?.height ?? fallback.height;
const resolution = `${width}x${height}`;
const duration = durationFromHtml(html, maxEnd);
const duration = durationFromRoot(root, maxEnd);
const size = totalSize(project.dir);
const typeCounts: Record<string, number> = {};