fix(cli): stage temporary HTML comparisons without recursive copies (#5286)

This commit is contained in:
Abdullah M. Nazif
2026-10-24 04:11:21 +00:00
committed by GitHub
parent ecf5db7070
commit bd0be1bdae
2 changed files with 120 additions and 11 deletions
+75 -1
View File
@@ -1,6 +1,6 @@
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { basename, join } from "node:path";
import { describe, expect, it, vi } from "vitest";
import {
buildCompareSuccessPayload,
@@ -9,6 +9,12 @@ import {
prepareCompareVariantProjects,
} from "./compare.js";
const temporaryRoot = vi.hoisted(() => ({ path: "" }));
vi.mock("node:os", async (importOriginal) => {
const actual = await importOriginal<typeof import("node:os")>();
return { ...actual, tmpdir: () => temporaryRoot.path || actual.tmpdir() };
});
function tempDir(): string {
return mkdtempSync(join(tmpdir(), "hf-compare-test-"));
}
@@ -78,6 +84,74 @@ describe("capCompareVariants", () => {
});
describe("prepareCompareVariantProjects", () => {
it("keeps earlier temporary staging folders out of later variants", () => {
const dir = tempDir();
const first = join(dir, "first.html");
const second = join(dir, "second.html");
writeFileSync(first, "<!doctype html><title>First</title>");
writeFileSync(second, "<!doctype html><title>Second</title>");
temporaryRoot.path = dir;
try {
const prepared = prepareCompareVariantProjects([
{ label: "First", inputPath: first, displayPath: "first.html" },
{ label: "Second", inputPath: second, displayPath: "second.html" },
]);
const firstStage = prepared[0];
const secondStage = prepared[1];
if (!firstStage || !secondStage) throw new Error("expected two staged variants");
expect(existsSync(join(secondStage.projectDir, basename(firstStage.projectDir)))).toBe(false);
expect(readFileSync(join(secondStage.projectDir, "first.html"), "utf8")).toContain("First");
expect(readFileSync(join(secondStage.projectDir, "index.html"), "utf8")).toContain("Second");
} finally {
temporaryRoot.path = "";
rmSync(dir, { recursive: true, force: true });
}
});
it.each(["direct", "nested"])(
"stages siblings when the temporary output is %s inside the source directory",
(placement) => {
const dir = tempDir();
const htmlFile = join(dir, "candidate.html");
mkdirSync(join(dir, "assets"));
writeFileSync(htmlFile, "<!doctype html><title>Temporary candidate</title>");
writeFileSync(join(dir, "assets", "asset.txt"), "sibling asset");
mkdirSync(join(dir, "node_modules"));
writeFileSync(join(dir, "node_modules", "ignored.txt"), "ignored");
temporaryRoot.path = placement === "direct" ? dir : join(dir, "temporary");
mkdirSync(temporaryRoot.path, { recursive: true });
writeFileSync(join(temporaryRoot.path, "sibling.txt"), "temporary-root sibling");
try {
const prepared = prepareCompareVariantProjects([
{ label: "Temp", inputPath: htmlFile, displayPath: "candidate.html" },
]);
const staged = prepared[0];
if (!staged) throw new Error("expected a staged variant");
expect(staged.stagedDir).toBe(staged.projectDir);
expect(readFileSync(join(staged.projectDir, "index.html"), "utf8")).toContain(
"Temporary candidate",
);
expect(readFileSync(join(staged.projectDir, "assets", "asset.txt"), "utf8")).toBe(
"sibling asset",
);
expect(
readFileSync(
join(staged.projectDir, placement === "direct" ? "" : "temporary", "sibling.txt"),
"utf8",
),
).toBe("temporary-root sibling");
expect(existsSync(join(staged.projectDir, "candidate.html"))).toBe(false);
expect(existsSync(join(staged.projectDir, "node_modules"))).toBe(false);
expect(readFileSync(htmlFile, "utf8")).toContain("Temporary candidate");
} finally {
temporaryRoot.path = "";
rmSync(dir, { recursive: true, force: true });
}
},
);
it("uses project directories directly and stages standalone html files as index.html", () => {
const dir = tempDir();
try {
+45 -10
View File
@@ -1,5 +1,15 @@
import { failCommand } from "../utils/commandResult.js";
import { cpSync, existsSync, mkdirSync, mkdtempSync, renameSync, rmSync, statSync } from "node:fs";
import {
cpSync,
existsSync,
mkdirSync,
mkdtempSync,
readdirSync,
realpathSync,
renameSync,
rmSync,
statSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { basename, dirname, extname, join } from "node:path";
import { defineCommand } from "citty";
@@ -16,6 +26,7 @@ import { trackCompareSheet } from "../telemetry/events.js";
import { serveStaticProjectHtml } from "../utils/staticProjectServer.js";
import { withMeta } from "../utils/updateCheck.js";
import type { Example } from "./_examples.js";
import { isPathInside } from "@hyperframes/parsers/asset-paths";
const MAX_COMPARE_VARIANTS = 16;
const MAX_COLUMNS = 4;
@@ -198,18 +209,39 @@ function inputError(variant: CompareVariantSpec): Error {
);
}
function stageHtmlVariant(variant: CompareVariantSpec): PreparedCompareVariant {
function copyVariantSiblings(
sourceDir: string,
destinationDir: string,
stagingPaths: readonly string[],
): void {
mkdirSync(destinationDir, { recursive: true });
for (const entry of readdirSync(sourceDir)) {
if (entry === "node_modules" || entry === ".git") continue;
const src = join(sourceDir, entry);
const dest = join(destinationDir, entry);
if (stagingPaths.includes(src)) continue;
if (stagingPaths.some((path) => isPathInside(path, src))) {
copyVariantSiblings(src, dest, stagingPaths);
continue;
}
cpSync(src, dest, {
recursive: true,
filter: (path) => basename(path) !== "node_modules" && basename(path) !== ".git",
});
}
}
function stageHtmlVariant(
variant: CompareVariantSpec,
existingStagedDirs: readonly string[],
): PreparedCompareVariant {
const stagedDir = mkdtempSync(join(tmpdir(), "hf-compare-variant-"));
try {
// Copy the composition's sibling files but skip heavy/irrelevant trees — a
// variant sitting next to node_modules or .git shouldn't drag them into tmp.
cpSync(dirname(variant.inputPath), stagedDir, {
recursive: true,
filter: (src) => {
const base = basename(src);
return base !== "node_modules" && base !== ".git";
},
});
const sourceDir = realpathSync(dirname(variant.inputPath));
const stagingPaths = [stagedDir, ...existingStagedDirs].map((path) => realpathSync(path));
copyVariantSiblings(sourceDir, realpathSync(stagedDir), stagingPaths);
const sourceName = basename(variant.inputPath);
if (sourceName !== "index.html") {
renameSync(join(stagedDir, sourceName), join(stagedDir, "index.html"));
@@ -243,7 +275,10 @@ export function prepareCompareVariantProjects(
continue;
}
if (stat.isFile() && extname(variant.inputPath).toLowerCase() === ".html") {
prepared.push(stageHtmlVariant(variant));
const stagedDirs = prepared.flatMap((project) =>
project.stagedDir ? [project.stagedDir] : [],
);
prepared.push(stageHtmlVariant(variant, stagedDirs));
continue;
}
throw inputError(variant);