mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-01 19:42:03 +00:00
fix(cli): prevent keyframe shots overwriting sources (#3534)
This commit is contained in:
@@ -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");
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user