fix(cli): prevent keyframe shots overwriting sources (#3534)

This commit is contained in:
Miguel Ángel
2026-08-29 02:43:37 +00:00
committed by GitHub
parent b28747df0f
commit e4dabf830c
2 changed files with 46 additions and 5 deletions
+24 -1
View File
@@ -1,4 +1,4 @@
import { existsSync, mkdtempSync, mkdirSync, writeFileSync } from "node:fs";
import { existsSync, linkSync, mkdtempSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { beforeAll, describe, expect, it } from "vitest";
@@ -28,6 +28,29 @@ describe("keyframes direct composition scope", () => {
});
describe("keyframes shot output", () => {
it("rejects an output path that would overwrite the composition source", () => {
const projectDir = mkdtempSync(join(tmpdir(), "hf-keyframes-shot-source-"));
const sourcePath = join(projectDir, "index.html");
writeFileSync(sourcePath, wrap(""));
expect(() => ensureShotOutputDir(sourcePath, sourcePath)).toThrow(
/must not overwrite the composition source/,
);
expect(readFileSync(sourcePath, "utf8")).toBe(wrap(""));
});
it("rejects an existing output alias that refers to the composition source", () => {
const projectDir = mkdtempSync(join(tmpdir(), "hf-keyframes-shot-alias-"));
const sourcePath = join(projectDir, "index.html");
const aliasPath = join(projectDir, "shot.png");
writeFileSync(sourcePath, wrap(""));
linkSync(sourcePath, aliasPath);
expect(() => ensureShotOutputDir(aliasPath, sourcePath)).toThrow(
/must not overwrite the composition source/,
);
});
it("creates a missing parent directory before writing --shot", () => {
const projectDir = mkdtempSync(join(tmpdir(), "hf-keyframes-shot-dir-"));
const outputDir = join(projectDir, "nested", "proofs");
+22 -4
View File
@@ -10,8 +10,8 @@
// exactly what it's editing. All geometry + SVG live in ./motionShotLayout.ts
// (pure, tested); this file only drives the browser and SAMPLES.
import { mkdirSync, writeFileSync } from "node:fs";
import { dirname } from "node:path";
import { mkdirSync, statSync, writeFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { resolveDiagnosticNavigationTimeoutMs } from "../utils/renderArgs.js";
import { resolveCompositionViewportFromHtml } from "../utils/compositionViewport.js";
import {
@@ -33,7 +33,25 @@ export interface ShotRequest {
selector: string;
}
export function ensureShotOutputDir(outPath: string): void {
function pathsReferToSameFile(firstPath: string, secondPath: string): boolean {
const first = resolve(firstPath);
const second = resolve(secondPath);
if (first === second) return true;
try {
const firstStat = statSync(first);
const secondStat = statSync(second);
return firstStat.dev === secondStat.dev && firstStat.ino === secondStat.ino;
} catch {
return false;
}
}
export function ensureShotOutputDir(outPath: string, sourcePath?: string): void {
if (sourcePath && pathsReferToSameFile(outPath, sourcePath)) {
throw new Error(
`--shot output must not overwrite the composition source: ${sourcePath}. Choose a separate .png path.`,
);
}
mkdirSync(dirname(outPath), { recursive: true });
}
@@ -644,7 +662,7 @@ export async function captureMotionPathShot(
outPath: string,
opts: ShotOptions = {},
): Promise<string> {
ensureShotOutputDir(outPath);
ensureShotOutputDir(outPath, resolve(projectDir, opts.entryFile ?? "index.html"));
let requests = requestsIn;
const samples = Math.max(1, Math.min(60, opts.samples ?? 9));
const layout = opts.layout ?? "path";