mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
fix(cli): reuse duplicate Figma nodes within asset batches (#5276)
This commit is contained in:
@@ -9,7 +9,7 @@ import {
|
||||
runAssetImportMany,
|
||||
type AssetImportDeps,
|
||||
} from "./asset.js";
|
||||
import { FigmaClientError, type FigmaClient } from "@hyperframes/core/figma";
|
||||
import { FigmaClientError, readManifest, type FigmaClient } from "@hyperframes/core/figma";
|
||||
|
||||
const dirs: string[] = [];
|
||||
function scratch(): string {
|
||||
@@ -179,6 +179,104 @@ describe("runAssetImport", () => {
|
||||
expect(new Set(results.map((r) => r.record.id)).size).toBe(3);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["KEY:1-2", "KEY:1-2", "KEY:3-4", "KEY:1-2"],
|
||||
["KEY:1-2", "https://www.figma.com/design/KEY/F?node-id=1-2", "KEY:3-4", "KEY:1:2"],
|
||||
])("imports each normalized node once for %j", async (...refs) => {
|
||||
const dir = scratch();
|
||||
const requests: string[][] = [];
|
||||
let downloads = 0;
|
||||
const importDeps = deps(dir, {
|
||||
client: fakeClient({
|
||||
renderNodes: (_fileKey, nodeIds, opts) => {
|
||||
requests.push([...nodeIds]);
|
||||
return Promise.resolve(
|
||||
nodeIds.map((nodeId) => ({ nodeId, url: `https://cdn/${nodeId}`, ext: opts.format })),
|
||||
);
|
||||
},
|
||||
}),
|
||||
download: async () => {
|
||||
downloads += 1;
|
||||
return PNG_BYTES;
|
||||
},
|
||||
});
|
||||
const results = await runAssetImportMany(refs, { format: "png" }, importDeps);
|
||||
expect(requests).toEqual([["1:2", "3:4"]]);
|
||||
expect(downloads).toBe(2);
|
||||
expect(results.map((r) => r.record.id)).toEqual([
|
||||
"image_001",
|
||||
"image_001",
|
||||
"image_002",
|
||||
"image_001",
|
||||
]);
|
||||
expect(results.map((r) => r.reused)).toEqual([false, true, false, true]);
|
||||
expect(readManifest(dir)).toHaveLength(2);
|
||||
const repeated = await runAssetImportMany(refs, { format: "png" }, importDeps);
|
||||
expect(repeated.every((r) => r.reused)).toBe(true);
|
||||
expect(requests).toHaveLength(1);
|
||||
expect(downloads).toBe(2);
|
||||
});
|
||||
|
||||
it("imports one missing node among duplicated cached and missing references", async () => {
|
||||
const dir = scratch();
|
||||
const cached = await runAssetImport("KEY:1-2", { format: "png" }, deps(dir));
|
||||
const requests: string[][] = [];
|
||||
const results = await runAssetImportMany(
|
||||
["KEY:1-2", "KEY:3-4", "KEY:1:2", "KEY:3:4"],
|
||||
{ format: "png", description: "Updated" },
|
||||
deps(dir, {
|
||||
client: fakeClient({
|
||||
renderNodes: (_fileKey, nodeIds) => {
|
||||
requests.push([...nodeIds]);
|
||||
return Promise.resolve(
|
||||
nodeIds.map((nodeId) => ({ nodeId, url: `https://cdn/${nodeId}`, ext: "png" })),
|
||||
);
|
||||
},
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(requests).toEqual([["3:4"]]);
|
||||
expect(results.map((r) => r.record.id)).toEqual([
|
||||
cached.record.id,
|
||||
"image_002",
|
||||
cached.record.id,
|
||||
"image_002",
|
||||
]);
|
||||
expect(results.map((r) => r.reused)).toEqual([true, false, true, true]);
|
||||
expect(results.every((r) => r.record.description === "Updated")).toBe(true);
|
||||
expect(readManifest(dir)).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("records one completed duplicate node when a later batch node fails", async () => {
|
||||
const dir = scratch();
|
||||
let downloads = 0;
|
||||
await expect(
|
||||
runAssetImportMany(
|
||||
["KEY:1-2", "KEY:1:2", "KEY:3-4"],
|
||||
{ format: "png" },
|
||||
deps(dir, {
|
||||
client: fakeClient({
|
||||
renderNodes: (_fileKey, nodeIds) =>
|
||||
Promise.resolve(
|
||||
nodeIds.map((nodeId) => ({
|
||||
nodeId,
|
||||
url: nodeId === "1:2" ? "https://cdn/1" : null,
|
||||
ext: "png",
|
||||
})),
|
||||
),
|
||||
}),
|
||||
download: async () => {
|
||||
downloads += 1;
|
||||
return PNG_BYTES;
|
||||
},
|
||||
}),
|
||||
),
|
||||
).rejects.toMatchObject({ code: "RENDER_FAILED", endpoint: "images" });
|
||||
expect(downloads).toBe(1);
|
||||
expect(readManifest(dir)).toHaveLength(1);
|
||||
expect(readFileSync(join(dir, ".media", "index.md"), "utf8")).toContain("1 asset");
|
||||
});
|
||||
|
||||
it("labels a batch-miss RENDER_FAILED with the images endpoint (telemetry parity with client.ts)", async () => {
|
||||
const dir = scratch();
|
||||
const missClient = fakeClient({
|
||||
|
||||
@@ -194,7 +194,13 @@ export async function runAssetImportMany(
|
||||
const slots: (AssetImportResult | null)[] = refs.map((r) =>
|
||||
reuseExisting(fileKey, r.nodeId, opts, version, deps, description, entity),
|
||||
);
|
||||
const missIndexes = slots.flatMap((s, i) => (s === null ? [i] : []));
|
||||
const nodeIndexes = new Map<string, number>();
|
||||
const missIndexes: number[] = [];
|
||||
for (const [i, ref] of refs.entries()) {
|
||||
if (nodeIndexes.has(ref.nodeId)) continue;
|
||||
nodeIndexes.set(ref.nodeId, i);
|
||||
if (slots[i] === null) missIndexes.push(i);
|
||||
}
|
||||
try {
|
||||
if (missIndexes.length > 0) {
|
||||
const missNodeIds = missIndexes.map((i) => refs[i]!.nodeId);
|
||||
@@ -231,9 +237,11 @@ export async function runAssetImportMany(
|
||||
// next import.
|
||||
safeRegenerateIndex(deps.projectDir);
|
||||
}
|
||||
return slots.map((s, i) => {
|
||||
if (!s) throw new Error(`figma asset import produced no result for "${refInputs[i]}"`);
|
||||
return s;
|
||||
return refs.map((ref, i) => {
|
||||
const firstIndex = nodeIndexes.get(ref.nodeId) ?? i;
|
||||
const result = slots[firstIndex];
|
||||
if (!result) throw new Error(`figma asset import produced no result for "${refInputs[i]}"`);
|
||||
return firstIndex === i ? result : { ...result, reused: true };
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user