Files
hyperframes/packages/core/src/figma/manifest.test.ts
T
Vance Ingalls 397c3ba7a8 feat(core): figma module foundations — types, parseFigmaRef, freeze, manifest, asset snippet (#1868)
## What

Foundations of the `@hyperframes/core/figma` module — the pure, transport-agnostic layer every later phase builds on:

- **`types.ts`** — `FigmaRef`, `FigmaProvenance`, `FigmaManifestRecord`, and the Motion model (`MotionDoc`/`MotionTrack`/`TimelineSpec`/`GsapTween`) shared across the stack.
- **`parseFigmaRef`** — normalizes any user input (full `/design|/file|/proto` URLs with `?node-id=1-2`, `fileKey:nodeId` shorthand, bare `fileKey`) into `{ fileKey, nodeId }`, including the URL-dash → API-colon node-id conversion.
- **`freeze.ts`** — `freezeBytes`/`freezeUrl`/`freezeLocalFile` with a 256 MB cap; every Figma asset is frozen to a local file before it can reach a composition (determinism: no render-time network).
- **`manifest.ts`** — the `.media/manifest.jsonl` ledger (same layout `media-use` writes, so a project has one shared media inventory without either skill depending on the other): append/read/find-by-node/next-id, with a pure type-guard (`isFigmaManifestRecord`) instead of `as`-casts.
- **`assetSnippet.ts`** — manifest record → composition `<img>` snippet with escaped attrs + `data-figma-id`.
- **publishConfig fix** — `./figma` added to `packages/core` `publishConfig.exports` (the packed-manifest CI gate requires every source export to have a dist mapping).

## Why

Design spec: `docs/superpowers/specs/2026-06-30-figma-asset-integration-design.md`. These functions are deliberately transport-agnostic — when the project reversed from MCP-first to a REST/MCP split (spec §2), nothing in this layer changed. That was the point.

## Tests

Unit tests per module (URL variants, freeze cap edges, manifest round-trip/malformed-line tolerance, snippet escaping). All colocated `*.test.ts`, vitest, no network.

---
Stack (1/6): this PR → #1869#1870#1871#1872#1873

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-03 14:12:24 -07:00

94 lines
3.0 KiB
TypeScript

// @vitest-environment node
import { describe, expect, it, afterEach } from "vitest";
import { appendFileSync, mkdirSync, mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { appendRecord, findByFigmaNode, manifestPath, nextId, readManifest } from "./manifest";
import type { FigmaManifestRecord } from "./types";
const dirs: string[] = [];
function project(): string {
const d = mkdtempSync(join(tmpdir(), "hf-manifest-"));
dirs.push(d);
return d;
}
afterEach(() => {
for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true });
});
function rec(id: string, nodeId: string): FigmaManifestRecord {
return {
id,
type: "image",
path: `.media/images/${id}.png`,
source: "figma",
provenance: { source: "figma", fileKey: "FK", nodeId, format: "png" },
};
}
describe("manifest", () => {
it("appends and reads back records", () => {
const p = project();
appendRecord(p, rec("image_001", "1:2"));
appendRecord(p, rec("image_002", "3:4"));
const all = readManifest(p);
expect(all.map((r) => r.id)).toEqual(["image_001", "image_002"]);
expect(all[1]?.provenance.nodeId).toBe("3:4");
});
it("finds a record by figma node", () => {
const p = project();
appendRecord(p, rec("image_001", "1:2"));
expect(findByFigmaNode(p, "FK", "1:2")?.id).toBe("image_001");
expect(findByFigmaNode(p, "FK", "9:9")).toBeNull();
});
it("allocates incrementing ids per type", () => {
const p = project();
expect(nextId(p, "image")).toBe("image_001");
appendRecord(p, rec("image_001", "1:2"));
expect(nextId(p, "image")).toBe("image_002");
});
it("skips a manifest line that doesn't match the record shape", () => {
const p = project();
appendRecord(p, rec("image_001", "1:2"));
appendFileSync(manifestPath(p), JSON.stringify({ foo: "bar" }) + "\n");
appendRecord(p, rec("image_002", "3:4"));
expect(readManifest(p).map((r) => r.id)).toEqual(["image_001", "image_002"]);
});
it("nextId scans other writers' rows (media-use) so ids never collide", () => {
const p = project();
mkdirSync(join(p, ".media"), { recursive: true });
// media-use shaped row: no provenance.source — fails the figma guard
appendFileSync(
manifestPath(p),
JSON.stringify({
id: "image_007",
type: "image",
path: ".media/images/image_007.png",
source: "unsplash",
provenance: { provider: "unsplash" },
}) + "\n",
);
expect(nextId(p, "image")).toBe("image_008");
});
it("rejects manifest rows with a non image/video type", () => {
const p = project();
mkdirSync(join(p, ".media"), { recursive: true });
appendFileSync(
manifestPath(p),
JSON.stringify({
id: "audio_001",
type: "audio",
path: "x",
source: "figma:F/1",
provenance: { source: "figma", fileKey: "F", nodeId: "1:1" },
}) + "\n",
);
expect(readManifest(p)).toHaveLength(0);
});
});