fix(cli): honor direct entries in keyframe shots (#2217)

* fix(cli): honor direct entries in keyframe shots

* fix(core): rebase entry-authored asset paths for direct-entry bundling
This commit is contained in:
Miguel Ángel
2026-07-10 22:32:26 -04:00
committed by GitHub
parent de4e85add6
commit b95ddd74d5
5 changed files with 200 additions and 24 deletions
+29 -14
View File
@@ -1,6 +1,6 @@
import { defineCommand } from "citty";
import { existsSync, readFileSync, statSync } from "node:fs";
import { resolve, dirname, basename } from "node:path";
import { resolve, dirname, basename, join, relative, sep } from "node:path";
import { parseGsapScript, type GsapAnimation } from "@hyperframes/core/gsap-parser";
import type { Example } from "./_examples.js";
import { c } from "../ui/colors.js";
@@ -710,6 +710,7 @@ async function runOnionShot(
comps: SurfacedComposition[],
allComps: SurfacedComposition[],
projectDir: string | undefined,
entryFile: string | undefined,
args: ShotArgs & { selector?: string },
): Promise<boolean> {
const { captureMotionPathShot } = await import("./motionShot.js");
@@ -722,32 +723,34 @@ async function runOnionShot(
console.log(c.dim(guardError));
return true;
}
const saved = await captureMotionPathShot(
projectDir!,
requests,
resolve(args.shot!),
onionShotOptions(args),
);
const saved = await captureMotionPathShot(projectDir!, requests, resolve(args.shot!), {
...onionShotOptions(args),
entryFile,
});
printOnionShotSaved(saved, requests.length);
return false;
}
// Resolve the command target (a project dir or a single .html) into surfaced
// compositions, applying the optional --selector filter.
function resolveScope(args: { target?: string; selector?: string }): {
export function resolveScope(args: { target?: string; selector?: string }): {
comps: SurfacedComposition[];
allComps: SurfacedComposition[];
projectName: string;
projectDir: string | undefined;
entryFile: string | undefined;
} {
const raw = args.target?.trim();
let comps: SurfacedComposition[];
let projectName: string;
let projectDir: string | undefined;
let entryFile: string | undefined;
if (raw && raw.endsWith(".html") && existsSync(raw) && statSync(raw).isFile()) {
comps = [surfaceComposition(readFileSync(raw, "utf-8"), basename(raw), raw)];
projectName = basename(raw);
projectDir = dirname(raw);
const entryPath = resolve(raw);
comps = [surfaceComposition(readFileSync(entryPath, "utf-8"), basename(entryPath), entryPath)];
projectName = basename(entryPath);
projectDir = findProjectRoot(entryPath);
entryFile = relative(projectDir, entryPath).split(sep).join("/");
} else {
const project = resolveProject(raw);
comps = collectCompositions(project.indexPath);
@@ -777,7 +780,19 @@ function resolveScope(args: { target?: string; selector?: string }): {
cmp.anime.length > 0,
);
}
return { comps, allComps, projectName, projectDir };
return { comps, allComps, projectName, projectDir, entryFile };
}
function findProjectRoot(entryPath: string): string {
const entryDir = dirname(entryPath);
let candidate = entryDir;
for (;;) {
if (existsSync(join(candidate, "index.html"))) return candidate;
if (existsSync(join(candidate, ".git"))) return entryDir;
const parent = dirname(candidate);
if (parent === candidate) return entryDir;
candidate = parent;
}
}
function isEmptyComposition(cmp: SurfacedComposition): boolean {
@@ -917,12 +932,12 @@ function createKeyframesCommand(options: Partial<KeyframesCommandOptions> = {})
);
console.log();
}
const { comps: rawComps, allComps, projectName, projectDir } = resolveScope(args);
const { comps: rawComps, allComps, projectName, projectDir, entryFile } = resolveScope(args);
const comps = filterCompositionsByRuntime(rawComps, runtime);
// --shot: 3D onion-skin self-verify screenshot. Returns true when the command
// should stop (guard failure) so run() stays small.
if (args.shot && (await runOnionShot(comps, allComps, projectDir, args))) return;
if (args.shot && (await runOnionShot(comps, allComps, projectDir, entryFile, args))) return;
if (args.json) {
console.log(