mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-07 10:06:21 +00:00
feat(cli): warn when lambda --width/--height conflicts with composition
`--width 3840 --height 2160` against a composition with `data-width="1920"` silently produces a 1080p output because the runtime lays out the page at the composition's authored dimensions — real footgun we hit during a cost-analysis sweep. Warn early and point at `--output-resolution` (the supersampling escape hatch) so the user doesn't burn a 30-minute render learning the override rule. Skipped when `--output-resolution` is set (the supported supersampling path — the user is opting in), when `--json` is set (machine consumers), or when `index.html` isn't on disk (typical with `--site-id`). Helper lives in a shared module so render + render-batch agree on the parse + message. Tests cover both attribute orders, single/double quotes, the silent paths, and the warning path. Best-effort regex over the canonical attr shape — malformed HTML falls through to no warning rather than blocking the render. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
committed by
James Russo
co-authored by
Claude Opus 4.7
parent
d4384722e8
commit
ec1b7e1eff
@@ -0,0 +1,105 @@
|
||||
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { warnOnDimensionMismatch } from "./_dimensions.js";
|
||||
|
||||
const indexHtml = (width: number, height: number) =>
|
||||
`<!doctype html><html><body><div data-composition-id="root" data-width="${width}" data-height="${height}"></div></body></html>`;
|
||||
|
||||
describe("warnOnDimensionMismatch", () => {
|
||||
let dir: string;
|
||||
let warnSpy: ReturnType<typeof vi.fn>;
|
||||
let originalWarn: typeof console.warn;
|
||||
|
||||
beforeEach(() => {
|
||||
dir = mkdtempSync(join(tmpdir(), "hf-dim-mismatch-"));
|
||||
originalWarn = console.warn;
|
||||
warnSpy = vi.fn();
|
||||
console.warn = warnSpy as unknown as typeof console.warn;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
console.warn = originalWarn;
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function writeIndex(html: string): void {
|
||||
writeFileSync(join(dir, "index.html"), html);
|
||||
}
|
||||
|
||||
it("warns when the CLI dimensions don't match the composition", () => {
|
||||
writeIndex(indexHtml(1920, 1080));
|
||||
warnOnDimensionMismatch({
|
||||
projectDir: dir,
|
||||
cliWidth: 3840,
|
||||
cliHeight: 2160,
|
||||
outputResolution: undefined,
|
||||
quiet: false,
|
||||
});
|
||||
expect(warnSpy).toHaveBeenCalledTimes(1);
|
||||
const call = (warnSpy.mock.calls[0]?.[0] as string) ?? "";
|
||||
expect(call).toContain("3840×2160");
|
||||
expect(call).toContain("1920×1080");
|
||||
expect(call).toContain("--output-resolution");
|
||||
});
|
||||
|
||||
it("is silent when CLI and composition agree", () => {
|
||||
writeIndex(indexHtml(1920, 1080));
|
||||
warnOnDimensionMismatch({
|
||||
projectDir: dir,
|
||||
cliWidth: 1920,
|
||||
cliHeight: 1080,
|
||||
outputResolution: undefined,
|
||||
quiet: false,
|
||||
});
|
||||
expect(warnSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("is silent when --output-resolution is set (the supersampling path)", () => {
|
||||
writeIndex(indexHtml(1920, 1080));
|
||||
warnOnDimensionMismatch({
|
||||
projectDir: dir,
|
||||
cliWidth: 3840,
|
||||
cliHeight: 2160,
|
||||
outputResolution: "landscape-4k",
|
||||
quiet: false,
|
||||
});
|
||||
expect(warnSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("is silent when quiet=true (--json)", () => {
|
||||
writeIndex(indexHtml(1920, 1080));
|
||||
warnOnDimensionMismatch({
|
||||
projectDir: dir,
|
||||
cliWidth: 3840,
|
||||
cliHeight: 2160,
|
||||
outputResolution: undefined,
|
||||
quiet: true,
|
||||
});
|
||||
expect(warnSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("is silent when index.html is missing (typical with --site-id)", () => {
|
||||
warnOnDimensionMismatch({
|
||||
projectDir: dir,
|
||||
cliWidth: 3840,
|
||||
cliHeight: 2160,
|
||||
outputResolution: undefined,
|
||||
quiet: false,
|
||||
});
|
||||
expect(warnSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("is silent when the composition has no data-composition-id root", () => {
|
||||
writeIndex("<body><h1>just a comp</h1></body>");
|
||||
warnOnDimensionMismatch({
|
||||
projectDir: dir,
|
||||
cliWidth: 3840,
|
||||
cliHeight: 2160,
|
||||
outputResolution: undefined,
|
||||
quiet: false,
|
||||
});
|
||||
expect(warnSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Shared dimension-mismatch warning for `hyperframes lambda render` and
|
||||
* `lambda render-batch`. The runtime lays the page out at the composition's
|
||||
* `data-width`/`data-height`, so passing `--width 3840 --height 2160`
|
||||
* against a 1920×1080 composition silently produces a 1080p output. Warn
|
||||
* early and point at `--output-resolution` (the supersampling path).
|
||||
*/
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import type { CanvasResolution } from "@hyperframes/core";
|
||||
import { c } from "../../ui/colors.js";
|
||||
import { findCompositionDimensions } from "../../utils/compositionViewport.js";
|
||||
|
||||
export interface DimensionMismatchArgs {
|
||||
projectDir: string;
|
||||
cliWidth: number;
|
||||
cliHeight: number;
|
||||
outputResolution: CanvasResolution | undefined;
|
||||
/** Suppress the warning when stdout is reserved for machine output (--json). */
|
||||
quiet: boolean;
|
||||
}
|
||||
|
||||
// fallow-ignore-next-line complexity
|
||||
export function warnOnDimensionMismatch(args: DimensionMismatchArgs): void {
|
||||
if (args.quiet) return;
|
||||
if (args.outputResolution) return;
|
||||
let html: string;
|
||||
try {
|
||||
html = readFileSync(join(args.projectDir, "index.html"), "utf-8");
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
const composition = findCompositionDimensions(html);
|
||||
if (!composition) return;
|
||||
if (composition.width === args.cliWidth && composition.height === args.cliHeight) return;
|
||||
console.warn(
|
||||
c.warn(
|
||||
`--width/--height (${args.cliWidth}×${args.cliHeight}) disagrees with the composition's ` +
|
||||
`data-width/data-height (${composition.width}×${composition.height}). The runtime lays out ` +
|
||||
`the page at the composition's authored dimensions, so your output will be ` +
|
||||
`${composition.width}×${composition.height}, not ${args.cliWidth}×${args.cliHeight}.\n` +
|
||||
` To supersample to a higher resolution, pass --output-resolution (e.g. \`--output-resolution=4k\`).\n` +
|
||||
` To truly change layout dimensions, edit the composition's data-width/data-height in index.html.`,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
reportVariableIssues,
|
||||
validateVariablesAgainstSchema,
|
||||
} from "../../utils/variables.js";
|
||||
import { warnOnDimensionMismatch } from "./_dimensions.js";
|
||||
import { requireStack } from "./state.js";
|
||||
|
||||
// Dynamic-import the SDK so tsup keeps it out of the static-import head of
|
||||
@@ -165,6 +166,14 @@ export async function runRenderBatch(args: RenderBatchArgs): Promise<void> {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
warnOnDimensionMismatch({
|
||||
projectDir,
|
||||
cliWidth: args.width,
|
||||
cliHeight: args.height,
|
||||
outputResolution: args.outputResolution,
|
||||
quiet: args.json,
|
||||
});
|
||||
|
||||
// Pre-validate every entry's variables against the composition's
|
||||
// schema. Mismatches print as warnings; strict mode aborts before any
|
||||
// AWS call. Schema is loaded once and reused across entries — a 10k-row
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
resolveVariablesArg,
|
||||
validateVariablesAgainstProject,
|
||||
} from "../../utils/variables.js";
|
||||
import { warnOnDimensionMismatch } from "./_dimensions.js";
|
||||
import { requireStack, stateFilePath } from "./state.js";
|
||||
|
||||
// Dynamic-import the SDK so tsup keeps it out of the static-import head of
|
||||
@@ -72,6 +73,14 @@ export async function runRender(args: RenderArgs): Promise<void> {
|
||||
const stack = requireStack(args.stackName);
|
||||
const projectDir = resolvePath(args.projectDir);
|
||||
|
||||
warnOnDimensionMismatch({
|
||||
projectDir,
|
||||
cliWidth: args.width,
|
||||
cliHeight: args.height,
|
||||
outputResolution: args.outputResolution,
|
||||
quiet: args.json,
|
||||
});
|
||||
|
||||
// Resolve --variables / --variables-file using the same parser the local
|
||||
// `hyperframes render` uses. `resolveVariablesArg` exits(1) with a friendly
|
||||
// errorBox on parse errors so callers don't have to.
|
||||
|
||||
Reference in New Issue
Block a user