@visual-layer/figma-rest 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,67 @@
1
+ export interface FigmaBbox {
2
+ x: number;
3
+ y: number;
4
+ width: number;
5
+ height: number;
6
+ }
7
+ export interface FigmaNode {
8
+ id: string;
9
+ name: string;
10
+ type: string;
11
+ visible?: boolean;
12
+ absoluteBoundingBox?: FigmaBbox;
13
+ absoluteRenderBounds?: FigmaBbox;
14
+ characters?: string;
15
+ children?: FigmaNode[];
16
+ componentPropertyDefinitions?: Record<string, unknown>;
17
+ /** Passthrough for any other Figma fields. */
18
+ [k: string]: unknown;
19
+ }
20
+ export interface FigmaNodeResult {
21
+ document: FigmaNode;
22
+ version: string;
23
+ }
24
+ export interface FigmaClient {
25
+ getFileNodes(fileKey: string, nodeIds: readonly string[]): Promise<FigmaNodeResult>;
26
+ getNodeImage(fileKey: string, nodeIds: readonly string[], opts?: {
27
+ format?: 'png' | 'jpg';
28
+ scale?: number;
29
+ }): Promise<Record<string, string | null>>;
30
+ }
31
+ export declare class FigmaClientError extends Error {
32
+ readonly retryAfterMs?: number | undefined;
33
+ readonly code: string;
34
+ constructor(code: string, message: string, retryAfterMs?: number | undefined);
35
+ }
36
+ export interface HttpFigmaOptions {
37
+ token?: string;
38
+ fetch?: typeof fetch;
39
+ maxRetries?: number;
40
+ baseUrl?: string;
41
+ }
42
+ export declare class HttpFigmaClient implements FigmaClient {
43
+ private readonly token;
44
+ private readonly fetchImpl;
45
+ private readonly maxRetries;
46
+ private readonly baseUrl;
47
+ constructor(opts?: HttpFigmaOptions);
48
+ getFileNodes(fileKey: string, nodeIds: readonly string[]): Promise<FigmaNodeResult>;
49
+ getNodeImage(fileKey: string, nodeIds: readonly string[], opts?: {
50
+ format?: 'png' | 'jpg';
51
+ scale?: number;
52
+ }): Promise<Record<string, string | null>>;
53
+ private requestJson;
54
+ }
55
+ export interface MockEntry {
56
+ document: FigmaNode;
57
+ version: string;
58
+ images?: Record<string, string | null>;
59
+ }
60
+ export declare class MockFigmaClient implements FigmaClient {
61
+ private files;
62
+ constructor(files: Record<string, MockEntry>);
63
+ getFileNodes(fileKey: string, nodeIds: readonly string[]): Promise<FigmaNodeResult>;
64
+ getNodeImage(fileKey: string, nodeIds: readonly string[]): Promise<Record<string, string | null>>;
65
+ }
66
+ export declare function findNode(root: FigmaNode, id: string): FigmaNode | null;
67
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
package/dist/client.js ADDED
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Figma REST client. HttpFigmaClient talks to api.figma.com; MockFigmaClient
3
+ * serves fixture JSON with no network. Both implement FigmaClient so capture
4
+ * logic is identical against real or mocked data.
5
+ *
6
+ * Failure modes (§18.7):
7
+ * - missing_token: no FIGMA_TOKEN configured
8
+ * - auth_failed: 401/403
9
+ * - rate_limited: 429 (honors Retry-After up to the retry cap)
10
+ */
11
+ import { ErrorCodes } from '@visual-layer/core';
12
+ export class FigmaClientError extends Error {
13
+ retryAfterMs;
14
+ code;
15
+ constructor(code, message, retryAfterMs) {
16
+ super(`${code}: ${message}`);
17
+ this.retryAfterMs = retryAfterMs;
18
+ this.name = 'FigmaClientError';
19
+ this.code = code;
20
+ }
21
+ }
22
+ const API = 'https://api.figma.com';
23
+ export class HttpFigmaClient {
24
+ token;
25
+ fetchImpl;
26
+ maxRetries;
27
+ baseUrl;
28
+ constructor(opts = {}) {
29
+ this.token = opts.token ?? process.env['FIGMA_TOKEN'];
30
+ this.fetchImpl = opts.fetch ?? fetch;
31
+ this.maxRetries = opts.maxRetries ?? 3;
32
+ this.baseUrl = opts.baseUrl ?? API;
33
+ }
34
+ async getFileNodes(fileKey, nodeIds) {
35
+ // Figma /nodes expects comma-separated ids; encode each id individually and join with a
36
+ // literal comma so the separator survives URL encoding (encodeURIComponent would turn it into %2C).
37
+ const ids = nodeIds.map((id) => encodeURIComponent(id)).join(',');
38
+ const url = `${this.baseUrl}/v1/files/${encodeURIComponent(fileKey)}/nodes?ids=${ids}`;
39
+ const body = await this.requestJson(url);
40
+ const entry = body?.nodes?.[nodeIds[0]];
41
+ if (!entry)
42
+ throw new FigmaClientError(ErrorCodes.node_not_found, `node ${nodeIds[0]} not found in file ${fileKey}`);
43
+ return { document: entry.document, version: String(body?.version ?? '') };
44
+ }
45
+ async getNodeImage(fileKey, nodeIds, opts = {}) {
46
+ const ids = nodeIds.map((id) => encodeURIComponent(id)).join(',');
47
+ const params = new URLSearchParams();
48
+ params.set('ids', ids);
49
+ if (opts.format)
50
+ params.set('format', opts.format);
51
+ if (opts.scale)
52
+ params.set('scale', String(opts.scale));
53
+ const url = `${this.baseUrl}/v1/images/${encodeURIComponent(fileKey)}?${params.toString()}`;
54
+ const body = await this.requestJson(url);
55
+ return (body?.images ?? {});
56
+ }
57
+ async requestJson(url) {
58
+ if (!this.token) {
59
+ throw new FigmaClientError(ErrorCodes.missing_token, 'FIGMA_TOKEN not set');
60
+ }
61
+ let lastErr;
62
+ for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
63
+ const res = await this.fetchImpl(url, { headers: { 'X-Figma-Token': this.token } });
64
+ if (res.status === 401 || res.status === 403) {
65
+ throw new FigmaClientError(ErrorCodes.auth_failed, `figma auth failed (${res.status})`);
66
+ }
67
+ if (res.status === 429) {
68
+ const retryAfter = parseRetryAfter(res.headers.get('retry-after'));
69
+ if (attempt < this.maxRetries) {
70
+ await sleep(retryAfter);
71
+ continue;
72
+ }
73
+ throw new FigmaClientError(ErrorCodes.rate_limited, 'figma rate limit exceeded', retryAfter);
74
+ }
75
+ if (!res.ok) {
76
+ lastErr = new FigmaClientError('figma_http_error', `figma ${res.status}`);
77
+ if (attempt < this.maxRetries && res.status >= 500) {
78
+ await sleep(backoffMs(attempt));
79
+ continue;
80
+ }
81
+ throw lastErr;
82
+ }
83
+ return await res.json();
84
+ }
85
+ throw lastErr ?? new FigmaClientError('figma_http_error', 'figma request failed');
86
+ }
87
+ }
88
+ export class MockFigmaClient {
89
+ files;
90
+ constructor(files) {
91
+ this.files = files;
92
+ }
93
+ async getFileNodes(fileKey, nodeIds) {
94
+ const entry = this.files[fileKey];
95
+ if (!entry)
96
+ throw new FigmaClientError(ErrorCodes.node_not_found, `file ${fileKey} not in mock`);
97
+ const id = nodeIds[0];
98
+ if (id !== entry.document.id && !findNode(entry.document, id)) {
99
+ throw new FigmaClientError(ErrorCodes.node_not_found, `node ${id} not in mock file ${fileKey}`);
100
+ }
101
+ return { document: entry.document, version: entry.version };
102
+ }
103
+ async getNodeImage(fileKey, nodeIds) {
104
+ const entry = this.files[fileKey];
105
+ const images = entry?.images ?? {};
106
+ const out = {};
107
+ for (const id of nodeIds)
108
+ out[id] = images[id] ?? null;
109
+ return out;
110
+ }
111
+ }
112
+ export function findNode(root, id) {
113
+ if (root.id === id)
114
+ return root;
115
+ for (const c of root.children ?? []) {
116
+ const hit = findNode(c, id);
117
+ if (hit)
118
+ return hit;
119
+ }
120
+ return null;
121
+ }
122
+ function parseRetryAfter(h) {
123
+ if (!h)
124
+ return backoffMs(0);
125
+ const secs = Number(h);
126
+ if (Number.isFinite(secs))
127
+ return Math.min(secs * 1000, 10_000);
128
+ return backoffMs(0);
129
+ }
130
+ function backoffMs(attempt) {
131
+ return Math.min(1000 * 2 ** attempt, 8000);
132
+ }
133
+ function sleep(ms) {
134
+ return new Promise((r) => setTimeout(r, ms));
135
+ }
136
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @visual-layer/figma-rest public surface.
3
+ */
4
+ export * from './client.js';
5
+ export * from './normalize.js';
6
+ export declare const FIGMA_REST_VERSION = "0.1.0";
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAE/B,eAAO,MAAM,kBAAkB,UAAU,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @visual-layer/figma-rest public surface.
3
+ */
4
+ export * from './client.js';
5
+ export * from './normalize.js';
6
+ export const FIGMA_REST_VERSION = '0.1.0';
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAE/B,MAAM,CAAC,MAAM,kBAAkB,GAAG,OAAO,CAAC"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Normalize a Figma node tree into a ScreenGraph (source: "figma").
3
+ * Structure-first: derive interactables from FRAME / COMPONENT / INSTANCE / TEXT
4
+ * nodes, not from rendered pixels.
5
+ */
6
+ import { type ScreenGraph, type Warning } from '@visual-layer/core';
7
+ import type { FigmaClient } from './client.js';
8
+ export interface CaptureFigmaOptions {
9
+ fileKey: string;
10
+ nodeId: string;
11
+ /** Fetch a rendered image and attach as a screenshot artifact reference. */
12
+ includeImage?: boolean;
13
+ /** Optional explicit graph id. */
14
+ id?: string;
15
+ /** Override the viewport (Figma frames have their own size). */
16
+ viewport?: ScreenGraph['viewport'];
17
+ }
18
+ export interface CaptureFigmaResult {
19
+ graph: ScreenGraph;
20
+ /** Rendered image URL per node id, when includeImage was set. */
21
+ imageUrls?: Record<string, string | null>;
22
+ warnings: Warning[];
23
+ }
24
+ export declare function captureFigma(client: FigmaClient, opts: CaptureFigmaOptions): Promise<CaptureFigmaResult>;
25
+ //# sourceMappingURL=normalize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalize.d.ts","sourceRoot":"","sources":["../src/normalize.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EACL,KAAK,WAAW,EAEhB,KAAK,OAAO,EAQb,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAa,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1D,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,4EAA4E;IAC5E,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,kCAAkC;IAClC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,gEAAgE;IAChE,QAAQ,CAAC,EAAE,WAAW,CAAC,UAAU,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,WAAW,CAAC;IACnB,iEAAiE;IACjE,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC;IAC1C,QAAQ,EAAE,OAAO,EAAE,CAAC;CACrB;AAID,wBAAsB,YAAY,CAAC,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAkF9G"}
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Normalize a Figma node tree into a ScreenGraph (source: "figma").
3
+ * Structure-first: derive interactables from FRAME / COMPONENT / INSTANCE / TEXT
4
+ * nodes, not from rendered pixels.
5
+ */
6
+ import { SCHEMA_VERSION, deterministicId, warn, info, } from '@visual-layer/core';
7
+ const INTERACTIVE_TYPES = new Set(['COMPONENT', 'INSTANCE', 'FRAME', 'TEXT', 'RECTANGLE']);
8
+ export async function captureFigma(client, opts) {
9
+ const { document, version } = await client.getFileNodes(opts.fileKey, [opts.nodeId]);
10
+ const warnings = [];
11
+ const interactables = [];
12
+ const seenLabels = new Map();
13
+ const flat = [];
14
+ flatten(document, flat);
15
+ for (const node of flat) {
16
+ const type = node.type.toUpperCase();
17
+ if (!INTERACTIVE_TYPES.has(type))
18
+ continue;
19
+ if (node.visible === false) {
20
+ warnings.push(info('hidden_figma_node', `node "${node.name}" (${node.id}) invisible`, undefined));
21
+ continue;
22
+ }
23
+ const bb = node.absoluteBoundingBox;
24
+ if (!bb) {
25
+ warnings.push(warn('figma_node_missing_bbox', `node "${node.name}" has no bbox`, undefined));
26
+ continue;
27
+ }
28
+ const bbox = [bb.x, bb.y, bb.width, bb.height];
29
+ const role = inferRole(node);
30
+ const label = (node.characters ?? node.name ?? '').toString();
31
+ const sources = ['figma'];
32
+ const inter = {
33
+ id: deterministicId('i', `figma:${node.id}`),
34
+ role,
35
+ name: node.name,
36
+ text: node.characters,
37
+ bbox,
38
+ coordinateSpace: 'figma',
39
+ source: sources,
40
+ confidence: 0.85,
41
+ figmaNodeId: node.id,
42
+ };
43
+ interactables.push(inter);
44
+ if (label)
45
+ seenLabels.set(label, (seenLabels.get(label) ?? 0) + 1);
46
+ }
47
+ for (const [label, count] of seenLabels) {
48
+ if (count > 1)
49
+ warnings.push(warn('duplicate_label', `"${label}" appears ${count}x`, undefined));
50
+ }
51
+ let imageUrls;
52
+ let screenshot;
53
+ if (opts.includeImage) {
54
+ imageUrls = await client.getNodeImage(opts.fileKey, [opts.nodeId]).catch(() => ({ [opts.nodeId]: null }));
55
+ const url = imageUrls[opts.nodeId];
56
+ if (!url) {
57
+ warnings.push(warn('image_render_null', `image render for ${opts.nodeId} returned null`, undefined));
58
+ }
59
+ else {
60
+ // URL recorded; bytes are fetched separately at the artifact-store layer.
61
+ // ponytail: MVP stores URL ref, not bytes — fetching/validating PNG bytes is post-MVP.
62
+ screenshot = {
63
+ artifactId: deterministicId('img', url),
64
+ sha256: '0'.repeat(64), // placeholder; real hash computed when bytes are materialized
65
+ width: 0,
66
+ height: 0,
67
+ mimeType: 'image/png',
68
+ };
69
+ }
70
+ }
71
+ const graph = {
72
+ id: opts.id ?? deterministicId('sg', `figma:${opts.fileKey}:${opts.nodeId}:${version}`),
73
+ version: SCHEMA_VERSION,
74
+ source: 'figma',
75
+ createdAt: new Date().toISOString(),
76
+ ...(opts.viewport ? { viewport: opts.viewport } : {}),
77
+ ...(screenshot ? { screenshot } : {}),
78
+ interactables,
79
+ mappings: [],
80
+ figmaNodes: flat,
81
+ warnings,
82
+ provenance: [
83
+ { field: 'interactables', source: 'figma', confidence: 0.85, note: `fileKey=${opts.fileKey} node=${opts.nodeId} v=${version}` },
84
+ ],
85
+ };
86
+ return { graph, imageUrls, warnings };
87
+ }
88
+ function flatten(node, out) {
89
+ out.push(node);
90
+ for (const c of node.children ?? [])
91
+ flatten(c, out);
92
+ }
93
+ function inferRole(node) {
94
+ const t = node.type.toUpperCase();
95
+ if (t === 'COMPONENT' || t === 'INSTANCE')
96
+ return 'button';
97
+ if (t === 'TEXT')
98
+ return 'text';
99
+ if (t === 'FRAME')
100
+ return 'group';
101
+ return 'figure';
102
+ }
103
+ //# sourceMappingURL=normalize.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalize.js","sourceRoot":"","sources":["../src/normalize.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@visual-layer/figma-rest",
3
+ "version": "1.0.0",
4
+ "description": "Figma REST capture with mockable client, rate-limit, retry",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://gitlab.com/locotine/vision-layer.git",
10
+ "directory": "packages/figma-rest"
11
+ },
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "main": "./dist/index.js",
16
+ "types": "./dist/index.d.ts",
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.ts",
20
+ "import": "./dist/index.js"
21
+ }
22
+ },
23
+ "files": [
24
+ "dist"
25
+ ],
26
+ "dependencies": {
27
+ "zod": "^3.23.8",
28
+ "@visual-layer/core": "1.0.0"
29
+ },
30
+ "scripts": {
31
+ "build": "tsc -b",
32
+ "typecheck": "tsc -b"
33
+ }
34
+ }