@design-twin/dtwin 0.1.0-alpha.0 → 0.1.1
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/README.md +47 -4
- package/figma-plugin/code.js +4289 -0
- package/figma-plugin/manifest.json +28 -0
- package/figma-plugin/ui.html +722 -0
- package/layer/cross-check.js +2746 -0
- package/layer/drift-lint.js +1526 -0
- package/layer/get-component.js +553 -0
- package/layer/map-bootstrap.js +1404 -0
- package/layer/map-validate.js +397 -0
- package/package.json +5 -1
- package/src/commands.ts +14 -1
- package/src/figma-mcp.ts +51 -20
- package/src/figma-pull.ts +5 -0
- package/src/init.ts +37 -12
|
@@ -0,0 +1,4289 @@
|
|
|
1
|
+
// GENERATED from src/*.ts by build.ts — do not edit by hand. Run `bun run build`.
|
|
2
|
+
"use strict";
|
|
3
|
+
(() => {
|
|
4
|
+
// ../bridge/src/json-util.ts
|
|
5
|
+
function isRecord(x) {
|
|
6
|
+
return typeof x === "object" && x !== null && !Array.isArray(x);
|
|
7
|
+
}
|
|
8
|
+
function isUnknownArray(x) {
|
|
9
|
+
return Array.isArray(x);
|
|
10
|
+
}
|
|
11
|
+
function ifDefined(key, v) {
|
|
12
|
+
const o = {};
|
|
13
|
+
if (v !== void 0) o[key] = v;
|
|
14
|
+
return o;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// ../bridge/src/hex-color.ts
|
|
18
|
+
var HEX_COLOR_RE = /^#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
|
|
19
|
+
var HEX_COLOR_HINT = "a hex colour like #1A2B3C (#rgb, #rgba, #rrggbb or #rrggbbaa)";
|
|
20
|
+
function parseHexColor(value) {
|
|
21
|
+
if (typeof value !== "string" || !HEX_COLOR_RE.test(value)) return null;
|
|
22
|
+
let h = value.slice(1);
|
|
23
|
+
if (h.length <= 4) h = h.split("").map((c2) => c2 + c2).join("");
|
|
24
|
+
const chan = (i) => parseInt(h.slice(i, i + 2), 16) / 255;
|
|
25
|
+
const c = { r: chan(0), g: chan(2), b: chan(4) };
|
|
26
|
+
if (h.length === 8) c.a = chan(6);
|
|
27
|
+
return c;
|
|
28
|
+
}
|
|
29
|
+
function formatHex(c) {
|
|
30
|
+
const to = (x) => Math.round(Math.min(255, Math.max(0, x))).toString(16).padStart(2, "0");
|
|
31
|
+
const a = Math.round(c.a * 255);
|
|
32
|
+
return "#" + to(c.r) + to(c.g) + to(c.b) + (a < 255 ? to(a) : "");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ../bridge/src/pages-layout.ts
|
|
36
|
+
function safe(id) {
|
|
37
|
+
return String(id).replace(/[^a-zA-Z0-9]/g, "_");
|
|
38
|
+
}
|
|
39
|
+
function screenStem(name, id) {
|
|
40
|
+
return safe(name || "layer") + "__" + safe(id);
|
|
41
|
+
}
|
|
42
|
+
var shown = (c) => !!c && typeof c === "object" && !c.hidden;
|
|
43
|
+
function firstByName(node, name) {
|
|
44
|
+
if (!node || typeof node !== "object") return null;
|
|
45
|
+
if (node.name === name) return node;
|
|
46
|
+
for (const c of node.children || []) {
|
|
47
|
+
const found = shown(c) ? firstByName(c, name) : null;
|
|
48
|
+
if (found) return found;
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
var PLACEHOLDER_TEXT = /* @__PURE__ */ new Set(["text", "label"]);
|
|
53
|
+
function firstText(node) {
|
|
54
|
+
if (!node || typeof node !== "object") return null;
|
|
55
|
+
if (node.type === "TEXT" && typeof node.text === "string") {
|
|
56
|
+
const t = node.text.trim();
|
|
57
|
+
if (t && !PLACEHOLDER_TEXT.has(t.toLowerCase())) return node.text;
|
|
58
|
+
}
|
|
59
|
+
for (const c of node.children || []) {
|
|
60
|
+
const found = shown(c) ? firstText(c) : null;
|
|
61
|
+
if (found) return found;
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
function firstNamedText(node, name) {
|
|
66
|
+
if (!node || typeof node !== "object") return null;
|
|
67
|
+
if (node.type === "TEXT" && node.name === name && typeof node.text === "string" && node.text.trim()) {
|
|
68
|
+
return node.text;
|
|
69
|
+
}
|
|
70
|
+
for (const c of node.children || []) {
|
|
71
|
+
const found = shown(c) ? firstNamedText(c, name) : null;
|
|
72
|
+
if (found) return found;
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
function deriveTitle(root) {
|
|
77
|
+
const slot = firstByName(root, "Page Title");
|
|
78
|
+
if (slot) {
|
|
79
|
+
const named = firstNamedText(slot, "Title");
|
|
80
|
+
if (named) return named;
|
|
81
|
+
const anyText = firstText(slot);
|
|
82
|
+
if (anyText) return anyText;
|
|
83
|
+
}
|
|
84
|
+
const fallback = firstText(root);
|
|
85
|
+
return fallback || void 0;
|
|
86
|
+
}
|
|
87
|
+
function collectTexts(root, limit) {
|
|
88
|
+
const n = limit || 8;
|
|
89
|
+
const seen = /* @__PURE__ */ new Set();
|
|
90
|
+
const out = [];
|
|
91
|
+
(function walk(node, isRoot) {
|
|
92
|
+
if (!node || typeof node !== "object" || node.hidden && !isRoot || out.length >= n) return;
|
|
93
|
+
if (node.type === "TEXT" && typeof node.text === "string") {
|
|
94
|
+
const t = node.text.trim();
|
|
95
|
+
if (t && !seen.has(t)) {
|
|
96
|
+
seen.add(t);
|
|
97
|
+
out.push(t);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
for (const c of node.children || []) {
|
|
101
|
+
if (out.length >= n) break;
|
|
102
|
+
walk(c, false);
|
|
103
|
+
}
|
|
104
|
+
})(root, true);
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
function buildPageLayout(layersDoc, sep) {
|
|
108
|
+
const doc = layersDoc || {};
|
|
109
|
+
const { layers, index, ...rest } = doc;
|
|
110
|
+
const join = (...parts) => ["pages"].concat(parts).join(sep);
|
|
111
|
+
const byPage = /* @__PURE__ */ new Map();
|
|
112
|
+
const pages = [];
|
|
113
|
+
const layerFiles = [];
|
|
114
|
+
const usedDirs = /* @__PURE__ */ new Set();
|
|
115
|
+
const uniqueDir = (name) => {
|
|
116
|
+
const base = safe(name) || "page";
|
|
117
|
+
if (!usedDirs.has(base)) {
|
|
118
|
+
usedDirs.add(base);
|
|
119
|
+
return base;
|
|
120
|
+
}
|
|
121
|
+
let i = 2;
|
|
122
|
+
while (usedDirs.has(base + "_" + i)) i++;
|
|
123
|
+
usedDirs.add(base + "_" + i);
|
|
124
|
+
return base + "_" + i;
|
|
125
|
+
};
|
|
126
|
+
(layers || []).forEach((l, i) => {
|
|
127
|
+
const pageName = l.page || "(no page)";
|
|
128
|
+
const key = l.pageId || "name:" + pageName;
|
|
129
|
+
let bucket = byPage.get(key);
|
|
130
|
+
if (!bucket) {
|
|
131
|
+
const dir = uniqueDir(pageName);
|
|
132
|
+
bucket = { page: pageName, ...ifDefined("pageId", l.pageId), dir, index: join(dir, "index.json"), entries: [] };
|
|
133
|
+
byPage.set(key, bucket);
|
|
134
|
+
pages.push(bucket);
|
|
135
|
+
}
|
|
136
|
+
const base = screenStem(l.name, l.id) + ".json";
|
|
137
|
+
const src = layersDoc && layersDoc.sourceFile ? { sourceFile: layersDoc.sourceFile } : {};
|
|
138
|
+
const rowSrc = layersDoc && layersDoc.sourceFile && layersDoc.sourceFileKey ? { ...src, sourceFileKey: layersDoc.sourceFileKey } : src;
|
|
139
|
+
layerFiles.push({
|
|
140
|
+
path: join(bucket.dir, base),
|
|
141
|
+
data: { name: l.name, id: l.id, ...ifDefined("page", l.page), ...ifDefined("pageId", l.pageId), ...src, tree: l.tree, ...ifDefined("reference", l.reference), ...ifDefined("devResources", l.devResources) }
|
|
142
|
+
});
|
|
143
|
+
const title = l.tree ? deriveTitle(l.tree) : void 0;
|
|
144
|
+
const texts = l.tree ? collectTexts(l.tree) : void 0;
|
|
145
|
+
bucket.entries.push(Object.assign({}, (index || [])[i], { title, texts, ...rowSrc, file: join(bucket.dir, base) }));
|
|
146
|
+
});
|
|
147
|
+
const pageDirs = pages.map((b) => ({ page: b.page, ...ifDefined("pageId", b.pageId), dir: b.dir, index: b.index, layers: b.entries.length }));
|
|
148
|
+
const meta = Object.assign(rest, { pageDirs, layers: pages.flatMap((b) => b.entries) });
|
|
149
|
+
const indexFiles = pages.map((b) => ({ path: b.index, data: { page: b.page, ...ifDefined("pageId", b.pageId), layers: b.entries } }));
|
|
150
|
+
return { meta, layerFiles, indexFiles, rootIndex: join("index.json") };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ../bridge/src/errmsg.ts
|
|
154
|
+
var errMsg = (e) => typeof e === "string" ? e : String(e && e.message || e);
|
|
155
|
+
|
|
156
|
+
// ../bridge/src/svg-normalize.ts
|
|
157
|
+
var NUM_RE = /-?\d+\.\d+(?:[eE][+-]?\d+)?/g;
|
|
158
|
+
function normalizeSvgText(svg) {
|
|
159
|
+
return svg.replace(NUM_RE, (m) => {
|
|
160
|
+
const n = Number(m);
|
|
161
|
+
if (!Number.isFinite(n)) return m;
|
|
162
|
+
const fixed = n.toFixed(1);
|
|
163
|
+
return fixed === "-0.0" ? "0.0" : fixed;
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// src/util.ts
|
|
168
|
+
var exportedAt = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
169
|
+
function round(n) {
|
|
170
|
+
return typeof n === "number" ? Math.round(n * 100) / 100 : n;
|
|
171
|
+
}
|
|
172
|
+
var lower = (s) => s.toLowerCase();
|
|
173
|
+
var propName = (k) => {
|
|
174
|
+
const hash = k.indexOf("#");
|
|
175
|
+
return hash === -1 ? k : k.slice(0, hash);
|
|
176
|
+
};
|
|
177
|
+
var propType = (t) => t;
|
|
178
|
+
var nonEmpty = (o) => Object.keys(o).length ? o : void 0;
|
|
179
|
+
function putNonEmpty(o, key, v) {
|
|
180
|
+
const kept = v && nonEmpty(v);
|
|
181
|
+
if (kept) o[key] = kept;
|
|
182
|
+
}
|
|
183
|
+
var xy = (v) => ({ x: round(v.x), y: round(v.y) });
|
|
184
|
+
function putXY(o, key, v) {
|
|
185
|
+
if (v && typeof v === "object") o[key] = xy(v);
|
|
186
|
+
}
|
|
187
|
+
function easingCurve(ez) {
|
|
188
|
+
const o = {};
|
|
189
|
+
if (ez.easingFunctionCubicBezier) o.cubicBezier = ez.easingFunctionCubicBezier;
|
|
190
|
+
if (ez.easingFunctionSpring) o.spring = ez.easingFunctionSpring;
|
|
191
|
+
return o;
|
|
192
|
+
}
|
|
193
|
+
function asJson(v) {
|
|
194
|
+
if (v === null || typeof v === "string" || typeof v === "boolean" || typeof v === "number") return v;
|
|
195
|
+
if (Array.isArray(v)) return v.map((x) => {
|
|
196
|
+
const j = asJson(x);
|
|
197
|
+
return j === void 0 ? null : j;
|
|
198
|
+
});
|
|
199
|
+
if (typeof v === "object") {
|
|
200
|
+
const out = {};
|
|
201
|
+
for (const [k, x] of Object.entries(v)) {
|
|
202
|
+
const j = asJson(x);
|
|
203
|
+
if (j !== void 0) out[k] = j;
|
|
204
|
+
}
|
|
205
|
+
return out;
|
|
206
|
+
}
|
|
207
|
+
return void 0;
|
|
208
|
+
}
|
|
209
|
+
function isList(v) {
|
|
210
|
+
return Array.isArray(v);
|
|
211
|
+
}
|
|
212
|
+
function numProp(node, key) {
|
|
213
|
+
const v = node[key];
|
|
214
|
+
return typeof v === "number" ? v : void 0;
|
|
215
|
+
}
|
|
216
|
+
function anyProp(node, key) {
|
|
217
|
+
return node[key];
|
|
218
|
+
}
|
|
219
|
+
function rgbaToHex(c) {
|
|
220
|
+
return formatHex({ r: c.r * 255, g: c.g * 255, b: c.b * 255, a: c.a == null ? 1 : c.a });
|
|
221
|
+
}
|
|
222
|
+
var solidHex = (p) => rgbaToHex({ r: p.color.r, g: p.color.g, b: p.color.b, ...ifDefined("a", p.opacity) });
|
|
223
|
+
function solidFromFills(fills) {
|
|
224
|
+
if (!fills || fills === figma.mixed || !Array.isArray(fills)) return void 0;
|
|
225
|
+
const s = fills.find((f) => f.visible !== false && f.type === "SOLID");
|
|
226
|
+
return s ? solidHex(s) : void 0;
|
|
227
|
+
}
|
|
228
|
+
var B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
229
|
+
var B64_CHUNK = 16384;
|
|
230
|
+
function toBase64(bytes) {
|
|
231
|
+
const n = bytes.length;
|
|
232
|
+
let out = "";
|
|
233
|
+
let chunk = "";
|
|
234
|
+
for (let i = 0; i < n; i += 3) {
|
|
235
|
+
const t = (bytes[i] || 0) << 16 | (bytes[i + 1] || 0) << 8 | (bytes[i + 2] || 0);
|
|
236
|
+
chunk += B64.charAt(t >> 18 & 63) + B64.charAt(t >> 12 & 63) + (i + 1 < n ? B64.charAt(t >> 6 & 63) : "=") + (i + 2 < n ? B64.charAt(t & 63) : "=");
|
|
237
|
+
if (chunk.length >= B64_CHUNK) {
|
|
238
|
+
out += chunk;
|
|
239
|
+
chunk = "";
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return out + chunk;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ../bridge/src/design-system-layout.ts
|
|
246
|
+
var DIR = "design-system";
|
|
247
|
+
var COMPONENTS_DIR = "components";
|
|
248
|
+
var TOKENS = "tokens.json";
|
|
249
|
+
var COMPONENTS_LOCAL = "components.local.json";
|
|
250
|
+
var COMPONENTS_LIBRARY = "components.library.json";
|
|
251
|
+
var HYGIENE = "hygiene.json";
|
|
252
|
+
var MANIFEST = "design-system.json";
|
|
253
|
+
var STYLE_KIND_ROWS = {
|
|
254
|
+
paint: { kind: "paint", file: "styles.paint.json", key: "stylesPaint" },
|
|
255
|
+
text: { kind: "text", file: "styles.text.json", key: "stylesText" },
|
|
256
|
+
effect: { kind: "effect", file: "styles.effect.json", key: "stylesEffect" },
|
|
257
|
+
grid: { kind: "grid", file: "styles.grid.json", key: "stylesGrid" },
|
|
258
|
+
animation: { kind: "animation", file: "styles.animation.json", key: "stylesAnimation" }
|
|
259
|
+
// local custom animation styles
|
|
260
|
+
};
|
|
261
|
+
var STYLE_KINDS = Object.values(STYLE_KIND_ROWS);
|
|
262
|
+
function stylesOf(styles, kind) {
|
|
263
|
+
const list = styles ? styles[kind] : void 0;
|
|
264
|
+
return Array.isArray(list) ? list : [];
|
|
265
|
+
}
|
|
266
|
+
function byStyleKey(value) {
|
|
267
|
+
const out = {};
|
|
268
|
+
for (const row of STYLE_KINDS) out[row.key] = value(row);
|
|
269
|
+
return out;
|
|
270
|
+
}
|
|
271
|
+
function isLibraryEntry(c) {
|
|
272
|
+
return !!(c && c.remote === true);
|
|
273
|
+
}
|
|
274
|
+
function buildDesignSystemLayout(ds, sep) {
|
|
275
|
+
const d = ds || {};
|
|
276
|
+
const join = (name) => DIR + (sep || "/") + name;
|
|
277
|
+
const stamp = { ...ifDefined("exportedAt", d.exportedAt), ...ifDefined("file", d.file), ...ifDefined("colorProfile", d.colorProfile) };
|
|
278
|
+
const components = Array.isArray(d.components) ? d.components : [];
|
|
279
|
+
const rawLocal = components.filter((c) => !isLibraryEntry(c));
|
|
280
|
+
const library = components.filter(isLibraryEntry);
|
|
281
|
+
const hygiene = Array.isArray(d.hygiene) ? d.hygiene : [];
|
|
282
|
+
const usedNames = /* @__PURE__ */ new Set();
|
|
283
|
+
const uniqueDetailName = (name, id) => {
|
|
284
|
+
const base = safe(name || "component") + "__" + safe(id);
|
|
285
|
+
if (!usedNames.has(base)) {
|
|
286
|
+
usedNames.add(base);
|
|
287
|
+
return base;
|
|
288
|
+
}
|
|
289
|
+
let i = 2;
|
|
290
|
+
while (usedNames.has(base + "_" + i)) i++;
|
|
291
|
+
usedNames.add(base + "_" + i);
|
|
292
|
+
return base + "_" + i;
|
|
293
|
+
};
|
|
294
|
+
const componentFiles = [];
|
|
295
|
+
const local = rawLocal.map((c) => {
|
|
296
|
+
if (c.type === "COMPONENT" && c.node) {
|
|
297
|
+
const { node, ...rest } = c;
|
|
298
|
+
const detailName2 = uniqueDetailName(c.name, c.id);
|
|
299
|
+
const detailPath2 = DIR + (sep || "/") + COMPONENTS_DIR + (sep || "/") + detailName2 + ".json";
|
|
300
|
+
componentFiles.push({
|
|
301
|
+
path: detailPath2,
|
|
302
|
+
data: { ...stamp, ...ifDefined("id", c.id), ...ifDefined("key", c.key), name: c.name, node }
|
|
303
|
+
});
|
|
304
|
+
return { ...rest, nodeFile: detailPath2 };
|
|
305
|
+
}
|
|
306
|
+
if (c.type !== "COMPONENT_SET" || !Array.isArray(c.variants) || !c.variants.some((v) => v && v.node)) {
|
|
307
|
+
return c;
|
|
308
|
+
}
|
|
309
|
+
const slimVariants = c.variants.map((v) => {
|
|
310
|
+
const rest = Object.assign({}, v);
|
|
311
|
+
delete rest.node;
|
|
312
|
+
return rest;
|
|
313
|
+
});
|
|
314
|
+
const detailName = uniqueDetailName(c.name, c.id);
|
|
315
|
+
const detailPath = DIR + (sep || "/") + COMPONENTS_DIR + (sep || "/") + detailName + ".json";
|
|
316
|
+
componentFiles.push({
|
|
317
|
+
path: detailPath,
|
|
318
|
+
data: { ...stamp, ...ifDefined("setId", c.id), ...ifDefined("setKey", c.key), name: c.name, variants: c.variants }
|
|
319
|
+
});
|
|
320
|
+
return { ...c, variants: slimVariants, variantsFile: detailPath };
|
|
321
|
+
});
|
|
322
|
+
const files = [
|
|
323
|
+
{ path: join(TOKENS), data: { ...stamp, ...ifDefined("collections", d.collections), ...ifDefined("variables", d.variables) } },
|
|
324
|
+
...STYLE_KINDS.map((row) => ({ path: join(row.file), data: { ...stamp, styles: stylesOf(d.styles, row.kind) } })),
|
|
325
|
+
{ path: join(COMPONENTS_LOCAL), data: { ...stamp, components: local } },
|
|
326
|
+
{ path: join(COMPONENTS_LIBRARY), data: { ...stamp, components: library } },
|
|
327
|
+
{ path: join(HYGIENE), data: { ...stamp, hygiene } },
|
|
328
|
+
...componentFiles
|
|
329
|
+
];
|
|
330
|
+
const counts = {
|
|
331
|
+
collections: (d.collections || []).length,
|
|
332
|
+
variables: (d.variables || []).length,
|
|
333
|
+
...byStyleKey((row) => stylesOf(d.styles, row.kind).length),
|
|
334
|
+
components: local.length,
|
|
335
|
+
libraryComponents: library.length,
|
|
336
|
+
hygiene: hygiene.length
|
|
337
|
+
};
|
|
338
|
+
const manifest = {
|
|
339
|
+
...stamp,
|
|
340
|
+
files: {
|
|
341
|
+
tokens: join(TOKENS),
|
|
342
|
+
...byStyleKey((row) => join(row.file)),
|
|
343
|
+
componentsLocal: join(COMPONENTS_LOCAL),
|
|
344
|
+
componentsLibrary: join(COMPONENTS_LIBRARY),
|
|
345
|
+
hygiene: join(HYGIENE)
|
|
346
|
+
},
|
|
347
|
+
counts
|
|
348
|
+
};
|
|
349
|
+
const detailPrefix = DIR + (sep || "/") + COMPONENTS_DIR + (sep || "/");
|
|
350
|
+
if (files.some((f) => String(f.path).startsWith(detailPrefix))) {
|
|
351
|
+
manifest.files.componentsDir = DIR + (sep || "/") + COMPONENTS_DIR;
|
|
352
|
+
}
|
|
353
|
+
files.push({ path: MANIFEST, data: manifest });
|
|
354
|
+
return { files, manifest, counts, dir: DIR };
|
|
355
|
+
}
|
|
356
|
+
var DESIGN_SYSTEM_FILE_NAMES = [TOKENS, ...STYLE_KINDS.map((row) => row.file), COMPONENTS_LOCAL, COMPONENTS_LIBRARY, HYGIENE, MANIFEST];
|
|
357
|
+
|
|
358
|
+
// ../bridge/src/read-opts.ts
|
|
359
|
+
var WALK = ["node", "selection", "pages"];
|
|
360
|
+
var READ_OPTS = [
|
|
361
|
+
{
|
|
362
|
+
name: "css",
|
|
363
|
+
flag: "--css",
|
|
364
|
+
describe: "Include Figma's OWN computed CSS per node (getCSSAsync) \u2014 the design-to-code oracle. One async call per node, so larger/slower; use for a screen you're implementing.",
|
|
365
|
+
scopes: WALK
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
// NOT "Dev Mode only": per developers.figma.com only addMeasurement/editMeasurement/
|
|
369
|
+
// deleteMeasurement carry that restriction — PageNode.getMeasurements() has no such note. And no
|
|
370
|
+
// longer "current page": collect.ts reads measurements from the page(s) actually exported.
|
|
371
|
+
name: "measurements",
|
|
372
|
+
flag: "--measurements",
|
|
373
|
+
describe: "Include measurement redlines (spacing specs the designer placed) for the exported page(s). Reading them does not require Dev Mode.",
|
|
374
|
+
scopes: WALK
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
name: "pluginData",
|
|
378
|
+
flag: "--plugin-data",
|
|
379
|
+
describe: "Include own-scope plugin data (getPluginData) stamped on nodes \u2014 round-trip metadata. Usually empty unless the write plane wrote it.",
|
|
380
|
+
scopes: WALK
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
name: "motion",
|
|
384
|
+
flag: "--motion",
|
|
385
|
+
describe: "Include motion/animation reads (timelines, manual keyframe tracks, animations, applied animation styles). Free via the Plugin API; useful for Slides/prototype animation. Can be verbose.",
|
|
386
|
+
scopes: WALK
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
name: "sharedData",
|
|
390
|
+
flag: "--shared-data",
|
|
391
|
+
describe: "Include cross-plugin shared data (getSharedPluginData) \u2014 notably Tokens Studio applied tokens (the semantic token layer on files without native Figma Variables). Free; per-node.",
|
|
392
|
+
scopes: WALK
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
name: "variantVisuals",
|
|
396
|
+
flag: "--variant-visuals",
|
|
397
|
+
describe: "Walk each COMPONENT_SET's variant children and record their real layout/fills/radius/tokens/css \u2014 the master-component source of truth, not the component-set wrapper's own selection-chrome visuals. One node walk per variant; noticeably slower on large systems.",
|
|
398
|
+
scopes: [...WALK, "designSystem", "library"]
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
// The odd one out: the others ADD work, this one REMOVES it. Asset export is one exportAsync (a
|
|
402
|
+
// real render round-trip — SVG per vector, 2x PNG per image) PER NODE, run sequentially, and
|
|
403
|
+
// unlike getCSSAsync it was never gated despite being the same O(nodes) shape and far more
|
|
404
|
+
// expensive. On a real design-system page it dominates the export and can outgrow the bridge's
|
|
405
|
+
// frame limit. Opt-IN to skipping, so the default stays exactly as it was.
|
|
406
|
+
name: "skipAssets",
|
|
407
|
+
flag: "--no-assets",
|
|
408
|
+
describe: "Skip the per-node SVG/PNG render pass \u2014 the dominant cost on a big file. Structure, layout and tokens are unaffected; skipped nodes stay leaves marked `assetSkipped: true`. Asset BYTES are stripped from MCP results anyway, so this is usually pure savings here.",
|
|
409
|
+
scopes: WALK
|
|
410
|
+
}
|
|
411
|
+
];
|
|
412
|
+
function readOptDefaults() {
|
|
413
|
+
const o = {};
|
|
414
|
+
for (const d of READ_OPTS) o[d.name] = false;
|
|
415
|
+
return o;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// src/run.ts
|
|
419
|
+
function memoName(fetch, unavailable) {
|
|
420
|
+
const cache = /* @__PURE__ */ new Map();
|
|
421
|
+
const get = (id) => {
|
|
422
|
+
let p = cache.get(id);
|
|
423
|
+
if (!p) {
|
|
424
|
+
try {
|
|
425
|
+
p = Promise.resolve(fetch(id)).catch(() => null);
|
|
426
|
+
} catch (e) {
|
|
427
|
+
p = Promise.resolve(null);
|
|
428
|
+
}
|
|
429
|
+
if (unavailable) p = p.then((o) => {
|
|
430
|
+
if (!o && typeof id === "string" && id) unavailable(id);
|
|
431
|
+
return o;
|
|
432
|
+
});
|
|
433
|
+
cache.set(id, p);
|
|
434
|
+
}
|
|
435
|
+
return p;
|
|
436
|
+
};
|
|
437
|
+
const fn = (async (id) => {
|
|
438
|
+
const o = await get(id);
|
|
439
|
+
return o ? o.name : void 0;
|
|
440
|
+
});
|
|
441
|
+
fn.obj = get;
|
|
442
|
+
fn.ids = () => Array.from(cache.keys());
|
|
443
|
+
return fn;
|
|
444
|
+
}
|
|
445
|
+
var WARN_EXAMPLES = 10;
|
|
446
|
+
var Run = class {
|
|
447
|
+
constructor(info) {
|
|
448
|
+
this.readOpts = readOptDefaults();
|
|
449
|
+
this.readsFixed = false;
|
|
450
|
+
this.delivery = { sourceBytes: true };
|
|
451
|
+
this.claimed = false;
|
|
452
|
+
this.stats = {
|
|
453
|
+
nodes: 0,
|
|
454
|
+
assetsFailed: 0,
|
|
455
|
+
assetsSkipped: 0,
|
|
456
|
+
assetsSkippedInvisible: 0,
|
|
457
|
+
assetsGeometry: 0,
|
|
458
|
+
assetsHidden: 0,
|
|
459
|
+
hiddenNodes: 0,
|
|
460
|
+
truncated: 0
|
|
461
|
+
};
|
|
462
|
+
/** Per-run diagnostics so the agent knows what was and wasn't captured (never silent truncation). */
|
|
463
|
+
this.warnings = [];
|
|
464
|
+
this.grouped = /* @__PURE__ */ new Map();
|
|
465
|
+
/** The run's exported assets — { id, name, format, file, base64/text }. */
|
|
466
|
+
this.assets = [];
|
|
467
|
+
/** assets.ts's naming tables: content hash -> the file already written for it, and taken basename (case-folded)
|
|
468
|
+
* -> the hash that owns it. `byContent` holds the same Asset objects (base64/text payloads) as `assets`. */
|
|
469
|
+
this.assetFiles = { byContent: /* @__PURE__ */ new Map(), byName: /* @__PURE__ */ new Map() };
|
|
470
|
+
/** Intrinsic pixel size per image hash, resolved once per run via getImageByHash().getSizeAsync(). A design
|
|
471
|
+
* reuses the same image across many fills/nodes, so memoize to avoid O(nodes) downloads. Holds the in-flight
|
|
472
|
+
* PROMISE: paints resolve concurrently, so a value-cache let two fills sharing a hash both miss — costing a
|
|
473
|
+
* second download AND pushing the source bytes into `assets` twice. */
|
|
474
|
+
this.imageSizes = /* @__PURE__ */ new Map();
|
|
475
|
+
this.varName = memoName((id) => figma.variables.getVariableByIdAsync(id), (id) => this.warn("variable metadata unavailable for '" + id + "' \u2014 token names unavailable; raw alias IDs retained where supported"));
|
|
476
|
+
this.styleName = memoName((id) => figma.getStyleByIdAsync(id));
|
|
477
|
+
/** Prototype destinations. */
|
|
478
|
+
this.nodeName = memoName((id) => figma.getNodeByIdAsync(id));
|
|
479
|
+
/** Variable collections, read to turn a node's pinned variable-mode ids into human names (theming). A file has a
|
|
480
|
+
* handful reused across every node, so the whole object is cached per run (its .modes list too) — the same
|
|
481
|
+
* memo, through `.obj`. */
|
|
482
|
+
this.collection = memoName((id) => figma.variables && figma.variables.getVariableCollectionByIdAsync ? figma.variables.getVariableCollectionByIdAsync(id) : Promise.resolve(null), (id) => {
|
|
483
|
+
if (figma.variables && typeof figma.variables.getVariableCollectionByIdAsync === "function") {
|
|
484
|
+
this.warn("collection metadata unavailable for '" + id + "' \u2014 collection/mode names unavailable; raw mode IDs retained where supported");
|
|
485
|
+
}
|
|
486
|
+
}).obj;
|
|
487
|
+
/** Who asked this run to stop, if anyone: the designer's Cancel button, or the bridge giving up on the request
|
|
488
|
+
* (progress.ts requestAbandon). One field rather than two flags so the run fails with exactly one, truthful
|
|
489
|
+
* message. Starts null with the run, so a cancel that landed while nothing was running — or after the run it
|
|
490
|
+
* was meant for had already finished — cannot kill the next one. */
|
|
491
|
+
this.cancel = null;
|
|
492
|
+
/** The page the walk is on (progress.ts enterPage), so the ticks that are not page boundaries can say WHERE they
|
|
493
|
+
* are. */
|
|
494
|
+
this.page = void 0;
|
|
495
|
+
/** When this run last posted a throttled progress frame. */
|
|
496
|
+
this.lastPost = 0;
|
|
497
|
+
/** Bound, so it can be handed on as a warning sink (`findNodeById(id, r.warn)`). */
|
|
498
|
+
this.warn = (msg) => {
|
|
499
|
+
this.warnings.push(msg);
|
|
500
|
+
};
|
|
501
|
+
/** `kind` is the stable grouping key (a sentence with no node-specific text in it); `ref` identifies
|
|
502
|
+
* the node — pass "<name> (<id>)" so the examples are actionable. */
|
|
503
|
+
this.warnKind = (kind, ref) => {
|
|
504
|
+
let g = this.grouped.get(kind);
|
|
505
|
+
if (!g) {
|
|
506
|
+
g = { count: 0, examples: [] };
|
|
507
|
+
this.grouped.set(kind, g);
|
|
508
|
+
}
|
|
509
|
+
g.count++;
|
|
510
|
+
if (ref && g.examples.length < WARN_EXAMPLES) g.examples.push(ref);
|
|
511
|
+
};
|
|
512
|
+
this.info = info;
|
|
513
|
+
}
|
|
514
|
+
/** The opt-in reads this run makes: all off until its collector names them (useReads). */
|
|
515
|
+
get reads() {
|
|
516
|
+
return this.readOpts;
|
|
517
|
+
}
|
|
518
|
+
/** Fix this run's read options. Once per run: a read cannot change what it reads halfway through, so a walk that
|
|
519
|
+
* needs a different policy says so in its own arguments (serialize.ts AssetPolicy). */
|
|
520
|
+
useReads(opts) {
|
|
521
|
+
if (this.readsFixed) throw new Error("a run's read options are fixed once, by its collector");
|
|
522
|
+
this.readsFixed = true;
|
|
523
|
+
const r = readOptDefaults();
|
|
524
|
+
for (const k of Object.keys(r)) r[k] = !!(opts && opts[k]);
|
|
525
|
+
this.readOpts = r;
|
|
526
|
+
}
|
|
527
|
+
/** See Delivery.sourceBytes. */
|
|
528
|
+
get sourceBytes() {
|
|
529
|
+
return this.delivery.sourceBytes;
|
|
530
|
+
}
|
|
531
|
+
/** The run's warnings: the one-off lines, then one line per kind (warnKind). A fresh array per call. */
|
|
532
|
+
warningLines() {
|
|
533
|
+
const out = this.warnings.slice();
|
|
534
|
+
this.grouped.forEach((g, kind) => {
|
|
535
|
+
let line = kind + ": " + g.count + " node(s)";
|
|
536
|
+
if (g.examples.length) {
|
|
537
|
+
line += " \u2014 e.g. " + g.examples.join("; ");
|
|
538
|
+
if (g.count > g.examples.length) line += " (+" + (g.count - g.examples.length) + " more)";
|
|
539
|
+
}
|
|
540
|
+
out.push(line);
|
|
541
|
+
});
|
|
542
|
+
return out;
|
|
543
|
+
}
|
|
544
|
+
/** @internal claimRun's bookkeeping: the first collector to claim a scheduled run sets its delivery. */
|
|
545
|
+
claim(delivery) {
|
|
546
|
+
if (this.claimed) return false;
|
|
547
|
+
this.claimed = true;
|
|
548
|
+
if (delivery && delivery.sourceBytes === false) this.delivery = { sourceBytes: false };
|
|
549
|
+
return true;
|
|
550
|
+
}
|
|
551
|
+
/** Drop the run's asset payloads (tens of MB of base64 PNG/SVG). A collector has already returned its own
|
|
552
|
+
* assets.slice(), and a run that THREW hands them to nobody; the dedupe tables go too, since `byContent` points
|
|
553
|
+
* at the same objects. Clearing drops the references only; the returned objects are never touched. */
|
|
554
|
+
release() {
|
|
555
|
+
this.assets.length = 0;
|
|
556
|
+
this.assetFiles.byContent.clear();
|
|
557
|
+
this.assetFiles.byName.clear();
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
var SKIP_ASSETS_NOTE = (n) => `--no-assets: ${n} node(s) that would have exported an SVG/PNG were skipped; each is marked 'assetSkipped: true' in the tree and stays a leaf, exactly as the full export serializes it (structure, layout and tokens are unaffected). The per-root reference screenshot is still captured \u2014 it is one render per exported root, not per node.`;
|
|
561
|
+
function manifestOf(r) {
|
|
562
|
+
const note = r.reads.skipAssets && r.stats.assetsSkipped ? SKIP_ASSETS_NOTE(r.stats.assetsSkipped) : null;
|
|
563
|
+
const all = r.warningLines();
|
|
564
|
+
const reads = Object.keys(r.reads).filter((k) => r.reads[k]);
|
|
565
|
+
return { ...r.stats, skipped: r.stats.truncated, reads, warnings: note ? all.concat(note) : all };
|
|
566
|
+
}
|
|
567
|
+
var current = null;
|
|
568
|
+
var last = null;
|
|
569
|
+
function install(r) {
|
|
570
|
+
if (current && current !== r) current.release();
|
|
571
|
+
current = r;
|
|
572
|
+
last = r;
|
|
573
|
+
return r;
|
|
574
|
+
}
|
|
575
|
+
function openRun(info) {
|
|
576
|
+
return install(new Run(info));
|
|
577
|
+
}
|
|
578
|
+
function closeRun(r) {
|
|
579
|
+
r.release();
|
|
580
|
+
if (current === r) current = null;
|
|
581
|
+
}
|
|
582
|
+
function activeRun() {
|
|
583
|
+
return current && current.info ? current : null;
|
|
584
|
+
}
|
|
585
|
+
function run() {
|
|
586
|
+
return current || install(new Run(null));
|
|
587
|
+
}
|
|
588
|
+
function claimRun(delivery) {
|
|
589
|
+
if (current && current.info) {
|
|
590
|
+
if (!current.claim(delivery)) throw new Error("one collector per run: '" + current.info.label + "' already has its read");
|
|
591
|
+
return current;
|
|
592
|
+
}
|
|
593
|
+
const r = install(new Run(null));
|
|
594
|
+
r.claim(delivery);
|
|
595
|
+
return r;
|
|
596
|
+
}
|
|
597
|
+
function assetRetention() {
|
|
598
|
+
return last ? { assets: last.assets.length, byContent: last.assetFiles.byContent.size, byName: last.assetFiles.byName.size } : { assets: 0, byContent: 0, byName: 0 };
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// src/progress.ts
|
|
602
|
+
var CANCELLED_MESSAGE = "export cancelled by the designer in Figma";
|
|
603
|
+
var ABANDONED_MESSAGE = "export cancelled: the bridge request that asked for it was abandoned (its caller timed out, stalled out, or disconnected)";
|
|
604
|
+
var CANCELLED = "__designTwinCancelled";
|
|
605
|
+
var ABANDONED = "__designTwinAbandoned";
|
|
606
|
+
function cancelledError() {
|
|
607
|
+
const e = new Error(CANCELLED_MESSAGE);
|
|
608
|
+
e[CANCELLED] = true;
|
|
609
|
+
return e;
|
|
610
|
+
}
|
|
611
|
+
function abandonedError() {
|
|
612
|
+
const e = new Error(ABANDONED_MESSAGE);
|
|
613
|
+
e[CANCELLED] = true;
|
|
614
|
+
e[ABANDONED] = true;
|
|
615
|
+
return e;
|
|
616
|
+
}
|
|
617
|
+
function isAbandonment(e) {
|
|
618
|
+
return !!e && typeof e === "object" && e[ABANDONED] === true;
|
|
619
|
+
}
|
|
620
|
+
var MIN_INTERVAL_MS = 250;
|
|
621
|
+
function post(m) {
|
|
622
|
+
try {
|
|
623
|
+
if (figma && figma.ui && typeof figma.ui.postMessage === "function") figma.ui.postMessage(m);
|
|
624
|
+
} catch (e) {
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
function postRunBegin(info) {
|
|
628
|
+
post({ type: "run-begin", source: info.source, label: info.label, ...ifDefined("requestId", info.requestId) });
|
|
629
|
+
}
|
|
630
|
+
function postRunEnd(abandoned) {
|
|
631
|
+
post(abandoned ? { type: "run-end", abandoned: true } : { type: "run-end" });
|
|
632
|
+
}
|
|
633
|
+
function postQueued(run2) {
|
|
634
|
+
if (run2.source !== "bridge") return;
|
|
635
|
+
post({ type: "progress", phase: "queued", source: "bridge", label: run2.label, ...ifDefined("requestId", run2.requestId) });
|
|
636
|
+
}
|
|
637
|
+
function postStarted(run2) {
|
|
638
|
+
if (run2.source !== "bridge") return;
|
|
639
|
+
post({ type: "progress", phase: "start", source: "bridge", label: run2.label, ...ifDefined("requestId", run2.requestId) });
|
|
640
|
+
}
|
|
641
|
+
function requestCancel() {
|
|
642
|
+
const r = activeRun();
|
|
643
|
+
if (!r || !r.info) return null;
|
|
644
|
+
r.cancel = "designer";
|
|
645
|
+
return r.info;
|
|
646
|
+
}
|
|
647
|
+
function requestAbandon(matches) {
|
|
648
|
+
const r = activeRun();
|
|
649
|
+
if (!r || !r.info || !matches(r.info)) return null;
|
|
650
|
+
if (r.cancel === null) r.cancel = "abandoned";
|
|
651
|
+
return r.info;
|
|
652
|
+
}
|
|
653
|
+
function checkCancelled() {
|
|
654
|
+
const r = activeRun();
|
|
655
|
+
if (!r) return;
|
|
656
|
+
if (r.cancel === "designer") throw cancelledError();
|
|
657
|
+
if (r.cancel === "abandoned") throw abandonedError();
|
|
658
|
+
}
|
|
659
|
+
function progress(phase, extra, force) {
|
|
660
|
+
const r = activeRun();
|
|
661
|
+
if (!r || !r.info) return;
|
|
662
|
+
const now = Date.now();
|
|
663
|
+
if (!force && now - r.lastPost < MIN_INTERVAL_MS) return;
|
|
664
|
+
r.lastPost = now;
|
|
665
|
+
post({ type: "progress", phase, source: r.info.source, label: r.info.label, ...ifDefined("page", r.page), ...extra });
|
|
666
|
+
}
|
|
667
|
+
function enterPage(phase, index, of, name, pageId, extra) {
|
|
668
|
+
const r = activeRun();
|
|
669
|
+
if (!r) return;
|
|
670
|
+
r.page = { index, of, name, ...ifDefined("pageId", pageId) };
|
|
671
|
+
progress(phase, extra, true);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// src/state.ts
|
|
675
|
+
var queuedRuns = /* @__PURE__ */ new Set();
|
|
676
|
+
var runChain = Promise.resolve();
|
|
677
|
+
var inChain = 0;
|
|
678
|
+
function serializeRun(fn, run2) {
|
|
679
|
+
return enqueue(fn, run2, true);
|
|
680
|
+
}
|
|
681
|
+
function scheduleRun(kind, fn, run2) {
|
|
682
|
+
if (kind === "identity") return fn();
|
|
683
|
+
return enqueue(fn, run2, kind === "exclusive");
|
|
684
|
+
}
|
|
685
|
+
function enqueue(fn, run2, bracketed) {
|
|
686
|
+
const ticket = { run: run2, abandoned: false };
|
|
687
|
+
queuedRuns.add(ticket);
|
|
688
|
+
const waits = inChain > 0;
|
|
689
|
+
if (waits) postQueued(run2);
|
|
690
|
+
inChain++;
|
|
691
|
+
const go = () => {
|
|
692
|
+
queuedRuns.delete(ticket);
|
|
693
|
+
if (ticket.abandoned) {
|
|
694
|
+
inChain--;
|
|
695
|
+
return Promise.reject(abandonedError());
|
|
696
|
+
}
|
|
697
|
+
if (!bracketed && waits) postStarted(run2);
|
|
698
|
+
const ran = bracketed ? bracket(fn, run2) : Promise.resolve().then(fn);
|
|
699
|
+
return ran.finally(() => {
|
|
700
|
+
inChain--;
|
|
701
|
+
});
|
|
702
|
+
};
|
|
703
|
+
const next = runChain.then(go, go);
|
|
704
|
+
runChain = next.then(() => {
|
|
705
|
+
}, () => {
|
|
706
|
+
});
|
|
707
|
+
return next;
|
|
708
|
+
}
|
|
709
|
+
function cancelBridgeRequest(id) {
|
|
710
|
+
for (const t of queuedRuns) {
|
|
711
|
+
if (t.run.source === "bridge" && t.run.requestId === id) {
|
|
712
|
+
t.abandoned = true;
|
|
713
|
+
return t.run;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
return requestAbandon((r) => r.source === "bridge" && r.requestId === id);
|
|
717
|
+
}
|
|
718
|
+
function cancelBridgeRuns() {
|
|
719
|
+
let hit = 0;
|
|
720
|
+
for (const t of queuedRuns) {
|
|
721
|
+
if (t.run.source === "bridge" && !t.abandoned) {
|
|
722
|
+
t.abandoned = true;
|
|
723
|
+
hit++;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
if (requestAbandon((r) => r.source === "bridge")) hit++;
|
|
727
|
+
return hit;
|
|
728
|
+
}
|
|
729
|
+
async function bracket(fn, run2) {
|
|
730
|
+
const ctx = openRun(run2);
|
|
731
|
+
postRunBegin(run2);
|
|
732
|
+
let abandoned = false;
|
|
733
|
+
try {
|
|
734
|
+
return await fn();
|
|
735
|
+
} catch (e) {
|
|
736
|
+
abandoned = isAbandonment(e);
|
|
737
|
+
throw e;
|
|
738
|
+
} finally {
|
|
739
|
+
closeRun(ctx);
|
|
740
|
+
postRunEnd(abandoned);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
async function loadAllPages(consequence, sink) {
|
|
744
|
+
if (!figma.loadAllPagesAsync) return;
|
|
745
|
+
try {
|
|
746
|
+
await figma.loadAllPagesAsync();
|
|
747
|
+
} catch (e) {
|
|
748
|
+
sink("loadAllPagesAsync failed (" + consequence + "): " + errMsg(e));
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// ../bridge/src/node-id.ts
|
|
753
|
+
var ID = "[A-Za-z0-9%:;_-]+";
|
|
754
|
+
var NODE_ID_RE = new RegExp("node-id=(" + ID + ")");
|
|
755
|
+
function decode(s) {
|
|
756
|
+
try {
|
|
757
|
+
return decodeURIComponent(s);
|
|
758
|
+
} catch (e) {
|
|
759
|
+
return s;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
var COLON_ID_RE = /^I?\d+:\d+(?:[;:]\d+:\d+)*$/;
|
|
763
|
+
function normalizeNodeId(raw) {
|
|
764
|
+
if (!raw) return void 0;
|
|
765
|
+
const s = decode(String(raw).trim()).replace(/-/g, ":");
|
|
766
|
+
return COLON_ID_RE.test(s) || /^\d+$/.test(s) ? s : void 0;
|
|
767
|
+
}
|
|
768
|
+
function parseNodeId(input) {
|
|
769
|
+
if (!input) return void 0;
|
|
770
|
+
const s = String(input).trim();
|
|
771
|
+
try {
|
|
772
|
+
const nid = new URL(s).searchParams.get("node-id");
|
|
773
|
+
if (nid) return normalizeNodeId(nid);
|
|
774
|
+
} catch (e) {
|
|
775
|
+
}
|
|
776
|
+
const m = s.match(NODE_ID_RE);
|
|
777
|
+
if (m) return normalizeNodeId(m[1]);
|
|
778
|
+
return normalizeNodeId(s);
|
|
779
|
+
}
|
|
780
|
+
function toNodeId(raw) {
|
|
781
|
+
return parseNodeId(raw) || (raw == null ? "" : String(raw));
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// ../bridge/src/commands.ts
|
|
785
|
+
var WRITE_OP_NAMES = ["createFrame", "createText", "setFill", "setText"];
|
|
786
|
+
var WRITE_OPS = {
|
|
787
|
+
createFrame: { effect: "creates", required: [], shows: {} },
|
|
788
|
+
createText: { effect: "creates", required: ["text"], shows: { newText: "text" } },
|
|
789
|
+
setFill: { effect: "overwrites", required: ["nodeId", "color"], shows: { newFill: "color" } },
|
|
790
|
+
setText: { effect: "overwrites", required: ["nodeId", "text"], shows: { newText: "text" } }
|
|
791
|
+
};
|
|
792
|
+
function missingWriteFields(op, fields) {
|
|
793
|
+
return WRITE_OPS[op].required.filter((k) => fields[k] === void 0);
|
|
794
|
+
}
|
|
795
|
+
var EDITS_OFF_MESSAGE = "Edits are off in the Design Twin plugin \u2014 tick 'Allow edits' in the plugin window, then retry. Nothing in the file was changed.";
|
|
796
|
+
var COMMAND_TRAITS = {
|
|
797
|
+
ping: { run: "identity", export: false },
|
|
798
|
+
whoami: { run: "identity", export: false },
|
|
799
|
+
getSelection: { run: "scene", export: false },
|
|
800
|
+
listPages: { run: "scene", export: false },
|
|
801
|
+
listChildren: { run: "scene", export: false },
|
|
802
|
+
listLibraries: { run: "scene", export: false },
|
|
803
|
+
exportFull: { run: "exclusive", export: true },
|
|
804
|
+
exportDesignSystem: { run: "exclusive", export: true },
|
|
805
|
+
exportLibrary: { run: "exclusive", export: true },
|
|
806
|
+
exportSelection: { run: "exclusive", export: true },
|
|
807
|
+
exportNode: { run: "exclusive", export: true },
|
|
808
|
+
screenshot: { run: "exclusive", export: true },
|
|
809
|
+
write: { run: "exclusive", export: false }
|
|
810
|
+
};
|
|
811
|
+
function isCmd(x) {
|
|
812
|
+
return Object.prototype.hasOwnProperty.call(COMMAND_TRAITS, x);
|
|
813
|
+
}
|
|
814
|
+
var COMMANDS = Object.keys(COMMAND_TRAITS).filter(isCmd);
|
|
815
|
+
function runKind(cmd) {
|
|
816
|
+
return COMMAND_TRAITS[cmd].run;
|
|
817
|
+
}
|
|
818
|
+
var NO_NODE_ID = "No node id provided.";
|
|
819
|
+
var isBool = (v) => typeof v === "boolean";
|
|
820
|
+
var isStr = (v) => typeof v === "string";
|
|
821
|
+
var isNum = (v) => typeof v === "number";
|
|
822
|
+
function opt(a, key, ok, what, label = key) {
|
|
823
|
+
const v = a[key];
|
|
824
|
+
return v === void 0 || ok(v) ? null : `\`${label}\` is not ${what}`;
|
|
825
|
+
}
|
|
826
|
+
function first(errs) {
|
|
827
|
+
for (const e of errs) if (e !== null) return e;
|
|
828
|
+
return null;
|
|
829
|
+
}
|
|
830
|
+
var READ_OPT_NAMES = READ_OPTS.map((o) => o.name);
|
|
831
|
+
var readOptArgs = (a) => first(READ_OPT_NAMES.map((k) => opt(a, k, isBool, "a boolean")));
|
|
832
|
+
var nodeArgs = (a) => a.nodeId === void 0 || a.nodeId === null ? NO_NODE_ID : isStr(a.nodeId) ? null : "`nodeId` is not a string";
|
|
833
|
+
var designSystemArgs = (a) => opt(a, "variantVisuals", isBool, "a boolean");
|
|
834
|
+
var isPageSel = (v) => isStr(v) || Array.isArray(v) && v.every(isStr);
|
|
835
|
+
var isWriteOpName = (v) => isStr(v) && Object.prototype.hasOwnProperty.call(WRITE_OPS, v);
|
|
836
|
+
var isPadding = (v) => Array.isArray(v) && v.length === 4 && v.every(isNum);
|
|
837
|
+
var WRITE_STR_FIELDS = ["parentId", "name", "fill", "text", "nodeId", "color"];
|
|
838
|
+
var WRITE_NUM_FIELDS = ["width", "height", "itemSpacing", "fontSize"];
|
|
839
|
+
function writeOpError(o, i) {
|
|
840
|
+
const at = `ops[${i}]`;
|
|
841
|
+
if (!isRecord(o)) return `\`${at}\` is not an object`;
|
|
842
|
+
const op = o.op;
|
|
843
|
+
if (!isWriteOpName(op)) return `\`${at}.op\` is not one of ${WRITE_OP_NAMES.join(" | ")}`;
|
|
844
|
+
const typeErr = first([
|
|
845
|
+
...WRITE_STR_FIELDS.map((k) => opt(o, k, isStr, "a string", `${at}.${k}`)),
|
|
846
|
+
...WRITE_NUM_FIELDS.map((k) => opt(o, k, isNum, "a number", `${at}.${k}`)),
|
|
847
|
+
opt(o, "layoutMode", (v) => v === "HORIZONTAL" || v === "VERTICAL", "HORIZONTAL or VERTICAL", `${at}.layoutMode`),
|
|
848
|
+
opt(o, "padding", isPadding, "four numbers", `${at}.padding`)
|
|
849
|
+
]);
|
|
850
|
+
if (typeErr !== null) return typeErr;
|
|
851
|
+
const missing = missingWriteFields(op, o);
|
|
852
|
+
return missing.length ? `\`${at}\` (${op}): missing ${missing.join(", ")}` : null;
|
|
853
|
+
}
|
|
854
|
+
var writeArgs = (a) => {
|
|
855
|
+
if (!Array.isArray(a.ops)) return a.ops === void 0 ? "`ops` is missing" : "`ops` is not an array";
|
|
856
|
+
return first(a.ops.map(writeOpError));
|
|
857
|
+
};
|
|
858
|
+
var noArgs = () => null;
|
|
859
|
+
var ARGS_CHECKS = {
|
|
860
|
+
ping: noArgs,
|
|
861
|
+
whoami: noArgs,
|
|
862
|
+
getSelection: noArgs,
|
|
863
|
+
listPages: (a) => opt(a, "depth", (v) => v === 1 || v === 2, "1 or 2"),
|
|
864
|
+
listChildren: nodeArgs,
|
|
865
|
+
listLibraries: noArgs,
|
|
866
|
+
exportFull: (a) => first([readOptArgs(a), opt(a, "allPages", isBool, "a boolean"), opt(a, "page", isPageSel, "a string or an array of strings")]),
|
|
867
|
+
exportDesignSystem: designSystemArgs,
|
|
868
|
+
exportLibrary: (a) => first([designSystemArgs(a), opt(a, "asLibrary", isStr, "a string")]),
|
|
869
|
+
exportSelection: readOptArgs,
|
|
870
|
+
exportNode: (a) => first([nodeArgs(a), readOptArgs(a)]),
|
|
871
|
+
screenshot: (a) => first([nodeArgs(a), opt(a, "scale", isNum, "a number")]),
|
|
872
|
+
write: writeArgs
|
|
873
|
+
};
|
|
874
|
+
function argsShapeError(cmd, args) {
|
|
875
|
+
if (!isRecord(args)) return `bad ${cmd} args: not an object`;
|
|
876
|
+
const e = ARGS_CHECKS[cmd](args);
|
|
877
|
+
return e === null || e === NO_NODE_ID ? e : `bad ${cmd} args: ${e}`;
|
|
878
|
+
}
|
|
879
|
+
function hasArgs(cmd, a) {
|
|
880
|
+
return ARGS_CHECKS[cmd](a) === null;
|
|
881
|
+
}
|
|
882
|
+
function parseCommandRequest(cmd, args) {
|
|
883
|
+
if (!isCmd(cmd)) return { ok: false, error: "unknown cmd: " + cmd };
|
|
884
|
+
const a = isRecord(args) ? args : {};
|
|
885
|
+
const bad = () => ({ ok: false, error: argsShapeError(cmd, a) || `bad ${cmd} args` });
|
|
886
|
+
switch (cmd) {
|
|
887
|
+
case "ping":
|
|
888
|
+
return hasArgs(cmd, a) ? { ok: true, req: { cmd, args: a } } : bad();
|
|
889
|
+
case "whoami":
|
|
890
|
+
return hasArgs(cmd, a) ? { ok: true, req: { cmd, args: a } } : bad();
|
|
891
|
+
case "getSelection":
|
|
892
|
+
return hasArgs(cmd, a) ? { ok: true, req: { cmd, args: a } } : bad();
|
|
893
|
+
case "listPages":
|
|
894
|
+
return hasArgs(cmd, a) ? { ok: true, req: { cmd, args: a } } : bad();
|
|
895
|
+
case "listChildren":
|
|
896
|
+
return hasArgs(cmd, a) ? { ok: true, req: { cmd, args: a } } : bad();
|
|
897
|
+
case "listLibraries":
|
|
898
|
+
return hasArgs(cmd, a) ? { ok: true, req: { cmd, args: a } } : bad();
|
|
899
|
+
case "exportFull":
|
|
900
|
+
return hasArgs(cmd, a) ? { ok: true, req: { cmd, args: a } } : bad();
|
|
901
|
+
case "exportDesignSystem":
|
|
902
|
+
return hasArgs(cmd, a) ? { ok: true, req: { cmd, args: a } } : bad();
|
|
903
|
+
case "exportLibrary":
|
|
904
|
+
return hasArgs(cmd, a) ? { ok: true, req: { cmd, args: a } } : bad();
|
|
905
|
+
case "exportSelection":
|
|
906
|
+
return hasArgs(cmd, a) ? { ok: true, req: { cmd, args: a } } : bad();
|
|
907
|
+
case "exportNode":
|
|
908
|
+
return hasArgs(cmd, a) ? { ok: true, req: { cmd, args: a } } : bad();
|
|
909
|
+
case "screenshot":
|
|
910
|
+
return hasArgs(cmd, a) ? { ok: true, req: { cmd, args: a } } : bad();
|
|
911
|
+
case "write":
|
|
912
|
+
return hasArgs(cmd, a) ? { ok: true, req: { cmd, args: a } } : bad();
|
|
913
|
+
default: {
|
|
914
|
+
const exhaustive = cmd;
|
|
915
|
+
return { ok: false, error: "unknown cmd: " + String(exhaustive) };
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// src/layout.ts
|
|
921
|
+
var ALIGN = {
|
|
922
|
+
MIN: "flex-start",
|
|
923
|
+
CENTER: "center",
|
|
924
|
+
MAX: "flex-end",
|
|
925
|
+
SPACE_BETWEEN: "space-between",
|
|
926
|
+
SPACE_EVENLY: "space-evenly",
|
|
927
|
+
SPACE_AROUND: "space-around",
|
|
928
|
+
BASELINE: "baseline",
|
|
929
|
+
// AUTO is never looked up (counterAxisAlignContent skips it via the `!== "AUTO"` check below); the
|
|
930
|
+
// value exists only to make this Record total over AlignKey.
|
|
931
|
+
AUTO: "flex-start"
|
|
932
|
+
};
|
|
933
|
+
function flexIntent(src) {
|
|
934
|
+
const l = { display: "flex", flexDirection: src.layoutMode === "VERTICAL" ? "column" : "row" };
|
|
935
|
+
const isAutoSpacing = src.primaryAxisAlignItems === "SPACE_BETWEEN" || src.primaryAxisAlignItems === "SPACE_EVENLY" || src.primaryAxisAlignItems === "SPACE_AROUND";
|
|
936
|
+
if (src.itemSpacing && !isAutoSpacing) l.gap = src.itemSpacing;
|
|
937
|
+
const pad = [src.paddingTop, src.paddingRight, src.paddingBottom, src.paddingLeft];
|
|
938
|
+
if (pad.some((p) => p)) l.padding = pad;
|
|
939
|
+
if (src.primaryAxisAlignItems && src.primaryAxisAlignItems !== "MIN") l.justifyContent = ALIGN[src.primaryAxisAlignItems];
|
|
940
|
+
if (src.counterAxisAlignItems && src.counterAxisAlignItems !== "MIN") l.alignItems = ALIGN[src.counterAxisAlignItems];
|
|
941
|
+
if (src.layoutWrap === "WRAP") {
|
|
942
|
+
l.flexWrap = "wrap";
|
|
943
|
+
if (src.counterAxisSpacing && src.counterAxisAlignContent !== "SPACE_BETWEEN") {
|
|
944
|
+
if (l.flexDirection === "column") l.columnGap = src.counterAxisSpacing;
|
|
945
|
+
else l.rowGap = src.counterAxisSpacing;
|
|
946
|
+
}
|
|
947
|
+
if (src.counterAxisAlignContent && src.counterAxisAlignContent !== "AUTO") l.alignContent = ALIGN[src.counterAxisAlignContent];
|
|
948
|
+
}
|
|
949
|
+
return l;
|
|
950
|
+
}
|
|
951
|
+
function simplifyGrid(g) {
|
|
952
|
+
if (!g) return void 0;
|
|
953
|
+
const o = { ...ifDefined("pattern", g.pattern ? lower(g.pattern) : void 0) };
|
|
954
|
+
if (typeof g.sectionSize === "number") o.size = round(g.sectionSize);
|
|
955
|
+
if (g.pattern !== "GRID") {
|
|
956
|
+
if (typeof g.gutterSize === "number") o.gutter = round(g.gutterSize);
|
|
957
|
+
if (typeof g.count === "number" && g.count !== Infinity) o.count = g.count;
|
|
958
|
+
if (typeof g.offset === "number") o.offset = round(g.offset);
|
|
959
|
+
if (g.alignment) o.alignment = lower(g.alignment);
|
|
960
|
+
}
|
|
961
|
+
if (g.visible === false) o.visible = false;
|
|
962
|
+
return o;
|
|
963
|
+
}
|
|
964
|
+
function simplifyTrack(t) {
|
|
965
|
+
const o = { ...ifDefined("type", t.type ? lower(t.type) : void 0) };
|
|
966
|
+
if (typeof t.value === "number") o.value = round(t.value);
|
|
967
|
+
return o;
|
|
968
|
+
}
|
|
969
|
+
var GRID_SELF = { MIN: "start", CENTER: "center", MAX: "end" };
|
|
970
|
+
function layout(node) {
|
|
971
|
+
if (!("children" in node)) return void 0;
|
|
972
|
+
const n = node;
|
|
973
|
+
if (!("layoutMode" in node) || n.layoutMode === "NONE") {
|
|
974
|
+
if ("inferredAutoLayout" in node && n.inferredAutoLayout) {
|
|
975
|
+
const l2 = flexIntent(n.inferredAutoLayout);
|
|
976
|
+
l2.inferred = true;
|
|
977
|
+
return l2;
|
|
978
|
+
}
|
|
979
|
+
if (!("width" in node)) return void 0;
|
|
980
|
+
return { mode: "absolute", width: round(n.width), height: round(n.height) };
|
|
981
|
+
}
|
|
982
|
+
if (n.layoutMode === "GRID") {
|
|
983
|
+
const g = { display: "grid" };
|
|
984
|
+
const pad = [n.paddingTop, n.paddingRight, n.paddingBottom, n.paddingLeft];
|
|
985
|
+
if (pad.some((p) => p)) g.padding = pad;
|
|
986
|
+
if ("gridColumnCount" in node && typeof n.gridColumnCount === "number") g.columns = n.gridColumnCount;
|
|
987
|
+
if ("gridRowCount" in node && typeof n.gridRowCount === "number") g.rows = n.gridRowCount;
|
|
988
|
+
if ("gridColumnGap" in node && typeof n.gridColumnGap === "number") g.columnGap = n.gridColumnGap;
|
|
989
|
+
if ("gridRowGap" in node && typeof n.gridRowGap === "number") g.rowGap = n.gridRowGap;
|
|
990
|
+
if ("gridColumnSizes" in node && Array.isArray(n.gridColumnSizes) && n.gridColumnSizes.length) g.columnSizes = n.gridColumnSizes.map(simplifyTrack);
|
|
991
|
+
if ("gridRowSizes" in node && Array.isArray(n.gridRowSizes) && n.gridRowSizes.length) g.rowSizes = n.gridRowSizes.map(simplifyTrack);
|
|
992
|
+
if ("gridItemsPositioning" in node && n.gridItemsPositioning && n.gridItemsPositioning !== "MANUAL") g.autoFlow = String(n.gridItemsPositioning).toLowerCase();
|
|
993
|
+
if ("gridAutoTracks" in node && n.gridAutoTracks && n.gridAutoTracks !== "NONE") g.autoTracks = String(n.gridAutoTracks).toLowerCase();
|
|
994
|
+
return g;
|
|
995
|
+
}
|
|
996
|
+
const l = flexIntent(n);
|
|
997
|
+
if ("itemReverseZIndex" in node && n.itemReverseZIndex) l.reverseZ = true;
|
|
998
|
+
return l;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// src/variables.ts
|
|
1002
|
+
function isVariableAlias(v) {
|
|
1003
|
+
return !!v && typeof v === "object" && "type" in v && v.type === "VARIABLE_ALIAS";
|
|
1004
|
+
}
|
|
1005
|
+
async function resolveVar(alias) {
|
|
1006
|
+
if (!isVariableAlias(alias)) return void 0;
|
|
1007
|
+
return run().varName(alias.id);
|
|
1008
|
+
}
|
|
1009
|
+
async function resolveBoundMap(bound) {
|
|
1010
|
+
if (!bound) return void 0;
|
|
1011
|
+
const out = {};
|
|
1012
|
+
for (const key of Object.keys(bound)) {
|
|
1013
|
+
const val = bound[key];
|
|
1014
|
+
if (Array.isArray(val)) {
|
|
1015
|
+
const names = (await Promise.all(val.map(resolveVar))).filter((x) => !!x);
|
|
1016
|
+
const [first2, ...rest] = names;
|
|
1017
|
+
if (first2 !== void 0) out[key] = rest.length ? names : first2;
|
|
1018
|
+
} else {
|
|
1019
|
+
const n = await resolveVar(val);
|
|
1020
|
+
if (n) out[key] = n;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
return nonEmpty(out);
|
|
1024
|
+
}
|
|
1025
|
+
async function boundTokens(node) {
|
|
1026
|
+
return resolveBoundMap(node.boundVariables);
|
|
1027
|
+
}
|
|
1028
|
+
async function styleName(id) {
|
|
1029
|
+
if (!id || id === figma.mixed) return void 0;
|
|
1030
|
+
return run().styleName(id);
|
|
1031
|
+
}
|
|
1032
|
+
var STYLE_FIELDS = [
|
|
1033
|
+
["fillStyleId", "fill"],
|
|
1034
|
+
["strokeStyleId", "stroke"],
|
|
1035
|
+
["effectStyleId", "effect"],
|
|
1036
|
+
["textStyleId", "text"],
|
|
1037
|
+
["gridStyleId", "grid"]
|
|
1038
|
+
// a frame referencing a shared layout-grid style
|
|
1039
|
+
];
|
|
1040
|
+
async function nodeStyles(node) {
|
|
1041
|
+
const names = await Promise.all(
|
|
1042
|
+
STYLE_FIELDS.map(([field]) => {
|
|
1043
|
+
if (!(field in node)) return Promise.resolve(void 0);
|
|
1044
|
+
const id = anyProp(node, field);
|
|
1045
|
+
return typeof id === "string" || id === figma.mixed ? styleName(id) : Promise.resolve(void 0);
|
|
1046
|
+
})
|
|
1047
|
+
);
|
|
1048
|
+
const out = {};
|
|
1049
|
+
STYLE_FIELDS.forEach(([, key], i) => {
|
|
1050
|
+
const n = names[i];
|
|
1051
|
+
if (n) out[key] = n;
|
|
1052
|
+
});
|
|
1053
|
+
return nonEmpty(out);
|
|
1054
|
+
}
|
|
1055
|
+
async function resolveModeMap(raw, skipSingleMode) {
|
|
1056
|
+
if (!raw || !Object.keys(raw).length) return void 0;
|
|
1057
|
+
const out = {};
|
|
1058
|
+
for (const [collectionId, modeId] of Object.entries(raw)) {
|
|
1059
|
+
const c = await run().collection(collectionId);
|
|
1060
|
+
if (!c || !Array.isArray(c.modes)) {
|
|
1061
|
+
out[collectionId] = modeId;
|
|
1062
|
+
continue;
|
|
1063
|
+
}
|
|
1064
|
+
if (skipSingleMode && c.modes.length <= 1) continue;
|
|
1065
|
+
const m = c.modes.find((x) => x.modeId === modeId);
|
|
1066
|
+
out[c.name] = m ? m.name : modeId;
|
|
1067
|
+
}
|
|
1068
|
+
return nonEmpty(out);
|
|
1069
|
+
}
|
|
1070
|
+
function variableModes(node) {
|
|
1071
|
+
return resolveModeMap(node.explicitVariableModes, false);
|
|
1072
|
+
}
|
|
1073
|
+
function resolvedModes(node) {
|
|
1074
|
+
return resolveModeMap(node.resolvedVariableModes, true);
|
|
1075
|
+
}
|
|
1076
|
+
function isComposedColor(v) {
|
|
1077
|
+
return typeof v === "object" && "opacity" in v && "color" in v;
|
|
1078
|
+
}
|
|
1079
|
+
function isRgb(x) {
|
|
1080
|
+
return !!x && typeof x === "object" && "r" in x && typeof x.r === "number" && "g" in x && typeof x.g === "number" && "b" in x && typeof x.b === "number";
|
|
1081
|
+
}
|
|
1082
|
+
function composedFromExpression(v) {
|
|
1083
|
+
const u = v;
|
|
1084
|
+
if (!u || typeof u !== "object" || !("type" in u) || u.type !== "VARIABLE_EXPRESSION") return null;
|
|
1085
|
+
if (!("expressionFunction" in u) || u.expressionFunction !== "COMPOSE_COLOR") return null;
|
|
1086
|
+
if (!("expressionArguments" in u) || !isUnknownArray(u.expressionArguments) || u.expressionArguments.length !== 2) return null;
|
|
1087
|
+
const [c, o] = u.expressionArguments;
|
|
1088
|
+
if (isVariableAlias(c)) {
|
|
1089
|
+
if (isVariableAlias(o) || typeof o === "number") return { color: c, opacity: o };
|
|
1090
|
+
return null;
|
|
1091
|
+
}
|
|
1092
|
+
return isRgb(c) && isVariableAlias(o) ? { color: c, opacity: o } : null;
|
|
1093
|
+
}
|
|
1094
|
+
function toComposed(v) {
|
|
1095
|
+
return isComposedColor(v) ? v : composedFromExpression(v);
|
|
1096
|
+
}
|
|
1097
|
+
function colorIsAlias(v) {
|
|
1098
|
+
return isVariableAlias(v.color);
|
|
1099
|
+
}
|
|
1100
|
+
function aliasIds(v) {
|
|
1101
|
+
if (isVariableAlias(v)) return v.id ? [v.id] : [];
|
|
1102
|
+
const c = toComposed(v);
|
|
1103
|
+
if (!c) return [];
|
|
1104
|
+
const out = [];
|
|
1105
|
+
if (isVariableAlias(c.color) && c.color.id) out.push(c.color.id);
|
|
1106
|
+
if (isVariableAlias(c.opacity) && c.opacity.id) out.push(c.opacity.id);
|
|
1107
|
+
return out;
|
|
1108
|
+
}
|
|
1109
|
+
async function aliasValue(a) {
|
|
1110
|
+
return { aliasOf: await run().varName(a.id) || a.id };
|
|
1111
|
+
}
|
|
1112
|
+
async function composedValue(v) {
|
|
1113
|
+
if (colorIsAlias(v)) {
|
|
1114
|
+
const [color, opacity] = await Promise.all([aliasValue(v.color), isVariableAlias(v.opacity) ? aliasValue(v.opacity) : v.opacity]);
|
|
1115
|
+
return { composed: { color, opacity } };
|
|
1116
|
+
}
|
|
1117
|
+
return { composed: { color: rgbaToHex(v.color), opacity: await aliasValue(v.opacity) } };
|
|
1118
|
+
}
|
|
1119
|
+
async function resolveModeValue(v, resolvedType) {
|
|
1120
|
+
if (isVariableAlias(v)) return aliasValue(v);
|
|
1121
|
+
if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") return v;
|
|
1122
|
+
if (isComposedColor(v)) return composedValue(v);
|
|
1123
|
+
const fromExpression = composedFromExpression(v);
|
|
1124
|
+
if (fromExpression) return composedValue(fromExpression);
|
|
1125
|
+
if (resolvedType === "COLOR" && "r" in v && typeof v.r === "number") return rgbaToHex(v);
|
|
1126
|
+
return verbatimValue(v);
|
|
1127
|
+
}
|
|
1128
|
+
function verbatimValue(v) {
|
|
1129
|
+
const o = v;
|
|
1130
|
+
return o;
|
|
1131
|
+
}
|
|
1132
|
+
async function dumpVariables(opts) {
|
|
1133
|
+
const asLibrary = !!(opts && opts.asLibrary);
|
|
1134
|
+
const pendingPublish = [];
|
|
1135
|
+
const publishOf2 = async (o) => {
|
|
1136
|
+
try {
|
|
1137
|
+
if (!asLibrary || !o || typeof o.getPublishStatusAsync !== "function") return void 0;
|
|
1138
|
+
const s = await o.getPublishStatusAsync();
|
|
1139
|
+
return s ? String(s).toLowerCase() : void 0;
|
|
1140
|
+
} catch (e) {
|
|
1141
|
+
return void 0;
|
|
1142
|
+
}
|
|
1143
|
+
};
|
|
1144
|
+
const [collections, localVars] = await Promise.all([
|
|
1145
|
+
figma.variables.getLocalVariableCollectionsAsync(),
|
|
1146
|
+
figma.variables.getLocalVariablesAsync()
|
|
1147
|
+
]);
|
|
1148
|
+
const collById = new Map(collections.map((c) => [c.id, c]));
|
|
1149
|
+
const collOf = (cid) => collById.get(cid);
|
|
1150
|
+
const localIds = new Set(localVars.map((v) => v.id));
|
|
1151
|
+
const variables = [];
|
|
1152
|
+
const hygiene = [];
|
|
1153
|
+
const aliasTargets = (v) => {
|
|
1154
|
+
const out = [];
|
|
1155
|
+
for (const value of Object.values(v.valuesByMode || {})) out.push(...aliasIds(value));
|
|
1156
|
+
return out;
|
|
1157
|
+
};
|
|
1158
|
+
const remoteVars = [];
|
|
1159
|
+
const seen = new Set(localIds);
|
|
1160
|
+
let frontier = [...new Set(run().varName.ids().concat(...localVars.map(aliasTargets)))].filter((id) => !seen.has(id));
|
|
1161
|
+
while (frontier.length) {
|
|
1162
|
+
for (const id of frontier) seen.add(id);
|
|
1163
|
+
const fetched = (await Promise.all(frontier.map((id) => run().varName.obj(id)))).filter(Boolean);
|
|
1164
|
+
remoteVars.push(...fetched);
|
|
1165
|
+
frontier = [...new Set([].concat(...fetched.map(aliasTargets)))].filter((id) => !seen.has(id));
|
|
1166
|
+
}
|
|
1167
|
+
const missingCollIds = [...new Set(remoteVars.map((v) => v.variableCollectionId).filter((cid) => cid && !collById.has(cid)))];
|
|
1168
|
+
for (const c of await Promise.all(missingCollIds.map((cid) => run().collection(cid)))) {
|
|
1169
|
+
if (c) collById.set(c.id, c);
|
|
1170
|
+
}
|
|
1171
|
+
const allCollections = [...collById.values()];
|
|
1172
|
+
const modeName = {};
|
|
1173
|
+
for (const c of allCollections) for (const m of c.modes) if (!(m.modeId in modeName)) modeName[m.modeId] = m.name;
|
|
1174
|
+
const resolvedIds = new Set(localIds);
|
|
1175
|
+
for (const v of remoteVars) resolvedIds.add(v.id);
|
|
1176
|
+
if (remoteVars.length) {
|
|
1177
|
+
hygiene.push(
|
|
1178
|
+
remoteVars.length + " variable(s) referenced by this file come from a published LIBRARY, not this file \u2014 included below with remote:true (getLocalVariablesAsync cannot enumerate them)"
|
|
1179
|
+
);
|
|
1180
|
+
}
|
|
1181
|
+
for (const v of localVars.concat(remoteVars)) {
|
|
1182
|
+
const values = {};
|
|
1183
|
+
let hasAlias = false;
|
|
1184
|
+
const modes = await Promise.all(Object.entries(v.valuesByMode).map(async ([modeId, raw]) => ({ modeId, raw, value: await resolveModeValue(raw, v.resolvedType) })));
|
|
1185
|
+
for (const { modeId, raw, value } of modes) {
|
|
1186
|
+
if (isVariableAlias(raw) || toComposed(raw)) hasAlias = true;
|
|
1187
|
+
for (const id of isVariableAlias(raw) ? [raw.id] : aliasIds(raw)) {
|
|
1188
|
+
if (!resolvedIds.has(id)) hygiene.push("broken alias in '" + v.name + "' \u2014 target " + id + " could not be resolved");
|
|
1189
|
+
}
|
|
1190
|
+
values[modeName[modeId] || modeId] = value;
|
|
1191
|
+
}
|
|
1192
|
+
const coll = collOf(v.variableCollectionId);
|
|
1193
|
+
const collKey = coll ? coll.key : void 0;
|
|
1194
|
+
const rec = {
|
|
1195
|
+
name: v.name,
|
|
1196
|
+
type: v.resolvedType,
|
|
1197
|
+
collection: (coll || {}).name,
|
|
1198
|
+
...ifDefined("collectionKey", typeof collKey === "string" && collKey ? collKey : void 0),
|
|
1199
|
+
// tier: alias => semantic; raw+meaningfully-scoped => semantic leaf; raw+unscoped => primitive.
|
|
1200
|
+
// ALL_SCOPES is Figma's default catch-all (it pollutes every picker — see the hygiene flag below),
|
|
1201
|
+
// so it does NOT count as a meaningful scope; otherwise almost every variable would read semantic.
|
|
1202
|
+
tier: hasAlias ? "semantic" : v.scopes && v.scopes.some((s) => s !== "ALL_SCOPES") ? "semantic" : "primitive",
|
|
1203
|
+
values
|
|
1204
|
+
};
|
|
1205
|
+
if (v.scopes && v.scopes.length) rec.scopes = v.scopes;
|
|
1206
|
+
putNonEmpty(rec, "codeSyntax", v.codeSyntax);
|
|
1207
|
+
if (v.description) rec.description = v.description;
|
|
1208
|
+
if (v.remote) rec.remote = true;
|
|
1209
|
+
if (v.hiddenFromPublishing) rec.hiddenFromPublishing = true;
|
|
1210
|
+
if (v.key) rec.key = v.key;
|
|
1211
|
+
if (asLibrary) pendingPublish.push({ rec, obj: v });
|
|
1212
|
+
variables.push(rec);
|
|
1213
|
+
if (v.scopes && v.scopes.indexOf("ALL_SCOPES") !== -1) hygiene.push("ALL_SCOPES on '" + v.name + "' (pollutes every picker)");
|
|
1214
|
+
const modeCount = (collOf(v.variableCollectionId) || { modes: [] }).modes.length || 1;
|
|
1215
|
+
if (!hasAlias && v.resolvedType === "COLOR" && modeCount > 1) {
|
|
1216
|
+
hygiene.push("semantic color '" + v.name + "' holds a raw value in a multi-mode collection (breaks theming)");
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
if (pendingPublish.length) {
|
|
1220
|
+
const st = await Promise.all(pendingPublish.map((p) => publishOf2(p.obj)));
|
|
1221
|
+
for (const [i, p] of pendingPublish.entries()) {
|
|
1222
|
+
const s = st[i];
|
|
1223
|
+
if (s) p.rec.publish = s;
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
const collPublish = {};
|
|
1227
|
+
if (asLibrary) {
|
|
1228
|
+
const st = await Promise.all(allCollections.map((c) => publishOf2(c)));
|
|
1229
|
+
for (const [i, c] of allCollections.entries()) {
|
|
1230
|
+
const s = st[i];
|
|
1231
|
+
if (s) collPublish[c.id] = s;
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
return {
|
|
1235
|
+
// Local collections plus any LIBRARY collection a referenced remote variable belongs to — the
|
|
1236
|
+
// token emitters need each collection's default mode to pick the `:root` value.
|
|
1237
|
+
collections: allCollections.map((c) => ({
|
|
1238
|
+
name: c.name,
|
|
1239
|
+
modes: c.modes.map((m) => m.name),
|
|
1240
|
+
// Which mode is the base/`:root` default (vs. the override theme) — codegen otherwise guesses.
|
|
1241
|
+
...ifDefined("default", modeName[c.defaultModeId] || void 0),
|
|
1242
|
+
theming: c.modes.length > 1,
|
|
1243
|
+
// Extended collection (its modes inherit from a root collection via parentModeId) — codegen
|
|
1244
|
+
// should treat it as an override layer, not a standalone theme. Flag it; deep parent-mode
|
|
1245
|
+
// resolution (mode.parentModeId -> root mode) is deferred.
|
|
1246
|
+
...ifDefined("extended", c.isExtension === true ? true : void 0),
|
|
1247
|
+
...ifDefined("hiddenFromPublishing", c.hiddenFromPublishing === true ? true : void 0),
|
|
1248
|
+
...ifDefined("key", c.key || void 0),
|
|
1249
|
+
// durable cross-file collection identity
|
|
1250
|
+
...ifDefined("publish", collPublish[c.id] || void 0)
|
|
1251
|
+
// library mode only
|
|
1252
|
+
})),
|
|
1253
|
+
variables,
|
|
1254
|
+
hygiene
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
// src/assets.ts
|
|
1259
|
+
var ASSET_DIR = "assets/";
|
|
1260
|
+
function contentHash(a) {
|
|
1261
|
+
const src = a.text != null ? normalizeSvgText(a.text) : a.base64 != null ? a.base64 : "";
|
|
1262
|
+
let h = 2166136261;
|
|
1263
|
+
for (let i = 0; i < src.length; i++) {
|
|
1264
|
+
h ^= src.charCodeAt(i);
|
|
1265
|
+
h = h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24)) >>> 0;
|
|
1266
|
+
}
|
|
1267
|
+
return (h >>> 0).toString(16).padStart(8, "0") + "-" + src.length.toString(36);
|
|
1268
|
+
}
|
|
1269
|
+
function assetSafe(x) {
|
|
1270
|
+
return String(x).replace(/[^a-zA-Z0-9-]+/g, "_").replace(/_+/g, "_");
|
|
1271
|
+
}
|
|
1272
|
+
function baseNameFor(a) {
|
|
1273
|
+
if (a.kind === "reference") return safe(a.id.replace(/:ref$/, "")) + "_ref";
|
|
1274
|
+
if (a.kind === "source") return safe(a.id);
|
|
1275
|
+
const last2 = String(a.name || "").split("/").pop() || "";
|
|
1276
|
+
const cleaned = assetSafe(last2.trim()).replace(/^[-_]+|[-_]+$/g, "");
|
|
1277
|
+
return cleaned || safe(a.id);
|
|
1278
|
+
}
|
|
1279
|
+
function register(a) {
|
|
1280
|
+
const fmt = safe(a.format);
|
|
1281
|
+
const dedupable = a.kind !== "reference";
|
|
1282
|
+
const hash = contentHash(a);
|
|
1283
|
+
const r = run();
|
|
1284
|
+
const { byContent, byName } = r.assetFiles;
|
|
1285
|
+
if (dedupable) {
|
|
1286
|
+
const hit = byContent.get(hash + "." + fmt);
|
|
1287
|
+
if (hit) {
|
|
1288
|
+
(hit.asset.from || (hit.asset.from = [hit.asset.id])).push(a.id);
|
|
1289
|
+
return ASSET_DIR + hit.file;
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
const base = baseNameFor(a);
|
|
1293
|
+
let file = base + "." + fmt;
|
|
1294
|
+
const key = file.toLowerCase();
|
|
1295
|
+
const taken = byName.get(key);
|
|
1296
|
+
if (taken !== void 0 && taken !== hash) {
|
|
1297
|
+
const [head = ""] = hash.split("-");
|
|
1298
|
+
file = base + "-" + head.slice(0, 6) + "." + fmt;
|
|
1299
|
+
}
|
|
1300
|
+
byName.set(file.toLowerCase(), hash);
|
|
1301
|
+
const asset = { ...a, file, hash };
|
|
1302
|
+
r.assets.push(asset);
|
|
1303
|
+
if (dedupable) byContent.set(hash + "." + fmt, { file, asset });
|
|
1304
|
+
return ASSET_DIR + file;
|
|
1305
|
+
}
|
|
1306
|
+
function imageFormat(b) {
|
|
1307
|
+
if (b.length >= 8 && b[0] === 137 && b[1] === 80 && b[2] === 78 && b[3] === 71) return "png";
|
|
1308
|
+
if (b.length >= 3 && b[0] === 255 && b[1] === 216 && b[2] === 255) return "jpg";
|
|
1309
|
+
if (b.length >= 12 && b[0] === 82 && b[1] === 73 && b[8] === 87 && b[9] === 69 && b[10] === 66 && b[11] === 80) return "webp";
|
|
1310
|
+
if (b.length >= 6 && b[0] === 71 && b[1] === 73 && b[2] === 70) return "gif";
|
|
1311
|
+
return "bin";
|
|
1312
|
+
}
|
|
1313
|
+
function collectSourceImage(hash) {
|
|
1314
|
+
if (!hash) return Promise.resolve(void 0);
|
|
1315
|
+
const sizes = run().imageSizes;
|
|
1316
|
+
let p = sizes.get(hash);
|
|
1317
|
+
if (!p) {
|
|
1318
|
+
p = readSourceImage(hash);
|
|
1319
|
+
sizes.set(hash, p);
|
|
1320
|
+
}
|
|
1321
|
+
return p;
|
|
1322
|
+
}
|
|
1323
|
+
async function readSourceImage(hash) {
|
|
1324
|
+
let size;
|
|
1325
|
+
try {
|
|
1326
|
+
const img = figma.getImageByHash(hash);
|
|
1327
|
+
if (img) {
|
|
1328
|
+
const s = await img.getSizeAsync();
|
|
1329
|
+
if (s && typeof s.width === "number") size = { w: s.width, h: s.height };
|
|
1330
|
+
if (run().sourceBytes) {
|
|
1331
|
+
try {
|
|
1332
|
+
const bytes = await img.getBytesAsync();
|
|
1333
|
+
if (bytes && bytes.length) register({ id: "img:" + hash, name: hash, format: imageFormat(bytes), base64: toBase64(bytes), kind: "source" });
|
|
1334
|
+
} catch (e) {
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
} catch (e) {
|
|
1339
|
+
run().warn("source image unavailable for hash " + hash + " (" + errMsg(e) + ")");
|
|
1340
|
+
}
|
|
1341
|
+
return size;
|
|
1342
|
+
}
|
|
1343
|
+
var VECTOR_TYPES = /* @__PURE__ */ new Set(["VECTOR", "BOOLEAN_OPERATION", "STAR", "LINE", "POLYGON", "ELLIPSE"]);
|
|
1344
|
+
var ICON_CONTAINER_TYPES = /* @__PURE__ */ new Set(["FRAME", "INSTANCE", "GROUP", "COMPONENT"]);
|
|
1345
|
+
function paints(v) {
|
|
1346
|
+
return Array.isArray(v) ? v : null;
|
|
1347
|
+
}
|
|
1348
|
+
function hasVisiblePaint(node, fills) {
|
|
1349
|
+
if (fills && fills.some((p) => p.visible !== false)) return true;
|
|
1350
|
+
const s = paints("strokes" in node ? node.strokes : void 0);
|
|
1351
|
+
return !!s && s.some((p) => p.visible !== false);
|
|
1352
|
+
}
|
|
1353
|
+
function hasArea(node) {
|
|
1354
|
+
if (!("width" in node) || typeof node.width !== "number" || typeof node.height !== "number") return true;
|
|
1355
|
+
return node.width > 0 && node.height > 0;
|
|
1356
|
+
}
|
|
1357
|
+
function paintsNothing(node, isVector, fills) {
|
|
1358
|
+
return !hasArea(node) || isVector && !hasVisiblePaint(node, fills);
|
|
1359
|
+
}
|
|
1360
|
+
function geometryOf(node) {
|
|
1361
|
+
const ds = (g) => g ? g.map((p) => p && p.data).filter((d) => typeof d === "string" && !!d) : [];
|
|
1362
|
+
const fills = ds("fillGeometry" in node ? node.fillGeometry : void 0);
|
|
1363
|
+
const strokes = ds("strokeGeometry" in node ? node.strokeGeometry : void 0);
|
|
1364
|
+
if (!fills.length && !strokes.length) return void 0;
|
|
1365
|
+
const out = {};
|
|
1366
|
+
if (fills.length) out.fills = fills;
|
|
1367
|
+
if (strokes.length) out.strokes = strokes;
|
|
1368
|
+
if ("width" in node && typeof node.width === "number") {
|
|
1369
|
+
out.w = round(node.width);
|
|
1370
|
+
out.h = round(node.height);
|
|
1371
|
+
}
|
|
1372
|
+
return out;
|
|
1373
|
+
}
|
|
1374
|
+
async function collectAsset(node, hidden, policy) {
|
|
1375
|
+
const r = run();
|
|
1376
|
+
const stats = r.stats;
|
|
1377
|
+
const isVector = VECTOR_TYPES.has(node.type);
|
|
1378
|
+
const rawFills = "fills" in node ? node.fills : void 0;
|
|
1379
|
+
const fills = Array.isArray(rawFills) ? rawFills : null;
|
|
1380
|
+
const kids = "children" in node && Array.isArray(node.children) ? node.children : null;
|
|
1381
|
+
const hasImage = !!fills && fills.some((f) => f.type === "IMAGE" && f.visible !== false) && !(kids && kids.length > 0);
|
|
1382
|
+
let iconLike = false;
|
|
1383
|
+
if (ICON_CONTAINER_TYPES.has(node.type) && node.name && /icon|logo|illustration|avatar/i.test(node.name) && "width" in node && "findOne" in node && Math.max(node.width, node.height) <= 96) {
|
|
1384
|
+
try {
|
|
1385
|
+
iconLike = !node.findOne((x) => x.type === "TEXT");
|
|
1386
|
+
} catch (e) {
|
|
1387
|
+
iconLike = false;
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
if (!iconLike && ICON_CONTAINER_TYPES.has(node.type) && node.isAsset === true && "findOne" in node) {
|
|
1391
|
+
try {
|
|
1392
|
+
iconLike = !node.findOne((x) => x.type === "TEXT");
|
|
1393
|
+
} catch (e) {
|
|
1394
|
+
iconLike = false;
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
if ((policy === "none" || r.reads.skipAssets) && (isVector || iconLike || hasImage)) {
|
|
1398
|
+
if (policy !== "none") stats.assetsSkipped++;
|
|
1399
|
+
return { skipped: true };
|
|
1400
|
+
}
|
|
1401
|
+
if (hidden && (isVector || iconLike || hasImage)) {
|
|
1402
|
+
if ((isVector || iconLike) && paintsNothing(node, isVector, fills)) {
|
|
1403
|
+
stats.assetsSkippedInvisible++;
|
|
1404
|
+
return void 0;
|
|
1405
|
+
}
|
|
1406
|
+
stats.assetsHidden++;
|
|
1407
|
+
return { skipped: "hidden" };
|
|
1408
|
+
}
|
|
1409
|
+
if (isVector || iconLike || hasImage) {
|
|
1410
|
+
checkCancelled();
|
|
1411
|
+
progress("assets", { nodes: stats.nodes, assets: r.assets.length });
|
|
1412
|
+
}
|
|
1413
|
+
if (isVector || iconLike) {
|
|
1414
|
+
if (paintsNothing(node, isVector, fills)) {
|
|
1415
|
+
stats.assetsSkippedInvisible++;
|
|
1416
|
+
return void 0;
|
|
1417
|
+
}
|
|
1418
|
+
let svg = null;
|
|
1419
|
+
let reason = "empty/invalid SVG";
|
|
1420
|
+
try {
|
|
1421
|
+
if ("exportAsync" in node) svg = await node.exportAsync({ format: "SVG_STRING" });
|
|
1422
|
+
} catch (e) {
|
|
1423
|
+
reason = errMsg(e);
|
|
1424
|
+
}
|
|
1425
|
+
if (svg && svg.indexOf("<svg") !== -1) {
|
|
1426
|
+
return { path: register({ id: node.id, name: node.name, format: "svg", text: svg }) };
|
|
1427
|
+
}
|
|
1428
|
+
const geo = isVector ? geometryOf(node) : void 0;
|
|
1429
|
+
if (geo) {
|
|
1430
|
+
stats.assetsGeometry++;
|
|
1431
|
+
return { geometry: geo };
|
|
1432
|
+
}
|
|
1433
|
+
stats.assetsFailed++;
|
|
1434
|
+
r.warnKind("asset export failed (no geometry to fall back on)", node.name + " (" + node.id + "): " + reason);
|
|
1435
|
+
return void 0;
|
|
1436
|
+
}
|
|
1437
|
+
try {
|
|
1438
|
+
if (hasImage && "exportAsync" in node) {
|
|
1439
|
+
const bytes = await node.exportAsync({ format: "PNG", constraint: { type: "SCALE", value: 2 }, useAbsoluteBounds: true });
|
|
1440
|
+
if (!bytes || !bytes.length) {
|
|
1441
|
+
stats.assetsFailed++;
|
|
1442
|
+
r.warnKind("image asset export empty", node.name + " (" + node.id + ")");
|
|
1443
|
+
return void 0;
|
|
1444
|
+
}
|
|
1445
|
+
return { path: register({ id: node.id, name: node.name, format: "png", base64: toBase64(bytes) }) };
|
|
1446
|
+
}
|
|
1447
|
+
} catch (e) {
|
|
1448
|
+
stats.assetsFailed++;
|
|
1449
|
+
r.warnKind("image asset export failed", node.name + " (" + node.id + "): " + errMsg(e));
|
|
1450
|
+
}
|
|
1451
|
+
return void 0;
|
|
1452
|
+
}
|
|
1453
|
+
function referenceScale(node, opts) {
|
|
1454
|
+
if (opts && typeof opts.scale === "number" && opts.scale > 0) return opts.scale;
|
|
1455
|
+
const w = "width" in node ? node.width || 0 : 0;
|
|
1456
|
+
const h = "height" in node ? node.height || 0 : 0;
|
|
1457
|
+
return Math.min(2, 2048 / (Math.max(w, h) || 1));
|
|
1458
|
+
}
|
|
1459
|
+
async function collectReference(node, opts) {
|
|
1460
|
+
if (!node || !("exportAsync" in node) || !("width" in node)) return void 0;
|
|
1461
|
+
try {
|
|
1462
|
+
const value = referenceScale(node, opts);
|
|
1463
|
+
const bytes = await node.exportAsync({ format: "PNG", constraint: { type: "SCALE", value } });
|
|
1464
|
+
if (!bytes || !bytes.length) {
|
|
1465
|
+
run().warn("reference screenshot empty: " + node.name);
|
|
1466
|
+
return void 0;
|
|
1467
|
+
}
|
|
1468
|
+
return register({ id: node.id + ":ref", name: node.name + " (reference)", format: "png", base64: toBase64(bytes), kind: "reference" });
|
|
1469
|
+
} catch (e) {
|
|
1470
|
+
run().warn("reference screenshot failed: " + node.name + " (" + errMsg(e) + ")");
|
|
1471
|
+
return void 0;
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
async function devResources(node) {
|
|
1475
|
+
if (!node) return void 0;
|
|
1476
|
+
try {
|
|
1477
|
+
const rs = await node.getDevResourcesAsync({ includeChildren: true });
|
|
1478
|
+
if (!Array.isArray(rs) || !rs.length) return void 0;
|
|
1479
|
+
return rs.map((r) => {
|
|
1480
|
+
const o = { name: r.name, url: r.url };
|
|
1481
|
+
if (r.nodeId) o.nodeId = r.nodeId;
|
|
1482
|
+
if (r.inheritedNodeId) o.inheritedNodeId = r.inheritedNodeId;
|
|
1483
|
+
return o;
|
|
1484
|
+
});
|
|
1485
|
+
} catch (e) {
|
|
1486
|
+
run().warn("dev resources unavailable for '" + node.name + "' (" + errMsg(e) + ")");
|
|
1487
|
+
return void 0;
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
// src/paint.ts
|
|
1492
|
+
var FILTER_KEYS = ["exposure", "contrast", "saturation", "temperature", "tint", "highlights", "shadows"];
|
|
1493
|
+
function imageFilters(f) {
|
|
1494
|
+
if (!f.filters || typeof f.filters !== "object") return void 0;
|
|
1495
|
+
const src = f.filters;
|
|
1496
|
+
const out = {};
|
|
1497
|
+
for (const k of FILTER_KEYS) {
|
|
1498
|
+
const v = src[k];
|
|
1499
|
+
if (typeof v === "number" && v) out[k] = round(v);
|
|
1500
|
+
}
|
|
1501
|
+
return nonEmpty(out);
|
|
1502
|
+
}
|
|
1503
|
+
function paintExtras(f, o) {
|
|
1504
|
+
if (f.blendMode && f.blendMode !== "NORMAL") o.blend = f.blendMode.toLowerCase();
|
|
1505
|
+
if (typeof f.opacity === "number" && f.opacity < 1) o.opacity = round(f.opacity);
|
|
1506
|
+
return o;
|
|
1507
|
+
}
|
|
1508
|
+
function mediaPaint(f, type, hash, transform) {
|
|
1509
|
+
const o = paintExtras(f, { type, ...ifDefined("scaleMode", f.scaleMode ? lower(f.scaleMode) : void 0) });
|
|
1510
|
+
if (hash) o.hash = hash;
|
|
1511
|
+
if (f.scaleMode === "TILE" && typeof f.scalingFactor === "number") o.scale = f.scalingFactor;
|
|
1512
|
+
if (typeof f.rotation === "number" && f.rotation) o.rotation = f.rotation;
|
|
1513
|
+
if (Array.isArray(transform)) o.transform = transform;
|
|
1514
|
+
const flt = imageFilters(f);
|
|
1515
|
+
if (flt) o.filters = flt;
|
|
1516
|
+
return o;
|
|
1517
|
+
}
|
|
1518
|
+
async function shaderColor(c, tokenKey, tokens) {
|
|
1519
|
+
if (isVariableAlias(c)) {
|
|
1520
|
+
const name = await resolveVar(c);
|
|
1521
|
+
if (name) tokens[tokenKey] = name;
|
|
1522
|
+
return void 0;
|
|
1523
|
+
}
|
|
1524
|
+
return rgbaToHex(c);
|
|
1525
|
+
}
|
|
1526
|
+
async function shaderValue(v, defId, tokens) {
|
|
1527
|
+
if (typeof v === "boolean" || typeof v === "string") return v;
|
|
1528
|
+
if (typeof v === "number") return round(v);
|
|
1529
|
+
if (v == null || typeof v !== "object") return void 0;
|
|
1530
|
+
if (isVariableAlias(v)) {
|
|
1531
|
+
const name = await resolveVar(v);
|
|
1532
|
+
if (name) tokens[defId] = name;
|
|
1533
|
+
return void 0;
|
|
1534
|
+
}
|
|
1535
|
+
if ("stops" in v) {
|
|
1536
|
+
const stops = await Promise.all(v.stops.map(async (s, i) => {
|
|
1537
|
+
const color = await shaderColor(s.color, `${defId}.stops.${i}.color`, tokens);
|
|
1538
|
+
return color === void 0 ? { pos: round(s.position) } : { pos: round(s.position), color };
|
|
1539
|
+
}));
|
|
1540
|
+
return { stops };
|
|
1541
|
+
}
|
|
1542
|
+
if ("color" in v) {
|
|
1543
|
+
const color = await shaderColor(v.color, `${defId}.color`, tokens);
|
|
1544
|
+
return color === void 0 ? xy(v) : { ...xy(v), color };
|
|
1545
|
+
}
|
|
1546
|
+
if ("radius" in v) {
|
|
1547
|
+
return "angle" in v ? { ...xy(v), radius: round(v.radius), angle: round(v.angle) } : { ...xy(v), radius: round(v.radius) };
|
|
1548
|
+
}
|
|
1549
|
+
if ("x2" in v) return { ...xy(v), x2: round(v.x2), y2: round(v.y2) };
|
|
1550
|
+
if ("x" in v) return xy(v);
|
|
1551
|
+
if ("r" in v && "g" in v && "b" in v) return { color: rgbaToHex(v) };
|
|
1552
|
+
return void 0;
|
|
1553
|
+
}
|
|
1554
|
+
async function shaderDefs(meta) {
|
|
1555
|
+
if (!meta || typeof meta !== "object") return void 0;
|
|
1556
|
+
const out = {};
|
|
1557
|
+
for (const [defId, d] of Object.entries(meta)) {
|
|
1558
|
+
if (!d || typeof d.name !== "string" || typeof d.type !== "string") continue;
|
|
1559
|
+
const def = { name: d.name, type: lower(d.type) };
|
|
1560
|
+
if (d.defaultValue != null) {
|
|
1561
|
+
const dt = {};
|
|
1562
|
+
const dv = await shaderValue(d.defaultValue, "default", dt);
|
|
1563
|
+
if (dv !== void 0) def.default = dv;
|
|
1564
|
+
putNonEmpty(def, "tokens", dt);
|
|
1565
|
+
}
|
|
1566
|
+
if (typeof d.description === "string" && d.description) def.description = d.description;
|
|
1567
|
+
out[defId] = def;
|
|
1568
|
+
}
|
|
1569
|
+
return out;
|
|
1570
|
+
}
|
|
1571
|
+
async function putShaderProperties(o, props, meta) {
|
|
1572
|
+
const out = {};
|
|
1573
|
+
const tokens = {};
|
|
1574
|
+
if (props && typeof props === "object") {
|
|
1575
|
+
for (const [defId, pv] of Object.entries(props)) {
|
|
1576
|
+
const v = await shaderValue(pv, defId, tokens);
|
|
1577
|
+
if (v !== void 0) out[defId] = v;
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
putNonEmpty(o, "properties", out);
|
|
1581
|
+
putNonEmpty(o, "propertyDefs", await shaderDefs(meta));
|
|
1582
|
+
putNonEmpty(o, "tokens", tokens);
|
|
1583
|
+
}
|
|
1584
|
+
async function simplifyFills(fills) {
|
|
1585
|
+
if (!fills || fills === figma.mixed || !Array.isArray(fills)) return void 0;
|
|
1586
|
+
const vis = fills.filter((f) => f.visible !== false);
|
|
1587
|
+
const out = await Promise.all(
|
|
1588
|
+
vis.map(async (f) => {
|
|
1589
|
+
let o;
|
|
1590
|
+
if (f.type === "SOLID") {
|
|
1591
|
+
o = { type: "solid", color: solidHex(f) };
|
|
1592
|
+
if (f.blendMode && f.blendMode !== "NORMAL") o.blend = f.blendMode.toLowerCase();
|
|
1593
|
+
} else if (f.type.startsWith("GRADIENT")) {
|
|
1594
|
+
const g = f;
|
|
1595
|
+
const gp = paintExtras(g, { type: "gradient", kind: g.type });
|
|
1596
|
+
if (isList(g.gradientStops)) {
|
|
1597
|
+
gp.stops = await Promise.all(
|
|
1598
|
+
g.gradientStops.map(async (s) => {
|
|
1599
|
+
const stop = { pos: round(s.position), color: rgbaToHex(s.color) };
|
|
1600
|
+
const bv = await resolveBoundMap(s.boundVariables);
|
|
1601
|
+
if (bv) stop.tokens = bv;
|
|
1602
|
+
return stop;
|
|
1603
|
+
})
|
|
1604
|
+
);
|
|
1605
|
+
}
|
|
1606
|
+
if (Array.isArray(g.gradientTransform)) gp.transform = g.gradientTransform;
|
|
1607
|
+
o = gp;
|
|
1608
|
+
} else if (f.type === "IMAGE") {
|
|
1609
|
+
const mp = mediaPaint(f, "image", f.imageHash, f.imageTransform);
|
|
1610
|
+
const size = await collectSourceImage(f.imageHash);
|
|
1611
|
+
if (size) mp.intrinsicSize = size;
|
|
1612
|
+
o = mp;
|
|
1613
|
+
} else if (f.type === "VIDEO") {
|
|
1614
|
+
o = mediaPaint(f, "video", f.videoHash, f.videoTransform);
|
|
1615
|
+
} else if (f.type === "PATTERN") {
|
|
1616
|
+
const pp = paintExtras(f, { type: "pattern" });
|
|
1617
|
+
if (f.sourceNodeId) pp.sourceNodeId = f.sourceNodeId;
|
|
1618
|
+
if (f.tileType) pp.tileType = String(f.tileType).toLowerCase();
|
|
1619
|
+
if (typeof f.scalingFactor === "number") pp.scale = f.scalingFactor;
|
|
1620
|
+
if (f.spacing) pp.spacing = xy(f.spacing);
|
|
1621
|
+
if (f.horizontalAlignment) pp.align = String(f.horizontalAlignment).toLowerCase();
|
|
1622
|
+
o = pp;
|
|
1623
|
+
} else if (f.type === "SHADER") {
|
|
1624
|
+
const sp = paintExtras(f, { type: "shader" });
|
|
1625
|
+
if (f.id) sp.shaderId = f.id;
|
|
1626
|
+
await putShaderProperties(sp, f.properties, f.propertyMetadata);
|
|
1627
|
+
o = sp;
|
|
1628
|
+
} else {
|
|
1629
|
+
const future = f;
|
|
1630
|
+
o = paintExtras(future, { type: future.type.toLowerCase() });
|
|
1631
|
+
}
|
|
1632
|
+
const pbv = f.type === "SOLID" ? await resolveBoundMap(f.boundVariables) : void 0;
|
|
1633
|
+
if (pbv) o.tokens = pbv;
|
|
1634
|
+
return o;
|
|
1635
|
+
})
|
|
1636
|
+
);
|
|
1637
|
+
return out.length ? out : void 0;
|
|
1638
|
+
}
|
|
1639
|
+
var SIDE_WEIGHTS = [
|
|
1640
|
+
["strokeTopWeight", "top"],
|
|
1641
|
+
["strokeRightWeight", "right"],
|
|
1642
|
+
["strokeBottomWeight", "bottom"],
|
|
1643
|
+
["strokeLeftWeight", "left"]
|
|
1644
|
+
];
|
|
1645
|
+
async function simplifyStrokes(node) {
|
|
1646
|
+
if (!("strokes" in node) || !isList(node.strokes)) return void 0;
|
|
1647
|
+
const vis = node.strokes.filter((s) => s.visible !== false);
|
|
1648
|
+
if (!vis.length) return void 0;
|
|
1649
|
+
const out = {};
|
|
1650
|
+
const solids = vis.filter((s) => s.type === "SOLID");
|
|
1651
|
+
if (solids.length) out.colors = solids.map(solidHex);
|
|
1652
|
+
if (vis.some((s) => s.type !== "SOLID")) {
|
|
1653
|
+
const paints2 = await simplifyFills(vis);
|
|
1654
|
+
if (paints2) out.paints = paints2;
|
|
1655
|
+
}
|
|
1656
|
+
const strokeNode = node;
|
|
1657
|
+
const strokeWeight = strokeNode.strokeWeight;
|
|
1658
|
+
if (strokeWeight && strokeWeight !== figma.mixed) {
|
|
1659
|
+
out.weight = strokeWeight;
|
|
1660
|
+
} else if (strokeWeight === figma.mixed) {
|
|
1661
|
+
const sides = {};
|
|
1662
|
+
SIDE_WEIGHTS.forEach(([k, s]) => {
|
|
1663
|
+
const v = numProp(node, k);
|
|
1664
|
+
if (v != null) sides[s] = v;
|
|
1665
|
+
});
|
|
1666
|
+
putNonEmpty(out, "weights", sides);
|
|
1667
|
+
}
|
|
1668
|
+
const n = strokeNode;
|
|
1669
|
+
if (n.strokeAlign) out.align = lower(n.strokeAlign);
|
|
1670
|
+
if (n.dashPattern && n.dashPattern.length) out.dash = [...n.dashPattern];
|
|
1671
|
+
if ("strokeCap" in node && n.strokeCap && n.strokeCap !== figma.mixed && n.strokeCap !== "NONE") out.cap = String(n.strokeCap).toLowerCase();
|
|
1672
|
+
if ("strokeJoin" in node && n.strokeJoin && n.strokeJoin !== figma.mixed && n.strokeJoin !== "MITER") out.join = String(n.strokeJoin).toLowerCase();
|
|
1673
|
+
if ("strokeMiterLimit" in node && typeof n.strokeMiterLimit === "number" && n.strokeMiterLimit !== 4) out.miter = n.strokeMiterLimit;
|
|
1674
|
+
const vw = n.variableWidthStrokeProperties;
|
|
1675
|
+
if (vw && vw.widthProfile) {
|
|
1676
|
+
const variableWidth = { profile: lower(vw.widthProfile) };
|
|
1677
|
+
if (vw.widthProfile === "CUSTOM" && isList(vw.variableWidthPoints) && vw.variableWidthPoints.length) {
|
|
1678
|
+
variableWidth.points = vw.variableWidthPoints.map((p) => ({ pos: round(p.position), width: round(p.width) }));
|
|
1679
|
+
}
|
|
1680
|
+
out.variableWidth = variableWidth;
|
|
1681
|
+
}
|
|
1682
|
+
if ("complexStrokeProperties" in node) {
|
|
1683
|
+
const complex = complexStroke(node.complexStrokeProperties);
|
|
1684
|
+
if (complex) out.complex = complex;
|
|
1685
|
+
}
|
|
1686
|
+
return out;
|
|
1687
|
+
}
|
|
1688
|
+
function complexStroke(c) {
|
|
1689
|
+
if (!c || typeof c !== "object") return void 0;
|
|
1690
|
+
if (c.type === "DYNAMIC") return { type: "dynamic", frequency: round(c.frequency), wiggle: round(c.wiggle), smoothen: round(c.smoothen) };
|
|
1691
|
+
if (c.type !== "BRUSH") return void 0;
|
|
1692
|
+
if (c.brushType === "STRETCH") return { type: "brush", brushType: "stretch", brushName: lower(c.brushName), direction: lower(c.direction) };
|
|
1693
|
+
return {
|
|
1694
|
+
type: "brush",
|
|
1695
|
+
brushType: "scatter",
|
|
1696
|
+
brushName: lower(c.brushName),
|
|
1697
|
+
gap: round(c.gap),
|
|
1698
|
+
wiggle: round(c.wiggle),
|
|
1699
|
+
sizeJitter: round(c.sizeJitter),
|
|
1700
|
+
angularJitter: round(c.angularJitter),
|
|
1701
|
+
rotation: round(c.rotation)
|
|
1702
|
+
};
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
// src/effects.ts
|
|
1706
|
+
var GLASS_FIELDS = ["lightIntensity", "lightAngle", "refraction", "depth", "dispersion", "radius"];
|
|
1707
|
+
function effectBody(e) {
|
|
1708
|
+
if (e.type === "DROP_SHADOW" || e.type === "INNER_SHADOW") {
|
|
1709
|
+
const o = { type: lower(e.type) };
|
|
1710
|
+
if (e.color) o.color = rgbaToHex(e.color);
|
|
1711
|
+
if (e.offset) o.offset = xy(e.offset);
|
|
1712
|
+
if (typeof e.radius === "number") o.radius = round(e.radius);
|
|
1713
|
+
if (typeof e.spread === "number" && e.spread) o.spread = round(e.spread);
|
|
1714
|
+
if (e.blendMode && e.blendMode !== "NORMAL") o.blendMode = e.blendMode.toLowerCase();
|
|
1715
|
+
if (e.type === "DROP_SHADOW" && e.showShadowBehindNode) o.behindNode = true;
|
|
1716
|
+
return o;
|
|
1717
|
+
}
|
|
1718
|
+
if (e.type === "LAYER_BLUR" || e.type === "BACKGROUND_BLUR") {
|
|
1719
|
+
const o = { type: lower(e.type) };
|
|
1720
|
+
if (typeof e.radius === "number") o.radius = round(e.radius);
|
|
1721
|
+
if (e.blurType && e.blurType !== "NORMAL") o.blurType = lower(e.blurType);
|
|
1722
|
+
const be = e;
|
|
1723
|
+
putXY(o, "startOffset", be.startOffset);
|
|
1724
|
+
putXY(o, "endOffset", be.endOffset);
|
|
1725
|
+
if (typeof be.startRadius === "number") o.startRadius = round(be.startRadius);
|
|
1726
|
+
return o;
|
|
1727
|
+
}
|
|
1728
|
+
if (e.type === "NOISE") {
|
|
1729
|
+
const o = { type: "noise" };
|
|
1730
|
+
if (e.noiseType) o.noiseType = String(e.noiseType).toLowerCase();
|
|
1731
|
+
if (e.color) o.color = rgbaToHex(e.color);
|
|
1732
|
+
if (typeof e.density === "number") o.density = round(e.density);
|
|
1733
|
+
if (typeof e.noiseSize === "number") o.noiseSize = round(e.noiseSize);
|
|
1734
|
+
putXY(o, "noiseSizeVector", e.noiseSizeVector);
|
|
1735
|
+
if (e.noiseType === "DUOTONE" && e.secondaryColor) o.secondaryColor = rgbaToHex(e.secondaryColor);
|
|
1736
|
+
if (e.noiseType === "MULTITONE" && typeof e.opacity === "number") o.opacity = round(e.opacity);
|
|
1737
|
+
if (e.blendMode && e.blendMode !== "NORMAL") o.blendMode = e.blendMode.toLowerCase();
|
|
1738
|
+
return o;
|
|
1739
|
+
}
|
|
1740
|
+
if (e.type === "GLASS") {
|
|
1741
|
+
const o = { type: "glass" };
|
|
1742
|
+
for (const k of GLASS_FIELDS) {
|
|
1743
|
+
const v = e[k];
|
|
1744
|
+
if (typeof v === "number") o[k] = round(v);
|
|
1745
|
+
}
|
|
1746
|
+
return o;
|
|
1747
|
+
}
|
|
1748
|
+
if (e.type === "TEXTURE") {
|
|
1749
|
+
const o = { type: "texture" };
|
|
1750
|
+
if (typeof e.noiseSize === "number") o.noiseSize = round(e.noiseSize);
|
|
1751
|
+
putXY(o, "noiseSizeVector", e.noiseSizeVector);
|
|
1752
|
+
if (typeof e.radius === "number") o.radius = round(e.radius);
|
|
1753
|
+
if (typeof e.clipToShape === "boolean") o.clipToShape = e.clipToShape;
|
|
1754
|
+
return o;
|
|
1755
|
+
}
|
|
1756
|
+
if (e.type === "SHADER") {
|
|
1757
|
+
const o = { type: "shader" };
|
|
1758
|
+
if (e.id) o.shaderId = e.id;
|
|
1759
|
+
return o;
|
|
1760
|
+
}
|
|
1761
|
+
const future = e;
|
|
1762
|
+
return { type: future.type.toLowerCase() };
|
|
1763
|
+
}
|
|
1764
|
+
async function simplifyEffects(effects) {
|
|
1765
|
+
if (!isList(effects)) return void 0;
|
|
1766
|
+
const vis = effects.filter((e) => e.visible !== false);
|
|
1767
|
+
const out = await Promise.all(
|
|
1768
|
+
vis.map(async (e) => {
|
|
1769
|
+
const o = effectBody(e);
|
|
1770
|
+
if (e.type === "SHADER" && o.type === "shader") await putShaderProperties(o, e.properties, e.propertyMetadata);
|
|
1771
|
+
const bv = await resolveBoundMap("boundVariables" in e ? e.boundVariables : void 0);
|
|
1772
|
+
if (bv) o.tokens = o.tokens ? { ...o.tokens, ...bv } : bv;
|
|
1773
|
+
return o;
|
|
1774
|
+
})
|
|
1775
|
+
);
|
|
1776
|
+
return out.length ? out : void 0;
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
// src/text.ts
|
|
1780
|
+
var lenUnit = (v) => ({ value: round(v.value), unit: v.unit === "PERCENT" ? "percent" : "px" });
|
|
1781
|
+
function lineH(v) {
|
|
1782
|
+
if (!v || v === figma.mixed) return void 0;
|
|
1783
|
+
if (v.unit === "AUTO") return { unit: "auto" };
|
|
1784
|
+
return lenUnit(v);
|
|
1785
|
+
}
|
|
1786
|
+
function letterS(v) {
|
|
1787
|
+
if (!v || v === figma.mixed || !v.value) return void 0;
|
|
1788
|
+
return lenUnit(v);
|
|
1789
|
+
}
|
|
1790
|
+
function decoLen(v) {
|
|
1791
|
+
if (!v || v === figma.mixed || v.unit === "AUTO" || typeof v.value !== "number") return void 0;
|
|
1792
|
+
return lenUnit(v);
|
|
1793
|
+
}
|
|
1794
|
+
function fontObj(src) {
|
|
1795
|
+
const f = {};
|
|
1796
|
+
f.size = src.fontSize !== figma.mixed ? src.fontSize : "mixed";
|
|
1797
|
+
if (src.fontName && src.fontName !== figma.mixed) {
|
|
1798
|
+
f.family = src.fontName.family;
|
|
1799
|
+
f.weight = src.fontName.style;
|
|
1800
|
+
const vs = src.fontName.variationSettings;
|
|
1801
|
+
if (vs && Object.keys(vs).length) f.variationSettings = { ...vs };
|
|
1802
|
+
}
|
|
1803
|
+
if (typeof src.fontWeight === "number") f.weightValue = src.fontWeight;
|
|
1804
|
+
if (src.fontStyle === "ITALIC") f.fontStyle = "italic";
|
|
1805
|
+
const lh = lineH(src.lineHeight);
|
|
1806
|
+
if (lh) f.lineHeight = lh;
|
|
1807
|
+
const ls = letterS(src.letterSpacing);
|
|
1808
|
+
if (ls) f.letterSpacing = ls;
|
|
1809
|
+
if (src.textCase && src.textCase !== figma.mixed && src.textCase !== "ORIGINAL") f.case = lower(src.textCase);
|
|
1810
|
+
if (src.textDecoration && src.textDecoration !== figma.mixed && src.textDecoration !== "NONE") f.decoration = lower(src.textDecoration);
|
|
1811
|
+
if (f.decoration) {
|
|
1812
|
+
if (src.textDecorationStyle && src.textDecorationStyle !== figma.mixed && src.textDecorationStyle !== "SOLID") f.decorationStyle = String(src.textDecorationStyle).toLowerCase();
|
|
1813
|
+
const tdc = src.textDecorationColor && src.textDecorationColor !== figma.mixed ? src.textDecorationColor.value : void 0;
|
|
1814
|
+
const dc = solidFromFills(tdc && tdc !== "AUTO" ? [tdc] : void 0);
|
|
1815
|
+
if (dc) f.decorationColor = dc;
|
|
1816
|
+
const th = decoLen(src.textDecorationThickness);
|
|
1817
|
+
if (th) f.decorationThickness = th;
|
|
1818
|
+
const off = decoLen(src.textDecorationOffset);
|
|
1819
|
+
if (off) f.decorationOffset = off;
|
|
1820
|
+
if (src.textDecorationSkipInk === false) f.decorationSkipInk = false;
|
|
1821
|
+
}
|
|
1822
|
+
const feats = src.openTypeFeatures;
|
|
1823
|
+
if (feats && feats !== figma.mixed) {
|
|
1824
|
+
const on = Object.keys(feats).filter((k) => feats[k]);
|
|
1825
|
+
if (on.length) f.openType = on;
|
|
1826
|
+
}
|
|
1827
|
+
const color = solidFromFills(src.fills);
|
|
1828
|
+
if (color) f.color = color;
|
|
1829
|
+
return f;
|
|
1830
|
+
}
|
|
1831
|
+
var TEXT_SEG_FIELDS = [
|
|
1832
|
+
"fontName",
|
|
1833
|
+
"fontSize",
|
|
1834
|
+
"fontWeight",
|
|
1835
|
+
"lineHeight",
|
|
1836
|
+
"letterSpacing",
|
|
1837
|
+
"textCase",
|
|
1838
|
+
"textDecoration",
|
|
1839
|
+
"textDecorationStyle",
|
|
1840
|
+
"textDecorationColor",
|
|
1841
|
+
"textDecorationThickness",
|
|
1842
|
+
"textDecorationOffset",
|
|
1843
|
+
"textDecorationSkipInk",
|
|
1844
|
+
"fills",
|
|
1845
|
+
"hyperlink",
|
|
1846
|
+
"listOptions",
|
|
1847
|
+
"indentation",
|
|
1848
|
+
"listSpacing",
|
|
1849
|
+
"openTypeFeatures",
|
|
1850
|
+
"textStyleId",
|
|
1851
|
+
"fillStyleId",
|
|
1852
|
+
"boundVariables",
|
|
1853
|
+
"textWrapStyle",
|
|
1854
|
+
"fontStyle",
|
|
1855
|
+
"paragraphIndent",
|
|
1856
|
+
"paragraphSpacing",
|
|
1857
|
+
"textStyleOverrides"
|
|
1858
|
+
];
|
|
1859
|
+
function overrideName(o) {
|
|
1860
|
+
if (typeof o === "string") return o;
|
|
1861
|
+
if (o !== null && typeof o === "object" && "type" in o && typeof o.type === "string") return o.type;
|
|
1862
|
+
return void 0;
|
|
1863
|
+
}
|
|
1864
|
+
function styleOverrides(s) {
|
|
1865
|
+
const ov = s.textStyleOverrides;
|
|
1866
|
+
if (!s.textStyleId || !Array.isArray(ov) || !ov.length) return void 0;
|
|
1867
|
+
const names = [];
|
|
1868
|
+
for (const o of ov) {
|
|
1869
|
+
const n = overrideName(o);
|
|
1870
|
+
if (n !== void 0) names.push(n.toLowerCase());
|
|
1871
|
+
}
|
|
1872
|
+
return names.length ? names : void 0;
|
|
1873
|
+
}
|
|
1874
|
+
function inlineExtras(src, out) {
|
|
1875
|
+
const hl = src.hyperlink;
|
|
1876
|
+
if (hl && hl !== figma.mixed && hl.value) {
|
|
1877
|
+
if (hl.type === "NODE") out.linkNode = hl.value;
|
|
1878
|
+
else out.href = hl.value;
|
|
1879
|
+
}
|
|
1880
|
+
const lo = src.listOptions;
|
|
1881
|
+
if (lo && lo !== figma.mixed && lo.type && lo.type !== "NONE") out.list = lo.type.toLowerCase();
|
|
1882
|
+
if (typeof src.indentation === "number" && src.indentation) out.indent = src.indentation;
|
|
1883
|
+
}
|
|
1884
|
+
async function serializeText(node) {
|
|
1885
|
+
const out = { text: node.characters };
|
|
1886
|
+
let segs;
|
|
1887
|
+
try {
|
|
1888
|
+
segs = node.getStyledTextSegments(TEXT_SEG_FIELDS);
|
|
1889
|
+
} catch (e) {
|
|
1890
|
+
segs = void 0;
|
|
1891
|
+
}
|
|
1892
|
+
let font;
|
|
1893
|
+
const s0 = segs && segs[0];
|
|
1894
|
+
if (segs && segs.length > 1 && s0) {
|
|
1895
|
+
const wrapMixed = "textWrapStyle" in node && node.textWrapStyle === figma.mixed;
|
|
1896
|
+
const spacingMixed = "paragraphSpacing" in node && node.paragraphSpacing === figma.mixed;
|
|
1897
|
+
const indentMixed = "paragraphIndent" in node && node.paragraphIndent === figma.mixed;
|
|
1898
|
+
out.runs = await Promise.all(
|
|
1899
|
+
segs.map(async (s) => {
|
|
1900
|
+
const r = { text: s.characters, font: fontObj(s) };
|
|
1901
|
+
inlineExtras(s, r);
|
|
1902
|
+
const [ts, fs, bv] = await Promise.all([styleName(s.textStyleId), styleName(s.fillStyleId), resolveBoundMap(s.boundVariables)]);
|
|
1903
|
+
if (ts) r.textStyle = ts;
|
|
1904
|
+
if (fs) r.fillStyle = fs;
|
|
1905
|
+
if (bv) r.tokens = bv;
|
|
1906
|
+
if (wrapMixed && typeof s.textWrapStyle === "string" && s.textWrapStyle !== "AUTO") r.textWrap = lower(s.textWrapStyle);
|
|
1907
|
+
if (spacingMixed && typeof s.paragraphSpacing === "number" && s.paragraphSpacing) r.paragraphSpacing = s.paragraphSpacing;
|
|
1908
|
+
if (indentMixed && typeof s.paragraphIndent === "number" && s.paragraphIndent) r.paragraphIndent = s.paragraphIndent;
|
|
1909
|
+
const ov = styleOverrides(s);
|
|
1910
|
+
if (ov) r.textStyleOverrides = ov;
|
|
1911
|
+
return r;
|
|
1912
|
+
})
|
|
1913
|
+
);
|
|
1914
|
+
font = out.font = fontObj(s0);
|
|
1915
|
+
} else {
|
|
1916
|
+
font = out.font = fontObj(s0 ? s0 : node);
|
|
1917
|
+
const bv = await resolveBoundMap(s0 ? s0.boundVariables : void 0);
|
|
1918
|
+
if (bv) out.textTokens = bv;
|
|
1919
|
+
inlineExtras(s0 || node, out);
|
|
1920
|
+
const ov = s0 ? styleOverrides(s0) : void 0;
|
|
1921
|
+
if (ov) out.textStyleOverrides = ov;
|
|
1922
|
+
}
|
|
1923
|
+
if ("textAlignHorizontal" in node && node.textAlignHorizontal) font.align = lower(node.textAlignHorizontal);
|
|
1924
|
+
if ("paragraphSpacing" in node && typeof node.paragraphSpacing === "number" && node.paragraphSpacing) font.paragraphSpacing = node.paragraphSpacing;
|
|
1925
|
+
if ("paragraphIndent" in node && typeof node.paragraphIndent === "number" && node.paragraphIndent) font.paragraphIndent = node.paragraphIndent;
|
|
1926
|
+
if ("listSpacing" in node && typeof node.listSpacing === "number" && node.listSpacing) font.listSpacing = node.listSpacing;
|
|
1927
|
+
if ("leadingTrim" in node && node.leadingTrim && node.leadingTrim !== figma.mixed && node.leadingTrim !== "NONE") font.leadingTrim = node.leadingTrim.toLowerCase();
|
|
1928
|
+
if ("textAlignVertical" in node && node.textAlignVertical && node.textAlignVertical !== "TOP") font.valign = lower(node.textAlignVertical);
|
|
1929
|
+
if ("textWrapStyle" in node) {
|
|
1930
|
+
const tw = node.textWrapStyle;
|
|
1931
|
+
if (typeof tw === "string" && tw !== "AUTO") font.textWrap = lower(tw);
|
|
1932
|
+
}
|
|
1933
|
+
if ("hangingList" in node && node.hangingList === true) font.hangingList = true;
|
|
1934
|
+
if ("hangingPunctuation" in node && node.hangingPunctuation === true) font.hangingPunctuation = true;
|
|
1935
|
+
if ("type" in node && node.type === "TEXT") {
|
|
1936
|
+
if (node.textAutoResize && node.textAutoResize !== "NONE") out.autoResize = lower(node.textAutoResize);
|
|
1937
|
+
if (node.textTruncation === "ENDING") out.truncate = true;
|
|
1938
|
+
if (typeof node.maxLines === "number" && node.maxLines) out.maxLines = node.maxLines;
|
|
1939
|
+
}
|
|
1940
|
+
if (node.hasMissingFont === true) {
|
|
1941
|
+
out.missingFont = true;
|
|
1942
|
+
const name = "name" in node ? node.name : void 0;
|
|
1943
|
+
const id = "id" in node ? node.id : void 0;
|
|
1944
|
+
run().warnKind("missing font \u2014 Figma is substituting a fallback; the recorded family may differ from the render", name + " (" + id + ")");
|
|
1945
|
+
}
|
|
1946
|
+
if ("width" in node && node.width === 0) {
|
|
1947
|
+
const name = "name" in node ? node.name : void 0;
|
|
1948
|
+
const id = "id" in node ? node.id : void 0;
|
|
1949
|
+
run().warnKind("zero-width text node (possible collapsed thread)", name + " (" + id + ")");
|
|
1950
|
+
}
|
|
1951
|
+
return out;
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
// src/prototype.ts
|
|
1955
|
+
function simplifyTransition(tr) {
|
|
1956
|
+
const t = { ...ifDefined("type", tr.type ? tr.type.toLowerCase() : void 0) };
|
|
1957
|
+
if ("direction" in tr) {
|
|
1958
|
+
t.direction = tr.direction.toLowerCase();
|
|
1959
|
+
t.matchLayers = tr.matchLayers;
|
|
1960
|
+
}
|
|
1961
|
+
t.duration = tr.duration;
|
|
1962
|
+
const ez = tr.easing;
|
|
1963
|
+
if (ez && ez.type) {
|
|
1964
|
+
t.easing = ez.type.toLowerCase();
|
|
1965
|
+
Object.assign(t, easingCurve(ez));
|
|
1966
|
+
}
|
|
1967
|
+
return t;
|
|
1968
|
+
}
|
|
1969
|
+
async function simplifyVariableData(vd) {
|
|
1970
|
+
if (!vd || typeof vd !== "object") return vd;
|
|
1971
|
+
const v = "value" in vd ? vd.value : vd;
|
|
1972
|
+
if (isVariableAlias(v)) {
|
|
1973
|
+
return { token: await run().varName(v.id) || v.id };
|
|
1974
|
+
}
|
|
1975
|
+
return v;
|
|
1976
|
+
}
|
|
1977
|
+
var isAction = (a) => !!a;
|
|
1978
|
+
async function serializeAction(a) {
|
|
1979
|
+
if (!a) return null;
|
|
1980
|
+
const ao = { type: (a.type || "").toLowerCase() };
|
|
1981
|
+
if (a.type === "URL") {
|
|
1982
|
+
ao.url = a.url;
|
|
1983
|
+
if (a.openInNewTab === true) ao.openInNewTab = true;
|
|
1984
|
+
} else if (a.type === "UPDATE_MEDIA_RUNTIME") {
|
|
1985
|
+
if (a.destinationId) {
|
|
1986
|
+
ao.destinationId = a.destinationId;
|
|
1987
|
+
const dn = await run().nodeName(a.destinationId);
|
|
1988
|
+
if (dn) ao.destination = dn;
|
|
1989
|
+
}
|
|
1990
|
+
ao.mediaAction = String(a.mediaAction).toLowerCase();
|
|
1991
|
+
if (a.mediaAction === "SKIP_FORWARD" || a.mediaAction === "SKIP_BACKWARD") ao.amountToSkip = a.amountToSkip;
|
|
1992
|
+
if (a.mediaAction === "SKIP_TO") ao.newTimestamp = a.newTimestamp;
|
|
1993
|
+
} else if (a.type === "SET_VARIABLE") {
|
|
1994
|
+
if (a.variableId) ao.variable = await run().varName(a.variableId) || a.variableId;
|
|
1995
|
+
if (a.variableValue != null) ao.value = await simplifyVariableData(a.variableValue);
|
|
1996
|
+
} else if (a.type === "SET_VARIABLE_MODE") {
|
|
1997
|
+
if (a.variableCollectionId) {
|
|
1998
|
+
const c = await run().collection(a.variableCollectionId);
|
|
1999
|
+
ao.collection = c ? c.name : a.variableCollectionId;
|
|
2000
|
+
if (a.variableModeId) {
|
|
2001
|
+
const m = c && Array.isArray(c.modes) ? c.modes.find((x) => x.modeId === a.variableModeId) : null;
|
|
2002
|
+
ao.mode = m ? m.name : a.variableModeId;
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
} else if (a.type === "CONDITIONAL") {
|
|
2006
|
+
ao.conditionalBlocks = await Promise.all(
|
|
2007
|
+
a.conditionalBlocks.map(async (blk) => {
|
|
2008
|
+
const b = {};
|
|
2009
|
+
if (blk.condition) b.condition = await simplifyVariableData(blk.condition);
|
|
2010
|
+
if (Array.isArray(blk.actions)) b.actions = (await Promise.all(blk.actions.map(serializeAction))).filter(isAction);
|
|
2011
|
+
return b;
|
|
2012
|
+
})
|
|
2013
|
+
);
|
|
2014
|
+
} else if (a.type === "NODE") {
|
|
2015
|
+
if (a.destinationId) {
|
|
2016
|
+
ao.destinationId = a.destinationId;
|
|
2017
|
+
const dn = await run().nodeName(a.destinationId);
|
|
2018
|
+
if (dn) ao.destination = dn;
|
|
2019
|
+
}
|
|
2020
|
+
if (a.navigation) ao.navigation = a.navigation.toLowerCase();
|
|
2021
|
+
if (a.preserveScrollPosition === true) ao.preserveScroll = true;
|
|
2022
|
+
if (a.resetScrollPosition === true) ao.resetScroll = true;
|
|
2023
|
+
if (a.resetVideoPosition === true) ao.resetVideo = true;
|
|
2024
|
+
if (a.resetInteractiveComponents === true) ao.resetInteractive = true;
|
|
2025
|
+
if (a.overlayRelativePosition) ao.overlayOffset = xy(a.overlayRelativePosition);
|
|
2026
|
+
if (a.transition) ao.transition = simplifyTransition(a.transition);
|
|
2027
|
+
}
|
|
2028
|
+
return ao;
|
|
2029
|
+
}
|
|
2030
|
+
async function simplifyReactions(node) {
|
|
2031
|
+
if (!("reactions" in node)) return void 0;
|
|
2032
|
+
const reactions = node.reactions;
|
|
2033
|
+
if (!isList(reactions) || !reactions.length) return void 0;
|
|
2034
|
+
const out = [];
|
|
2035
|
+
for (const r of reactions) {
|
|
2036
|
+
const o = {};
|
|
2037
|
+
if (r.trigger) {
|
|
2038
|
+
const tg = r.trigger;
|
|
2039
|
+
o.trigger = tg.type ? tg.type.toLowerCase() : "unknown";
|
|
2040
|
+
if (tg.type === "AFTER_TIMEOUT") o.timeout = tg.timeout;
|
|
2041
|
+
if (tg.type === "MOUSE_UP" || tg.type === "MOUSE_DOWN" || tg.type === "MOUSE_ENTER" || tg.type === "MOUSE_LEAVE") o.delay = tg.delay;
|
|
2042
|
+
if (tg.type === "ON_KEY_DOWN") {
|
|
2043
|
+
if (tg.keyCodes.length) o.keyCodes = [...tg.keyCodes];
|
|
2044
|
+
o.device = String(tg.device).toLowerCase();
|
|
2045
|
+
}
|
|
2046
|
+
if (tg.type === "ON_MEDIA_HIT") o.mediaHitTime = tg.mediaHitTime;
|
|
2047
|
+
}
|
|
2048
|
+
const actions = isList(r.actions) ? r.actions : r.action ? [r.action] : [];
|
|
2049
|
+
const acts = (await Promise.all(actions.map(serializeAction))).filter(isAction);
|
|
2050
|
+
if (acts.length) o.actions = acts;
|
|
2051
|
+
if (o.trigger || o.actions) out.push(o);
|
|
2052
|
+
}
|
|
2053
|
+
return out.length ? out : void 0;
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
// ../bridge/src/instance-overrides.ts
|
|
2057
|
+
var OVERRIDE_CAP = 100;
|
|
2058
|
+
|
|
2059
|
+
// src/libraries.ts
|
|
2060
|
+
var UNKNOWN_LIBRARY = "unknown-library";
|
|
2061
|
+
function readRegistry(sink) {
|
|
2062
|
+
try {
|
|
2063
|
+
const root = figma.root;
|
|
2064
|
+
const raw = typeof root.getPluginData === "function" && root.getPluginData("libraryRegistry");
|
|
2065
|
+
if (!raw) return {};
|
|
2066
|
+
const parsed = JSON.parse(raw);
|
|
2067
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
2068
|
+
const out = {};
|
|
2069
|
+
for (const k of Object.keys(parsed)) {
|
|
2070
|
+
const v = parsed[k];
|
|
2071
|
+
if (typeof v === "string" && v) out[k] = v;
|
|
2072
|
+
}
|
|
2073
|
+
return out;
|
|
2074
|
+
} catch (e) {
|
|
2075
|
+
sink("library registry (document pluginData 'libraryRegistry') is unreadable (" + errMsg(e) + ") \u2014 remote components stay unattributed");
|
|
2076
|
+
return {};
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
async function allInstances(sink) {
|
|
2080
|
+
await loadAllPages("library component scan may be incomplete", sink);
|
|
2081
|
+
const out = [];
|
|
2082
|
+
const prevSkip = figma.skipInvisibleInstanceChildren;
|
|
2083
|
+
try {
|
|
2084
|
+
try {
|
|
2085
|
+
figma.skipInvisibleInstanceChildren = true;
|
|
2086
|
+
} catch (e) {
|
|
2087
|
+
}
|
|
2088
|
+
for (const page of figma.root.children) {
|
|
2089
|
+
try {
|
|
2090
|
+
out.push(...page.findAllWithCriteria({ types: ["INSTANCE"] }));
|
|
2091
|
+
} catch (e) {
|
|
2092
|
+
sink("library scan: page '" + page.name + "' could not be traversed (" + errMsg(e) + ") \u2014 its library components are missing");
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
} finally {
|
|
2096
|
+
try {
|
|
2097
|
+
figma.skipInvisibleInstanceChildren = prevSkip;
|
|
2098
|
+
} catch (e) {
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
return out;
|
|
2102
|
+
}
|
|
2103
|
+
async function mainOf(inst) {
|
|
2104
|
+
try {
|
|
2105
|
+
return await inst.getMainComponentAsync();
|
|
2106
|
+
} catch (e) {
|
|
2107
|
+
return null;
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
function mainNames(main) {
|
|
2111
|
+
const parent = main.parent || null;
|
|
2112
|
+
if (parent && parent.type === "COMPONENT_SET") return { name: parent.name, variantOf: main.name };
|
|
2113
|
+
return { name: main.name };
|
|
2114
|
+
}
|
|
2115
|
+
function tryDefinitions(main) {
|
|
2116
|
+
try {
|
|
2117
|
+
const defs = main.componentPropertyDefinitions;
|
|
2118
|
+
if (!defs || !Object.keys(defs).length) return void 0;
|
|
2119
|
+
const props = {};
|
|
2120
|
+
for (const [k, d] of Object.entries(defs)) {
|
|
2121
|
+
const p = { key: k, type: propType(d.type) };
|
|
2122
|
+
if (d.type === "VARIANT" && Array.isArray(d.variantOptions)) p.options = d.variantOptions;
|
|
2123
|
+
if (d.defaultValue !== void 0) p.default = d.defaultValue;
|
|
2124
|
+
if (d.type === "INSTANCE_SWAP" && Array.isArray(d.preferredValues) && d.preferredValues.length) {
|
|
2125
|
+
p.preferredValues = d.preferredValues.map((v) => ({ type: v.type, key: v.key }));
|
|
2126
|
+
}
|
|
2127
|
+
if (d.description) p.description = d.description;
|
|
2128
|
+
props[propName(k)] = p;
|
|
2129
|
+
}
|
|
2130
|
+
return nonEmpty(props);
|
|
2131
|
+
} catch (e) {
|
|
2132
|
+
return void 0;
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
function aggregateFromInstances(instances) {
|
|
2136
|
+
const props = {};
|
|
2137
|
+
for (const inst of instances) {
|
|
2138
|
+
let cp;
|
|
2139
|
+
try {
|
|
2140
|
+
cp = inst.componentProperties;
|
|
2141
|
+
} catch (e) {
|
|
2142
|
+
continue;
|
|
2143
|
+
}
|
|
2144
|
+
if (!cp) continue;
|
|
2145
|
+
for (const k of Object.keys(cp)) {
|
|
2146
|
+
const v = cp[k];
|
|
2147
|
+
if (!v) continue;
|
|
2148
|
+
const name = propName(k);
|
|
2149
|
+
const p = props[name] || (props[name] = { key: k, type: propType(v.type), observed: [] });
|
|
2150
|
+
if (v.value !== void 0 && p.observed.indexOf(v.value) === -1) p.observed.push(v.value);
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
return nonEmpty(props);
|
|
2154
|
+
}
|
|
2155
|
+
async function collectLibraryComponents(sinkIn) {
|
|
2156
|
+
const sink = sinkIn || ((m) => run().warn(m));
|
|
2157
|
+
const registry = readRegistry(sink);
|
|
2158
|
+
const instances = await allInstances(sink);
|
|
2159
|
+
const mains = await Promise.all(instances.map((i) => mainOf(i)));
|
|
2160
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
2161
|
+
let unkeyed = 0;
|
|
2162
|
+
for (const [i, inst] of instances.entries()) {
|
|
2163
|
+
const main = mains[i];
|
|
2164
|
+
if (!main) continue;
|
|
2165
|
+
if (!main.remote) continue;
|
|
2166
|
+
const key = main.key;
|
|
2167
|
+
if (!key) {
|
|
2168
|
+
unkeyed++;
|
|
2169
|
+
continue;
|
|
2170
|
+
}
|
|
2171
|
+
const bucket = byKey.get(key);
|
|
2172
|
+
if (bucket) bucket.instances.push(inst);
|
|
2173
|
+
else byKey.set(key, { main, instances: [inst] });
|
|
2174
|
+
}
|
|
2175
|
+
if (unkeyed) sink(unkeyed + " remote component instance(s) have a main component with no publish key \u2014 omitted from the library catalog");
|
|
2176
|
+
const out = [];
|
|
2177
|
+
for (const [key, bucket] of byKey) {
|
|
2178
|
+
const names = mainNames(bucket.main);
|
|
2179
|
+
const entry = {
|
|
2180
|
+
name: names.name,
|
|
2181
|
+
key,
|
|
2182
|
+
type: "COMPONENT",
|
|
2183
|
+
remote: true,
|
|
2184
|
+
source: registry[key] || UNKNOWN_LIBRARY,
|
|
2185
|
+
uses: bucket.instances.length
|
|
2186
|
+
// how many instances of it are in THIS file
|
|
2187
|
+
};
|
|
2188
|
+
if (names.variantOf) entry.variant = names.variantOf;
|
|
2189
|
+
if (bucket.main.description) entry.description = bucket.main.description;
|
|
2190
|
+
const defs = tryDefinitions(bucket.main);
|
|
2191
|
+
if (defs) {
|
|
2192
|
+
entry.props = defs;
|
|
2193
|
+
entry.derivedFrom = "definitions";
|
|
2194
|
+
} else {
|
|
2195
|
+
const observed = aggregateFromInstances(bucket.instances);
|
|
2196
|
+
if (observed) entry.props = observed;
|
|
2197
|
+
entry.derivedFrom = "instances";
|
|
2198
|
+
}
|
|
2199
|
+
out.push(entry);
|
|
2200
|
+
}
|
|
2201
|
+
return out;
|
|
2202
|
+
}
|
|
2203
|
+
async function libraryVariableCollections(sink) {
|
|
2204
|
+
const byLibrary = /* @__PURE__ */ new Map();
|
|
2205
|
+
let collections = [];
|
|
2206
|
+
try {
|
|
2207
|
+
collections = await figma.teamLibrary.getAvailableLibraryVariableCollectionsAsync();
|
|
2208
|
+
} catch (e) {
|
|
2209
|
+
sink(
|
|
2210
|
+
"team-library variables unavailable (" + errMsg(e) + ') \u2014 needs manifest permissions:["teamlibrary"] and a plan with shared libraries; library COMPONENTS below are unaffected'
|
|
2211
|
+
);
|
|
2212
|
+
return byLibrary;
|
|
2213
|
+
}
|
|
2214
|
+
if (!collections || !collections.length) {
|
|
2215
|
+
sink(
|
|
2216
|
+
"no team libraries are enabled for this file, so no library VARIABLE collections are listed \u2014 any rows below come from this file itself or from components it consumes. Enable libraries in Figma (Assets > Libraries) if you expected more; an empty list here is normal, not a failure"
|
|
2217
|
+
);
|
|
2218
|
+
return byLibrary;
|
|
2219
|
+
}
|
|
2220
|
+
const counts = await Promise.all(
|
|
2221
|
+
collections.map(async (c) => {
|
|
2222
|
+
try {
|
|
2223
|
+
const vars = await figma.teamLibrary.getVariablesInLibraryCollectionAsync(c.key);
|
|
2224
|
+
return vars ? vars.length : 0;
|
|
2225
|
+
} catch (e) {
|
|
2226
|
+
sink("library collection '" + c.name + "': variables could not be listed (" + errMsg(e) + ") \u2014 count omitted");
|
|
2227
|
+
return void 0;
|
|
2228
|
+
}
|
|
2229
|
+
})
|
|
2230
|
+
);
|
|
2231
|
+
for (const [i, c] of collections.entries()) {
|
|
2232
|
+
const lib = c.libraryName || UNKNOWN_LIBRARY;
|
|
2233
|
+
const entry = { key: c.key, name: c.name };
|
|
2234
|
+
const count = counts[i];
|
|
2235
|
+
if (count !== void 0) entry.variableCount = count;
|
|
2236
|
+
const list = byLibrary.get(lib);
|
|
2237
|
+
if (list) list.push(entry);
|
|
2238
|
+
else byLibrary.set(lib, [entry]);
|
|
2239
|
+
}
|
|
2240
|
+
return byLibrary;
|
|
2241
|
+
}
|
|
2242
|
+
async function listLibraries() {
|
|
2243
|
+
const warnings = [];
|
|
2244
|
+
const sink = (m) => warnings.push(m);
|
|
2245
|
+
const [varsByLibrary, components] = await Promise.all([
|
|
2246
|
+
libraryVariableCollections(sink),
|
|
2247
|
+
collectLibraryComponents(sink).catch((e) => {
|
|
2248
|
+
sink("library component scan failed (" + errMsg(e) + ") \u2014 component counts are missing, variable collections below are unaffected");
|
|
2249
|
+
return [];
|
|
2250
|
+
})
|
|
2251
|
+
]);
|
|
2252
|
+
const compBySource = /* @__PURE__ */ new Map();
|
|
2253
|
+
for (const c of components) {
|
|
2254
|
+
const src = c.source || UNKNOWN_LIBRARY;
|
|
2255
|
+
compBySource.set(src, (compBySource.get(src) || 0) + 1);
|
|
2256
|
+
}
|
|
2257
|
+
const libraries = [];
|
|
2258
|
+
const localCollections = [];
|
|
2259
|
+
try {
|
|
2260
|
+
const [colls, vars] = await Promise.all([
|
|
2261
|
+
figma.variables.getLocalVariableCollectionsAsync(),
|
|
2262
|
+
figma.variables.getLocalVariablesAsync()
|
|
2263
|
+
]);
|
|
2264
|
+
const perColl = /* @__PURE__ */ new Map();
|
|
2265
|
+
for (const v of vars) perColl.set(v.variableCollectionId, (perColl.get(v.variableCollectionId) || 0) + 1);
|
|
2266
|
+
for (const c of colls) localCollections.push({ key: c.key || c.id, name: c.name, variableCount: perColl.get(c.id) || 0 });
|
|
2267
|
+
} catch (e) {
|
|
2268
|
+
sink("local variable collections could not be listed (" + errMsg(e) + ")");
|
|
2269
|
+
}
|
|
2270
|
+
libraries.push({
|
|
2271
|
+
key: figma.fileKey || "local",
|
|
2272
|
+
name: figma.root && figma.root.name || "(this file)",
|
|
2273
|
+
kind: "local",
|
|
2274
|
+
variableCollections: localCollections,
|
|
2275
|
+
// no componentCount: local components are enumerated properly by the design-system catalog
|
|
2276
|
+
note: "this file \u2014 its local components are listed in full by the design-system export, not counted here"
|
|
2277
|
+
});
|
|
2278
|
+
const seenSources = /* @__PURE__ */ new Set();
|
|
2279
|
+
for (const [libName, collections] of varsByLibrary) {
|
|
2280
|
+
seenSources.add(libName);
|
|
2281
|
+
const componentCount = compBySource.get(libName);
|
|
2282
|
+
const firstCollection = collections[0];
|
|
2283
|
+
libraries.push({
|
|
2284
|
+
key: firstCollection ? firstCollection.key : libName,
|
|
2285
|
+
// libraries themselves have no key — a collection key is the closest stable handle
|
|
2286
|
+
name: libName,
|
|
2287
|
+
kind: "library",
|
|
2288
|
+
variableCollections: collections,
|
|
2289
|
+
...ifDefined("componentCount", componentCount),
|
|
2290
|
+
note: componentCount === void 0 ? "variable collections are complete; component attribution is impossible via the API \u2014 any components from this library are counted under '" + UNKNOWN_LIBRARY + "'" : "componentCount counts components USED IN THIS FILE (attributed by the local registry), not everything the library offers \u2014 library components cannot be enumerated"
|
|
2291
|
+
});
|
|
2292
|
+
}
|
|
2293
|
+
for (const [source, count] of compBySource) {
|
|
2294
|
+
if (seenSources.has(source)) continue;
|
|
2295
|
+
libraries.push({
|
|
2296
|
+
key: source,
|
|
2297
|
+
name: source,
|
|
2298
|
+
kind: "library",
|
|
2299
|
+
variableCollections: [],
|
|
2300
|
+
componentCount: count,
|
|
2301
|
+
note: source === UNKNOWN_LIBRARY ? "components consumed from published libraries that the API cannot attribute to a source (only VARIABLES carry a libraryName). componentCount counts DISTINCT components USED IN THIS FILE, not what any library offers." : "componentCount counts components USED IN THIS FILE (attributed by the local registry), not everything the library offers"
|
|
2302
|
+
});
|
|
2303
|
+
}
|
|
2304
|
+
return { exportedAt: exportedAt(), ...ifDefined("file", figma.root && figma.root.name || void 0), libraries, warnings };
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
// src/motion.ts
|
|
2308
|
+
function keyframeValue(kv) {
|
|
2309
|
+
if (!kv || typeof kv !== "object") return kv;
|
|
2310
|
+
switch (kv.type) {
|
|
2311
|
+
case "FLOAT":
|
|
2312
|
+
return round(kv.value);
|
|
2313
|
+
case "COLOR":
|
|
2314
|
+
return kv.value ? rgbaToHex(kv.value) : void 0;
|
|
2315
|
+
case "VECTOR":
|
|
2316
|
+
return kv.value ? { ...xy(kv.value) } : void 0;
|
|
2317
|
+
case "CIRCLE":
|
|
2318
|
+
return kv.value ? { ...xy(kv.value), radius: round(kv.value.radius) } : void 0;
|
|
2319
|
+
case "LINE":
|
|
2320
|
+
return kv.value ? { ...xy(kv.value), x2: round(kv.value.x2), y2: round(kv.value.y2) } : void 0;
|
|
2321
|
+
case "BOOL":
|
|
2322
|
+
case "TEXT_DATA":
|
|
2323
|
+
case "CIRCLE_POINT":
|
|
2324
|
+
return kv.value;
|
|
2325
|
+
// no transformation needed
|
|
2326
|
+
// RGBA is an interface (no index signature), so the colour is re-spread into a plain object literal —
|
|
2327
|
+
// same keys, same order, same JSON.
|
|
2328
|
+
case "COLOR_POINT": {
|
|
2329
|
+
const cp = kv.value;
|
|
2330
|
+
return cp ? { ...cp, color: { ...cp.color } } : asJson(cp);
|
|
2331
|
+
}
|
|
2332
|
+
default: {
|
|
2333
|
+
const unknownMember = kv;
|
|
2334
|
+
return asJson(unknownMember.value);
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
async function aliasValue2(id) {
|
|
2339
|
+
return { aliasOf: await run().varName(id) || id };
|
|
2340
|
+
}
|
|
2341
|
+
async function motionEasing(ez) {
|
|
2342
|
+
if (!ez || !ez.type) return void 0;
|
|
2343
|
+
if (ez.type === "VARIABLE_ALIAS") return aliasValue2(ez.id);
|
|
2344
|
+
return { type: String(ez.type).toLowerCase(), ...easingCurve(ez) };
|
|
2345
|
+
}
|
|
2346
|
+
async function keyframes(list) {
|
|
2347
|
+
if (!isList(list) || !list.length) return void 0;
|
|
2348
|
+
return Promise.all(list.map(async (k) => {
|
|
2349
|
+
const o = { t: round(k.timelinePosition), ...ifDefined("value", keyframeValue(k.value)) };
|
|
2350
|
+
const ez = await motionEasing(k.easing);
|
|
2351
|
+
if (ez) o.easing = ez;
|
|
2352
|
+
return o;
|
|
2353
|
+
}));
|
|
2354
|
+
}
|
|
2355
|
+
async function manualTrack(binding) {
|
|
2356
|
+
if (!binding || typeof binding !== "object") return void 0;
|
|
2357
|
+
const o = {};
|
|
2358
|
+
const base = keyframeValue(binding.baseValue);
|
|
2359
|
+
if (base !== void 0) o.base = base;
|
|
2360
|
+
const kf = await keyframes(binding.keyframes);
|
|
2361
|
+
if (kf) o.keyframes = kf;
|
|
2362
|
+
return o;
|
|
2363
|
+
}
|
|
2364
|
+
async function animationTrack(binding) {
|
|
2365
|
+
if (!binding || typeof binding !== "object") return void 0;
|
|
2366
|
+
const o = {};
|
|
2367
|
+
const base = keyframeValue(binding.baseValue);
|
|
2368
|
+
if (base !== void 0) o.base = base;
|
|
2369
|
+
if (typeof binding.timelineDuration === "number") o.duration = round(binding.timelineDuration);
|
|
2370
|
+
if (isList(binding.tracks) && binding.tracks.length) {
|
|
2371
|
+
o.tracks = await Promise.all(binding.tracks.map(async (t) => {
|
|
2372
|
+
const to = {};
|
|
2373
|
+
if (t.keyframeOperation === "OFFSET" || t.keyframeOperation === "SCALE") to.op = t.keyframeOperation === "OFFSET" ? "offset" : "scale";
|
|
2374
|
+
const kf = await keyframes(t.keyframes);
|
|
2375
|
+
if (kf) to.keyframes = kf;
|
|
2376
|
+
return to;
|
|
2377
|
+
}));
|
|
2378
|
+
}
|
|
2379
|
+
return o;
|
|
2380
|
+
}
|
|
2381
|
+
async function stylePropValue(v) {
|
|
2382
|
+
if (v === void 0 || v === null) return void 0;
|
|
2383
|
+
if (typeof v === "object") return motionEasing(v);
|
|
2384
|
+
return v;
|
|
2385
|
+
}
|
|
2386
|
+
function isPropertyBindings(t) {
|
|
2387
|
+
return "properties" in t;
|
|
2388
|
+
}
|
|
2389
|
+
async function bindingMap(map, conv) {
|
|
2390
|
+
if (!map || typeof map !== "object") return void 0;
|
|
2391
|
+
const entries = Object.entries(map);
|
|
2392
|
+
const converted = await Promise.all(entries.map(([, binding]) => conv(binding)));
|
|
2393
|
+
const out = {};
|
|
2394
|
+
entries.forEach(([key], i) => putNonEmpty(out, key, converted[i]));
|
|
2395
|
+
return nonEmpty(out);
|
|
2396
|
+
}
|
|
2397
|
+
async function paintMap(map, conv) {
|
|
2398
|
+
if (!map || typeof map !== "object") return void 0;
|
|
2399
|
+
const out = {};
|
|
2400
|
+
for (const [index, entry] of Object.entries(map)) {
|
|
2401
|
+
if (!entry || typeof entry !== "object") continue;
|
|
2402
|
+
if (isPropertyBindings(entry)) {
|
|
2403
|
+
const properties = await bindingMap(entry.properties, conv);
|
|
2404
|
+
if (properties) out[index] = { properties };
|
|
2405
|
+
} else {
|
|
2406
|
+
putNonEmpty(out, index, await conv(entry));
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
return nonEmpty(out);
|
|
2410
|
+
}
|
|
2411
|
+
async function effectMap(map, conv) {
|
|
2412
|
+
if (!map || typeof map !== "object") return void 0;
|
|
2413
|
+
const out = {};
|
|
2414
|
+
for (const [index, entry] of Object.entries(map)) {
|
|
2415
|
+
if (!entry || typeof entry !== "object") continue;
|
|
2416
|
+
const { properties, ...fields } = entry;
|
|
2417
|
+
const extra = {};
|
|
2418
|
+
putNonEmpty(extra, "properties", await bindingMap(properties, conv));
|
|
2419
|
+
putNonEmpty(out, index, Object.assign(await bindingMap(fields, conv) || {}, extra));
|
|
2420
|
+
}
|
|
2421
|
+
return nonEmpty(out);
|
|
2422
|
+
}
|
|
2423
|
+
async function motionMap(map, conv) {
|
|
2424
|
+
if (!map || typeof map !== "object") return void 0;
|
|
2425
|
+
const { fills, strokes, effects, ...scalars } = map;
|
|
2426
|
+
const indexed = {};
|
|
2427
|
+
putNonEmpty(indexed, "fills", await paintMap(fills, conv));
|
|
2428
|
+
putNonEmpty(indexed, "strokes", await paintMap(strokes, conv));
|
|
2429
|
+
putNonEmpty(indexed, "effects", await effectMap(effects, conv));
|
|
2430
|
+
return nonEmpty(Object.assign(await bindingMap(scalars, conv) || {}, indexed));
|
|
2431
|
+
}
|
|
2432
|
+
async function collectMotion(node) {
|
|
2433
|
+
const out = {};
|
|
2434
|
+
if ("timelines" in node && isList(node.timelines) && node.timelines.length) {
|
|
2435
|
+
out.timelines = node.timelines.map((t) => ({ id: t.id, duration: round(t.duration) }));
|
|
2436
|
+
}
|
|
2437
|
+
if ("manualKeyframeTracks" in node) {
|
|
2438
|
+
const tracks = await motionMap(node.manualKeyframeTracks, manualTrack);
|
|
2439
|
+
if (tracks) out.manualTracks = tracks;
|
|
2440
|
+
}
|
|
2441
|
+
if ("animations" in node) {
|
|
2442
|
+
const anims = await motionMap(node.animations, animationTrack);
|
|
2443
|
+
if (anims) out.animations = anims;
|
|
2444
|
+
}
|
|
2445
|
+
if ("animationStyles" in node && isList(node.animationStyles) && node.animationStyles.length) {
|
|
2446
|
+
out.styles = await Promise.all(node.animationStyles.map(async (s) => {
|
|
2447
|
+
const kind = s.type;
|
|
2448
|
+
if (typeof kind === "string" && kind !== "" && kind !== "FIGMA" && kind !== "CUSTOM") return unknownKindStyle(s, kind);
|
|
2449
|
+
if (s.type === "CUSTOM") {
|
|
2450
|
+
const o = { id: s.id, name: s.name, styleId: s.styleId, custom: true };
|
|
2451
|
+
if (typeof s.timelineOffset === "number" && s.timelineOffset) o.timelineOffset = round(s.timelineOffset);
|
|
2452
|
+
return Object.assign(o, await customStyleDetail(s.styleId));
|
|
2453
|
+
}
|
|
2454
|
+
return animationStyleEntry(s);
|
|
2455
|
+
}));
|
|
2456
|
+
}
|
|
2457
|
+
return nonEmpty(out);
|
|
2458
|
+
}
|
|
2459
|
+
function unknownKindStyle(s, kind) {
|
|
2460
|
+
const fields = s;
|
|
2461
|
+
const o = {};
|
|
2462
|
+
if (typeof fields.id === "string") o.id = fields.id;
|
|
2463
|
+
if (typeof fields.name === "string") o.name = fields.name;
|
|
2464
|
+
if (typeof fields.styleId === "string") o.styleId = fields.styleId;
|
|
2465
|
+
o.kind = kind.toLowerCase();
|
|
2466
|
+
if (typeof fields.timelineOffset === "number" && fields.timelineOffset) o.timelineOffset = round(fields.timelineOffset);
|
|
2467
|
+
return o;
|
|
2468
|
+
}
|
|
2469
|
+
async function animationStyleEntry(s) {
|
|
2470
|
+
const o = { id: s.id, name: s.name, styleId: s.styleId };
|
|
2471
|
+
if (typeof s.description === "string" && s.description) o.description = s.description;
|
|
2472
|
+
if (typeof s.duration === "number") o.duration = round(s.duration);
|
|
2473
|
+
if (typeof s.timelineOffset === "number" && s.timelineOffset) o.timelineOffset = round(s.timelineOffset);
|
|
2474
|
+
if (s.props) {
|
|
2475
|
+
const entries = Object.entries(s.props);
|
|
2476
|
+
const converted = await Promise.all(entries.map(([, v]) => stylePropValue(v)));
|
|
2477
|
+
const props = {};
|
|
2478
|
+
entries.forEach(([key], i) => {
|
|
2479
|
+
const c = converted[i];
|
|
2480
|
+
if (c !== void 0) props[key] = c;
|
|
2481
|
+
});
|
|
2482
|
+
if (Object.keys(props).length) o.props = props;
|
|
2483
|
+
}
|
|
2484
|
+
return o;
|
|
2485
|
+
}
|
|
2486
|
+
async function customAnimationEntries(style) {
|
|
2487
|
+
try {
|
|
2488
|
+
const list = style.animationEntries;
|
|
2489
|
+
if (!isList(list)) return void 0;
|
|
2490
|
+
return await Promise.all(list.map(animationStyleEntry));
|
|
2491
|
+
} catch (e) {
|
|
2492
|
+
return void 0;
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
async function customStyleDetail(styleId) {
|
|
2496
|
+
const style = await run().styleName.obj(styleId);
|
|
2497
|
+
if (!style || style.type !== "CUSTOM_ANIMATION") return {};
|
|
2498
|
+
try {
|
|
2499
|
+
const detail = {};
|
|
2500
|
+
if (typeof style.description === "string" && style.description) detail.description = style.description;
|
|
2501
|
+
const entries = await customAnimationEntries(style);
|
|
2502
|
+
if (entries) detail.entries = entries;
|
|
2503
|
+
return detail;
|
|
2504
|
+
} catch (e) {
|
|
2505
|
+
return {};
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2509
|
+
// src/components.ts
|
|
2510
|
+
async function instanceComponentRef(node) {
|
|
2511
|
+
if (node.type !== "INSTANCE") return void 0;
|
|
2512
|
+
try {
|
|
2513
|
+
const main = await node.getMainComponentAsync();
|
|
2514
|
+
if (!main) return void 0;
|
|
2515
|
+
const ref = { name: main.name };
|
|
2516
|
+
if (main.id) ref.id = main.id;
|
|
2517
|
+
if (main.key) ref.key = main.key;
|
|
2518
|
+
if (main.remote) ref.remote = true;
|
|
2519
|
+
const parent = main.parent;
|
|
2520
|
+
if (parent && parent.type === "COMPONENT_SET") {
|
|
2521
|
+
ref.setId = parent.id;
|
|
2522
|
+
if (parent.key) ref.setKey = parent.key;
|
|
2523
|
+
ref.setName = parent.name;
|
|
2524
|
+
ref.variant = main.name;
|
|
2525
|
+
}
|
|
2526
|
+
return ref;
|
|
2527
|
+
} catch (e) {
|
|
2528
|
+
return void 0;
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
var PROP_REF_KEYS = ["visible", "characters", "mainComponent"];
|
|
2532
|
+
function componentPropRefs(node) {
|
|
2533
|
+
if (!("componentPropertyReferences" in node)) return void 0;
|
|
2534
|
+
const refs = node.componentPropertyReferences;
|
|
2535
|
+
if (!refs) return void 0;
|
|
2536
|
+
const out = {};
|
|
2537
|
+
for (const k of PROP_REF_KEYS) {
|
|
2538
|
+
const ref = refs[k];
|
|
2539
|
+
if (ref) out[k] = propName(ref);
|
|
2540
|
+
}
|
|
2541
|
+
return nonEmpty(out);
|
|
2542
|
+
}
|
|
2543
|
+
function instanceOverrides(node) {
|
|
2544
|
+
if (node.type !== "INSTANCE") return void 0;
|
|
2545
|
+
const overrides = node.overrides;
|
|
2546
|
+
if (!Array.isArray(overrides) || !overrides.length) return void 0;
|
|
2547
|
+
const list = overrides.filter((o) => o && Array.isArray(o.overriddenFields) && o.overriddenFields.length).map((o) => ({ id: o.id, fields: o.overriddenFields }));
|
|
2548
|
+
if (!list.length) return void 0;
|
|
2549
|
+
if (list.length > OVERRIDE_CAP) {
|
|
2550
|
+
run().warn("instance '" + node.name + "' has " + list.length + " overrides \u2014 truncated to " + OVERRIDE_CAP);
|
|
2551
|
+
return list.slice(0, OVERRIDE_CAP);
|
|
2552
|
+
}
|
|
2553
|
+
return list;
|
|
2554
|
+
}
|
|
2555
|
+
var VISUAL_CORNER_KEYS = [
|
|
2556
|
+
["topLeftRadius", "tl"],
|
|
2557
|
+
["topRightRadius", "tr"],
|
|
2558
|
+
["bottomRightRadius", "br"],
|
|
2559
|
+
["bottomLeftRadius", "bl"]
|
|
2560
|
+
];
|
|
2561
|
+
async function simplifyVisuals(node) {
|
|
2562
|
+
const out = {};
|
|
2563
|
+
const [fills, strokes, effects] = await Promise.all([
|
|
2564
|
+
simplifyFills("fills" in node ? node.fills : void 0),
|
|
2565
|
+
simplifyStrokes(node),
|
|
2566
|
+
simplifyEffects("effects" in node ? node.effects : void 0)
|
|
2567
|
+
]);
|
|
2568
|
+
if (fills) out.fills = fills;
|
|
2569
|
+
if (strokes) out.strokes = strokes;
|
|
2570
|
+
if (effects) out.effects = effects;
|
|
2571
|
+
if ("cornerRadius" in node) {
|
|
2572
|
+
if (node.cornerRadius !== figma.mixed && node.cornerRadius) out.radius = node.cornerRadius;
|
|
2573
|
+
else if (node.cornerRadius === figma.mixed) {
|
|
2574
|
+
const corners = {};
|
|
2575
|
+
for (const [k, s] of VISUAL_CORNER_KEYS) {
|
|
2576
|
+
if (!(k in node)) continue;
|
|
2577
|
+
const r = node[k];
|
|
2578
|
+
if (typeof r === "number" && r) corners[s] = r;
|
|
2579
|
+
}
|
|
2580
|
+
putNonEmpty(out, "radius", corners);
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
if ("opacity" in node && typeof node.opacity === "number" && node.opacity < 1) out.opacity = round(node.opacity);
|
|
2584
|
+
if ("blendMode" in node && node.blendMode && node.blendMode !== "NORMAL" && node.blendMode !== "PASS_THROUGH") out.blendMode = String(node.blendMode).toLowerCase();
|
|
2585
|
+
return nonEmpty(out);
|
|
2586
|
+
}
|
|
2587
|
+
function variantValues(main) {
|
|
2588
|
+
try {
|
|
2589
|
+
if (main.variantProperties && Object.keys(main.variantProperties).length) return { ...main.variantProperties };
|
|
2590
|
+
} catch (e) {
|
|
2591
|
+
}
|
|
2592
|
+
const name = main.name || "";
|
|
2593
|
+
const out = {};
|
|
2594
|
+
for (const part of name.split(",")) {
|
|
2595
|
+
const eq = part.indexOf("=");
|
|
2596
|
+
if (eq === -1) continue;
|
|
2597
|
+
const k = part.slice(0, eq).trim();
|
|
2598
|
+
const v = part.slice(eq + 1).trim();
|
|
2599
|
+
if (k && v) out[k] = v;
|
|
2600
|
+
}
|
|
2601
|
+
return nonEmpty(out);
|
|
2602
|
+
}
|
|
2603
|
+
var VARIANT_WALK_DEPTH = 3;
|
|
2604
|
+
function serializeVariant(main, serialize2) {
|
|
2605
|
+
return serialize2(main, 60 - VARIANT_WALK_DEPTH, void 0, void 0, void 0, "none");
|
|
2606
|
+
}
|
|
2607
|
+
async function collectComponentCatalog(hygiene, asLibrary, serialize2) {
|
|
2608
|
+
const variantVisuals = run().reads.variantVisuals;
|
|
2609
|
+
const components = [];
|
|
2610
|
+
const pendingPublish = [];
|
|
2611
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
2612
|
+
const variantsBySet = /* @__PURE__ */ new Map();
|
|
2613
|
+
const entriesBySetId = /* @__PURE__ */ new Map();
|
|
2614
|
+
const standalone = [];
|
|
2615
|
+
const prevSkip = figma.skipInvisibleInstanceChildren;
|
|
2616
|
+
try {
|
|
2617
|
+
try {
|
|
2618
|
+
figma.skipInvisibleInstanceChildren = true;
|
|
2619
|
+
} catch (e) {
|
|
2620
|
+
}
|
|
2621
|
+
const catalogPages = figma.root.children;
|
|
2622
|
+
let catalogIndex = 0;
|
|
2623
|
+
for (const page of catalogPages) {
|
|
2624
|
+
checkCancelled();
|
|
2625
|
+
enterPage("design-system", ++catalogIndex, catalogPages.length, page.name, page.id, { components: components.length });
|
|
2626
|
+
let nodes = [];
|
|
2627
|
+
try {
|
|
2628
|
+
nodes = page.findAllWithCriteria({ types: ["COMPONENT_SET", "COMPONENT"] });
|
|
2629
|
+
} catch (e) {
|
|
2630
|
+
run().warn("component catalog: page '" + page.name + "' could not be traversed (" + errMsg(e) + ") \u2014 its components are missing");
|
|
2631
|
+
continue;
|
|
2632
|
+
}
|
|
2633
|
+
for (const n of nodes) {
|
|
2634
|
+
if (n.type === "COMPONENT" && n.parent && n.parent.type === "COMPONENT_SET") {
|
|
2635
|
+
if (variantVisuals) {
|
|
2636
|
+
const list = variantsBySet.get(n.parent.id);
|
|
2637
|
+
if (list) list.push(n);
|
|
2638
|
+
else variantsBySet.set(n.parent.id, [n]);
|
|
2639
|
+
}
|
|
2640
|
+
continue;
|
|
2641
|
+
}
|
|
2642
|
+
const entry = { name: n.name, id: n.id, type: n.type, page: page.name, pageId: page.id };
|
|
2643
|
+
if (n.description) entry.description = n.description;
|
|
2644
|
+
if (n.remote) entry.remote = true;
|
|
2645
|
+
if (n.key) entry.key = n.key;
|
|
2646
|
+
if (isList(n.documentationLinks) && n.documentationLinks.length) entry.docs = n.documentationLinks.map((d) => d.uri).filter(Boolean);
|
|
2647
|
+
try {
|
|
2648
|
+
const defs = n.componentPropertyDefinitions;
|
|
2649
|
+
if (defs && Object.keys(defs).length) {
|
|
2650
|
+
const props = {};
|
|
2651
|
+
entry.props = props;
|
|
2652
|
+
let variantCombos = 1;
|
|
2653
|
+
const defEntries = Object.entries(defs);
|
|
2654
|
+
const boundPerKey = await Promise.all(defEntries.map(([, d]) => resolveBoundMap(d.boundVariables)));
|
|
2655
|
+
for (const [i, [k, d]] of defEntries.entries()) {
|
|
2656
|
+
const p = { key: k, type: propType(d.type) };
|
|
2657
|
+
if (d.type === "VARIANT") {
|
|
2658
|
+
if (d.variantOptions !== void 0) p.options = d.variantOptions;
|
|
2659
|
+
variantCombos *= d.variantOptions ? d.variantOptions.length : 1;
|
|
2660
|
+
}
|
|
2661
|
+
if (d.defaultValue !== void 0) p.default = d.defaultValue;
|
|
2662
|
+
if ((d.type === "INSTANCE_SWAP" || d.type === "SLOT") && Array.isArray(d.preferredValues) && d.preferredValues.length) {
|
|
2663
|
+
p.preferredValues = d.preferredValues.map((v) => ({ type: v.type, key: v.key }));
|
|
2664
|
+
}
|
|
2665
|
+
if (d.description) p.description = d.description;
|
|
2666
|
+
if (d.type === "SLOT" && d.slotSettings) {
|
|
2667
|
+
const ss = d.slotSettings;
|
|
2668
|
+
const slot = {};
|
|
2669
|
+
if (typeof ss.stretchChildOnInsert === "boolean") slot.stretchChildOnInsert = ss.stretchChildOnInsert;
|
|
2670
|
+
if (typeof ss.displayEmptyByDefault === "boolean") slot.displayEmptyByDefault = ss.displayEmptyByDefault;
|
|
2671
|
+
if (typeof ss.minChildren === "number") slot.minChildren = ss.minChildren;
|
|
2672
|
+
if (typeof ss.maxChildren === "number") slot.maxChildren = ss.maxChildren;
|
|
2673
|
+
if (typeof ss.allowPreferredValuesOnly === "boolean") slot.allowPreferredValuesOnly = ss.allowPreferredValuesOnly;
|
|
2674
|
+
putNonEmpty(p, "slotSettings", slot);
|
|
2675
|
+
}
|
|
2676
|
+
const dbv = boundPerKey[i];
|
|
2677
|
+
if (dbv) p.tokens = dbv;
|
|
2678
|
+
props[propName(k)] = p;
|
|
2679
|
+
}
|
|
2680
|
+
if (variantCombos > 30) hygiene.push("variant explosion: '" + n.name + "' has " + variantCombos + " combinations (>30 \u2014 consider boolean/instance-swap props)");
|
|
2681
|
+
}
|
|
2682
|
+
} catch (e) {
|
|
2683
|
+
run().warn("component '" + n.name + "': property definitions unreadable (" + errMsg(e) + ") \u2014 props omitted");
|
|
2684
|
+
}
|
|
2685
|
+
if (/^(Component|Frame)\s*\d+$/.test(n.name)) hygiene.push("unnamed component: '" + n.name + "'");
|
|
2686
|
+
if (seenNames.has(n.name)) hygiene.push("duplicate component name: '" + n.name + "'");
|
|
2687
|
+
seenNames.add(n.name);
|
|
2688
|
+
try {
|
|
2689
|
+
const visuals = await simplifyVisuals(n);
|
|
2690
|
+
if (visuals) entry.visuals = visuals;
|
|
2691
|
+
} catch (e) {
|
|
2692
|
+
run().warn("component '" + n.name + "': visuals unreadable (" + errMsg(e) + ") \u2014 visuals omitted");
|
|
2693
|
+
}
|
|
2694
|
+
if (asLibrary) pendingPublish.push({ entry, node: n });
|
|
2695
|
+
if (variantVisuals && n.type === "COMPONENT_SET") entriesBySetId.set(n.id, entry);
|
|
2696
|
+
if (variantVisuals && n.type === "COMPONENT") standalone.push({ entry, node: n });
|
|
2697
|
+
components.push(entry);
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
} finally {
|
|
2701
|
+
try {
|
|
2702
|
+
figma.skipInvisibleInstanceChildren = prevSkip;
|
|
2703
|
+
} catch (e) {
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
if (serialize2) {
|
|
2707
|
+
for (const { entry, node: n } of standalone) {
|
|
2708
|
+
try {
|
|
2709
|
+
const node = await serializeVariant(n, serialize2);
|
|
2710
|
+
if (node) entry.node = node;
|
|
2711
|
+
} catch (e) {
|
|
2712
|
+
run().warn("component '" + n.name + "': visuals unreadable (" + errMsg(e) + ") \u2014 node tree omitted");
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
if (variantVisuals && serialize2 && variantsBySet.size) {
|
|
2717
|
+
for (const [setId, variants] of variantsBySet) {
|
|
2718
|
+
const entry = entriesBySetId.get(setId);
|
|
2719
|
+
if (!entry) continue;
|
|
2720
|
+
const out = [];
|
|
2721
|
+
for (const v of variants) {
|
|
2722
|
+
try {
|
|
2723
|
+
const node = await serializeVariant(v, serialize2);
|
|
2724
|
+
const o = { id: v.id, name: v.name };
|
|
2725
|
+
if (v.key) o.key = v.key;
|
|
2726
|
+
const values = variantValues(v);
|
|
2727
|
+
if (values) o.values = values;
|
|
2728
|
+
if (node) o.node = node;
|
|
2729
|
+
out.push(o);
|
|
2730
|
+
} catch (e) {
|
|
2731
|
+
run().warn("component '" + v.name + "': variant visuals unreadable (" + errMsg(e) + ") \u2014 omitted");
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
if (out.length) entry.variants = out;
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
if (pendingPublish.length) {
|
|
2738
|
+
const statuses = await Promise.all(pendingPublish.map((p) => publishOf(p.node)));
|
|
2739
|
+
for (const [i, p] of pendingPublish.entries()) {
|
|
2740
|
+
const s = statuses[i];
|
|
2741
|
+
if (s) p.entry.publish = s;
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
return components;
|
|
2745
|
+
}
|
|
2746
|
+
async function publishOf(o) {
|
|
2747
|
+
try {
|
|
2748
|
+
if (!o || typeof o.getPublishStatusAsync !== "function") return void 0;
|
|
2749
|
+
const s = await o.getPublishStatusAsync();
|
|
2750
|
+
return s ? String(s).toLowerCase() : void 0;
|
|
2751
|
+
} catch (e) {
|
|
2752
|
+
return void 0;
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
async function buildDesignSystem(opts, serialize2) {
|
|
2756
|
+
const asLibrary = !!(opts && opts.asLibrary);
|
|
2757
|
+
const safeList = async (fn) => {
|
|
2758
|
+
try {
|
|
2759
|
+
return await fn();
|
|
2760
|
+
} catch (e) {
|
|
2761
|
+
return [];
|
|
2762
|
+
}
|
|
2763
|
+
};
|
|
2764
|
+
const [paint, text, effect, grid, animation] = await Promise.all([
|
|
2765
|
+
safeList(() => figma.getLocalPaintStylesAsync()),
|
|
2766
|
+
safeList(() => figma.getLocalTextStylesAsync()),
|
|
2767
|
+
safeList(() => figma.getLocalEffectStylesAsync()),
|
|
2768
|
+
safeList(() => figma.getLocalGridStylesAsync ? figma.getLocalGridStylesAsync() : Promise.resolve([])),
|
|
2769
|
+
safeList(() => figma.getLocalCustomAnimationStylesAsync ? figma.getLocalCustomAnimationStylesAsync() : Promise.resolve([]))
|
|
2770
|
+
]);
|
|
2771
|
+
const hygiene = [];
|
|
2772
|
+
await loadAllPages("component catalog may be incomplete", (m) => run().warn(m));
|
|
2773
|
+
const components = await collectComponentCatalog(hygiene, asLibrary, serialize2);
|
|
2774
|
+
if (asLibrary) {
|
|
2775
|
+
hygiene.push(
|
|
2776
|
+
"library mode: this catalog is the COMPLETE set of components defined in this library file. Publish status is per-object (publish: current | changed | unpublished); Figma exposes no way to list what the last PUBLISHED snapshot contained, so a component deleted here but still live in the library is not visible."
|
|
2777
|
+
);
|
|
2778
|
+
} else try {
|
|
2779
|
+
const seenKeys = new Set(components.map((c) => c.key).filter(Boolean));
|
|
2780
|
+
const remote = await collectLibraryComponents((m) => run().warn(m));
|
|
2781
|
+
let added = 0;
|
|
2782
|
+
for (const entry of remote) {
|
|
2783
|
+
if (seenKeys.has(entry.key)) continue;
|
|
2784
|
+
seenKeys.add(entry.key);
|
|
2785
|
+
components.push(entry);
|
|
2786
|
+
added++;
|
|
2787
|
+
}
|
|
2788
|
+
if (added) {
|
|
2789
|
+
hygiene.push(
|
|
2790
|
+
added + ' component(s) in this catalog come from a published LIBRARY, not this file \u2014 flagged remote:true. Entries with derivedFrom:"instances" have props INFERRED from the instances present here (a sample, not the complete set).'
|
|
2791
|
+
);
|
|
2792
|
+
}
|
|
2793
|
+
} catch (e) {
|
|
2794
|
+
run().warn("library component catalog failed (" + errMsg(e) + ") \u2014 components consumed from published libraries are missing from the catalog");
|
|
2795
|
+
}
|
|
2796
|
+
let colorProfile;
|
|
2797
|
+
try {
|
|
2798
|
+
if (figma.root && figma.root.documentColorProfile) colorProfile = String(figma.root.documentColorProfile).toLowerCase();
|
|
2799
|
+
} catch (e) {
|
|
2800
|
+
}
|
|
2801
|
+
const styleMeta = async (s) => {
|
|
2802
|
+
const m = {};
|
|
2803
|
+
if (s.key) m.key = s.key;
|
|
2804
|
+
if (s.id) m.id = s.id;
|
|
2805
|
+
if (s.remote) m.remote = true;
|
|
2806
|
+
if (isList(s.documentationLinks) && s.documentationLinks.length) {
|
|
2807
|
+
m.docs = s.documentationLinks.map((d) => d.uri).filter(Boolean);
|
|
2808
|
+
}
|
|
2809
|
+
if (asLibrary) {
|
|
2810
|
+
const p = await publishOf(s);
|
|
2811
|
+
if (p) m.publish = p;
|
|
2812
|
+
}
|
|
2813
|
+
return m;
|
|
2814
|
+
};
|
|
2815
|
+
const [paintStyles, textStyles, effectStyles] = await Promise.all([
|
|
2816
|
+
// Paint styles carry their ACTUAL colors, not just a name.
|
|
2817
|
+
// `tokens` (boundVariables) was read for TEXT styles only, so a paint style bound to a color
|
|
2818
|
+
// variable silently lost that link — the binding is what makes it a token rather than a hex.
|
|
2819
|
+
Promise.all(paint.map(async (s) => ({ name: s.name, ...ifDefined("paints", await simplifyFills(s.paints)), ...ifDefined("tokens", await resolveBoundMap(s.boundVariables)), ...ifDefined("description", s.description || void 0), ...await styleMeta(s) }))),
|
|
2820
|
+
Promise.all(
|
|
2821
|
+
text.map(async (s) => ({
|
|
2822
|
+
name: s.name,
|
|
2823
|
+
size: s.fontSize,
|
|
2824
|
+
font: s.fontName && s.fontName.family,
|
|
2825
|
+
weight: s.fontName && s.fontName.style,
|
|
2826
|
+
...ifDefined("lineHeight", lineH(s.lineHeight)),
|
|
2827
|
+
...ifDefined("letterSpacing", letterS(s.letterSpacing)),
|
|
2828
|
+
...ifDefined("case", s.textCase && s.textCase !== "ORIGINAL" ? s.textCase.toLowerCase() : void 0),
|
|
2829
|
+
...ifDefined("decoration", s.textDecoration && s.textDecoration !== "NONE" ? s.textDecoration.toLowerCase() : void 0),
|
|
2830
|
+
...ifDefined("paragraphSpacing", s.paragraphSpacing || void 0),
|
|
2831
|
+
...ifDefined("paragraphIndent", s.paragraphIndent || void 0),
|
|
2832
|
+
...ifDefined("leadingTrim", s.leadingTrim && s.leadingTrim !== "NONE" ? String(s.leadingTrim).toLowerCase() : void 0),
|
|
2833
|
+
...ifDefined("listSpacing", s.listSpacing || void 0),
|
|
2834
|
+
// TextStyle.textWrapStyle (Plugin API 2026-08-14) — AUTO | BALANCE | PRETTY; AUTO is the
|
|
2835
|
+
// default and is skipped. Maps 1:1 onto CSS `text-wrap`. Same read as text.ts's per-node
|
|
2836
|
+
// one, minus the mixed guard: a TextStyle is uniform by definition.
|
|
2837
|
+
...ifDefined("textWrap", s.textWrapStyle && s.textWrapStyle !== "AUTO" ? String(s.textWrapStyle).toLowerCase() : void 0),
|
|
2838
|
+
...ifDefined("tokens", await resolveBoundMap(s.boundVariables)),
|
|
2839
|
+
...ifDefined("description", s.description || void 0),
|
|
2840
|
+
...await styleMeta(s)
|
|
2841
|
+
}))
|
|
2842
|
+
),
|
|
2843
|
+
Promise.all(effect.map(async (s) => ({ name: s.name, ...ifDefined("effects", await simplifyEffects(s.effects)), ...ifDefined("tokens", await resolveBoundMap(s.boundVariables)), ...ifDefined("description", s.description || void 0), ...await styleMeta(s) })))
|
|
2844
|
+
]);
|
|
2845
|
+
const styles = {
|
|
2846
|
+
paint: paintStyles,
|
|
2847
|
+
text: textStyles,
|
|
2848
|
+
effect: effectStyles,
|
|
2849
|
+
// Layout-grid styles (column/row grids) — the responsive grid tokens. The grid VALUES are sync;
|
|
2850
|
+
// the shared catalog meta (key/publish) is not, so the map is fanned out like its three siblings.
|
|
2851
|
+
grid: await Promise.all(
|
|
2852
|
+
grid.map(async (s) => ({
|
|
2853
|
+
name: s.name,
|
|
2854
|
+
...ifDefined("grids", Array.isArray(s.layoutGrids) ? s.layoutGrids.map(simplifyGrid).filter((g) => !!g) : void 0),
|
|
2855
|
+
...ifDefined("tokens", await resolveBoundMap(s.boundVariables)),
|
|
2856
|
+
...ifDefined("description", s.description || void 0),
|
|
2857
|
+
...await styleMeta(s)
|
|
2858
|
+
}))
|
|
2859
|
+
),
|
|
2860
|
+
// Custom animation styles: the Figma animations each one contains (CustomAnimationStyle.
|
|
2861
|
+
// animationEntries), in the same compact form a node's applied animation style takes (motion.ts).
|
|
2862
|
+
// Their props can bind EASING variables, so they resolve before dumpVariables too.
|
|
2863
|
+
animation: await Promise.all(
|
|
2864
|
+
animation.map(async (s) => ({
|
|
2865
|
+
name: s.name,
|
|
2866
|
+
...ifDefined("entries", await customAnimationEntries(s)),
|
|
2867
|
+
...ifDefined("description", s.description || void 0),
|
|
2868
|
+
...await styleMeta(s)
|
|
2869
|
+
}))
|
|
2870
|
+
)
|
|
2871
|
+
};
|
|
2872
|
+
const vars = await dumpVariables(opts);
|
|
2873
|
+
let source;
|
|
2874
|
+
if (asLibrary) {
|
|
2875
|
+
let fileKey;
|
|
2876
|
+
try {
|
|
2877
|
+
if (typeof figma.fileKey !== "undefined") fileKey = figma.fileKey || void 0;
|
|
2878
|
+
} catch (e) {
|
|
2879
|
+
}
|
|
2880
|
+
if (!fileKey) hygiene.push("figma.fileKey unavailable \u2014 the library output directory falls back to a name slug, so a RENAMED library will land in a new directory");
|
|
2881
|
+
source = {
|
|
2882
|
+
role: "library",
|
|
2883
|
+
...ifDefined("libraryName", opts && opts.asLibrary || figma.root && figma.root.name || void 0),
|
|
2884
|
+
...ifDefined("fileKey", fileKey),
|
|
2885
|
+
collectionKeys: vars.collections.map((c) => c.key).filter((k) => !!k)
|
|
2886
|
+
};
|
|
2887
|
+
}
|
|
2888
|
+
return {
|
|
2889
|
+
exportedAt: exportedAt(),
|
|
2890
|
+
...ifDefined("file", figma.root && figma.root.name || void 0),
|
|
2891
|
+
...ifDefined("source", source),
|
|
2892
|
+
...ifDefined("colorProfile", colorProfile),
|
|
2893
|
+
// legacy | srgb | display_p3 — whether emitted colors should be sRGB or wide-gamut
|
|
2894
|
+
collections: vars.collections,
|
|
2895
|
+
variables: vars.variables,
|
|
2896
|
+
styles,
|
|
2897
|
+
components,
|
|
2898
|
+
hygiene: vars.hygiene.concat(hygiene)
|
|
2899
|
+
// variable hygiene first, then component hygiene
|
|
2900
|
+
};
|
|
2901
|
+
}
|
|
2902
|
+
|
|
2903
|
+
// src/serialize.ts
|
|
2904
|
+
var MAX_DEPTH = 60;
|
|
2905
|
+
var SIZE_LIMIT_KEYS = ["minWidth", "maxWidth", "minHeight", "maxHeight"];
|
|
2906
|
+
var CORNER_KEYS = [
|
|
2907
|
+
["topLeftRadius", "tl"],
|
|
2908
|
+
["topRightRadius", "tr"],
|
|
2909
|
+
["bottomRightRadius", "br"],
|
|
2910
|
+
["bottomLeftRadius", "bl"]
|
|
2911
|
+
];
|
|
2912
|
+
var RADIUS_SENTINEL = 1e4;
|
|
2913
|
+
function radiusOut(out, raw) {
|
|
2914
|
+
if (raw < RADIUS_SENTINEL) return round(raw);
|
|
2915
|
+
out.radiusFull = true;
|
|
2916
|
+
const box = out.box;
|
|
2917
|
+
const w = box && typeof box.w === "number" ? box.w : void 0;
|
|
2918
|
+
const h = box && typeof box.h === "number" ? box.h : void 0;
|
|
2919
|
+
if (w === void 0 && h === void 0) return round(raw);
|
|
2920
|
+
return round(Math.min(w === void 0 ? Infinity : w, h === void 0 ? Infinity : h) / 2);
|
|
2921
|
+
}
|
|
2922
|
+
function pluginData(node) {
|
|
2923
|
+
let keys = [];
|
|
2924
|
+
try {
|
|
2925
|
+
keys = node.getPluginDataKeys();
|
|
2926
|
+
} catch (e) {
|
|
2927
|
+
return void 0;
|
|
2928
|
+
}
|
|
2929
|
+
if (!keys || !keys.length) return void 0;
|
|
2930
|
+
const out = {};
|
|
2931
|
+
for (const k of keys) {
|
|
2932
|
+
try {
|
|
2933
|
+
const v = node.getPluginData(k);
|
|
2934
|
+
if (v) out[k] = v;
|
|
2935
|
+
} catch (e) {
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2938
|
+
return nonEmpty(out);
|
|
2939
|
+
}
|
|
2940
|
+
var SHARED_NAMESPACES = ["tokens"];
|
|
2941
|
+
function sharedData(node) {
|
|
2942
|
+
const out = {};
|
|
2943
|
+
for (const ns of SHARED_NAMESPACES) {
|
|
2944
|
+
let keys = [];
|
|
2945
|
+
try {
|
|
2946
|
+
keys = node.getSharedPluginDataKeys(ns) || [];
|
|
2947
|
+
} catch (e) {
|
|
2948
|
+
continue;
|
|
2949
|
+
}
|
|
2950
|
+
const bucket = {};
|
|
2951
|
+
for (const k of keys) {
|
|
2952
|
+
try {
|
|
2953
|
+
const v = node.getSharedPluginData(ns, k);
|
|
2954
|
+
if (v) bucket[k] = v;
|
|
2955
|
+
} catch (e) {
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2958
|
+
putNonEmpty(out, ns, bucket);
|
|
2959
|
+
}
|
|
2960
|
+
return nonEmpty(out);
|
|
2961
|
+
}
|
|
2962
|
+
async function nodeCss(node) {
|
|
2963
|
+
try {
|
|
2964
|
+
const css = await node.getCSSAsync();
|
|
2965
|
+
return css && Object.keys(css).length ? css : void 0;
|
|
2966
|
+
} catch (e) {
|
|
2967
|
+
return void 0;
|
|
2968
|
+
}
|
|
2969
|
+
}
|
|
2970
|
+
function leaf(out) {
|
|
2971
|
+
delete out.layout;
|
|
2972
|
+
return out;
|
|
2973
|
+
}
|
|
2974
|
+
function bakeTransform(out) {
|
|
2975
|
+
const st = {};
|
|
2976
|
+
if (out.rotation !== void 0) {
|
|
2977
|
+
st.rotation = out.rotation;
|
|
2978
|
+
delete out.rotation;
|
|
2979
|
+
}
|
|
2980
|
+
if (out.flipped) {
|
|
2981
|
+
st.flipped = true;
|
|
2982
|
+
delete out.flipped;
|
|
2983
|
+
}
|
|
2984
|
+
if (out.skew !== void 0) {
|
|
2985
|
+
st.skew = out.skew;
|
|
2986
|
+
delete out.skew;
|
|
2987
|
+
}
|
|
2988
|
+
putNonEmpty(out, "sourceTransform", st);
|
|
2989
|
+
}
|
|
2990
|
+
async function serialize(node, depth, parentControlsLayout, underHidden, inGrid, assets = "run") {
|
|
2991
|
+
checkCancelled();
|
|
2992
|
+
const r = run();
|
|
2993
|
+
const stats = r.stats;
|
|
2994
|
+
if (depth > MAX_DEPTH) {
|
|
2995
|
+
stats.truncated++;
|
|
2996
|
+
if (stats.truncated === 1) r.warn("depth limit " + MAX_DEPTH + " reached \u2014 deep subtrees truncated (first: " + node.name + ")");
|
|
2997
|
+
return null;
|
|
2998
|
+
}
|
|
2999
|
+
stats.nodes++;
|
|
3000
|
+
const out = { type: node.type, name: node.name, id: node.id };
|
|
3001
|
+
if ("visible" in node && node.visible === false) out.hidden = true;
|
|
3002
|
+
const hidden = !!out.hidden || !!underHidden;
|
|
3003
|
+
if (hidden) stats.hiddenNodes++;
|
|
3004
|
+
if (node.type === "INSTANCE" && node.componentProperties) {
|
|
3005
|
+
const props = {};
|
|
3006
|
+
const propTokens = {};
|
|
3007
|
+
const propEntries = Object.entries(node.componentProperties);
|
|
3008
|
+
const bound = await Promise.all(propEntries.map(([, cp]) => resolveBoundMap(cp.boundVariables)));
|
|
3009
|
+
propEntries.forEach(([k, cp], i) => {
|
|
3010
|
+
props[propName(k)] = cp.value;
|
|
3011
|
+
const bv = bound[i];
|
|
3012
|
+
if (bv && bv.value) propTokens[propName(k)] = bv.value;
|
|
3013
|
+
});
|
|
3014
|
+
putNonEmpty(out, "props", props);
|
|
3015
|
+
putNonEmpty(out, "propTokens", propTokens);
|
|
3016
|
+
}
|
|
3017
|
+
if (node.type === "COMPONENT" || node.type === "COMPONENT_SET") out.component = node.name;
|
|
3018
|
+
const lay = layout(node);
|
|
3019
|
+
if (lay) out.layout = lay;
|
|
3020
|
+
const absoluteInParent = "layoutPositioning" in node && node.layoutPositioning === "ABSOLUTE";
|
|
3021
|
+
if (absoluteInParent) out.absolute = true;
|
|
3022
|
+
if ("layoutGrow" in node && node.layoutGrow) out.grow = node.layoutGrow;
|
|
3023
|
+
if ("layoutAlign" in node && node.layoutAlign && node.layoutAlign !== "INHERIT") out.alignSelf = lower(node.layoutAlign);
|
|
3024
|
+
if ("layoutSizingHorizontal" in node && node.layoutSizingHorizontal && node.layoutSizingHorizontal !== "FIXED") out.widthMode = lower(node.layoutSizingHorizontal);
|
|
3025
|
+
if ("layoutSizingVertical" in node && node.layoutSizingVertical && node.layoutSizingVertical !== "FIXED") out.heightMode = lower(node.layoutSizingVertical);
|
|
3026
|
+
if ("overflowDirection" in node && node.overflowDirection && node.overflowDirection !== "NONE") out.scroll = lower(node.overflowDirection);
|
|
3027
|
+
if (inGrid && !absoluteInParent) {
|
|
3028
|
+
if ("gridColumnSpan" in node && typeof node.gridColumnSpan === "number" && node.gridColumnSpan !== 1) out.gridColumnSpan = node.gridColumnSpan;
|
|
3029
|
+
if ("gridRowSpan" in node && typeof node.gridRowSpan === "number" && node.gridRowSpan !== 1) out.gridRowSpan = node.gridRowSpan;
|
|
3030
|
+
if ("gridColumnAnchorIndex" in node && typeof node.gridColumnAnchorIndex === "number" && node.gridColumnAnchorIndex >= 0) out.gridColumnStart = node.gridColumnAnchorIndex;
|
|
3031
|
+
if ("gridRowAnchorIndex" in node && typeof node.gridRowAnchorIndex === "number" && node.gridRowAnchorIndex >= 0) out.gridRowStart = node.gridRowAnchorIndex;
|
|
3032
|
+
if ("gridChildHorizontalAlign" in node && node.gridChildHorizontalAlign && node.gridChildHorizontalAlign !== "AUTO") out.gridJustifySelf = GRID_SELF[node.gridChildHorizontalAlign];
|
|
3033
|
+
if ("gridChildVerticalAlign" in node && node.gridChildVerticalAlign && node.gridChildVerticalAlign !== "AUTO") out.gridAlignSelf = GRID_SELF[node.gridChildVerticalAlign];
|
|
3034
|
+
}
|
|
3035
|
+
if ((!parentControlsLayout || absoluteInParent) && "x" in node && typeof node.x === "number") {
|
|
3036
|
+
out.x = round(node.x);
|
|
3037
|
+
out.y = round(node.y);
|
|
3038
|
+
}
|
|
3039
|
+
if ("absoluteBoundingBox" in node && node.absoluteBoundingBox) {
|
|
3040
|
+
const b = node.absoluteBoundingBox;
|
|
3041
|
+
const box = { w: round(b.width), h: round(b.height) };
|
|
3042
|
+
out.box = box;
|
|
3043
|
+
if (!parentControlsLayout || absoluteInParent) {
|
|
3044
|
+
box.x = round(b.x);
|
|
3045
|
+
box.y = round(b.y);
|
|
3046
|
+
}
|
|
3047
|
+
const rb = "absoluteRenderBounds" in node ? node.absoluteRenderBounds : null;
|
|
3048
|
+
if (rb && (rb.x !== b.x || rb.y !== b.y || rb.width !== b.width || rb.height !== b.height)) {
|
|
3049
|
+
out.renderBox = { ...xy(rb), w: round(rb.width), h: round(rb.height) };
|
|
3050
|
+
}
|
|
3051
|
+
}
|
|
3052
|
+
if ("layoutGrids" in node && Array.isArray(node.layoutGrids) && node.layoutGrids.length) {
|
|
3053
|
+
out.layoutGrids = node.layoutGrids.map(simplifyGrid).filter((g) => !!g);
|
|
3054
|
+
}
|
|
3055
|
+
if ("annotations" in node && isList(node.annotations) && node.annotations.length) {
|
|
3056
|
+
out.annotations = node.annotations.map((a) => {
|
|
3057
|
+
const o = {};
|
|
3058
|
+
if (a.label) o.label = a.label;
|
|
3059
|
+
if (a.labelMarkdown) o.markdown = a.labelMarkdown;
|
|
3060
|
+
if (a.categoryId) o.categoryId = a.categoryId;
|
|
3061
|
+
if (isList(a.properties) && a.properties.length) o.props = a.properties.map((p) => p.type);
|
|
3062
|
+
return o;
|
|
3063
|
+
});
|
|
3064
|
+
}
|
|
3065
|
+
if ("devStatus" in node && node.devStatus && node.devStatus.type) {
|
|
3066
|
+
out.devStatus = lower(node.devStatus.type);
|
|
3067
|
+
if (node.devStatus.description) out.devStatusNote = node.devStatus.description;
|
|
3068
|
+
}
|
|
3069
|
+
let sizeLimits;
|
|
3070
|
+
for (const k of SIZE_LIMIT_KEYS) {
|
|
3071
|
+
const v = numProp(node, k);
|
|
3072
|
+
if (k in node && typeof v === "number") (sizeLimits || (sizeLimits = {}))[k] = round(v);
|
|
3073
|
+
}
|
|
3074
|
+
if (sizeLimits) out.sizeLimits = sizeLimits;
|
|
3075
|
+
if ("constraints" in node && node.constraints && (node.constraints.horizontal !== "MIN" || node.constraints.vertical !== "MIN")) {
|
|
3076
|
+
out.pin = { h: lower(node.constraints.horizontal), v: lower(node.constraints.vertical) };
|
|
3077
|
+
}
|
|
3078
|
+
if ("numberOfFixedChildren" in node && typeof node.numberOfFixedChildren === "number" && node.numberOfFixedChildren > 0) {
|
|
3079
|
+
out.fixedChildren = node.numberOfFixedChildren;
|
|
3080
|
+
}
|
|
3081
|
+
if ("clipsContent" in node && node.clipsContent) out.clip = true;
|
|
3082
|
+
const [fills, strokes, effects] = await Promise.all([
|
|
3083
|
+
simplifyFills("fills" in node ? node.fills : void 0),
|
|
3084
|
+
simplifyStrokes(node),
|
|
3085
|
+
simplifyEffects("effects" in node ? node.effects : void 0)
|
|
3086
|
+
]);
|
|
3087
|
+
if (fills) out.fills = fills;
|
|
3088
|
+
if (strokes) out.strokes = strokes;
|
|
3089
|
+
if ("strokesIncludedInLayout" in node && node.strokesIncludedInLayout) out.strokesInLayout = true;
|
|
3090
|
+
if (effects) out.effects = effects;
|
|
3091
|
+
if ("cornerRadius" in node) {
|
|
3092
|
+
if (node.cornerRadius !== figma.mixed && node.cornerRadius) out.radius = radiusOut(out, node.cornerRadius);
|
|
3093
|
+
else if (node.cornerRadius === figma.mixed) {
|
|
3094
|
+
const corners = {};
|
|
3095
|
+
for (const [k, s] of CORNER_KEYS) {
|
|
3096
|
+
const v = numProp(node, k);
|
|
3097
|
+
if (k in node && typeof v === "number" && v) corners[s] = radiusOut(out, v);
|
|
3098
|
+
}
|
|
3099
|
+
putNonEmpty(out, "radius", corners);
|
|
3100
|
+
}
|
|
3101
|
+
}
|
|
3102
|
+
if ("opacity" in node && node.opacity < 1) out.opacity = round(node.opacity);
|
|
3103
|
+
if ("rotation" in node && node.rotation) out.rotation = round(node.rotation);
|
|
3104
|
+
if ("relativeTransform" in node && Array.isArray(node.relativeTransform) && node.relativeTransform.length === 2) {
|
|
3105
|
+
const m = node.relativeTransform;
|
|
3106
|
+
const a = m[0][0], c = m[0][1], b = m[1][0], d = m[1][1];
|
|
3107
|
+
if (a * d - c * b < 0) out.flipped = true;
|
|
3108
|
+
const sx = Math.sqrt(a * a + b * b);
|
|
3109
|
+
if (sx > 1e-6) {
|
|
3110
|
+
const skewDeg = Math.atan2(a * c + b * d, sx * sx) * 180 / Math.PI;
|
|
3111
|
+
if (Math.abs(skewDeg) > 0.01) out.skew = round(skewDeg);
|
|
3112
|
+
}
|
|
3113
|
+
}
|
|
3114
|
+
if ("blendMode" in node && node.blendMode && node.blendMode !== "NORMAL" && node.blendMode !== "PASS_THROUGH") out.blendMode = node.blendMode.toLowerCase();
|
|
3115
|
+
if ("isMask" in node && node.isMask) out.mask = true;
|
|
3116
|
+
if (out.mask && "maskType" in node && node.maskType && node.maskType !== "ALPHA") out.maskType = String(node.maskType).toLowerCase();
|
|
3117
|
+
if ("cornerSmoothing" in node && typeof node.cornerSmoothing === "number" && node.cornerSmoothing) out.cornerSmoothing = round(node.cornerSmoothing);
|
|
3118
|
+
if ("targetAspectRatio" in node && node.targetAspectRatio && node.targetAspectRatio.y) out.aspectRatio = round(node.targetAspectRatio.x / node.targetAspectRatio.y);
|
|
3119
|
+
if (node.type === "ELLIPSE" && node.arcData) {
|
|
3120
|
+
const a = node.arcData;
|
|
3121
|
+
const isFull = (!a.startingAngle || a.startingAngle === 0) && Math.abs((a.endingAngle || 0) - Math.PI * 2) < 1e-4 && !a.innerRadius;
|
|
3122
|
+
if (!isFull) {
|
|
3123
|
+
const arc = {};
|
|
3124
|
+
if (typeof a.startingAngle === "number") arc.start = round(a.startingAngle);
|
|
3125
|
+
if (typeof a.endingAngle === "number") arc.end = round(a.endingAngle);
|
|
3126
|
+
if (typeof a.innerRadius === "number" && a.innerRadius) arc.innerRadius = round(a.innerRadius);
|
|
3127
|
+
putNonEmpty(out, "arc", arc);
|
|
3128
|
+
}
|
|
3129
|
+
}
|
|
3130
|
+
if (node.type === "STAR") {
|
|
3131
|
+
const shape = {};
|
|
3132
|
+
if (typeof node.pointCount === "number") shape.points = node.pointCount;
|
|
3133
|
+
if (typeof node.innerRadius === "number") shape.innerRadius = round(node.innerRadius);
|
|
3134
|
+
putNonEmpty(out, "shape", shape);
|
|
3135
|
+
}
|
|
3136
|
+
if (node.type === "POLYGON" && typeof node.pointCount === "number") out.shape = { points: node.pointCount };
|
|
3137
|
+
if (node.type === "BOOLEAN_OPERATION" && node.booleanOperation) out.booleanOp = lower(node.booleanOperation);
|
|
3138
|
+
if ("exportSettings" in node && isList(node.exportSettings) && node.exportSettings.length) {
|
|
3139
|
+
out.exportSettings = node.exportSettings.map((es) => {
|
|
3140
|
+
const o = { format: String(es.format || "").toLowerCase() };
|
|
3141
|
+
if ("suffix" in es && es.suffix) o.suffix = es.suffix;
|
|
3142
|
+
const con = "constraint" in es ? es.constraint : void 0;
|
|
3143
|
+
if (con && typeof con.value === "number" && !(con.type === "SCALE" && con.value === 1)) {
|
|
3144
|
+
o.constraint = { type: String(con.type || "").toLowerCase(), value: round(con.value) };
|
|
3145
|
+
}
|
|
3146
|
+
return o;
|
|
3147
|
+
});
|
|
3148
|
+
}
|
|
3149
|
+
if ("detachedInfo" in node && node.detachedInfo) {
|
|
3150
|
+
out.detachedFrom = node.detachedInfo.type === "library" ? { key: node.detachedInfo.componentKey } : { componentId: node.detachedInfo.componentId };
|
|
3151
|
+
}
|
|
3152
|
+
if (node.type === "INSTANCE" && "exposedInstances" in node) {
|
|
3153
|
+
try {
|
|
3154
|
+
if (Array.isArray(node.exposedInstances) && node.exposedInstances.length) out.exposedInstances = node.exposedInstances.map((i) => i.id);
|
|
3155
|
+
} catch (e) {
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
3158
|
+
if ("overlayPositionType" in node) {
|
|
3159
|
+
const hasScrim = node.overlayBackground && node.overlayBackground.type === "SOLID_COLOR" && node.overlayBackground.color;
|
|
3160
|
+
const closeOutside = node.overlayBackgroundInteraction === "CLOSE_ON_CLICK_OUTSIDE";
|
|
3161
|
+
const posSet = node.overlayPositionType && node.overlayPositionType !== "CENTER";
|
|
3162
|
+
if (hasScrim || closeOutside || posSet) {
|
|
3163
|
+
const ov = {};
|
|
3164
|
+
if (node.overlayPositionType) ov.position = node.overlayPositionType.toLowerCase();
|
|
3165
|
+
if (closeOutside) ov.closeOnClickOutside = true;
|
|
3166
|
+
if (hasScrim && node.overlayBackground.type === "SOLID_COLOR") ov.background = rgbaToHex(node.overlayBackground.color);
|
|
3167
|
+
out.overlay = ov;
|
|
3168
|
+
}
|
|
3169
|
+
}
|
|
3170
|
+
if (node.type === "TEXT" || node.type === "TEXT_PATH") Object.assign(out, await serializeText(node));
|
|
3171
|
+
const propRefs = componentPropRefs(node);
|
|
3172
|
+
if (propRefs) out.propRefs = propRefs;
|
|
3173
|
+
const overrides = instanceOverrides(node);
|
|
3174
|
+
if (overrides) out.overrides = overrides;
|
|
3175
|
+
if (r.reads.pluginData) {
|
|
3176
|
+
const pd = pluginData(node);
|
|
3177
|
+
if (pd) out.pluginData = pd;
|
|
3178
|
+
}
|
|
3179
|
+
if (r.reads.motion) {
|
|
3180
|
+
const m = await collectMotion(node);
|
|
3181
|
+
if (m) out.motion = m;
|
|
3182
|
+
}
|
|
3183
|
+
if (r.reads.sharedData) {
|
|
3184
|
+
const sd = sharedData(node);
|
|
3185
|
+
if (sd) out.sharedData = sd;
|
|
3186
|
+
}
|
|
3187
|
+
const [mainRef, tokens, styles, asset, reactions, varModes, css] = await Promise.all([
|
|
3188
|
+
instanceComponentRef(node),
|
|
3189
|
+
boundTokens(node),
|
|
3190
|
+
nodeStyles(node),
|
|
3191
|
+
collectAsset(node, hidden, assets),
|
|
3192
|
+
simplifyReactions(node),
|
|
3193
|
+
variableModes(node),
|
|
3194
|
+
r.reads.css ? nodeCss(node) : Promise.resolve(void 0)
|
|
3195
|
+
]);
|
|
3196
|
+
if (mainRef) {
|
|
3197
|
+
out.component = mainRef.name;
|
|
3198
|
+
out.mainComponent = mainRef;
|
|
3199
|
+
}
|
|
3200
|
+
if (tokens) out.tokens = tokens;
|
|
3201
|
+
if (styles) out.styles = styles;
|
|
3202
|
+
if (reactions) out.reactions = reactions;
|
|
3203
|
+
if (varModes) out.variableModes = varModes;
|
|
3204
|
+
if (css) out.css = css;
|
|
3205
|
+
if (asset && "skipped" in asset) {
|
|
3206
|
+
out.assetSkipped = asset.skipped;
|
|
3207
|
+
return leaf(out);
|
|
3208
|
+
}
|
|
3209
|
+
if (asset && "geometry" in asset) {
|
|
3210
|
+
out.geometry = asset.geometry;
|
|
3211
|
+
return leaf(out);
|
|
3212
|
+
}
|
|
3213
|
+
if (asset) {
|
|
3214
|
+
out.asset = asset.path;
|
|
3215
|
+
bakeTransform(out);
|
|
3216
|
+
return leaf(out);
|
|
3217
|
+
}
|
|
3218
|
+
if (node.type === "TABLE" && typeof node.numRows === "number" && typeof node.numColumns === "number") {
|
|
3219
|
+
const grid = await Promise.all(
|
|
3220
|
+
Array.from({ length: node.numRows }, (_, r2) => Promise.all(
|
|
3221
|
+
Array.from({ length: node.numColumns }, async (__, cc) => {
|
|
3222
|
+
try {
|
|
3223
|
+
const cell = node.cellAt(r2, cc);
|
|
3224
|
+
if (!cell) return void 0;
|
|
3225
|
+
const co = { row: r2, col: cc };
|
|
3226
|
+
if (cell.text) Object.assign(co, await serializeText(cell.text));
|
|
3227
|
+
const cfills = await simplifyFills(cell.fills);
|
|
3228
|
+
if (cfills) co.fills = cfills;
|
|
3229
|
+
return co;
|
|
3230
|
+
} catch (e) {
|
|
3231
|
+
return void 0;
|
|
3232
|
+
}
|
|
3233
|
+
})
|
|
3234
|
+
))
|
|
3235
|
+
);
|
|
3236
|
+
const rows = grid.map((row) => row.filter((c) => !!c)).filter((row) => row.length);
|
|
3237
|
+
if (rows.length) out.tableCells = rows;
|
|
3238
|
+
}
|
|
3239
|
+
const children = "children" in node ? node.children : void 0;
|
|
3240
|
+
if (children && children.length) {
|
|
3241
|
+
const controlsChildren = "layoutMode" in node && !!node.layoutMode && node.layoutMode !== "NONE";
|
|
3242
|
+
const grid = "layoutMode" in node && node.layoutMode === "GRID";
|
|
3243
|
+
const nFixed = "numberOfFixedChildren" in node && typeof node.numberOfFixedChildren === "number" ? node.numberOfFixedChildren : 0;
|
|
3244
|
+
const pinnedFrom = children.length - nFixed;
|
|
3245
|
+
const kids = [];
|
|
3246
|
+
for (let i = 0; i < children.length; i++) {
|
|
3247
|
+
const c = children[i];
|
|
3248
|
+
if (!c) continue;
|
|
3249
|
+
const s = await serialize(c, depth + 1, controlsChildren && !(nFixed > 0 && i >= pinnedFrom), hidden, grid, assets);
|
|
3250
|
+
if (s) kids.push(s);
|
|
3251
|
+
}
|
|
3252
|
+
if (kids.length) out.children = kids;
|
|
3253
|
+
}
|
|
3254
|
+
return out;
|
|
3255
|
+
}
|
|
3256
|
+
|
|
3257
|
+
// src/collect.ts
|
|
3258
|
+
async function serializeWithRefs(node, underHidden) {
|
|
3259
|
+
const tree = await serialize(node, 0, void 0, underHidden);
|
|
3260
|
+
if (!tree) return { tree: null };
|
|
3261
|
+
const [ref, dev, modes] = await Promise.all([collectReference(node), devResources(node), resolvedModes(node)]);
|
|
3262
|
+
if (modes) tree.resolvedModes = modes;
|
|
3263
|
+
return { tree, ...ifDefined("ref", ref || void 0), ...ifDefined("dev", dev || void 0) };
|
|
3264
|
+
}
|
|
3265
|
+
function hiddenAncestor(node) {
|
|
3266
|
+
for (let p = node.parent; p && p.type !== "PAGE" && p.type !== "DOCUMENT"; p = p.parent) {
|
|
3267
|
+
if ("visible" in p && p.visible === false) return true;
|
|
3268
|
+
}
|
|
3269
|
+
return false;
|
|
3270
|
+
}
|
|
3271
|
+
async function rootTree(r, node) {
|
|
3272
|
+
const self = "visible" in node && node.visible === false;
|
|
3273
|
+
const underHidden = hiddenAncestor(node);
|
|
3274
|
+
if (self || underHidden) {
|
|
3275
|
+
r.warn("'" + node.name + "' (" + node.id + ") is hidden " + (self ? "itself" : "under a hidden ancestor") + ' \u2014 its graphics are not exported (assetSkipped:"hidden") and its reference image may be blank');
|
|
3276
|
+
}
|
|
3277
|
+
const { tree, ref, dev } = await serializeWithRefs(node, underHidden);
|
|
3278
|
+
if (!tree) return null;
|
|
3279
|
+
if (ref) tree.reference = ref;
|
|
3280
|
+
if (dev) tree.devResources = dev;
|
|
3281
|
+
return tree;
|
|
3282
|
+
}
|
|
3283
|
+
async function screenResult(r, title, fileBase, nodes, origin) {
|
|
3284
|
+
const variables = await dumpVariables();
|
|
3285
|
+
const screen = { exportedAt: exportedAt(), screen: title, nodes, manifest: manifestOf(r) };
|
|
3286
|
+
const page = origin && origin.page;
|
|
3287
|
+
if (page) {
|
|
3288
|
+
screen.page = page.name;
|
|
3289
|
+
screen.pageId = page.id;
|
|
3290
|
+
}
|
|
3291
|
+
if (origin && origin.nodeId) screen.nodeId = origin.nodeId;
|
|
3292
|
+
const measurements = collectMeasurements(r);
|
|
3293
|
+
if (measurements) screen.measurements = measurements;
|
|
3294
|
+
const out = { screenName: safe(fileBase), screen, variables, assets: r.assets.slice() };
|
|
3295
|
+
if (page) {
|
|
3296
|
+
out.page = page.name;
|
|
3297
|
+
out.pageId = page.id;
|
|
3298
|
+
}
|
|
3299
|
+
if (origin && origin.nodeId) out.nodeId = origin.nodeId;
|
|
3300
|
+
return out;
|
|
3301
|
+
}
|
|
3302
|
+
function summarize(nd) {
|
|
3303
|
+
const o = { name: nd.name, id: nd.id, type: nd.type };
|
|
3304
|
+
if ("width" in nd) {
|
|
3305
|
+
o.w = Math.round(nd.width);
|
|
3306
|
+
o.h = Math.round(nd.height);
|
|
3307
|
+
}
|
|
3308
|
+
if ("visible" in nd && nd.visible === false) o.hidden = true;
|
|
3309
|
+
if ("children" in nd) {
|
|
3310
|
+
try {
|
|
3311
|
+
o.childCount = nd.children.length;
|
|
3312
|
+
} catch (e) {
|
|
3313
|
+
}
|
|
3314
|
+
}
|
|
3315
|
+
return o;
|
|
3316
|
+
}
|
|
3317
|
+
function collisionGroups(rows) {
|
|
3318
|
+
const by = /* @__PURE__ */ new Map();
|
|
3319
|
+
for (const r of rows) {
|
|
3320
|
+
if (typeof r.w !== "number") continue;
|
|
3321
|
+
const k = JSON.stringify([r.name, r.w, r.h]);
|
|
3322
|
+
const g = by.get(k);
|
|
3323
|
+
if (g) g.push(r);
|
|
3324
|
+
else by.set(k, [r]);
|
|
3325
|
+
}
|
|
3326
|
+
return Array.from(by.values()).filter((g) => g.length > 1);
|
|
3327
|
+
}
|
|
3328
|
+
function collisionNote(g, what, tellBy) {
|
|
3329
|
+
const first2 = g[0];
|
|
3330
|
+
const name = first2 ? first2.name : "";
|
|
3331
|
+
const size = first2 ? first2.w + "\xD7" + first2.h : "";
|
|
3332
|
+
return g.length + " " + what + " share name '" + name + "' and size " + size + " \u2014 told apart by " + tellBy + "; when those match too, `dtwin screenshot <id> --scale 0.25` each (" + g.map((r) => r.id).join(", ") + ")";
|
|
3333
|
+
}
|
|
3334
|
+
var TITLE_VISIT_CAP = 5e3;
|
|
3335
|
+
var TitleCapReached = class extends Error {
|
|
3336
|
+
};
|
|
3337
|
+
function spend(b, fresh) {
|
|
3338
|
+
if (b.listing && b.listing.spent) return false;
|
|
3339
|
+
if (++b.visits > b.cap) {
|
|
3340
|
+
if (!b.soft) throw new TitleCapReached();
|
|
3341
|
+
b.capped = true;
|
|
3342
|
+
return false;
|
|
3343
|
+
}
|
|
3344
|
+
if (fresh && b.listing && ++b.listing.reads > b.listing.cap) {
|
|
3345
|
+
b.listing.spent = true;
|
|
3346
|
+
return false;
|
|
3347
|
+
}
|
|
3348
|
+
return true;
|
|
3349
|
+
}
|
|
3350
|
+
function liveWalkNode(n, ref) {
|
|
3351
|
+
let src;
|
|
3352
|
+
const kids = [];
|
|
3353
|
+
const children = {
|
|
3354
|
+
[Symbol.iterator]: () => {
|
|
3355
|
+
let i = 0;
|
|
3356
|
+
return {
|
|
3357
|
+
next: () => {
|
|
3358
|
+
if (!src) src = "children" in n ? n.children : [];
|
|
3359
|
+
const c = src[i];
|
|
3360
|
+
if (!c || !spend(ref.budget, !kids[i])) return { done: true, value: void 0 };
|
|
3361
|
+
const w = kids[i] || (kids[i] = liveWalkNode(c, ref));
|
|
3362
|
+
i++;
|
|
3363
|
+
return { done: false, value: w };
|
|
3364
|
+
}
|
|
3365
|
+
};
|
|
3366
|
+
}
|
|
3367
|
+
};
|
|
3368
|
+
return {
|
|
3369
|
+
name: n.name,
|
|
3370
|
+
type: n.type,
|
|
3371
|
+
hidden: "visible" in n && n.visible === false,
|
|
3372
|
+
get text() {
|
|
3373
|
+
return n.type === "TEXT" ? n.characters : "";
|
|
3374
|
+
},
|
|
3375
|
+
// read only when a walker hits a TEXT
|
|
3376
|
+
children
|
|
3377
|
+
};
|
|
3378
|
+
}
|
|
3379
|
+
function rowWalk(node) {
|
|
3380
|
+
const ref = { budget: { visits: 0, cap: TITLE_VISIT_CAP } };
|
|
3381
|
+
return { root: liveWalkNode(node, ref), ref };
|
|
3382
|
+
}
|
|
3383
|
+
function titleOf(w) {
|
|
3384
|
+
w.ref.budget = { visits: 1, cap: TITLE_VISIT_CAP };
|
|
3385
|
+
try {
|
|
3386
|
+
return deriveTitle(w.root);
|
|
3387
|
+
} catch (e) {
|
|
3388
|
+
if (!(e instanceof TitleCapReached)) throw e;
|
|
3389
|
+
}
|
|
3390
|
+
w.ref.budget.visits = 1;
|
|
3391
|
+
return firstText(w.root) || void 0;
|
|
3392
|
+
}
|
|
3393
|
+
var DISTINCT_TEXTS = 3;
|
|
3394
|
+
var DISTINCT_TEXT_CHARS = 60;
|
|
3395
|
+
var DISTINCT_TEXT_SCAN = 200;
|
|
3396
|
+
var DISTINCT_TEXT_LISTING_READS = 2e4;
|
|
3397
|
+
var oneLine = (t) => t.replace(/\s+/g, " ").trim();
|
|
3398
|
+
function titlesTellApart(g) {
|
|
3399
|
+
const titles = g.map((r) => r.title);
|
|
3400
|
+
return titles.every((t) => !!t) && new Set(titles).size === g.length;
|
|
3401
|
+
}
|
|
3402
|
+
function addDistinctTexts(g, walks, listing, sink) {
|
|
3403
|
+
let skipped = 0;
|
|
3404
|
+
const textsOf = g.map((row) => {
|
|
3405
|
+
const w = walks.get(row.id);
|
|
3406
|
+
if (!w) return { texts: /* @__PURE__ */ new Set(), cut: false };
|
|
3407
|
+
if (listing.spent) {
|
|
3408
|
+
skipped++;
|
|
3409
|
+
return void 0;
|
|
3410
|
+
}
|
|
3411
|
+
const budget = { visits: 1, cap: TITLE_VISIT_CAP, soft: true, listing };
|
|
3412
|
+
w.ref.budget = budget;
|
|
3413
|
+
const texts = collectTexts(w.root, DISTINCT_TEXT_SCAN).map(oneLine).filter((t) => t && !PLACEHOLDER_TEXT.has(t.toLowerCase()));
|
|
3414
|
+
if (listing.spent) {
|
|
3415
|
+
skipped++;
|
|
3416
|
+
return { texts: new Set(texts), cut: true };
|
|
3417
|
+
}
|
|
3418
|
+
if (budget.capped) sink("`distinctTexts` for '" + row.name + "' (" + row.id + ") read only its first " + TITLE_VISIT_CAP + " nodes");
|
|
3419
|
+
return { texts: new Set(texts), cut: false };
|
|
3420
|
+
});
|
|
3421
|
+
if (skipped) return g.filter((row) => walks.has(row.id)).map((row) => row.id);
|
|
3422
|
+
g.forEach((row, i) => {
|
|
3423
|
+
const mine = textsOf[i];
|
|
3424
|
+
if (!mine || mine.cut) return;
|
|
3425
|
+
const others = textsOf.filter((o, j) => j !== i && !!o).map((o) => o.texts);
|
|
3426
|
+
const unique = [];
|
|
3427
|
+
const notCommon = [];
|
|
3428
|
+
for (const t of mine.texts) {
|
|
3429
|
+
if (others.every((o) => !o.has(t))) unique.push(t);
|
|
3430
|
+
else if (others.some((o) => !o.has(t))) notCommon.push(t);
|
|
3431
|
+
}
|
|
3432
|
+
const picked = unique.concat(notCommon).slice(0, DISTINCT_TEXTS).map((t) => Array.from(t).slice(0, DISTINCT_TEXT_CHARS).join(""));
|
|
3433
|
+
if (picked.length) row.distinctTexts = picked;
|
|
3434
|
+
});
|
|
3435
|
+
return [];
|
|
3436
|
+
}
|
|
3437
|
+
async function loadPageSafely(page, sink, what) {
|
|
3438
|
+
if (typeof page.loadAsync !== "function") return false;
|
|
3439
|
+
try {
|
|
3440
|
+
await page.loadAsync();
|
|
3441
|
+
} catch (e) {
|
|
3442
|
+
sink("page '" + page.name + "' failed to load" + (what ? " " + what : "") + ": " + errMsg(e));
|
|
3443
|
+
return false;
|
|
3444
|
+
}
|
|
3445
|
+
return true;
|
|
3446
|
+
}
|
|
3447
|
+
function pageChildren(page, sink, consequence) {
|
|
3448
|
+
try {
|
|
3449
|
+
return page.children;
|
|
3450
|
+
} catch (e) {
|
|
3451
|
+
sink("page '" + page.name + "' could not be read (not loaded) \u2014 " + consequence + ": " + errMsg(e));
|
|
3452
|
+
return null;
|
|
3453
|
+
}
|
|
3454
|
+
}
|
|
3455
|
+
async function findNodeById(nodeId, sink) {
|
|
3456
|
+
let node = await figma.getNodeByIdAsync(nodeId);
|
|
3457
|
+
if (!node) {
|
|
3458
|
+
for (const page of figma.root.children) {
|
|
3459
|
+
if (!await loadPageSafely(page, sink, "during node lookup")) continue;
|
|
3460
|
+
node = await figma.getNodeByIdAsync(nodeId);
|
|
3461
|
+
if (node) break;
|
|
3462
|
+
}
|
|
3463
|
+
}
|
|
3464
|
+
if (!node) throw new Error("Node " + nodeId + " is not in the open file \u2014 open the Figma file this link points to, then retry.");
|
|
3465
|
+
return node;
|
|
3466
|
+
}
|
|
3467
|
+
function resolvePages(wanted, all) {
|
|
3468
|
+
const avail = () => all.map((p) => `${p.id} ${JSON.stringify(p.name)}`).join("\n ");
|
|
3469
|
+
const pick = (sel, matches, how) => {
|
|
3470
|
+
if (matches.length > 1) {
|
|
3471
|
+
throw new Error(`page name ${JSON.stringify(sel)} is ambiguous (${matches.length} pages ${how}) \u2014 use its id. Available pages:
|
|
3472
|
+
${avail()}`);
|
|
3473
|
+
}
|
|
3474
|
+
const only = matches[0];
|
|
3475
|
+
return only === void 0 ? null : only;
|
|
3476
|
+
};
|
|
3477
|
+
const resolveOne = (sel) => {
|
|
3478
|
+
const byId = all.find((p) => p.id === sel);
|
|
3479
|
+
if (byId) return byId;
|
|
3480
|
+
const exact = pick(sel, all.filter((p) => p.name === sel), "share it");
|
|
3481
|
+
if (exact) return exact;
|
|
3482
|
+
const ci = pick(sel, all.filter((p) => p.name.trim().toLowerCase() === sel.toLowerCase()), "match ignoring case");
|
|
3483
|
+
if (ci) return ci;
|
|
3484
|
+
throw new Error(`no page matches ${JSON.stringify(sel)}. Available pages:
|
|
3485
|
+
${avail()}`);
|
|
3486
|
+
};
|
|
3487
|
+
const picked = [];
|
|
3488
|
+
for (const sel of wanted) {
|
|
3489
|
+
const p = resolveOne(sel);
|
|
3490
|
+
if (!picked.some((x) => x.id === p.id)) picked.push(p);
|
|
3491
|
+
}
|
|
3492
|
+
return picked;
|
|
3493
|
+
}
|
|
3494
|
+
var CSS_AUTO_NODE_CAP = 60;
|
|
3495
|
+
function subtreeIsSmall(node) {
|
|
3496
|
+
let count = 0;
|
|
3497
|
+
const stack = [node];
|
|
3498
|
+
while (stack.length) {
|
|
3499
|
+
const n = stack.pop();
|
|
3500
|
+
if (++count > CSS_AUTO_NODE_CAP) return false;
|
|
3501
|
+
if (n && "children" in n && isList(n.children)) {
|
|
3502
|
+
for (const c of n.children) stack.push(c);
|
|
3503
|
+
}
|
|
3504
|
+
}
|
|
3505
|
+
return true;
|
|
3506
|
+
}
|
|
3507
|
+
function autoCss(opts, node) {
|
|
3508
|
+
const o = { ...opts };
|
|
3509
|
+
if (o.css === void 0 && subtreeIsSmall(node)) o.css = true;
|
|
3510
|
+
return o;
|
|
3511
|
+
}
|
|
3512
|
+
var DEFAULT_PAGE_BG = ["#ffffff", "#f5f5f5", "#e5e5e5"];
|
|
3513
|
+
function isDefaultBg(f) {
|
|
3514
|
+
if (!f) return true;
|
|
3515
|
+
const only = f[0];
|
|
3516
|
+
return f.length === 1 && only !== void 0 && only.type === "solid" && DEFAULT_PAGE_BG.indexOf(only.color) !== -1;
|
|
3517
|
+
}
|
|
3518
|
+
async function pageBackground(page) {
|
|
3519
|
+
const out = {};
|
|
3520
|
+
const bg = await simplifyFills(page.backgrounds);
|
|
3521
|
+
if (bg && !isDefaultBg(bg)) out.background = bg;
|
|
3522
|
+
const proto = await simplifyFills(page.prototypeBackgrounds);
|
|
3523
|
+
if (proto && !isDefaultBg(proto)) out.prototypeBackground = proto;
|
|
3524
|
+
return nonEmpty(out);
|
|
3525
|
+
}
|
|
3526
|
+
function countNodes(tree) {
|
|
3527
|
+
let n = 1;
|
|
3528
|
+
const kids = tree.children;
|
|
3529
|
+
if (Array.isArray(kids)) for (const k of kids) n += countNodes(k);
|
|
3530
|
+
return n;
|
|
3531
|
+
}
|
|
3532
|
+
function collectMeasurements(r, pages) {
|
|
3533
|
+
if (!r.reads.measurements) return void 0;
|
|
3534
|
+
const targets = pages && pages.length ? pages : [figma.currentPage];
|
|
3535
|
+
const out = [];
|
|
3536
|
+
for (const page of targets) {
|
|
3537
|
+
if (typeof page.getMeasurements !== "function") continue;
|
|
3538
|
+
try {
|
|
3539
|
+
const ms = page.getMeasurements();
|
|
3540
|
+
if (!Array.isArray(ms) || !ms.length) continue;
|
|
3541
|
+
const side = (e) => e ? { nodeId: e.node && e.node.id, side: e.side } : void 0;
|
|
3542
|
+
for (const m of ms) {
|
|
3543
|
+
const o = { page: page.name, pageId: page.id, ...ifDefined("start", side(m.start)), ...ifDefined("end", side(m.end)) };
|
|
3544
|
+
if (m.freeText) o.text = m.freeText;
|
|
3545
|
+
if (m.offset != null) o.offset = m.offset;
|
|
3546
|
+
out.push(o);
|
|
3547
|
+
}
|
|
3548
|
+
} catch (e) {
|
|
3549
|
+
r.warn("measurements unavailable for page '" + page.name + "': " + errMsg(e));
|
|
3550
|
+
}
|
|
3551
|
+
}
|
|
3552
|
+
return out.length ? out : void 0;
|
|
3553
|
+
}
|
|
3554
|
+
async function withAllInstanceChildren(read) {
|
|
3555
|
+
const previous = figma.skipInvisibleInstanceChildren;
|
|
3556
|
+
try {
|
|
3557
|
+
figma.skipInvisibleInstanceChildren = false;
|
|
3558
|
+
return await read();
|
|
3559
|
+
} finally {
|
|
3560
|
+
figma.skipInvisibleInstanceChildren = previous;
|
|
3561
|
+
}
|
|
3562
|
+
}
|
|
3563
|
+
async function collectSelection(opts) {
|
|
3564
|
+
return withAllInstanceChildren(() => collectSelectionTree(opts));
|
|
3565
|
+
}
|
|
3566
|
+
async function collectSelectionTree(opts) {
|
|
3567
|
+
const r = claimRun();
|
|
3568
|
+
const sel = figma.currentPage.selection;
|
|
3569
|
+
const first2 = sel[0];
|
|
3570
|
+
if (!first2) throw new Error("Select at least one frame first.");
|
|
3571
|
+
r.useReads(sel.length === 1 ? autoCss(opts, first2) : opts);
|
|
3572
|
+
const nodes = [];
|
|
3573
|
+
for (const nd of sel) {
|
|
3574
|
+
const tree = await rootTree(r, nd);
|
|
3575
|
+
if (tree) nodes.push(tree);
|
|
3576
|
+
}
|
|
3577
|
+
return screenResult(r, sel.length === 1 ? first2.name : "selection", first2.name, nodes, { page: figma.currentPage, nodeId: first2.id });
|
|
3578
|
+
}
|
|
3579
|
+
function pageOf(node) {
|
|
3580
|
+
let n = node;
|
|
3581
|
+
while (n && n.type !== "PAGE") n = n.parent;
|
|
3582
|
+
return n && n.type === "PAGE" ? n : null;
|
|
3583
|
+
}
|
|
3584
|
+
async function collectNode(rawId, opts) {
|
|
3585
|
+
return withAllInstanceChildren(() => collectNodeTree(rawId, opts));
|
|
3586
|
+
}
|
|
3587
|
+
async function collectNodeTree(rawId, opts) {
|
|
3588
|
+
const r = claimRun();
|
|
3589
|
+
const nodeId = toNodeId(rawId);
|
|
3590
|
+
if (!nodeId) throw new Error(NO_NODE_ID);
|
|
3591
|
+
const node = await findNodeById(nodeId, r.warn);
|
|
3592
|
+
r.useReads(autoCss(opts, node));
|
|
3593
|
+
try {
|
|
3594
|
+
const page = pageOf(node);
|
|
3595
|
+
if (page && page !== figma.currentPage) await figma.setCurrentPageAsync(page);
|
|
3596
|
+
if ("visible" in node && node.parent) figma.currentPage.selection = [node];
|
|
3597
|
+
if (figma.viewport && "visible" in node) figma.viewport.scrollAndZoomIntoView([node]);
|
|
3598
|
+
} catch (e) {
|
|
3599
|
+
}
|
|
3600
|
+
const tree = await rootTree(r, node);
|
|
3601
|
+
if (!tree) throw new Error("Node " + nodeId + " is hidden or not exportable.");
|
|
3602
|
+
return screenResult(r, node.name, node.name, [tree], { page: pageOf(node), nodeId });
|
|
3603
|
+
}
|
|
3604
|
+
async function collectScreenshot(rawId, opts) {
|
|
3605
|
+
return withAllInstanceChildren(() => collectScreenshotOf(rawId, opts));
|
|
3606
|
+
}
|
|
3607
|
+
async function collectScreenshotOf(rawId, opts) {
|
|
3608
|
+
const r = claimRun();
|
|
3609
|
+
const nodeId = toNodeId(rawId);
|
|
3610
|
+
if (!nodeId) throw new Error(NO_NODE_ID);
|
|
3611
|
+
const node = await findNodeById(nodeId, r.warn);
|
|
3612
|
+
const reference = await collectReference(node, opts);
|
|
3613
|
+
if (!reference) {
|
|
3614
|
+
throw new Error("Node " + nodeId + " could not be rendered (hidden, zero-size, or the export failed \u2014 see warnings).");
|
|
3615
|
+
}
|
|
3616
|
+
const size = "width" in node && "height" in node ? { w: round(node.width), h: round(node.height) } : {};
|
|
3617
|
+
return { id: node.id, name: node.name, type: node.type, ...size, scale: referenceScale(node, opts), reference, manifest: manifestOf(r), assets: r.assets.slice() };
|
|
3618
|
+
}
|
|
3619
|
+
var TOP_LEVEL_TYPES = /* @__PURE__ */ new Set([
|
|
3620
|
+
"FRAME",
|
|
3621
|
+
"COMPONENT",
|
|
3622
|
+
"COMPONENT_SET",
|
|
3623
|
+
"INSTANCE",
|
|
3624
|
+
"GROUP",
|
|
3625
|
+
"SECTION",
|
|
3626
|
+
"TEXT",
|
|
3627
|
+
"RECTANGLE",
|
|
3628
|
+
"ELLIPSE",
|
|
3629
|
+
"POLYGON",
|
|
3630
|
+
"STAR",
|
|
3631
|
+
"LINE",
|
|
3632
|
+
"VECTOR",
|
|
3633
|
+
"BOOLEAN_OPERATION",
|
|
3634
|
+
"TABLE"
|
|
3635
|
+
]);
|
|
3636
|
+
async function listPages(opts) {
|
|
3637
|
+
return withAllInstanceChildren(() => listPagesTree(opts));
|
|
3638
|
+
}
|
|
3639
|
+
async function listPagesTree(opts) {
|
|
3640
|
+
const depth = opts && opts.depth === 1 ? 1 : 2;
|
|
3641
|
+
const localWarnings = [];
|
|
3642
|
+
const sink = (m) => localWarnings.push(m);
|
|
3643
|
+
const roots = figma.root.children;
|
|
3644
|
+
const currentId = figma.currentPage && figma.currentPage.id;
|
|
3645
|
+
const pages = [];
|
|
3646
|
+
let frameCount = 0;
|
|
3647
|
+
if (depth >= 2) {
|
|
3648
|
+
await Promise.all(roots.map((p) => loadPageSafely(p, sink, "\u2014 its frames may be missing")));
|
|
3649
|
+
}
|
|
3650
|
+
for (const page of roots) {
|
|
3651
|
+
const entry = { name: page.name, id: page.id };
|
|
3652
|
+
if (page.id === currentId) entry.current = true;
|
|
3653
|
+
if (depth >= 2) {
|
|
3654
|
+
const children = pageChildren(page, sink, "its frames are NOT listed");
|
|
3655
|
+
if (!children) {
|
|
3656
|
+
entry.unreadable = true;
|
|
3657
|
+
pages.push(entry);
|
|
3658
|
+
continue;
|
|
3659
|
+
}
|
|
3660
|
+
const frames = children.filter((nd) => TOP_LEVEL_TYPES.has(nd.type)).map(summarize);
|
|
3661
|
+
for (const g of collisionGroups(frames)) sink("page '" + page.name + "': " + collisionNote(g, "frames", "`childCount`"));
|
|
3662
|
+
frameCount += frames.length;
|
|
3663
|
+
entry.frames = frames;
|
|
3664
|
+
}
|
|
3665
|
+
pages.push(entry);
|
|
3666
|
+
}
|
|
3667
|
+
return {
|
|
3668
|
+
exportedAt: exportedAt(),
|
|
3669
|
+
file: figma.root.name,
|
|
3670
|
+
depth,
|
|
3671
|
+
pages,
|
|
3672
|
+
manifest: { pages: pages.length, ...ifDefined("frames", depth >= 2 ? frameCount : void 0), warnings: localWarnings }
|
|
3673
|
+
};
|
|
3674
|
+
}
|
|
3675
|
+
async function listChildren(rawId) {
|
|
3676
|
+
return withAllInstanceChildren(() => listChildrenTree(rawId));
|
|
3677
|
+
}
|
|
3678
|
+
async function listChildrenTree(rawId) {
|
|
3679
|
+
const nodeId = toNodeId(rawId);
|
|
3680
|
+
if (!nodeId) throw new Error(NO_NODE_ID);
|
|
3681
|
+
const localWarnings = [];
|
|
3682
|
+
const sink = (m) => localWarnings.push(m);
|
|
3683
|
+
const node = await findNodeById(nodeId, sink);
|
|
3684
|
+
if (!("children" in node)) throw new Error("Node " + nodeId + " (" + node.type + ") is a leaf \u2014 it has no children to list.");
|
|
3685
|
+
if (node.type === "PAGE") await loadPageSafely(node, sink, "before listing its children");
|
|
3686
|
+
let kids;
|
|
3687
|
+
try {
|
|
3688
|
+
kids = node.children;
|
|
3689
|
+
} catch (e) {
|
|
3690
|
+
throw new Error("Node " + nodeId + "'s children could not be read: " + errMsg(e));
|
|
3691
|
+
}
|
|
3692
|
+
const children = [];
|
|
3693
|
+
if (node.type === "DOCUMENT") {
|
|
3694
|
+
await Promise.all(kids.map((p) => loadPageSafely(p, sink, "\u2014 hasChildren omitted")));
|
|
3695
|
+
}
|
|
3696
|
+
for (const nd of kids) {
|
|
3697
|
+
const c = summarize(nd);
|
|
3698
|
+
if (c.childCount !== void 0) c.hasChildren = c.childCount > 0;
|
|
3699
|
+
children.push(c);
|
|
3700
|
+
}
|
|
3701
|
+
const listing = { reads: 0, cap: DISTINCT_TEXT_LISTING_READS };
|
|
3702
|
+
const noDistinct = [];
|
|
3703
|
+
for (const g of collisionGroups(children)) {
|
|
3704
|
+
const walks = /* @__PURE__ */ new Map();
|
|
3705
|
+
for (const row of g) {
|
|
3706
|
+
const nd = kids.find((k) => k.id === row.id);
|
|
3707
|
+
if (!nd) continue;
|
|
3708
|
+
const w = rowWalk(nd);
|
|
3709
|
+
walks.set(row.id, w);
|
|
3710
|
+
try {
|
|
3711
|
+
const t = titleOf(w);
|
|
3712
|
+
if (t) row.title = t;
|
|
3713
|
+
} catch (e) {
|
|
3714
|
+
if (!(e instanceof TitleCapReached)) throw e;
|
|
3715
|
+
sink("no title for '" + row.name + "' (" + row.id + "): more than " + TITLE_VISIT_CAP + " nodes to search");
|
|
3716
|
+
}
|
|
3717
|
+
}
|
|
3718
|
+
if (!titlesTellApart(g)) noDistinct.push(...addDistinctTexts(g, walks, listing, sink));
|
|
3719
|
+
sink(collisionNote(g, "children", "`title`/`childCount`/`distinctTexts`"));
|
|
3720
|
+
}
|
|
3721
|
+
if (noDistinct.length) sink("`distinctTexts` skipped for " + noDistinct.length + " row(s) (" + noDistinct.slice(0, 6).join(", ") + (noDistinct.length > 6 ? ", \u2026" : "") + "): the listing's budget of " + DISTINCT_TEXT_LISTING_READS + " node reads ran out, and a group with a row not read in full gets none");
|
|
3722
|
+
return {
|
|
3723
|
+
exportedAt: exportedAt(),
|
|
3724
|
+
id: node.id,
|
|
3725
|
+
name: node.name,
|
|
3726
|
+
type: node.type,
|
|
3727
|
+
children,
|
|
3728
|
+
manifest: { children: children.length, warnings: localWarnings }
|
|
3729
|
+
};
|
|
3730
|
+
}
|
|
3731
|
+
async function collectDesignSystemOnly(opts) {
|
|
3732
|
+
return withAllInstanceChildren(() => collectDesignSystemTree(opts));
|
|
3733
|
+
}
|
|
3734
|
+
async function collectDesignSystemTree(opts) {
|
|
3735
|
+
const r = claimRun({ sourceBytes: false });
|
|
3736
|
+
r.useReads(opts);
|
|
3737
|
+
const designSystem = await buildDesignSystem(void 0, serialize);
|
|
3738
|
+
checkCancelled();
|
|
3739
|
+
if ((designSystem.variables || []).some((v) => v && v.remote === true) || (designSystem.components || []).some((c) => c && c.remote === true)) {
|
|
3740
|
+
designSystem.hygiene = [
|
|
3741
|
+
'design-system pull of a file that consumes libraries: library (remote) variables are only the ones this file references \u2014 this is not the library\'s catalog; open the library file and run `dtwin pull --as-library "<name>"` for it.',
|
|
3742
|
+
...Array.isArray(designSystem.hygiene) ? designSystem.hygiene : []
|
|
3743
|
+
];
|
|
3744
|
+
}
|
|
3745
|
+
return { designSystem };
|
|
3746
|
+
}
|
|
3747
|
+
async function collectLibraryFile(opts) {
|
|
3748
|
+
return withAllInstanceChildren(() => collectLibraryTree(opts));
|
|
3749
|
+
}
|
|
3750
|
+
async function collectLibraryTree(opts) {
|
|
3751
|
+
const r = claimRun({ sourceBytes: false });
|
|
3752
|
+
r.useReads(opts);
|
|
3753
|
+
const asLibrary = opts && opts.asLibrary || figma.root && figma.root.name || "library";
|
|
3754
|
+
const designSystem = await buildDesignSystem({ asLibrary }, serialize);
|
|
3755
|
+
checkCancelled();
|
|
3756
|
+
designSystem.hygiene = [
|
|
3757
|
+
"library pull: this is the COMPLETE local catalog of '" + asLibrary + "' \u2014 variables, styles and components, with full per-mode values. It is a snapshot of the library file's CURRENT state, which is not necessarily what consumers see: `publish` reports each object's own status (current | changed | unpublished).",
|
|
3758
|
+
...Array.isArray(designSystem.hygiene) ? designSystem.hygiene : []
|
|
3759
|
+
];
|
|
3760
|
+
return { designSystem };
|
|
3761
|
+
}
|
|
3762
|
+
async function collectFull(opts) {
|
|
3763
|
+
return withAllInstanceChildren(() => collectFullTree(opts));
|
|
3764
|
+
}
|
|
3765
|
+
async function collectFullTree(opts) {
|
|
3766
|
+
const r = claimRun();
|
|
3767
|
+
r.useReads(opts);
|
|
3768
|
+
const allPages = !!(opts && opts.allPages);
|
|
3769
|
+
if (allPages) await loadAllPages("all-pages export may miss pages", r.warn);
|
|
3770
|
+
const rawWanted = opts && opts.page !== void 0 ? (Array.isArray(opts.page) ? opts.page : [opts.page]).map((s) => String(s).trim()) : [];
|
|
3771
|
+
if (rawWanted.length && rawWanted.every((s) => !s)) {
|
|
3772
|
+
r.warn("page selector was empty after trimming \u2014 ignored, exported " + (allPages ? "every page instead" : "the current page instead"));
|
|
3773
|
+
}
|
|
3774
|
+
const wanted = rawWanted.filter(Boolean);
|
|
3775
|
+
if (allPages && wanted.length) {
|
|
3776
|
+
throw new Error("allPages and page select different scopes \u2014 pass only one (page:[ids] for a bounded pull, allPages for the whole file).");
|
|
3777
|
+
}
|
|
3778
|
+
let pages;
|
|
3779
|
+
if (allPages) {
|
|
3780
|
+
pages = figma.root.children;
|
|
3781
|
+
} else if (wanted.length) {
|
|
3782
|
+
const picked = resolvePages(wanted, figma.root.children);
|
|
3783
|
+
await Promise.all(picked.map((p) => loadPageSafely(p, r.warn, "")));
|
|
3784
|
+
pages = picked;
|
|
3785
|
+
} else {
|
|
3786
|
+
pages = [figma.currentPage];
|
|
3787
|
+
}
|
|
3788
|
+
const layers = [];
|
|
3789
|
+
const index = [];
|
|
3790
|
+
const flows = [];
|
|
3791
|
+
const pageSettings = [];
|
|
3792
|
+
let pageIndex = 0;
|
|
3793
|
+
for (const page of pages) {
|
|
3794
|
+
checkCancelled();
|
|
3795
|
+
enterPage("pages", ++pageIndex, pages.length, page.name, page.id, { nodes: r.stats.nodes, assets: r.assets.length });
|
|
3796
|
+
const children = pageChildren(page, r.warn, "its frames are missing from this export");
|
|
3797
|
+
if (!children) continue;
|
|
3798
|
+
const bg = await pageBackground(page);
|
|
3799
|
+
if (bg) pageSettings.push({ page: page.name, pageId: page.id, ...bg });
|
|
3800
|
+
if (isList(page.flowStartingPoints)) {
|
|
3801
|
+
for (const fp of page.flowStartingPoints) flows.push({ page: page.name, pageId: page.id, nodeId: fp.nodeId, name: fp.name });
|
|
3802
|
+
}
|
|
3803
|
+
const frames = [];
|
|
3804
|
+
for (const nd of children) {
|
|
3805
|
+
if (TOP_LEVEL_TYPES.has(nd.type)) frames.push(nd);
|
|
3806
|
+
else if (nd.visible !== false) r.warn("top-level " + nd.type + " '" + nd.name + "' on page '" + page.name + "' not exported (unhandled top-level type)");
|
|
3807
|
+
}
|
|
3808
|
+
for (const f of frames) {
|
|
3809
|
+
checkCancelled();
|
|
3810
|
+
const { tree, ref, dev } = await serializeWithRefs(f);
|
|
3811
|
+
if (tree) {
|
|
3812
|
+
layers.push({ name: f.name, id: f.id, page: page.name, pageId: page.id, tree, ...ifDefined("reference", ref), ...ifDefined("devResources", dev) });
|
|
3813
|
+
index.push({
|
|
3814
|
+
name: f.name,
|
|
3815
|
+
id: f.id,
|
|
3816
|
+
type: f.type,
|
|
3817
|
+
page: page.name,
|
|
3818
|
+
pageId: page.id,
|
|
3819
|
+
nodes: countNodes(tree),
|
|
3820
|
+
bytes: JSON.stringify(tree).length
|
|
3821
|
+
});
|
|
3822
|
+
}
|
|
3823
|
+
progress("pages", { nodes: r.stats.nodes, assets: r.assets.length });
|
|
3824
|
+
}
|
|
3825
|
+
}
|
|
3826
|
+
checkCancelled();
|
|
3827
|
+
progress("design-system", { nodes: r.stats.nodes, assets: r.assets.length }, true);
|
|
3828
|
+
const designSystem = await buildDesignSystem(void 0, serialize);
|
|
3829
|
+
checkCancelled();
|
|
3830
|
+
const measurements = collectMeasurements(r, pages);
|
|
3831
|
+
const layersDoc = {
|
|
3832
|
+
exportedAt: exportedAt(),
|
|
3833
|
+
// Three cases, not two. Folding an explicit --page into "current-page" left the consumer unable
|
|
3834
|
+
// to tell whether it got the page it asked for, next to a `page` field naming a page that was
|
|
3835
|
+
// never exported (the named page is loaded, NOT made current — so figma.currentPage is still
|
|
3836
|
+
// whatever the user happens to be looking at).
|
|
3837
|
+
scope: allPages ? "all-pages" : wanted.length ? "page" : "current-page",
|
|
3838
|
+
// `pages.map(...)[0]` rather than `pages[0]?.name`: the plugin bundle is ES2019, which has no `?.`.
|
|
3839
|
+
...ifDefined("page", allPages || pages.length !== 1 ? void 0 : pages.map((p) => p.name)[0]),
|
|
3840
|
+
...ifDefined("pages", allPages || pages.length > 1 ? pages.map((p) => p.name) : void 0),
|
|
3841
|
+
...ifDefined("flows", flows.length ? flows : void 0),
|
|
3842
|
+
...ifDefined("pageSettings", pageSettings.length ? pageSettings : void 0),
|
|
3843
|
+
...ifDefined("measurements", measurements || void 0),
|
|
3844
|
+
index,
|
|
3845
|
+
layers,
|
|
3846
|
+
manifest: manifestOf(r)
|
|
3847
|
+
};
|
|
3848
|
+
return { designSystem, layersDoc, assets: r.assets.slice() };
|
|
3849
|
+
}
|
|
3850
|
+
|
|
3851
|
+
// src/edits.ts
|
|
3852
|
+
var KEY = "allowEdits";
|
|
3853
|
+
var allowed = false;
|
|
3854
|
+
var loaded = (async () => {
|
|
3855
|
+
try {
|
|
3856
|
+
allowed = await figma.clientStorage.getAsync(KEY) === true;
|
|
3857
|
+
} catch (e) {
|
|
3858
|
+
console.error("[export] could not read the Allow edits setting; edits stay off:", e);
|
|
3859
|
+
allowed = false;
|
|
3860
|
+
}
|
|
3861
|
+
})();
|
|
3862
|
+
async function editsAllowed() {
|
|
3863
|
+
await loaded;
|
|
3864
|
+
return allowed;
|
|
3865
|
+
}
|
|
3866
|
+
function editsAllowedNow() {
|
|
3867
|
+
return allowed;
|
|
3868
|
+
}
|
|
3869
|
+
async function setEditsAllowed(on) {
|
|
3870
|
+
await loaded;
|
|
3871
|
+
allowed = on;
|
|
3872
|
+
try {
|
|
3873
|
+
await figma.clientStorage.setAsync(KEY, on);
|
|
3874
|
+
} catch (e) {
|
|
3875
|
+
console.error("[export] could not save the Allow edits setting:", e);
|
|
3876
|
+
}
|
|
3877
|
+
return allowed;
|
|
3878
|
+
}
|
|
3879
|
+
|
|
3880
|
+
// src/writes.ts
|
|
3881
|
+
function solidPaint(hex, what) {
|
|
3882
|
+
const c = parseHexColor(hex);
|
|
3883
|
+
if (!c) throw new Error(what + ": '" + String(hex) + "' is not a valid colour \u2014 expected " + HEX_COLOR_HINT);
|
|
3884
|
+
const p = { type: "SOLID", color: { r: c.r, g: c.g, b: c.b } };
|
|
3885
|
+
if (c.a !== void 0) p.opacity = c.a;
|
|
3886
|
+
return p;
|
|
3887
|
+
}
|
|
3888
|
+
async function loadNodeFonts(node) {
|
|
3889
|
+
const len = node.characters.length;
|
|
3890
|
+
const fonts = len > 0 ? node.getRangeAllFontNames(0, len) : node.fontName !== figma.mixed ? [node.fontName] : [];
|
|
3891
|
+
await Promise.all(fonts.map((f) => figma.loadFontAsync(f)));
|
|
3892
|
+
}
|
|
3893
|
+
async function resolveParent(parentId) {
|
|
3894
|
+
if (!parentId) return figma.currentPage;
|
|
3895
|
+
const p = await figma.getNodeByIdAsync(parentId);
|
|
3896
|
+
if (!p) throw new Error("parent '" + parentId + "' not found (invalid or removed id)");
|
|
3897
|
+
if (!("appendChild" in p)) throw new Error("parent '" + p.name + "' (" + p.type + ") cannot have children");
|
|
3898
|
+
if (p.type === "PAGE") await p.loadAsync();
|
|
3899
|
+
return p;
|
|
3900
|
+
}
|
|
3901
|
+
var EditsRevoked = class extends Error {
|
|
3902
|
+
};
|
|
3903
|
+
function consent() {
|
|
3904
|
+
if (!editsAllowedNow()) throw new EditsRevoked("'Allow edits' was turned off in the Design Twin plugin partway through this batch \u2014 the ops before this one were applied. Tick 'Allow edits' in the plugin window, then retry the rest.");
|
|
3905
|
+
}
|
|
3906
|
+
async function applyWrite(op) {
|
|
3907
|
+
switch (op.op) {
|
|
3908
|
+
case "createFrame": {
|
|
3909
|
+
const parent = await resolveParent(op.parentId);
|
|
3910
|
+
const fill = op.fill ? solidPaint(op.fill, "createFrame fill") : null;
|
|
3911
|
+
consent();
|
|
3912
|
+
const f = figma.createFrame();
|
|
3913
|
+
if (op.name) f.name = op.name;
|
|
3914
|
+
if (op.width && op.height) f.resize(op.width, op.height);
|
|
3915
|
+
if (op.layoutMode) f.layoutMode = op.layoutMode;
|
|
3916
|
+
if (op.itemSpacing != null) f.itemSpacing = op.itemSpacing;
|
|
3917
|
+
if (op.padding) {
|
|
3918
|
+
f.paddingTop = op.padding[0];
|
|
3919
|
+
f.paddingRight = op.padding[1];
|
|
3920
|
+
f.paddingBottom = op.padding[2];
|
|
3921
|
+
f.paddingLeft = op.padding[3];
|
|
3922
|
+
}
|
|
3923
|
+
if (fill) f.fills = [fill];
|
|
3924
|
+
parent.appendChild(f);
|
|
3925
|
+
return { id: f.id };
|
|
3926
|
+
}
|
|
3927
|
+
case "createText": {
|
|
3928
|
+
const parent = await resolveParent(op.parentId);
|
|
3929
|
+
const fill = op.fill ? solidPaint(op.fill, "createText fill") : null;
|
|
3930
|
+
consent();
|
|
3931
|
+
const t = figma.createText();
|
|
3932
|
+
await figma.loadFontAsync(t.fontName);
|
|
3933
|
+
if (!editsAllowedNow()) {
|
|
3934
|
+
t.remove();
|
|
3935
|
+
consent();
|
|
3936
|
+
}
|
|
3937
|
+
t.characters = op.text || "";
|
|
3938
|
+
if (op.fontSize) t.fontSize = op.fontSize;
|
|
3939
|
+
if (fill) t.fills = [fill];
|
|
3940
|
+
parent.appendChild(t);
|
|
3941
|
+
return { id: t.id };
|
|
3942
|
+
}
|
|
3943
|
+
// setFill/setText THROW rather than no-op. getNodeByIdAsync resolves to null for an invalid or
|
|
3944
|
+
// removed id (and for invisible instance children when skipInvisibleInstanceChildren is on), and
|
|
3945
|
+
// not every node type carries fills/characters. Returning {id} in those cases reported a write
|
|
3946
|
+
// that never happened — over MCP the agent reads that as success and builds on a false premise.
|
|
3947
|
+
case "setFill": {
|
|
3948
|
+
const nd = op.nodeId ? await figma.getNodeByIdAsync(op.nodeId) : null;
|
|
3949
|
+
if (!nd) throw new Error("setFill: no node with id '" + op.nodeId + "' (invalid or removed)");
|
|
3950
|
+
if (!("fills" in nd)) throw new Error("setFill: node '" + nd.name + "' (" + nd.type + ") has no fills");
|
|
3951
|
+
const paint = solidPaint(op.color, "setFill color");
|
|
3952
|
+
consent();
|
|
3953
|
+
nd.fills = [paint];
|
|
3954
|
+
return ifDefined("id", op.nodeId);
|
|
3955
|
+
}
|
|
3956
|
+
case "setText": {
|
|
3957
|
+
const nd = op.nodeId ? await figma.getNodeByIdAsync(op.nodeId) : null;
|
|
3958
|
+
if (!nd) throw new Error("setText: no node with id '" + op.nodeId + "' (invalid or removed)");
|
|
3959
|
+
if (nd.type !== "TEXT") throw new Error("setText: node '" + nd.name + "' is a " + nd.type + ", not TEXT");
|
|
3960
|
+
await loadNodeFonts(nd);
|
|
3961
|
+
consent();
|
|
3962
|
+
nd.characters = op.text || "";
|
|
3963
|
+
return ifDefined("id", op.nodeId);
|
|
3964
|
+
}
|
|
3965
|
+
default:
|
|
3966
|
+
throw new Error("unknown write op: " + op.op);
|
|
3967
|
+
}
|
|
3968
|
+
}
|
|
3969
|
+
async function applyWrites(ops) {
|
|
3970
|
+
const r = await gatedWrites(Array.isArray(ops) ? ops : []);
|
|
3971
|
+
announce(r);
|
|
3972
|
+
return r;
|
|
3973
|
+
}
|
|
3974
|
+
function announce(r) {
|
|
3975
|
+
const n = r.applied.length;
|
|
3976
|
+
const edits = n + " edit" + (n === 1 ? "" : "s");
|
|
3977
|
+
let text = null;
|
|
3978
|
+
if (r.refused === "edits-off") text = n ? "Design Twin applied " + edits + ", then stopped \u2014 'Allow edits' was turned off." : "Design Twin refused an edit request \u2014 'Allow edits' is off.";
|
|
3979
|
+
else if (r.ok && n) text = "Design Twin applied " + edits + " to this file.";
|
|
3980
|
+
else if (!r.ok && n) text = "Design Twin applied " + edits + ", then one failed: " + (r.error || "unknown error");
|
|
3981
|
+
if (text === null) return;
|
|
3982
|
+
try {
|
|
3983
|
+
figma.notify(text, r.ok ? void 0 : { error: true });
|
|
3984
|
+
} catch (e) {
|
|
3985
|
+
console.error("[export] figma.notify failed:", e);
|
|
3986
|
+
}
|
|
3987
|
+
const m = { type: "writes", applied: n, ok: r.ok, text };
|
|
3988
|
+
figma.ui.postMessage(m);
|
|
3989
|
+
}
|
|
3990
|
+
async function gatedWrites(list) {
|
|
3991
|
+
const applied = [];
|
|
3992
|
+
if (list.length && !await editsAllowed()) {
|
|
3993
|
+
return { ok: false, applied, refused: "edits-off", error: EDITS_OFF_MESSAGE };
|
|
3994
|
+
}
|
|
3995
|
+
if (list.length && figma.editorType === "dev") {
|
|
3996
|
+
const first2 = list[0];
|
|
3997
|
+
return {
|
|
3998
|
+
ok: false,
|
|
3999
|
+
applied,
|
|
4000
|
+
failedAt: 0,
|
|
4001
|
+
...ifDefined("failedOp", first2 && first2.op),
|
|
4002
|
+
error: "the file is open in Dev Mode, which is read-only for plugins \u2014 switch to Design mode (Shift+D) and re-run the plugin to write. Reads and exports work in Dev Mode."
|
|
4003
|
+
};
|
|
4004
|
+
}
|
|
4005
|
+
for (const [i, op] of list.entries()) {
|
|
4006
|
+
const bad = writeOpError(op, i);
|
|
4007
|
+
if (bad !== null) return { ok: false, applied, failedAt: i, ...ifDefined("failedOp", op && op.op), error: bad };
|
|
4008
|
+
}
|
|
4009
|
+
for (const [i, op] of list.entries()) {
|
|
4010
|
+
try {
|
|
4011
|
+
applied.push(await applyWrite(op));
|
|
4012
|
+
} catch (e) {
|
|
4013
|
+
return {
|
|
4014
|
+
ok: false,
|
|
4015
|
+
applied,
|
|
4016
|
+
failedAt: i,
|
|
4017
|
+
failedOp: op && op.op,
|
|
4018
|
+
error: errMsg(e),
|
|
4019
|
+
...e instanceof EditsRevoked ? { refused: "edits-off" } : {}
|
|
4020
|
+
};
|
|
4021
|
+
}
|
|
4022
|
+
}
|
|
4023
|
+
return { ok: true, applied };
|
|
4024
|
+
}
|
|
4025
|
+
|
|
4026
|
+
// src/bridge.ts
|
|
4027
|
+
var bridgeRun = (cmd, id) => ({ source: "bridge", label: cmd, ...ifDefined("requestId", id) });
|
|
4028
|
+
var INSTANCE_ID = "fig-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 8);
|
|
4029
|
+
var INSTANCE_STARTED_AT = Date.now();
|
|
4030
|
+
function readFileKey() {
|
|
4031
|
+
try {
|
|
4032
|
+
return typeof figma.fileKey === "string" ? figma.fileKey : void 0;
|
|
4033
|
+
} catch {
|
|
4034
|
+
return void 0;
|
|
4035
|
+
}
|
|
4036
|
+
}
|
|
4037
|
+
async function handleBridge(cmd, args, id) {
|
|
4038
|
+
const parsed = parseCommandRequest(cmd, args);
|
|
4039
|
+
if (!parsed.ok) throw new Error(parsed.error);
|
|
4040
|
+
const req = parsed.req;
|
|
4041
|
+
return scheduleRun(runKind(req.cmd), work(req), bridgeRun(req.cmd, id));
|
|
4042
|
+
}
|
|
4043
|
+
function work(req) {
|
|
4044
|
+
switch (req.cmd) {
|
|
4045
|
+
// The probe for the multi-file question: run it in two files at once and compare `instanceId`.
|
|
4046
|
+
// An identity read — it must stay answerable DURING a long export, since "is the other file's
|
|
4047
|
+
// connection still alive while this one works?" is half of what it exists to measure.
|
|
4048
|
+
//
|
|
4049
|
+
// It answers four things in one call:
|
|
4050
|
+
// instanceId / startedAt / uptimeMs — two distinct ids => two instances really do coexist; a
|
|
4051
|
+
// CHANGED id on a later call => Figma tore the plugin runtime down and re-ran it (the
|
|
4052
|
+
// southleft/figma-console-mcp#67 failure), which a reconnect alone would not reveal.
|
|
4053
|
+
// fileKey — undefined => `enablePrivatePluginApi` is not in effect for
|
|
4054
|
+
// a locally-imported plugin, so routing must fall back to a server-minted connection id.
|
|
4055
|
+
// file / page — human labels for a connection listing, never routing keys.
|
|
4056
|
+
case "whoami":
|
|
4057
|
+
return async () => {
|
|
4058
|
+
const fileKey = readFileKey();
|
|
4059
|
+
const r = {
|
|
4060
|
+
instanceId: INSTANCE_ID,
|
|
4061
|
+
startedAt: INSTANCE_STARTED_AT,
|
|
4062
|
+
uptimeMs: Date.now() - INSTANCE_STARTED_AT,
|
|
4063
|
+
file: figma.root.name,
|
|
4064
|
+
page: figma.currentPage.name,
|
|
4065
|
+
pageId: figma.currentPage.id,
|
|
4066
|
+
editorType: figma.editorType,
|
|
4067
|
+
// Baked in at build time (build.ts's esbuild `define`) from
|
|
4068
|
+
// figma-plugin/package.json — the one way to tell a stale plugin in Figma apart from a
|
|
4069
|
+
// freshly reloaded one, since neither startedAt nor code.js's mtime can. Reused verbatim by
|
|
4070
|
+
// the `hello` announcement below (main.ts's get-identity -> ui.html -> bridge), so `whoami`,
|
|
4071
|
+
// `dtwin list clients` and `dtwin doctor` can never disagree about which build is running.
|
|
4072
|
+
pluginVersion: "0.1.0",
|
|
4073
|
+
fileKey: fileKey === void 0 ? null : fileKey,
|
|
4074
|
+
fileKeyAvailable: typeof fileKey === "string" && fileKey.length > 0,
|
|
4075
|
+
// So figma_write's dry run can say a real write would be refused before anyone sends one.
|
|
4076
|
+
editsAllowed: await editsAllowed()
|
|
4077
|
+
};
|
|
4078
|
+
return r;
|
|
4079
|
+
};
|
|
4080
|
+
case "ping":
|
|
4081
|
+
return async () => {
|
|
4082
|
+
const r = { pong: true, page: figma.currentPage.name, file: figma.root.name };
|
|
4083
|
+
const fileKey = readFileKey();
|
|
4084
|
+
if (fileKey !== void 0) r.fileKey = fileKey;
|
|
4085
|
+
return r;
|
|
4086
|
+
};
|
|
4087
|
+
// The extraction/write commands mutate shared per-run state (and the document): exclusive runs, on the
|
|
4088
|
+
// same chain the UI's manual runs use, so a bridge pull and a manual export can never overlap.
|
|
4089
|
+
case "exportFull": {
|
|
4090
|
+
const a = req.args;
|
|
4091
|
+
return () => collectFull(a);
|
|
4092
|
+
}
|
|
4093
|
+
// The tokens/styles/components-only pull — no page/frame walk, no assets. See collect.ts's
|
|
4094
|
+
// collectDesignSystemOnly for the one tradeoff (library-variable completeness).
|
|
4095
|
+
case "exportDesignSystem": {
|
|
4096
|
+
const a = req.args;
|
|
4097
|
+
return () => collectDesignSystemOnly(a);
|
|
4098
|
+
}
|
|
4099
|
+
// The library-file pull: exclusive like its export siblings (listLibraries is only a scene read). It
|
|
4100
|
+
// runs the full catalog build and mutates the same per-run state they do.
|
|
4101
|
+
case "exportLibrary": {
|
|
4102
|
+
const a = req.args;
|
|
4103
|
+
return () => collectLibraryFile(a);
|
|
4104
|
+
}
|
|
4105
|
+
case "exportSelection": {
|
|
4106
|
+
const a = req.args;
|
|
4107
|
+
return () => collectSelection(a);
|
|
4108
|
+
}
|
|
4109
|
+
case "exportNode": {
|
|
4110
|
+
const a = req.args;
|
|
4111
|
+
return () => collectNode(a.nodeId, a);
|
|
4112
|
+
}
|
|
4113
|
+
// The on-demand single-node screenshot — deliberately its own op rather than a mode of exportNode:
|
|
4114
|
+
// it skips serialize() and the recursive asset walk entirely (see collectScreenshot's comment), so
|
|
4115
|
+
// routing it through exportNode's shape would mislead a caller into thinking it got a tree back.
|
|
4116
|
+
case "screenshot": {
|
|
4117
|
+
const a = req.args;
|
|
4118
|
+
return () => collectScreenshot(a.nodeId, ifDefined("scale", a.scale));
|
|
4119
|
+
}
|
|
4120
|
+
case "getSelection":
|
|
4121
|
+
return async () => figma.currentPage.selection.map((n) => ({ id: n.id, name: n.name, type: n.type }));
|
|
4122
|
+
// Scene reads, these three and getSelection: the cheap maps you consult to decide WHICH deep pull to run.
|
|
4123
|
+
// They mutate none of the per-run state an exclusive run owns, but they wait for the run ahead of them (it
|
|
4124
|
+
// may hold skipInvisibleInstanceChildren on — commands.ts RunKind), unbracketed: a listing behind a long
|
|
4125
|
+
// export waits for it, and a listing in an idle file answers at once.
|
|
4126
|
+
case "listPages": {
|
|
4127
|
+
const a = req.args;
|
|
4128
|
+
return () => listPages(a);
|
|
4129
|
+
}
|
|
4130
|
+
case "listChildren": {
|
|
4131
|
+
const nodeId = req.args.nodeId;
|
|
4132
|
+
return () => listChildren(nodeId);
|
|
4133
|
+
}
|
|
4134
|
+
// Its document walk is one findAllWithCriteria per page with skipInvisibleInstanceChildren on — the same
|
|
4135
|
+
// cost class as listPages depth 2, not that of an export.
|
|
4136
|
+
case "listLibraries":
|
|
4137
|
+
return () => listLibraries();
|
|
4138
|
+
// applyWrites refuses the batch whole while "Allow edits" is off (edits.ts) — the gate lives there, not
|
|
4139
|
+
// here, so no caller of it can skip it.
|
|
4140
|
+
case "write": {
|
|
4141
|
+
const ops = req.args.ops;
|
|
4142
|
+
return () => applyWrites(ops);
|
|
4143
|
+
}
|
|
4144
|
+
default: {
|
|
4145
|
+
const exhaustive = req;
|
|
4146
|
+
throw new Error("unknown cmd: " + String(exhaustive));
|
|
4147
|
+
}
|
|
4148
|
+
}
|
|
4149
|
+
}
|
|
4150
|
+
|
|
4151
|
+
// src/messages.ts
|
|
4152
|
+
var UI_TO_MAIN_TYPES = /* @__PURE__ */ new Set([
|
|
4153
|
+
"get-token",
|
|
4154
|
+
"set-token",
|
|
4155
|
+
"get-identity",
|
|
4156
|
+
"run-selection",
|
|
4157
|
+
"run-full",
|
|
4158
|
+
"get-allow-edits",
|
|
4159
|
+
"set-allow-edits",
|
|
4160
|
+
"cancel",
|
|
4161
|
+
"bridge"
|
|
4162
|
+
]);
|
|
4163
|
+
function isUIToMain(x) {
|
|
4164
|
+
if (!x || typeof x !== "object") return false;
|
|
4165
|
+
const t = x.type;
|
|
4166
|
+
return typeof t === "string" && UI_TO_MAIN_TYPES.has(t);
|
|
4167
|
+
}
|
|
4168
|
+
|
|
4169
|
+
// src/main.ts
|
|
4170
|
+
globalThis.__designExport = { serialize, collectSelection, collectNode, collectScreenshot, collectFull, collectDesignSystemOnly, collectLibraryFile, listPages, listChildren, buildDesignSystem, applyWrites, listLibraries, collectLibraryComponents, serializeRun, requestCancel, assetRetention };
|
|
4171
|
+
figma.showUI(__html__, { width: 360, height: 380, themeColors: true });
|
|
4172
|
+
console.log("[export] main.ts loaded (main thread)");
|
|
4173
|
+
var post2 = (m) => figma.ui.postMessage(m);
|
|
4174
|
+
async function runExport(label, collect, toFiles) {
|
|
4175
|
+
let r;
|
|
4176
|
+
try {
|
|
4177
|
+
r = await serializeRun(collect, { source: "ui", label });
|
|
4178
|
+
} catch (e) {
|
|
4179
|
+
post2({ type: "error", message: errMsg(e) });
|
|
4180
|
+
return;
|
|
4181
|
+
}
|
|
4182
|
+
const { files, layerFiles, summary, warnings } = toFiles(r);
|
|
4183
|
+
post2({ type: "files", files, ...ifDefined("layerFiles", layerFiles), assets: r.assets, summary, warnings: warnings ? warnings.length : 0 });
|
|
4184
|
+
}
|
|
4185
|
+
var runSelection = () => runExport("current selection", () => collectSelection(), (r) => ({
|
|
4186
|
+
files: [
|
|
4187
|
+
{ name: `${r.screenName}.json`, content: JSON.stringify(r.screen, null, 2), copyable: true },
|
|
4188
|
+
{ name: "variables.json", content: JSON.stringify(r.variables, null, 2) }
|
|
4189
|
+
],
|
|
4190
|
+
summary: `${r.screenName} \u2014 ${r.assets.length} asset(s)`,
|
|
4191
|
+
warnings: r.screen.manifest && r.screen.manifest.warnings
|
|
4192
|
+
}));
|
|
4193
|
+
var SEP = "__";
|
|
4194
|
+
var runFull = () => runExport("design system + page frames", () => collectFull(), (r) => {
|
|
4195
|
+
const { meta, layerFiles, indexFiles, rootIndex } = buildPageLayout(r.layersDoc, SEP);
|
|
4196
|
+
const ds = buildDesignSystemLayout(r.designSystem, SEP);
|
|
4197
|
+
const dsParts = ds.files.filter((f) => f.path !== "design-system.json");
|
|
4198
|
+
const batch = [...layerFiles, ...indexFiles, ...dsParts].map((f) => ({ name: f.path, content: JSON.stringify(f.data, null, 2) }));
|
|
4199
|
+
return {
|
|
4200
|
+
files: [
|
|
4201
|
+
{ name: "design-system.json", content: JSON.stringify(ds.manifest, null, 2) },
|
|
4202
|
+
{ name: rootIndex, content: JSON.stringify(meta, null, 2) }
|
|
4203
|
+
],
|
|
4204
|
+
layerFiles: batch,
|
|
4205
|
+
summary: `${layerFiles.length} layer(s) across ${meta.pageDirs.length} page(s), ${(r.designSystem.variables || []).length} vars, ${(r.designSystem.components || []).length} components, ${r.assets.length} asset(s)`,
|
|
4206
|
+
...ifDefined("warnings", r.layersDoc.manifest && r.layersDoc.manifest.warnings)
|
|
4207
|
+
};
|
|
4208
|
+
});
|
|
4209
|
+
function notifySelection() {
|
|
4210
|
+
const sel = figma.currentPage.selection;
|
|
4211
|
+
const first2 = sel[0];
|
|
4212
|
+
post2({ type: "selection", count: sel.length, name: first2 ? first2.name : null });
|
|
4213
|
+
}
|
|
4214
|
+
figma.ui.onmessage = async (raw) => {
|
|
4215
|
+
if (!isUIToMain(raw)) return;
|
|
4216
|
+
switch (raw.type) {
|
|
4217
|
+
case "get-token": {
|
|
4218
|
+
const token = await figma.clientStorage.getAsync("bridgeToken");
|
|
4219
|
+
post2({ type: "token", token: typeof token === "string" ? token : "" });
|
|
4220
|
+
break;
|
|
4221
|
+
}
|
|
4222
|
+
case "set-token": {
|
|
4223
|
+
await figma.clientStorage.setAsync("bridgeToken", raw.token || "");
|
|
4224
|
+
break;
|
|
4225
|
+
}
|
|
4226
|
+
case "get-identity": {
|
|
4227
|
+
let identity = {};
|
|
4228
|
+
try {
|
|
4229
|
+
identity = await handleBridge("whoami", {});
|
|
4230
|
+
} catch (e) {
|
|
4231
|
+
identity = { error: errMsg(e) };
|
|
4232
|
+
}
|
|
4233
|
+
post2({ type: "identity", identity });
|
|
4234
|
+
break;
|
|
4235
|
+
}
|
|
4236
|
+
case "get-allow-edits": {
|
|
4237
|
+
post2({ type: "allow-edits", allowed: await editsAllowed() });
|
|
4238
|
+
break;
|
|
4239
|
+
}
|
|
4240
|
+
case "set-allow-edits": {
|
|
4241
|
+
post2({ type: "allow-edits", allowed: await setEditsAllowed(raw.allowed === true) });
|
|
4242
|
+
break;
|
|
4243
|
+
}
|
|
4244
|
+
case "run-selection": {
|
|
4245
|
+
await runSelection();
|
|
4246
|
+
break;
|
|
4247
|
+
}
|
|
4248
|
+
case "run-full": {
|
|
4249
|
+
await runFull();
|
|
4250
|
+
break;
|
|
4251
|
+
}
|
|
4252
|
+
case "cancel": {
|
|
4253
|
+
if (raw.id !== void 0) {
|
|
4254
|
+
cancelBridgeRequest(raw.id);
|
|
4255
|
+
break;
|
|
4256
|
+
}
|
|
4257
|
+
if (raw.scope === "bridge") {
|
|
4258
|
+
cancelBridgeRuns();
|
|
4259
|
+
break;
|
|
4260
|
+
}
|
|
4261
|
+
const hit = requestCancel();
|
|
4262
|
+
post2({ type: "cancel-ack", accepted: !!hit, label: hit ? hit.label : null });
|
|
4263
|
+
break;
|
|
4264
|
+
}
|
|
4265
|
+
case "bridge": {
|
|
4266
|
+
let result;
|
|
4267
|
+
let error;
|
|
4268
|
+
try {
|
|
4269
|
+
result = await handleBridge(raw.cmd, raw.args, raw.id);
|
|
4270
|
+
} catch (e) {
|
|
4271
|
+
error = errMsg(e);
|
|
4272
|
+
}
|
|
4273
|
+
post2({ type: "bridge-result", ...ifDefined("id", raw.id), ok: !error, result, ...ifDefined("error", error) });
|
|
4274
|
+
break;
|
|
4275
|
+
}
|
|
4276
|
+
default: {
|
|
4277
|
+
const _exhaustive = raw;
|
|
4278
|
+
void _exhaustive;
|
|
4279
|
+
}
|
|
4280
|
+
}
|
|
4281
|
+
};
|
|
4282
|
+
figma.on("selectionchange", notifySelection);
|
|
4283
|
+
try {
|
|
4284
|
+
notifySelection();
|
|
4285
|
+
} catch (e) {
|
|
4286
|
+
console.error("[export] notifySelection failed:", e);
|
|
4287
|
+
}
|
|
4288
|
+
console.log("[export] main thread ready \u2014 onmessage registered");
|
|
4289
|
+
})();
|