mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 06:30:03 +00:00
feat(core,cli): figma REST client, asset import command, binding index (M0+M1) (#1870)
M0: renderNode/imageFills/variables/styles/nodeTree/fileVersion over api.figma.com with injectable fetch and typed capability errors (NO_TOKEN/BAD_TOKEN/REQUIRES_ENTERPRISE/RATE_LIMITED/RENDER_FAILED/ NODE_NOT_FOUND/HTTP_ERROR) per design spec 4.4. M1: svg sanitizer (scripts/foreignObject/handlers/external hrefs) + hyperframes figma asset: render -> sanitize -> freeze under .media/ -> manifest provenance -> snippet. Idempotent on fileKey:nodeId:format:scale:version; re-imports when the version moves. Plus the 7.1 binding index store (.media/figma-bindings.jsonl): exact-ID lookup incl. alias chains, per-project library-file answers, shared jsonl reader with the asset manifest. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
23f67da5e2
commit
fb13797d2f
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* Binding index — the machine-readable join between figma variable/style
|
||||
* identities and composition variables (design spec §7.1).
|
||||
*
|
||||
* Lives at .media/figma-bindings.jsonl, next to (but separate from) the
|
||||
* human-readable figma-tokens.json sidecar. Resolution is exact-ID only:
|
||||
* a missed link bakes a correct literal; a wrong link silently changes
|
||||
* color at the next brand refresh. Never match by value or name.
|
||||
*/
|
||||
|
||||
import { appendFileSync, mkdirSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { readJsonlValues } from "./jsonl";
|
||||
import { mediaDir } from "./manifest";
|
||||
|
||||
const BINDINGS_FILE = "figma-bindings.jsonl";
|
||||
|
||||
export interface FigmaBindingRecord {
|
||||
kind: "binding";
|
||||
/** the figma variable/style id as it appears in node data (exact match key) */
|
||||
figmaId: string;
|
||||
/** stable cross-file identity, when known ("id and key are stable over the lifetime") */
|
||||
key?: string;
|
||||
sourceFileKey: string;
|
||||
/** semantic→primitive alias chain, directly-bound id first */
|
||||
aliasChain?: string[];
|
||||
compositionVariableId: string;
|
||||
brandRole?: string;
|
||||
/** figma file version at import time — staleness check */
|
||||
version: string;
|
||||
}
|
||||
|
||||
interface LibraryRecord {
|
||||
kind: "library";
|
||||
libraryKey: string;
|
||||
fileKey: string;
|
||||
}
|
||||
|
||||
function bindingsPath(projectDir: string): string {
|
||||
return join(mediaDir(projectDir), BINDINGS_FILE);
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
function isBindingRecord(value: unknown): value is FigmaBindingRecord {
|
||||
return (
|
||||
isRecord(value) &&
|
||||
value.kind === "binding" &&
|
||||
typeof value.figmaId === "string" &&
|
||||
typeof value.sourceFileKey === "string" &&
|
||||
typeof value.compositionVariableId === "string" &&
|
||||
typeof value.version === "string"
|
||||
);
|
||||
}
|
||||
|
||||
function isLibraryRecord(value: unknown): value is LibraryRecord {
|
||||
return (
|
||||
isRecord(value) &&
|
||||
value.kind === "library" &&
|
||||
typeof value.libraryKey === "string" &&
|
||||
typeof value.fileKey === "string"
|
||||
);
|
||||
}
|
||||
|
||||
function readLines(projectDir: string): unknown[] {
|
||||
return readJsonlValues(bindingsPath(projectDir));
|
||||
}
|
||||
|
||||
function appendLine(projectDir: string, record: unknown): void {
|
||||
mkdirSync(mediaDir(projectDir), { recursive: true });
|
||||
appendFileSync(bindingsPath(projectDir), JSON.stringify(record) + "\n");
|
||||
}
|
||||
|
||||
export function readBindings(projectDir: string): FigmaBindingRecord[] {
|
||||
return readLines(projectDir).filter(isBindingRecord);
|
||||
}
|
||||
|
||||
export function appendBinding(projectDir: string, record: FigmaBindingRecord): void {
|
||||
appendLine(projectDir, record);
|
||||
}
|
||||
|
||||
/**
|
||||
* Upsert by figmaId: re-running a tokens import must REPLACE that file's
|
||||
* stale binding rows, not append duplicates — `findBindingByFigmaId`
|
||||
* returns the first match, so appended duplicates would pin lookups to the
|
||||
* stale record forever. Library rows and other files' bindings survive.
|
||||
*/
|
||||
export function upsertBindings(projectDir: string, records: FigmaBindingRecord[]): void {
|
||||
const incoming = new Set(records.map((r) => r.figmaId));
|
||||
const survivors = readLines(projectDir).filter(
|
||||
(line) => !(isBindingRecord(line) && incoming.has(line.figmaId)),
|
||||
);
|
||||
mkdirSync(mediaDir(projectDir), { recursive: true });
|
||||
const lines = [...survivors, ...records].map((r) => JSON.stringify(r)).join("\n");
|
||||
writeFileSync(bindingsPath(projectDir), lines.length > 0 ? lines + "\n" : "");
|
||||
}
|
||||
|
||||
/** Exact-ID lookup, checking alias chains too. Never value/name matching. */
|
||||
export function findBindingByFigmaId(
|
||||
projectDir: string,
|
||||
figmaId: string,
|
||||
): FigmaBindingRecord | null {
|
||||
for (const b of readBindings(projectDir)) {
|
||||
if (b.figmaId === figmaId) return b;
|
||||
if (b.aliasChain?.includes(figmaId)) return b;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Answered "which file is this library?" mappings — asked once per project. */
|
||||
export function readLibraryMap(projectDir: string): Record<string, string> {
|
||||
const map: Record<string, string> = {};
|
||||
for (const r of readLines(projectDir)) {
|
||||
if (isLibraryRecord(r)) map[r.libraryKey] = r.fileKey;
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
export function recordLibraryFile(projectDir: string, libraryKey: string, fileKey: string): void {
|
||||
const record: LibraryRecord = { kind: "library", libraryKey, fileKey };
|
||||
appendLine(projectDir, record);
|
||||
}
|
||||
Reference in New Issue
Block a user