@sangheepark/figma-ds-mcp 0.2.4 → 0.2.6
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/dist/tools/pipeline-tools.js +141 -13
- package/dist/tools/utility-tools.js +39 -12
- package/package.json +1 -1
|
@@ -112,18 +112,52 @@ function enrichSpec(traversal, mapping) {
|
|
|
112
112
|
if (component) {
|
|
113
113
|
node['library-key'] = component['library-key'];
|
|
114
114
|
// Step 2: _ds_props → variant-props + overrides 분리
|
|
115
|
+
// value 역검색 + default fallback 로직 포함
|
|
115
116
|
if (node._ds_props) {
|
|
116
117
|
const variantProps = {};
|
|
117
118
|
const overrides = {};
|
|
118
119
|
for (const [key, value] of Object.entries(node._ds_props)) {
|
|
119
120
|
if (component.variants && key in component.variants) {
|
|
120
|
-
//
|
|
121
|
+
// 1) exact key match → variant-props
|
|
121
122
|
if (typeof value === 'string' && component.defaults?.[key] !== value) {
|
|
122
123
|
variantProps[key] = value;
|
|
123
124
|
}
|
|
124
125
|
}
|
|
126
|
+
else if (component.variants && typeof value === 'string') {
|
|
127
|
+
// 2) exact match 실패 → value 역검색: 이 값을 가진 variant axis 찾기
|
|
128
|
+
const matchingAxes = [];
|
|
129
|
+
for (const [axis, options] of Object.entries(component.variants)) {
|
|
130
|
+
if (axis in variantProps)
|
|
131
|
+
continue; // 이미 매핑된 axis 스킵
|
|
132
|
+
if (options.includes(value) && component.defaults?.[axis] !== value) {
|
|
133
|
+
matchingAxes.push(axis);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (matchingAxes.length === 1) {
|
|
137
|
+
// 정확히 1개 axis에서만 발견 → 매핑
|
|
138
|
+
variantProps[matchingAxes[0]] = value;
|
|
139
|
+
}
|
|
140
|
+
// 0개 또는 2개 이상 → 매핑 포기 (default variant 사용)
|
|
141
|
+
}
|
|
142
|
+
else if (component.properties && typeof value === 'string') {
|
|
143
|
+
// 3) variant가 아닌 prop → property name 역검색
|
|
144
|
+
if (key in component.properties) {
|
|
145
|
+
// exact property name match
|
|
146
|
+
overrides[key] = value;
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
// exact match 실패 → TEXT property 자동 매핑
|
|
150
|
+
const textProps = Object.entries(component.properties)
|
|
151
|
+
.filter(([, type]) => type === 'TEXT')
|
|
152
|
+
.map(([name]) => name);
|
|
153
|
+
if (textProps.length === 1) {
|
|
154
|
+
overrides[textProps[0]] = value;
|
|
155
|
+
}
|
|
156
|
+
// 0개 또는 2개 이상 → 매핑 포기
|
|
157
|
+
}
|
|
158
|
+
}
|
|
125
159
|
else {
|
|
126
|
-
//
|
|
160
|
+
// boolean 등 기타 → overrides에 직접
|
|
127
161
|
overrides[key] = value;
|
|
128
162
|
}
|
|
129
163
|
}
|
|
@@ -222,6 +256,19 @@ function enrichSpec(traversal, mapping) {
|
|
|
222
256
|
node.layout.direction = 'column';
|
|
223
257
|
}
|
|
224
258
|
}
|
|
259
|
+
// 6-D: absolute + fill 정규화
|
|
260
|
+
// Figma에서 absolute 요소는 parent auto-layout에 참여하지 않으므로
|
|
261
|
+
// fill은 layoutSizingHorizontal/Vertical = 'FILL'로 처리 → constraints STRETCH 동작
|
|
262
|
+
// 별도 변환 불필요 — NodeBuilder가 absolute child에 fill을 적용하면 STRETCH로 동작
|
|
263
|
+
// 다만 x/y가 없으면 0으로 기본값 설정하여 parent 좌상단 정렬
|
|
264
|
+
if (node.layout?.positioning === 'absolute') {
|
|
265
|
+
if ((node.layout.width === 'fill' || node.layout.width === '100%') && node.layout.x === undefined) {
|
|
266
|
+
node.layout.x = '0';
|
|
267
|
+
}
|
|
268
|
+
if ((node.layout.height === 'fill' || node.layout.height === '100%') && node.layout.y === undefined) {
|
|
269
|
+
node.layout.y = '0';
|
|
270
|
+
}
|
|
271
|
+
}
|
|
225
272
|
// children 재귀
|
|
226
273
|
if (node.children) {
|
|
227
274
|
node.children.forEach((child, i) => {
|
|
@@ -230,13 +277,13 @@ function enrichSpec(traversal, mapping) {
|
|
|
230
277
|
}
|
|
231
278
|
}
|
|
232
279
|
walk(spec, 'root');
|
|
233
|
-
// Step 6-C: root frame sizing (
|
|
280
|
+
// Step 6-C: root frame sizing (viewport defaults: 1440×900 PC)
|
|
234
281
|
if (spec.layout) {
|
|
235
282
|
if (spec.layout.width === 'fill') {
|
|
236
283
|
spec.layout.width = '1440';
|
|
237
284
|
}
|
|
238
285
|
if (spec.layout.height === 'fill') {
|
|
239
|
-
spec.layout.height = '
|
|
286
|
+
spec.layout.height = '900';
|
|
240
287
|
}
|
|
241
288
|
}
|
|
242
289
|
return { spec, errors };
|
|
@@ -420,6 +467,80 @@ function gateCheck(spec) {
|
|
|
420
467
|
const pass = results.every(r => r.pass);
|
|
421
468
|
return { pass, results };
|
|
422
469
|
}
|
|
470
|
+
// --- _repeat: Component Auto-Extraction ---
|
|
471
|
+
function extractRepeatComponents(spec, defaultCount = 3) {
|
|
472
|
+
const extracted = new Map(); // name → component spec
|
|
473
|
+
function deepClone(obj) {
|
|
474
|
+
return JSON.parse(JSON.stringify(obj));
|
|
475
|
+
}
|
|
476
|
+
function getRepeatCount(node) {
|
|
477
|
+
if (typeof node._repeat === 'number' && node._repeat > 0)
|
|
478
|
+
return node._repeat;
|
|
479
|
+
return defaultCount;
|
|
480
|
+
}
|
|
481
|
+
// Depth-first walk: process children first so nested _repeat resolves before parent
|
|
482
|
+
function walk(node) {
|
|
483
|
+
// Process children first (depth-first)
|
|
484
|
+
if (node.children) {
|
|
485
|
+
for (const child of node.children) {
|
|
486
|
+
walk(child);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
// Now process this node's children for _repeat
|
|
490
|
+
if (!node.children)
|
|
491
|
+
return;
|
|
492
|
+
const newChildren = [];
|
|
493
|
+
for (const child of node.children) {
|
|
494
|
+
if (!child._repeat) {
|
|
495
|
+
newChildren.push(child);
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
const count = getRepeatCount(child);
|
|
499
|
+
const nodeAny = child;
|
|
500
|
+
const hasLibraryKey = !!nodeAny['library-key'];
|
|
501
|
+
const hasDs = !!child._ds;
|
|
502
|
+
// Library instance: just duplicate N times, no component extraction
|
|
503
|
+
if (hasLibraryKey || hasDs) {
|
|
504
|
+
const cleanChild = deepClone(child);
|
|
505
|
+
delete cleanChild._repeat;
|
|
506
|
+
for (let i = 0; i < count; i++) {
|
|
507
|
+
const instance = deepClone(cleanChild);
|
|
508
|
+
if (instance.name)
|
|
509
|
+
instance.name = `${instance.name}_${i + 1}`;
|
|
510
|
+
newChildren.push(instance);
|
|
511
|
+
}
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
// Custom frame: extract as component + insert instances
|
|
515
|
+
const compName = child.name || `RepeatComponent_${extracted.size + 1}`;
|
|
516
|
+
if (!extracted.has(compName)) {
|
|
517
|
+
// Extract as component
|
|
518
|
+
const compSpec = deepClone(child);
|
|
519
|
+
compSpec.type = 'component';
|
|
520
|
+
compSpec.name = compName;
|
|
521
|
+
delete compSpec._repeat;
|
|
522
|
+
extracted.set(compName, compSpec);
|
|
523
|
+
}
|
|
524
|
+
// Insert N instances in place
|
|
525
|
+
for (let i = 0; i < count; i++) {
|
|
526
|
+
newChildren.push({
|
|
527
|
+
type: 'instance',
|
|
528
|
+
component: compName,
|
|
529
|
+
name: `${compName}_${i + 1}`,
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
node.children = newChildren;
|
|
534
|
+
}
|
|
535
|
+
const page = deepClone(spec);
|
|
536
|
+
// Skip if root itself has _repeat (standalone component, no extraction needed)
|
|
537
|
+
if (page._repeat) {
|
|
538
|
+
delete page._repeat;
|
|
539
|
+
return { page, components: [] };
|
|
540
|
+
}
|
|
541
|
+
walk(page);
|
|
542
|
+
return { page, components: Array.from(extracted.values()) };
|
|
543
|
+
}
|
|
423
544
|
// fontWeight name → CSS number (reverse of display names in data files)
|
|
424
545
|
const FONT_WEIGHT_NAME_TO_NUM = {
|
|
425
546
|
thin: '100', hairline: '100',
|
|
@@ -946,9 +1067,16 @@ If no reviewNeeded, completes in 1 pass.`, {
|
|
|
946
1067
|
}],
|
|
947
1068
|
};
|
|
948
1069
|
}
|
|
1070
|
+
// Step 3.5: Extract _repeat components
|
|
1071
|
+
// Components are placed before pages so builder creates them first
|
|
1072
|
+
const finalSpecs = [];
|
|
1073
|
+
for (const spec of specs) {
|
|
1074
|
+
const { page, components } = extractRepeatComponents(spec);
|
|
1075
|
+
finalSpecs.push(...components, page);
|
|
1076
|
+
}
|
|
949
1077
|
// Step 4: Gate check each spec
|
|
950
1078
|
const gateResults = [];
|
|
951
|
-
for (const spec of
|
|
1079
|
+
for (const spec of finalSpecs) {
|
|
952
1080
|
gateResults.push(gateCheck(spec));
|
|
953
1081
|
}
|
|
954
1082
|
const allGatesPass = gateResults.every(g => g.pass);
|
|
@@ -956,18 +1084,18 @@ If no reviewNeeded, completes in 1 pass.`, {
|
|
|
956
1084
|
mkdirSync(dirname(outputPath), { recursive: true });
|
|
957
1085
|
const trees = [];
|
|
958
1086
|
const allStats = [];
|
|
959
|
-
if (
|
|
960
|
-
writeFileSync(outputPath, JSON.stringify(
|
|
961
|
-
trees.push(specToTree(
|
|
962
|
-
allStats.push(countSpecNodes(
|
|
1087
|
+
if (finalSpecs.length === 1) {
|
|
1088
|
+
writeFileSync(outputPath, JSON.stringify(finalSpecs[0], null, 2));
|
|
1089
|
+
trees.push(specToTree(finalSpecs[0]));
|
|
1090
|
+
allStats.push(countSpecNodes(finalSpecs[0]));
|
|
963
1091
|
}
|
|
964
1092
|
else {
|
|
965
1093
|
// Multiple specs → save each with index
|
|
966
|
-
for (let i = 0; i <
|
|
1094
|
+
for (let i = 0; i < finalSpecs.length; i++) {
|
|
967
1095
|
const specPath = outputPath.replace(/\.json$/, `_${i}.json`);
|
|
968
|
-
writeFileSync(specPath, JSON.stringify(
|
|
969
|
-
trees.push(specToTree(
|
|
970
|
-
allStats.push(countSpecNodes(
|
|
1096
|
+
writeFileSync(specPath, JSON.stringify(finalSpecs[i], null, 2));
|
|
1097
|
+
trees.push(specToTree(finalSpecs[i]));
|
|
1098
|
+
allStats.push(countSpecNodes(finalSpecs[i]));
|
|
971
1099
|
}
|
|
972
1100
|
}
|
|
973
1101
|
// Collect unresolved
|
|
@@ -57,20 +57,47 @@ function analyzeLayoutWarnings(spec) {
|
|
|
57
57
|
return warnings;
|
|
58
58
|
}
|
|
59
59
|
export function registerUtilityTools(server, bridge) {
|
|
60
|
-
// get_status — Check connection status
|
|
60
|
+
// get_status — Check connection status + bridge version/capabilities
|
|
61
61
|
server.tool('get_status', 'Check if the Figma Bridge plugin is connected and ready', {}, async () => {
|
|
62
62
|
const connected = bridge.isConnected();
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
},
|
|
72
|
-
|
|
73
|
-
}
|
|
63
|
+
if (!connected) {
|
|
64
|
+
return {
|
|
65
|
+
content: [{
|
|
66
|
+
type: 'text',
|
|
67
|
+
text: JSON.stringify({
|
|
68
|
+
connected: false,
|
|
69
|
+
message: 'Figma Bridge plugin is NOT connected. Please open the "Code to Figma Bridge" plugin in Figma.',
|
|
70
|
+
}, null, 2),
|
|
71
|
+
}],
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const bridgeStatus = await bridge.send('bridge-get-status');
|
|
76
|
+
return {
|
|
77
|
+
content: [{
|
|
78
|
+
type: 'text',
|
|
79
|
+
text: JSON.stringify({
|
|
80
|
+
connected: true,
|
|
81
|
+
message: 'Figma Bridge plugin is connected and ready.',
|
|
82
|
+
bridge: {
|
|
83
|
+
version: bridgeStatus.version || 'unknown',
|
|
84
|
+
capabilities: bridgeStatus.capabilities || [],
|
|
85
|
+
},
|
|
86
|
+
}, null, 2),
|
|
87
|
+
}],
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return {
|
|
92
|
+
content: [{
|
|
93
|
+
type: 'text',
|
|
94
|
+
text: JSON.stringify({
|
|
95
|
+
connected: true,
|
|
96
|
+
message: 'Figma Bridge plugin is connected but bridge-get-status failed. Bridge plugin dist may need rebuild.',
|
|
97
|
+
}, null, 2),
|
|
98
|
+
}],
|
|
99
|
+
};
|
|
100
|
+
}
|
|
74
101
|
});
|
|
75
102
|
// validate_spec — Validate a JSON spec without creating anything
|
|
76
103
|
server.tool('validate_spec', 'Validate a component/variable/style spec (JSON) without creating anything in Figma. Returns validation errors if any.', {
|
package/package.json
CHANGED