Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/packages/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@ This is suppressed in CI environments, non-TTY shells, and when `HYPERFRAMES_NO_
```bash
npx hyperframes render --output output.mp4
```
Render a specific composition instead of `index.html`:
```bash
npx hyperframes render -c compositions/intro.html -o intro.mp4
```
For deterministic output, add `--docker`:
```bash
npx hyperframes render --docker --output output.mp4
Expand Down
34 changes: 30 additions & 4 deletions packages/cli/src/commands/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { mkdirSync, readFileSync, statSync, writeFileSync, rmSync } from "node:f

export const examples: Example[] = [
["Render to MP4", "hyperframes render --output output.mp4"],
["Render a specific composition", "hyperframes render -c compositions/intro.html -o intro.mp4"],
["Render transparent overlay (ProRes)", "hyperframes render --format mov --output overlay.mov"],
["Render transparent WebM overlay", "hyperframes render --format webm --output overlay.webm"],
["High quality at 60fps", "hyperframes render --fps 60 --quality high --output hd.mp4"],
Expand Down Expand Up @@ -62,6 +63,12 @@ export default defineCommand({
description: "Project directory",
required: false,
},
composition: {
type: "string",
alias: "c",
description:
"Render a specific composition file instead of index.html (e.g. compositions/intro.html)",
},
output: {
type: "string",
alias: "o",
Expand Down Expand Up @@ -263,16 +270,30 @@ export default defineCommand({
process.exit(1);
}

// ── Validate composition entry file ──────────────────────────────────
const entryFile = args.composition?.trim() || undefined;
if (entryFile) {
const entryPath = resolve(project.dir, entryFile);
try {
statSync(entryPath);
} catch {
errorBox(
"Composition not found",
`"${entryFile}" does not exist in the project directory.`,
"Use 'hyperframes compositions' to list available compositions.",
);
process.exit(1);
}
}

// ── Print render plan ─────────────────────────────────────────────────
if (!quiet) {
const workerLabel =
workers != null ? `${workers} workers` : `auto workers (${CPU_CORE_COUNT} cores detected)`;
console.log("");
const nameLabel = entryFile ? project.name + "/" + entryFile : project.name;
console.log(
c.accent("\u25C6") +
" Rendering " +
c.accent(project.name) +
c.dim(" \u2192 " + outputPath),
c.accent("\u25C6") + " Rendering " + c.accent(nameLabel) + c.dim(" \u2192 " + outputPath),
);
console.log(c.dim(" " + fps + "fps \u00B7 " + quality + " \u00B7 " + workerLabel));
if (useGpu || useBrowserGpu) {
Expand Down Expand Up @@ -403,6 +424,7 @@ export default defineCommand({
videoBitrate,
quiet,
variables,
entryFile,
exitAfterComplete: true,
});
} else {
Expand All @@ -419,6 +441,7 @@ export default defineCommand({
quiet,
browserPath,
variables,
entryFile,
exitAfterComplete: true,
});
}
Expand All @@ -438,6 +461,7 @@ interface RenderOptions {
quiet: boolean;
browserPath?: string;
variables?: Record<string, unknown>;
entryFile?: string;
exitAfterComplete?: boolean;
}

Expand Down Expand Up @@ -713,6 +737,7 @@ async function renderDocker(
videoBitrate: options.videoBitrate,
quiet: options.quiet,
variables: options.variables,
entryFile: options.entryFile,
},
});

Expand Down Expand Up @@ -783,6 +808,7 @@ export async function renderLocal(
crf: options.crf,
videoBitrate: options.videoBitrate,
variables: options.variables,
entryFile: options.entryFile,
});

const onProgress = options.quiet
Expand Down
15 changes: 15 additions & 0 deletions packages/cli/src/utils/dockerRunArgs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,4 +210,19 @@ describe("buildDockerRunArgs", () => {
});
expect(args).not.toContain("--variables");
});

it("forwards --composition to the container when entryFile is set", () => {
const args = buildDockerRunArgs({
...FIXED_INPUT,
options: { ...BASE, entryFile: "compositions/intro.html" },
});
const idx = args.indexOf("--composition");
expect(idx).toBeGreaterThan(-1);
expect(args[idx + 1]).toBe("compositions/intro.html");
});

it("omits --composition when entryFile is not set", () => {
const args = buildDockerRunArgs({ ...FIXED_INPUT, options: BASE });
expect(args).not.toContain("--composition");
});
});
2 changes: 2 additions & 0 deletions packages/cli/src/utils/dockerRunArgs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export interface DockerRenderOptions {
videoBitrate?: string;
quiet: boolean;
variables?: Record<string, unknown>;
entryFile?: string;
}

export function buildDockerRunArgs(input: DockerRunArgsInput): string[] {
Expand Down Expand Up @@ -67,5 +68,6 @@ export function buildDockerRunArgs(input: DockerRunArgsInput): string[] {
...(options.variables && Object.keys(options.variables).length > 0
? ["--variables", JSON.stringify(options.variables)]
: []),
...(options.entryFile ? ["--composition", options.entryFile] : []),
];
}
Loading