localhost-figma-mcp 1.5.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.
- package/LICENSE +19 -0
- package/README.md +77 -0
- package/dist/THIRD_PARTY_LICENSES.txt +306 -0
- package/dist/figma-plugin/code.js +455 -0
- package/dist/figma-plugin/manifest.json +23 -0
- package/dist/figma-plugin/ui.html +961 -0
- package/dist/server.js +36966 -0
- package/dist/status.js +16 -0
- package/package.json +51 -0
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
(() => {
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __defProps = Object.defineProperties;
|
|
5
|
+
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
|
|
6
|
+
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
9
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
10
|
+
var __spreadValues = (a, b) => {
|
|
11
|
+
for (var prop in b || (b = {}))
|
|
12
|
+
if (__hasOwnProp.call(b, prop))
|
|
13
|
+
__defNormalProp(a, prop, b[prop]);
|
|
14
|
+
if (__getOwnPropSymbols)
|
|
15
|
+
for (var prop of __getOwnPropSymbols(b)) {
|
|
16
|
+
if (__propIsEnum.call(b, prop))
|
|
17
|
+
__defNormalProp(a, prop, b[prop]);
|
|
18
|
+
}
|
|
19
|
+
return a;
|
|
20
|
+
};
|
|
21
|
+
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
|
|
22
|
+
|
|
23
|
+
// src/plugin/build-layers.ts
|
|
24
|
+
var AUTO_LAYOUT_TOLERANCE = 2;
|
|
25
|
+
var WEIGHT_NAMES = {
|
|
26
|
+
100: ["Thin", "Hairline"],
|
|
27
|
+
200: ["ExtraLight", "Extra Light", "UltraLight"],
|
|
28
|
+
300: ["Light"],
|
|
29
|
+
400: ["Regular", "Normal", "Book"],
|
|
30
|
+
500: ["Medium"],
|
|
31
|
+
600: ["SemiBold", "Semi Bold", "DemiBold"],
|
|
32
|
+
700: ["Bold"],
|
|
33
|
+
800: ["ExtraBold", "Extra Bold", "UltraBold"],
|
|
34
|
+
900: ["Black", "Heavy"]
|
|
35
|
+
};
|
|
36
|
+
async function createFontPicker(substituted) {
|
|
37
|
+
var _a;
|
|
38
|
+
await figma.loadFontAsync({ family: "Inter", style: "Regular" }).catch(() => {
|
|
39
|
+
});
|
|
40
|
+
const styles = /* @__PURE__ */ new Map();
|
|
41
|
+
for (const { fontName } of await figma.listAvailableFontsAsync()) {
|
|
42
|
+
styles.set(fontName.family, [...(_a = styles.get(fontName.family)) != null ? _a : [], fontName.style]);
|
|
43
|
+
}
|
|
44
|
+
const cache = /* @__PURE__ */ new Map();
|
|
45
|
+
return async (requested, weight, italic) => {
|
|
46
|
+
var _a2, _b, _c;
|
|
47
|
+
const key = `${requested}|${weight}|${italic}`;
|
|
48
|
+
const cached = cache.get(key);
|
|
49
|
+
if (cached) return cached;
|
|
50
|
+
const family = styles.has(requested) ? requested : styles.has("Inter") ? "Inter" : [...styles.keys()][0];
|
|
51
|
+
const have = (_a2 = styles.get(family)) != null ? _a2 : [];
|
|
52
|
+
const names = WEIGHT_NAMES[Math.min(900, Math.max(100, Math.round(weight / 100) * 100))];
|
|
53
|
+
const italicNames = names.map((n) => n === "Regular" ? "Italic" : `${n} Italic`);
|
|
54
|
+
const wanted = italic ? italicNames : names;
|
|
55
|
+
const style = (_c = (_b = wanted.concat(names, "Regular").find((s) => have.includes(s))) != null ? _b : have[0]) != null ? _c : "Regular";
|
|
56
|
+
const font = { family, style };
|
|
57
|
+
if (family !== requested) substituted.add(`${requested} \u2192 ${family}`);
|
|
58
|
+
else if (!wanted.includes(style)) substituted.add(`${requested} ${wanted[0]} \u2192 ${style}`);
|
|
59
|
+
await figma.loadFontAsync(font);
|
|
60
|
+
cache.set(key, font);
|
|
61
|
+
return font;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
var solid = (c) => ({ type: "SOLID", color: c.rgb, opacity: c.a });
|
|
65
|
+
async function buildText(layer, context) {
|
|
66
|
+
const node = figma.createText();
|
|
67
|
+
node.fontName = await context.pickFont(layer.font, layer.weight, layer.italic);
|
|
68
|
+
if (layer.requestedFont) context.substitutedFonts.add(`${layer.requestedFont} \u2192 ${node.fontName.family}`);
|
|
69
|
+
node.characters = layer.text;
|
|
70
|
+
node.fontSize = Math.max(1, layer.size);
|
|
71
|
+
node.fills = [solid(layer.color)];
|
|
72
|
+
if (layer.lineHeight) node.lineHeight = { value: layer.lineHeight, unit: "PIXELS" };
|
|
73
|
+
if (layer.letterSpacing) node.letterSpacing = { value: layer.letterSpacing, unit: "PIXELS" };
|
|
74
|
+
node.textAlignHorizontal = layer.align;
|
|
75
|
+
if (layer.decoration) node.textDecoration = layer.decoration;
|
|
76
|
+
if (layer.singleLine) {
|
|
77
|
+
node.textAutoResize = "WIDTH_AND_HEIGHT";
|
|
78
|
+
} else {
|
|
79
|
+
node.resize(Math.max(1, layer.w + 2), node.height);
|
|
80
|
+
node.textAutoResize = "HEIGHT";
|
|
81
|
+
}
|
|
82
|
+
return node;
|
|
83
|
+
}
|
|
84
|
+
function buildSvg(layer) {
|
|
85
|
+
try {
|
|
86
|
+
const node = figma.createNodeFromSvg(layer.svg);
|
|
87
|
+
if (node.width > 0 && layer.w > 0) node.rescale(layer.w / node.width);
|
|
88
|
+
return node;
|
|
89
|
+
} catch (e) {
|
|
90
|
+
const placeholder = figma.createFrame();
|
|
91
|
+
placeholder.fills = [];
|
|
92
|
+
placeholder.resize(Math.max(0.01, layer.w), Math.max(0.01, layer.h));
|
|
93
|
+
return placeholder;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function frameFills(layer) {
|
|
97
|
+
const fills = [];
|
|
98
|
+
if (layer.bg) fills.push(solid(layer.bg));
|
|
99
|
+
if (layer.gradients) fills.push(...layer.gradients);
|
|
100
|
+
if (layer.image) {
|
|
101
|
+
try {
|
|
102
|
+
const imageHash = figma.createImage(figma.base64Decode(layer.image)).hash;
|
|
103
|
+
fills.push({ type: "IMAGE", imageHash, scaleMode: layer.fit === "contain" ? "FIT" : "FILL" });
|
|
104
|
+
} catch (e) {
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return fills;
|
|
108
|
+
}
|
|
109
|
+
var boxes = (kids) => kids.map((k) => ({ x: k.x, y: k.y, w: k.width, h: k.height }));
|
|
110
|
+
var drifted = (kids, expected) => kids.some((k, i) => {
|
|
111
|
+
const e = expected[i];
|
|
112
|
+
return [k.x - e.x, k.y - e.y, k.width - e.w, k.height - e.h].some((d) => Math.abs(d) > AUTO_LAYOUT_TOLERANCE);
|
|
113
|
+
});
|
|
114
|
+
function restore(node, layer, kids, expected) {
|
|
115
|
+
kids.forEach((k, i) => {
|
|
116
|
+
const e = expected[i];
|
|
117
|
+
if (Math.abs(k.width - e.w) > 0.01 || Math.abs(k.height - e.h) > 0.01) {
|
|
118
|
+
if (k.type === "TEXT") {
|
|
119
|
+
const mode = k.textAutoResize;
|
|
120
|
+
k.resize(Math.max(1, e.w), Math.max(1, e.h));
|
|
121
|
+
k.textAutoResize = mode;
|
|
122
|
+
} else {
|
|
123
|
+
k.resizeWithoutConstraints(Math.max(0.01, e.w), Math.max(0.01, e.h));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
k.x = e.x;
|
|
127
|
+
k.y = e.y;
|
|
128
|
+
});
|
|
129
|
+
node.resize(Math.max(0.01, layer.w), Math.max(0.01, layer.h));
|
|
130
|
+
}
|
|
131
|
+
function revert(node, layer, kids, expected) {
|
|
132
|
+
for (const k of kids) if (k.layoutPositioning === "ABSOLUTE") k.layoutPositioning = "AUTO";
|
|
133
|
+
node.layoutMode = "NONE";
|
|
134
|
+
restore(node, layer, kids, expected);
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
function spacingFromPositions(settings, flow, w, h) {
|
|
138
|
+
var _a;
|
|
139
|
+
const horizontal = settings.direction === "HORIZONTAL";
|
|
140
|
+
if (settings.wrap || !flow.length || settings.justify !== "MIN" && settings.justify !== "MAX") return void 0;
|
|
141
|
+
const start = (b) => horizontal ? b.x : b.y;
|
|
142
|
+
const size2 = (b) => horizontal ? b.w : b.h;
|
|
143
|
+
const cross = (b) => horizontal ? b.y : b.x;
|
|
144
|
+
const gaps = flow.slice(1).map((b, i) => start(b) - start(flow[i]) - size2(flow[i]));
|
|
145
|
+
if (gaps.some((g) => Math.abs(g - gaps[0]) > 1)) return void 0;
|
|
146
|
+
const padding = [...settings.padding];
|
|
147
|
+
const last = flow[flow.length - 1];
|
|
148
|
+
if (settings.justify === "MIN") padding[horizontal ? 3 : 0] = start(flow[0]);
|
|
149
|
+
else padding[horizontal ? 1 : 2] = (horizontal ? w : h) - start(last) - size2(last);
|
|
150
|
+
if (settings.align === "MIN" && flow.every((b) => Math.abs(cross(b) - cross(flow[0])) <= 1)) {
|
|
151
|
+
padding[horizontal ? 0 : 3] = cross(flow[0]);
|
|
152
|
+
}
|
|
153
|
+
return padding.some((p) => p < 0) ? void 0 : { gap: (_a = gaps[0]) != null ? _a : settings.gap, padding };
|
|
154
|
+
}
|
|
155
|
+
function applyAutoLayout(node, layer) {
|
|
156
|
+
const settings = layer.autoLayout;
|
|
157
|
+
const kids = [...node.children];
|
|
158
|
+
if (!settings || !kids.length) return false;
|
|
159
|
+
const expected = boxes(kids);
|
|
160
|
+
const horizontal = settings.direction === "HORIZONTAL";
|
|
161
|
+
const configure = (gap, padding) => {
|
|
162
|
+
node.layoutMode = settings.direction;
|
|
163
|
+
node.primaryAxisSizingMode = "FIXED";
|
|
164
|
+
node.counterAxisSizingMode = "FIXED";
|
|
165
|
+
node.layoutWrap = settings.wrap && horizontal ? "WRAP" : "NO_WRAP";
|
|
166
|
+
if (node.layoutWrap === "WRAP") node.counterAxisSpacing = settings.counterGap;
|
|
167
|
+
node.itemSpacing = gap;
|
|
168
|
+
[node.paddingTop, node.paddingRight, node.paddingBottom, node.paddingLeft] = padding;
|
|
169
|
+
node.primaryAxisAlignItems = settings.justify;
|
|
170
|
+
node.counterAxisAlignItems = settings.align === "BASELINE" && !horizontal ? "MIN" : settings.align;
|
|
171
|
+
kids.forEach((k, i) => {
|
|
172
|
+
var _a;
|
|
173
|
+
if ((_a = layer.children[i]) == null ? void 0 : _a.absolute) k.layoutPositioning = "ABSOLUTE";
|
|
174
|
+
});
|
|
175
|
+
restore(node, layer, kids, expected);
|
|
176
|
+
};
|
|
177
|
+
configure(settings.gap, settings.padding);
|
|
178
|
+
if (drifted(kids, expected)) {
|
|
179
|
+
const flow = expected.filter((_, i) => {
|
|
180
|
+
var _a;
|
|
181
|
+
return !((_a = layer.children[i]) == null ? void 0 : _a.absolute);
|
|
182
|
+
});
|
|
183
|
+
const spacing = spacingFromPositions(settings, flow, layer.w, layer.h);
|
|
184
|
+
if (!spacing) return revert(node, layer, kids, expected);
|
|
185
|
+
configure(spacing.gap, spacing.padding);
|
|
186
|
+
if (drifted(kids, expected)) return revert(node, layer, kids, expected);
|
|
187
|
+
}
|
|
188
|
+
kids.forEach((k, i) => {
|
|
189
|
+
const child = layer.children[i];
|
|
190
|
+
if (!child || child.absolute) return;
|
|
191
|
+
if (child.grow) {
|
|
192
|
+
k.layoutGrow = 1;
|
|
193
|
+
if (drifted(kids, expected)) {
|
|
194
|
+
k.layoutGrow = 0;
|
|
195
|
+
restore(node, layer, kids, expected);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (child.stretch) {
|
|
199
|
+
k.layoutAlign = "STRETCH";
|
|
200
|
+
if (drifted(kids, expected)) {
|
|
201
|
+
k.layoutAlign = "INHERIT";
|
|
202
|
+
restore(node, layer, kids, expected);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
function trackSpan(position, size2, starts) {
|
|
209
|
+
const count = (limit) => starts.filter((s) => s < limit).length - 1;
|
|
210
|
+
const first = Math.max(0, count(position + AUTO_LAYOUT_TOLERANCE));
|
|
211
|
+
const last = Math.max(first, count(position + size2 - AUTO_LAYOUT_TOLERANCE));
|
|
212
|
+
return [first, last - first + 1];
|
|
213
|
+
}
|
|
214
|
+
function cellAlign(offset, size2, cell) {
|
|
215
|
+
if (Math.abs(offset - (cell - size2) / 2) <= AUTO_LAYOUT_TOLERANCE && offset > AUTO_LAYOUT_TOLERANCE) return "CENTER";
|
|
216
|
+
if (Math.abs(offset - (cell - size2)) <= AUTO_LAYOUT_TOLERANCE && offset > AUTO_LAYOUT_TOLERANCE) return "MAX";
|
|
217
|
+
return "MIN";
|
|
218
|
+
}
|
|
219
|
+
function applyGrid(node, layer) {
|
|
220
|
+
const grid = layer.grid;
|
|
221
|
+
const kids = [...node.children];
|
|
222
|
+
if (!grid || !kids.length) return false;
|
|
223
|
+
const expected = boxes(kids);
|
|
224
|
+
const starts = (sizes, gap, offset) => {
|
|
225
|
+
let at = offset;
|
|
226
|
+
return sizes.map((size2) => {
|
|
227
|
+
const start = at;
|
|
228
|
+
at += size2 + gap;
|
|
229
|
+
return start;
|
|
230
|
+
});
|
|
231
|
+
};
|
|
232
|
+
const columns = starts(grid.columns, grid.columnGap, grid.padding[3]);
|
|
233
|
+
const rows = starts(grid.rows, grid.rowGap, grid.padding[0]);
|
|
234
|
+
const extent = (tracks, sizes, first, span) => tracks[first + span - 1] + sizes[first + span - 1] - tracks[first];
|
|
235
|
+
try {
|
|
236
|
+
node.layoutMode = "GRID";
|
|
237
|
+
node.gridColumnCount = grid.columns.length;
|
|
238
|
+
node.gridRowCount = grid.rows.length;
|
|
239
|
+
node.gridColumnSizes = grid.columns.map((value) => ({ type: "FIXED", value }));
|
|
240
|
+
node.gridRowSizes = grid.rows.map((value) => ({ type: "FIXED", value }));
|
|
241
|
+
node.gridColumnGap = grid.columnGap;
|
|
242
|
+
node.gridRowGap = grid.rowGap;
|
|
243
|
+
[node.paddingTop, node.paddingRight, node.paddingBottom, node.paddingLeft] = grid.padding;
|
|
244
|
+
kids.forEach((k, i) => {
|
|
245
|
+
var _a;
|
|
246
|
+
if ((_a = layer.children[i]) == null ? void 0 : _a.absolute) {
|
|
247
|
+
k.layoutPositioning = "ABSOLUTE";
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const e = expected[i];
|
|
251
|
+
const [column, columnSpan] = trackSpan(e.x, e.w, columns);
|
|
252
|
+
const [row, rowSpan] = trackSpan(e.y, e.h, rows);
|
|
253
|
+
k.setGridChildPosition(row, column);
|
|
254
|
+
k.gridColumnSpan = columnSpan;
|
|
255
|
+
k.gridRowSpan = rowSpan;
|
|
256
|
+
k.layoutSizingHorizontal = "FIXED";
|
|
257
|
+
k.layoutSizingVertical = "FIXED";
|
|
258
|
+
k.gridChildHorizontalAlign = cellAlign(
|
|
259
|
+
e.x - columns[column],
|
|
260
|
+
e.w,
|
|
261
|
+
extent(columns, grid.columns, column, columnSpan)
|
|
262
|
+
);
|
|
263
|
+
k.gridChildVerticalAlign = cellAlign(e.y - rows[row], e.h, extent(rows, grid.rows, row, rowSpan));
|
|
264
|
+
});
|
|
265
|
+
restore(node, layer, kids, expected);
|
|
266
|
+
return drifted(kids, expected) ? revert(node, layer, kids, expected) : true;
|
|
267
|
+
} catch (e) {
|
|
268
|
+
return revert(node, layer, kids, expected);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
async function buildFrame(layer, context) {
|
|
272
|
+
const node = figma.createFrame();
|
|
273
|
+
node.resize(Math.max(0.01, layer.w), Math.max(0.01, layer.h));
|
|
274
|
+
node.clipsContent = layer.clip;
|
|
275
|
+
node.fills = frameFills(layer);
|
|
276
|
+
if (layer.radius) {
|
|
277
|
+
[node.topLeftRadius, node.topRightRadius, node.bottomRightRadius, node.bottomLeftRadius] = layer.radius;
|
|
278
|
+
}
|
|
279
|
+
if (layer.border) {
|
|
280
|
+
node.strokes = [solid(layer.border.color)];
|
|
281
|
+
node.strokeAlign = "INSIDE";
|
|
282
|
+
[node.strokeTopWeight, node.strokeRightWeight, node.strokeBottomWeight, node.strokeLeftWeight] = layer.border.w;
|
|
283
|
+
}
|
|
284
|
+
if (layer.shadows) {
|
|
285
|
+
node.effects = layer.shadows.map((s) => ({
|
|
286
|
+
type: s.inset ? "INNER_SHADOW" : "DROP_SHADOW",
|
|
287
|
+
visible: true,
|
|
288
|
+
blendMode: "NORMAL",
|
|
289
|
+
color: __spreadProps(__spreadValues({}, s.color.rgb), { a: s.color.a }),
|
|
290
|
+
offset: { x: s.x, y: s.y },
|
|
291
|
+
radius: s.blur,
|
|
292
|
+
spread: s.spread
|
|
293
|
+
}));
|
|
294
|
+
}
|
|
295
|
+
for (const child of layer.children) await buildLayer(child, node, context);
|
|
296
|
+
if (applyAutoLayout(node, layer) || applyGrid(node, layer)) context.autoLayoutFrames++;
|
|
297
|
+
return node;
|
|
298
|
+
}
|
|
299
|
+
async function buildLayer(layer, parent, context) {
|
|
300
|
+
const node = layer.type === "TEXT" ? await buildText(layer, context) : layer.type === "SVG" ? buildSvg(layer) : await buildFrame(layer, context);
|
|
301
|
+
node.name = layer.name || node.name;
|
|
302
|
+
if (layer.opacity < 1) node.opacity = layer.opacity;
|
|
303
|
+
parent.appendChild(node);
|
|
304
|
+
if (layer.scale) {
|
|
305
|
+
const [sx, sy] = layer.scale;
|
|
306
|
+
node.rescale(Math.max(0.01, Math.min(sx, sy)));
|
|
307
|
+
if (sx !== sy && node.type === "FRAME") node.resizeWithoutConstraints(layer.w * sx, layer.h * sy);
|
|
308
|
+
}
|
|
309
|
+
if (layer.transform) {
|
|
310
|
+
node.relativeTransform = layer.transform;
|
|
311
|
+
} else {
|
|
312
|
+
node.x = layer.x;
|
|
313
|
+
node.y = layer.type === "TEXT" ? layer.y + (layer.h - node.height) / 2 : layer.y;
|
|
314
|
+
}
|
|
315
|
+
return node;
|
|
316
|
+
}
|
|
317
|
+
async function buildHtml(tree, { name, x, y }) {
|
|
318
|
+
const page = figma.currentPage;
|
|
319
|
+
const existing = page.children;
|
|
320
|
+
const substitutedFonts = /* @__PURE__ */ new Set();
|
|
321
|
+
const context = {
|
|
322
|
+
pickFont: await createFontPicker(substitutedFonts),
|
|
323
|
+
substitutedFonts,
|
|
324
|
+
autoLayoutFrames: 0
|
|
325
|
+
};
|
|
326
|
+
const root = await buildLayer(tree, page, context);
|
|
327
|
+
root.name = name;
|
|
328
|
+
root.x = x != null ? x : existing.length ? Math.max(...existing.map((n) => n.x + n.width)) + 200 : 0;
|
|
329
|
+
root.y = y != null ? y : existing.length ? Math.min(...existing.map((n) => n.y)) : 0;
|
|
330
|
+
page.selection = [root];
|
|
331
|
+
figma.viewport.scrollAndZoomIntoView([root]);
|
|
332
|
+
return {
|
|
333
|
+
id: root.id,
|
|
334
|
+
name: root.name,
|
|
335
|
+
layers: "findAll" in root ? root.findAll().length + 1 : 1,
|
|
336
|
+
autoLayoutFrames: context.autoLayoutFrames,
|
|
337
|
+
substitutedFonts: [...substitutedFonts]
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// src/plugin/execute.ts
|
|
342
|
+
var AsyncFunction = Object.getPrototypeOf(async () => {
|
|
343
|
+
}).constructor;
|
|
344
|
+
var isNode = (v) => typeof v.id === "string" && typeof v.type === "string" && "removed" in v;
|
|
345
|
+
function toJSON(value) {
|
|
346
|
+
return JSON.parse(
|
|
347
|
+
JSON.stringify(value != null ? value : null, (_key, v) => {
|
|
348
|
+
if (v && typeof v === "object" && isNode(v)) return { id: v.id, name: v.name, type: v.type };
|
|
349
|
+
if (typeof v === "symbol") return String(v);
|
|
350
|
+
return v;
|
|
351
|
+
})
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
function errorText(error) {
|
|
355
|
+
var _a;
|
|
356
|
+
const e = error;
|
|
357
|
+
const message = String((_a = e == null ? void 0 : e.message) != null ? _a : error);
|
|
358
|
+
return (e == null ? void 0 : e.stack) && !e.stack.includes(message) ? `${message}
|
|
359
|
+
${e.stack}` : message;
|
|
360
|
+
}
|
|
361
|
+
async function execute({ code, files }) {
|
|
362
|
+
const logs = [];
|
|
363
|
+
const log = (...args) => {
|
|
364
|
+
logs.push(args.map((a) => typeof a === "string" ? a : JSON.stringify(toJSON(a))).join(" "));
|
|
365
|
+
};
|
|
366
|
+
const decoded = {};
|
|
367
|
+
for (const [key, file] of Object.entries(files)) {
|
|
368
|
+
decoded[key] = "text" in file ? file.text : figma.base64Decode(file.b64);
|
|
369
|
+
}
|
|
370
|
+
try {
|
|
371
|
+
const fn = new AsyncFunction("figma", "console", "files", code);
|
|
372
|
+
const result = toJSON(await fn(figma, { log, info: log, warn: log, error: log }, decoded));
|
|
373
|
+
return logs.length ? { result, logs } : { result };
|
|
374
|
+
} catch (error) {
|
|
375
|
+
throw new Error(errorText(error) + (logs.length ? `
|
|
376
|
+
logs:
|
|
377
|
+
${logs.join("\n")}` : ""));
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// src/plugin/export.ts
|
|
382
|
+
var MAX_SCREENSHOT_PX = 2048;
|
|
383
|
+
async function findNode(id) {
|
|
384
|
+
const node = await figma.getNodeByIdAsync(id);
|
|
385
|
+
if (!node || !("exportAsync" in node)) throw new Error(`Node not found or not exportable: ${id}`);
|
|
386
|
+
return node;
|
|
387
|
+
}
|
|
388
|
+
function size(node) {
|
|
389
|
+
if (node.type !== "PAGE") return { w: node.width, h: node.height };
|
|
390
|
+
const kids = node.children;
|
|
391
|
+
if (!kids.length) return { w: 1, h: 1 };
|
|
392
|
+
const left = Math.min(...kids.map((n) => n.x));
|
|
393
|
+
const top = Math.min(...kids.map((n) => n.y));
|
|
394
|
+
return {
|
|
395
|
+
w: Math.max(...kids.map((n) => n.x + n.width)) - left,
|
|
396
|
+
h: Math.max(...kids.map((n) => n.y + n.height)) - top
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
async function screenshot({
|
|
400
|
+
nodeId,
|
|
401
|
+
scale
|
|
402
|
+
}) {
|
|
403
|
+
var _a;
|
|
404
|
+
const node = nodeId ? await findNode(nodeId) : (_a = figma.currentPage.selection[0]) != null ? _a : figma.currentPage;
|
|
405
|
+
const { w, h } = size(node);
|
|
406
|
+
const value = Math.min(scale, MAX_SCREENSHOT_PX / Math.max(w, h, 1));
|
|
407
|
+
const bytes = await node.exportAsync({ format: "PNG", constraint: { type: "SCALE", value } });
|
|
408
|
+
return { b64: figma.base64Encode(bytes) };
|
|
409
|
+
}
|
|
410
|
+
async function exportNodes({
|
|
411
|
+
nodeIds,
|
|
412
|
+
format,
|
|
413
|
+
scale
|
|
414
|
+
}) {
|
|
415
|
+
const nodes = nodeIds ? await Promise.all(nodeIds.map(findNode)) : figma.currentPage.selection;
|
|
416
|
+
if (!nodes.length) throw new Error("Nothing selected and no nodeIds given");
|
|
417
|
+
const settings = format === "PNG" || format === "JPG" ? { format, constraint: { type: "SCALE", value: scale } } : { format };
|
|
418
|
+
const out = [];
|
|
419
|
+
for (const node of nodes) {
|
|
420
|
+
out.push({ name: node.name, b64: figma.base64Encode(await node.exportAsync(settings)) });
|
|
421
|
+
}
|
|
422
|
+
return out;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// src/plugin/main.ts
|
|
426
|
+
var WIDTH = 340;
|
|
427
|
+
figma.showUI(__html__, { width: WIDTH, height: 200, themeColors: true, title: "Localhost MCP Bridge" });
|
|
428
|
+
var reply = (message) => figma.ui.postMessage(message);
|
|
429
|
+
function run(request) {
|
|
430
|
+
switch (request.type) {
|
|
431
|
+
case "execute":
|
|
432
|
+
return execute(request);
|
|
433
|
+
case "screenshot":
|
|
434
|
+
return screenshot(request);
|
|
435
|
+
case "export":
|
|
436
|
+
return exportNodes(request);
|
|
437
|
+
case "html":
|
|
438
|
+
throw new Error("html is measured in the UI first");
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
figma.ui.onmessage = async (message) => {
|
|
442
|
+
if (message.type === "ready") {
|
|
443
|
+
reply({ type: "file", name: figma.root.name });
|
|
444
|
+
const fonts = await figma.listAvailableFontsAsync();
|
|
445
|
+
return reply({ type: "fonts", families: [...new Set(fonts.map((f) => f.fontName.family))] });
|
|
446
|
+
}
|
|
447
|
+
if (message.type === "resize") return figma.ui.resize(WIDTH, message.height);
|
|
448
|
+
try {
|
|
449
|
+
const result = message.type === "build" ? await buildHtml(message.tree, message) : await run(message);
|
|
450
|
+
reply({ type: "response", id: message.id, result });
|
|
451
|
+
} catch (error) {
|
|
452
|
+
reply({ type: "response", id: message.id, error: errorText(error) });
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
})();
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Localhost MCP Bridge",
|
|
3
|
+
"id": "localhost-figma-mcp-bridge",
|
|
4
|
+
"api": "1.0.0",
|
|
5
|
+
"documentAccess": "dynamic-page",
|
|
6
|
+
"main": "code.js",
|
|
7
|
+
"ui": "ui.html",
|
|
8
|
+
"editorType": [
|
|
9
|
+
"figma",
|
|
10
|
+
"figjam",
|
|
11
|
+
"slides",
|
|
12
|
+
"dev"
|
|
13
|
+
],
|
|
14
|
+
"capabilities": [
|
|
15
|
+
"inspect"
|
|
16
|
+
],
|
|
17
|
+
"networkAccess": {
|
|
18
|
+
"allowedDomains": [
|
|
19
|
+
"http://localhost:38470"
|
|
20
|
+
],
|
|
21
|
+
"reasoning": "Connects only to the MCP server running on your own machine (localhost), which relays commands from your AI assistant. Nothing is sent anywhere else."
|
|
22
|
+
}
|
|
23
|
+
}
|