feat(cli): add --composition flag to render specific compositions#631
feat(cli): add --composition flag to render specific compositions#631miguel-heygen wants to merge 2 commits intomainfrom
Conversation
Expose the existing entryFile config in the producer through a new --composition / -c CLI flag. This lets users render individual composition files without restructuring their project: hyperframes render -c compositions/intro.html -o intro.mp4 The flag validates the file exists before starting the render, threads through both local and Docker render paths, and is documented in the CLI help, examples, and docs.
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
jrusso1020
left a comment
There was a problem hiding this comment.
Clean, minimal, well-designed. Plumbs an existing producer field through the CLI rather than inventing new mechanism.
What I checked
-
The producer-side claim is real.
packages/producer/src/server.ts:77already declaresentryFile?: stringonRenderJobOptions, parses it at line 100-103 (trim + truthy check), validates existence at line 124-126, and falls back to"index.html"at line 122. The PR's claim of "leverages the existingentryFileconfig — no engine changes needed" matches the source. ✓ -
Backward compat.
args.composition?.trim() || undefinedat line 273 — empty string and whitespace-only treated as not-set. Producer defaults toindex.htmlwhen undefined. Existing renders without the flag are byte-identical. ✓ -
Validation.
statSyncagainstresolve(project.dir, entryFile)at lines 277-285 with the barecatch {}→ friendlyerrorBox+process.exit(1). Manual test in the PR body shows the actual error UX. The bare catch can't distinguish "file missing" from "permission denied" but for a local CLI both lead the user to the same investigation. ✓ -
Docker passthrough. Conditional
--composition <entryFile>append atdockerRunArgs.ts:71. Two new tests atdockerRunArgs.test.ts:214-228cover both branches (forwards when set, omits when not). ✓ -
Render plan output.
nameLabel = entryFile ? project.name + "/" + entryFile : project.namemakes it visible to the user which file is being rendered when-cis passed. Reads cleanly. ✓ -
Naming.
--composition/-cis consistent with the existing HF CLI conventions (singular, kebab-case, lowercase short alias matching the first letter of the long form —-cfor--composition, just like-ofor--output). ✓ -
CI. All 42 checks green or in_progress (Render on windows-latest still running at review time). Lint, Typecheck, Build, Test, Test: runtime contract, CLI smoke, Tests on windows-latest, Smoke: global install — all pass.
Non-blocking observations
-
Test gap on the CLI-side validation branch. The PR adds the existence-check logic in
render.tsbut only tests the Docker passthrough. Arender.test.tstest for "render command surfaceserrorBoxand exits with code 1 when--compositionpoints to a missing file" would round out the coverage. Manual test in the PR body covers the user-visible behavior so this isn't blocking, but worth a small follow-up commit. -
Path containment.
--composition ../../../some/other/dir/file.htmlwould pass both the CLI's existence check and the producer's existence check atserver.ts:125.hyperframeLint.ts:55-57has a directory-containment guard (if (!absoluteEntryPath.startsWith(absProjectDir))) but the render path inserver.tsdoesn't. For a local CLI run this is a UX point rather than a security boundary — the user has shell access to anything they could pass via-c. Worth mirroring the guard inserver.tsfor symmetry with the lint service, but that's a separate refactor that doesn't belong in this PR. Flagging only because it's a quiet inconsistency between two adjacent producer surfaces.
LGTM. Posted as COMMENT per the stamp policy.
— Rames Jusso
vanceingalls
left a comment
There was a problem hiding this comment.
Verdict: comment — change is well-scoped and the docker plumbing is solid, but a few things should land before merge.
This is a clean exposure of an existing producer capability (entryFile) to the CLI surface. The diff is small, threads through both render paths, and adds the right kind of test for the docker arg wiring. Two real gaps below — neither is a hard blocker, but important #1 (path traversal) shouldn't ship without a fix or an explicit decision to defer.
Important
-
packages/cli/src/commands/render.ts:273-287— no path-traversal guard on--composition.
resolve(project.dir, entryFile)followed bystatSyncwill happily accept--composition ../../../etc/hosts(or anything outsideproject.dir). The exact same codebase already enforces this forentryFileinpackages/producer/src/services/hyperframeLint.ts:55-58:const absoluteEntryPath = resolve(absProjectDir, entryFile); if (!absoluteEntryPath.startsWith(absProjectDir)) { return { error: `Entry file must stay inside project directory: ${entryFile}` }; }
Render should mirror that. The producer ultimately reads the file via
join(projectDir, entryFile)inrenderOrchestrator.ts:1974, so the CLI is the right place to fail fast with a clear error. The Docker mount is:rowhich limits blast radius for the container path, but local renders run in the user's own FS context — escapingproject.diris real. Add the prefix check next to thestatSyncand re-use the lint message style. -
packages/cli/src/commands/render.ts— no test thatentryFileis forwarded tocreateRenderJob.
The existing test pattern is right there (render.test.ts:105-134— "forwards parsed --variables payload to createRenderJob" / "omits variables ... when not provided"). The docker-side wiring is well-covered by the newdockerRunArgs.test.tscases, but the local-render path has no equivalent assertion. A copy-paste of the variables test pattern withentryFile: "compositions/intro.html"would close this and match the file's existing convention. Without it, a future refactor ofrenderLocalcould dropentryFilesilently and CI would stay green. -
packages/cli/src/utils/dockerRunArgs.test.ts:150-179— "regression tripwire" test not updated.
The comment in that test explicitly says: "If a future option is added but only wired through to renderLocal, this test forces the author to update buildDockerRunArgs (and add a check here) too." The PR added a new option and added dedicated tests for it (good), but didn't extend the tripwire. That weakens the convention the file is trying to establish. AddentryFile: "compositions/intro.html"to the options andexpect(args).toContain("--composition")to the assertions block.
Nits
-
packages/cli/src/commands/render.ts:283— error message points users athyperframes compositions, but that command lists composition IDs (data-composition-id), not file paths.
Thesourcecolumn does surface the file path for sub-compositions, which covers the main use case. Still, a user who has a top-level-onlyindex.htmlwill runhyperframes compositions, see an ID likeintro, and try--composition intro— that'll fail. Consider either (a) updating thecompositionsoutput to always show a "render with: --composition " hint, or (b) softening the error to "Pass a path to a.htmlfile relative to the project directory (e.g.compositions/intro.html)." Out of scope for this PR if you want, but worth a follow-up issue. -
packages/cli/src/commands/render.ts:294—project.name + "/" + entryFiledoesn't normalize.
If someone runs--composition ./compositions/intro.html, the render-plan line printsmyproj/./compositions/intro.html. Cosmetic only.entryFile.replace(/^\.\//, "")orrelative()would be cleaner. -
packages/cli/src/commands/render.ts:66-71— the description "Render a specific composition file instead of index.html" is good, but doesn't mention the engine's actual constraint: sub-compositions using<template>wrappers require the project'sindex.htmlto exist and reference the file viadata-composition-src(seerenderOrchestrator.ts:1984-2000). Worth a one-line "must be referenced fromindex.htmlviadata-composition-srcfor sub-compositions" so users don't try to render an arbitrary HTML file and get a confusing error from the engine.
Praise
- Threading the option through both
renderLocalandrenderDockerin one PR (rather than the historical pattern of forgetting one path — see the--hdrregression that the dockerRunArgs file is now structured to prevent) is exactly right. - The validation-then-pass-through approach (no engine changes, leverages existing producer config) is the minimum-blast-radius design. Good call.
- Both forward and omit cases tested for the docker arg builder — that's the right shape.
— Vai
…ipwire - Add path-containment check mirroring hyperframeLint.ts: reject --composition paths that escape the project directory - Normalize leading ./ from composition paths for clean render plan output - Improve error message: suggest .html file path instead of compositions command - Add description note about <template> sub-composition constraint - Add render.test.ts: entryFile forwarded to createRenderJob (forward + omit) - Update dockerRunArgs tripwire test with entryFile coverage
Summary
--composition/-cflag tohyperframes renderto render a specific composition file instead ofindex.htmlentryFileconfig in the producer — no engine changes neededUsage
Changes
packages/cli/src/commands/render.ts--composition/-carg, validate file exists, passentryFileto both local and Docker render paths, show composition name in render plan outputpackages/cli/src/utils/dockerRunArgs.tsentryFiletoDockerRenderOptions, forward as--compositionflag to containerpackages/cli/src/utils/dockerRunArgs.test.ts--compositionwhen set, omits when notdocs/packages/cli.mdxTest results
Automated tests
dockerRunArgstests pass (13 existing + 2 new)bunx vitest run packages/cli/src/utils/dockerRunArgs.test.ts— 15/15 passedManual tests
Error handling — nonexistent file:
Happy path — render broadcast-kit composition:
Backwards compatible — no flag renders index.html as before: