Files
hyperframes/packages/core/src/figma/parseFigmaRef.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

33 lines
1.1 KiB
TypeScript

import type { FigmaRef } from "./types";
const FILE_KEY_RE = /\/(?:design|file|proto)\/([A-Za-z0-9]+)/;
function normalizeNodeId(raw: string): string {
return raw.replace("-", ":");
}
export function parseFigmaRef(input: string): FigmaRef {
const trimmed = input.trim();
if (trimmed.length === 0) throw new Error("parseFigmaRef: empty input");
if (!trimmed.includes("/")) {
const colon = trimmed.indexOf(":");
if (colon === -1) return { fileKey: trimmed };
const fileKey = trimmed.slice(0, colon);
const node = trimmed.slice(colon + 1);
if (fileKey.length === 0) throw new Error(`parseFigmaRef: invalid ref "${input}"`);
return node.length > 0 ? { fileKey, nodeId: normalizeNodeId(node) } : { fileKey };
}
const keyMatch = trimmed.match(FILE_KEY_RE);
const fileKey = keyMatch?.[1];
if (fileKey === undefined) throw new Error(`parseFigmaRef: no fileKey in "${input}"`);
const q = trimmed.indexOf("?");
if (q !== -1) {
const raw = new URLSearchParams(trimmed.slice(q + 1)).get("node-id");
if (raw !== null && raw.length > 0) return { fileKey, nodeId: normalizeNodeId(raw) };
}
return { fileKey };
}