@sangheepark/figma-ds-mcp 0.2.5 → 0.2.7
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 +180 -21
- package/dist/tools/utility-tools.js +39 -12
- package/package.json +1 -1
|
@@ -234,9 +234,14 @@ function enrichSpec(traversal, mapping) {
|
|
|
234
234
|
node.style['line-height'] = `${Math.round(lh * 100)}%`;
|
|
235
235
|
}
|
|
236
236
|
}
|
|
237
|
+
// _ds_type 제거 (downstream 미소비 — build 전에 정리)
|
|
238
|
+
if ('_ds_type' in node) {
|
|
239
|
+
delete node['_ds_type'];
|
|
240
|
+
}
|
|
237
241
|
// Step 6: CSS → Figma 정규화
|
|
238
|
-
// 6-pre: overflow
|
|
239
|
-
|
|
242
|
+
// 6-pre: overflow → layout.clip-content 변환 (6-A 필터링 전에 처리)
|
|
243
|
+
const overflow = node.style && node.style['overflow'];
|
|
244
|
+
if (overflow === 'hidden' || overflow === 'scroll' || overflow === 'auto') {
|
|
240
245
|
node.layout = node.layout || {};
|
|
241
246
|
node.layout['clip-content'] = true;
|
|
242
247
|
delete node.style['overflow'];
|
|
@@ -277,13 +282,13 @@ function enrichSpec(traversal, mapping) {
|
|
|
277
282
|
}
|
|
278
283
|
}
|
|
279
284
|
walk(spec, 'root');
|
|
280
|
-
// Step 6-C: root frame sizing (
|
|
285
|
+
// Step 6-C: root frame sizing (viewport defaults: 1440×900 PC)
|
|
281
286
|
if (spec.layout) {
|
|
282
287
|
if (spec.layout.width === 'fill') {
|
|
283
288
|
spec.layout.width = '1440';
|
|
284
289
|
}
|
|
285
290
|
if (spec.layout.height === 'fill') {
|
|
286
|
-
spec.layout.height = '
|
|
291
|
+
spec.layout.height = '900';
|
|
287
292
|
}
|
|
288
293
|
}
|
|
289
294
|
return { spec, errors };
|
|
@@ -467,6 +472,127 @@ function gateCheck(spec) {
|
|
|
467
472
|
const pass = results.every(r => r.pass);
|
|
468
473
|
return { pass, results };
|
|
469
474
|
}
|
|
475
|
+
// --- _repeat: Component Auto-Extraction ---
|
|
476
|
+
function extractRepeatComponents(spec, defaultCount = 3) {
|
|
477
|
+
const extracted = new Map(); // name → component spec
|
|
478
|
+
function deepClone(obj) {
|
|
479
|
+
return JSON.parse(JSON.stringify(obj));
|
|
480
|
+
}
|
|
481
|
+
function getRepeatCount(node) {
|
|
482
|
+
if (typeof node._repeat === 'number' && node._repeat > 0)
|
|
483
|
+
return node._repeat;
|
|
484
|
+
return defaultCount;
|
|
485
|
+
}
|
|
486
|
+
// Depth-first walk: process children first so nested _repeat resolves before parent
|
|
487
|
+
function walk(node) {
|
|
488
|
+
// Process children first (depth-first)
|
|
489
|
+
if (node.children) {
|
|
490
|
+
for (const child of node.children) {
|
|
491
|
+
walk(child);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
// Now process this node's children for _repeat
|
|
495
|
+
if (!node.children)
|
|
496
|
+
return;
|
|
497
|
+
const newChildren = [];
|
|
498
|
+
for (const child of node.children) {
|
|
499
|
+
if (!child._repeat) {
|
|
500
|
+
newChildren.push(child);
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
const count = getRepeatCount(child);
|
|
504
|
+
const nodeAny = child;
|
|
505
|
+
const hasLibraryKey = !!nodeAny['library-key'];
|
|
506
|
+
const hasDs = !!child._ds;
|
|
507
|
+
// Library instance: just duplicate N times, no component extraction
|
|
508
|
+
if (hasLibraryKey || hasDs) {
|
|
509
|
+
const cleanChild = deepClone(child);
|
|
510
|
+
delete cleanChild._repeat;
|
|
511
|
+
for (let i = 0; i < count; i++) {
|
|
512
|
+
const instance = deepClone(cleanChild);
|
|
513
|
+
if (instance.name)
|
|
514
|
+
instance.name = `${instance.name}_${i + 1}`;
|
|
515
|
+
newChildren.push(instance);
|
|
516
|
+
}
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
// Custom frame: extract as component + insert instances
|
|
520
|
+
const compName = child.name || `RepeatComponent_${extracted.size + 1}`;
|
|
521
|
+
if (!extracted.has(compName)) {
|
|
522
|
+
// Extract as component
|
|
523
|
+
const compSpec = deepClone(child);
|
|
524
|
+
compSpec.type = 'component';
|
|
525
|
+
compSpec.name = compName;
|
|
526
|
+
delete compSpec._repeat;
|
|
527
|
+
extracted.set(compName, compSpec);
|
|
528
|
+
}
|
|
529
|
+
// Insert N instances in place
|
|
530
|
+
for (let i = 0; i < count; i++) {
|
|
531
|
+
newChildren.push({
|
|
532
|
+
type: 'instance',
|
|
533
|
+
component: compName,
|
|
534
|
+
name: `${compName}_${i + 1}`,
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
node.children = newChildren;
|
|
539
|
+
}
|
|
540
|
+
const page = deepClone(spec);
|
|
541
|
+
// Skip if root itself has _repeat (standalone component, no extraction needed)
|
|
542
|
+
if (page._repeat) {
|
|
543
|
+
delete page._repeat;
|
|
544
|
+
return { page, components: [] };
|
|
545
|
+
}
|
|
546
|
+
walk(page);
|
|
547
|
+
return { page, components: Array.from(extracted.values()) };
|
|
548
|
+
}
|
|
549
|
+
function countNodes(node) {
|
|
550
|
+
let count = 1;
|
|
551
|
+
if (node.children) {
|
|
552
|
+
for (const child of node.children)
|
|
553
|
+
count += countNodes(child);
|
|
554
|
+
}
|
|
555
|
+
return count;
|
|
556
|
+
}
|
|
557
|
+
function splitSections(pageSpec, components, outputDir) {
|
|
558
|
+
mkdirSync(join(outputDir, 'components'), { recursive: true });
|
|
559
|
+
mkdirSync(join(outputDir, 'sections'), { recursive: true });
|
|
560
|
+
// Save components
|
|
561
|
+
const compEntries = [];
|
|
562
|
+
for (const comp of components) {
|
|
563
|
+
const name = comp.name || `Component_${compEntries.length}`;
|
|
564
|
+
const relPath = `components/${name}.json`;
|
|
565
|
+
writeFileSync(join(outputDir, relPath), JSON.stringify(comp, null, 2));
|
|
566
|
+
compEntries.push({ name, specPath: relPath, nodeCount: countNodes(comp) });
|
|
567
|
+
}
|
|
568
|
+
// Split page into sections: use _section markers or top-level children
|
|
569
|
+
const sectionEntries = [];
|
|
570
|
+
const rootLayout = { ...(pageSpec.layout || {}) };
|
|
571
|
+
const rootStyle = pageSpec.style ? { ...pageSpec.style } : undefined;
|
|
572
|
+
const pageName = pageSpec.name || 'Page';
|
|
573
|
+
if (pageSpec.children && pageSpec.children.length > 0) {
|
|
574
|
+
for (let i = 0; i < pageSpec.children.length; i++) {
|
|
575
|
+
const child = pageSpec.children[i];
|
|
576
|
+
const name = child.name || `Section_${i}`;
|
|
577
|
+
const relPath = `sections/${name}.json`;
|
|
578
|
+
writeFileSync(join(outputDir, relPath), JSON.stringify(child, null, 2));
|
|
579
|
+
sectionEntries.push({ name, specPath: relPath, nodeCount: countNodes(child), order: i });
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
let totalNodes = 1; // root
|
|
583
|
+
for (const c of compEntries)
|
|
584
|
+
totalNodes += c.nodeCount;
|
|
585
|
+
for (const s of sectionEntries)
|
|
586
|
+
totalNodes += s.nodeCount;
|
|
587
|
+
const manifest = {
|
|
588
|
+
page: { name: pageName, rootLayout, rootStyle },
|
|
589
|
+
components: compEntries,
|
|
590
|
+
sections: sectionEntries,
|
|
591
|
+
stats: { totalNodes, components: compEntries.length, sections: sectionEntries.length },
|
|
592
|
+
};
|
|
593
|
+
writeFileSync(join(outputDir, 'manifest.json'), JSON.stringify(manifest, null, 2));
|
|
594
|
+
return manifest;
|
|
595
|
+
}
|
|
470
596
|
// fontWeight name → CSS number (reverse of display names in data files)
|
|
471
597
|
const FONT_WEIGHT_NAME_TO_NUM = {
|
|
472
598
|
thin: '100', hairline: '100',
|
|
@@ -838,16 +964,28 @@ Run after validate_traversal. If outputPath is provided, spec is saved to file a
|
|
|
838
964
|
}],
|
|
839
965
|
};
|
|
840
966
|
});
|
|
967
|
+
// Helper: resolve traversals from either JSON param or file paths
|
|
968
|
+
function resolveTraversals(traversals, traversalPaths) {
|
|
969
|
+
if (traversalPaths && traversalPaths.length > 0) {
|
|
970
|
+
return traversalPaths.map(p => JSON.parse(readFileSync(p, 'utf-8')));
|
|
971
|
+
}
|
|
972
|
+
if (traversals && traversals.length > 0) {
|
|
973
|
+
return traversals;
|
|
974
|
+
}
|
|
975
|
+
throw new Error('Either traversals or traversalPaths must be provided');
|
|
976
|
+
}
|
|
841
977
|
// generate_mapping — D5
|
|
842
978
|
server.tool('generate_mapping', `Generate mapping.json from traversal JSON + blueprint data files. Zero LLM involvement.
|
|
843
979
|
Reads *-component-sets.json and *-style.json from dataDir. Matches components (3-stage: exact → case-insensitive → word-split), tokens (HEX → variable), text styles (fontSize/fontWeight → style name).
|
|
844
980
|
Saves result to outputPath. Replaces ds-data-lookup sub-agent.`, {
|
|
845
|
-
traversals: z.array(z.record(z.unknown())).describe('Array of traversal JSON root nodes'),
|
|
981
|
+
traversals: z.array(z.record(z.unknown())).optional().describe('Array of traversal JSON root nodes'),
|
|
982
|
+
traversalPaths: z.array(z.string()).optional().describe('File paths to traversal JSON files (alternative to traversals — avoids large param)'),
|
|
846
983
|
dataDir: z.string().describe('Path to blueprint data directory (e.g., .claude/blueprint/data/)'),
|
|
847
984
|
outputPath: z.string().describe('File path to save mapping.json'),
|
|
848
|
-
}, async ({ traversals, dataDir, outputPath }) => {
|
|
985
|
+
}, async ({ traversals, traversalPaths, dataDir, outputPath }) => {
|
|
849
986
|
try {
|
|
850
|
-
const
|
|
987
|
+
const tNodes = resolveTraversals(traversals, traversalPaths);
|
|
988
|
+
const result = generateMapping(tNodes, dataDir);
|
|
851
989
|
mkdirSync(dirname(outputPath), { recursive: true });
|
|
852
990
|
writeFileSync(outputPath, JSON.stringify(result, null, 2));
|
|
853
991
|
// Summary for context (not the full mapping)
|
|
@@ -859,7 +997,7 @@ Saves result to outputPath. Replaces ds-data-lookup sub-agent.`, {
|
|
|
859
997
|
allDs.add(node._ds);
|
|
860
998
|
node.children?.forEach(collectDs);
|
|
861
999
|
}
|
|
862
|
-
|
|
1000
|
+
tNodes.forEach(collectDs);
|
|
863
1001
|
const unmatchedDs = [];
|
|
864
1002
|
for (const ds of allDs) {
|
|
865
1003
|
if (!matchedDs.has(ds) && !reviewDs.has(ds))
|
|
@@ -900,13 +1038,14 @@ Saves result to outputPath. Replaces ds-data-lookup sub-agent.`, {
|
|
|
900
1038
|
server.tool('run_pipeline', `Run the full spec pipeline: generate_mapping → validate → enrich → gate → tree in one call.
|
|
901
1039
|
Supports 2-pass pattern: Pass 1 returns reviewNeeded (ambiguous matches). Agent decides, then Pass 2 with resolutions auto-applies decisions.
|
|
902
1040
|
If no reviewNeeded, completes in 1 pass.`, {
|
|
903
|
-
traversals: z.array(z.record(z.unknown())).describe('Array of traversal JSON root nodes'),
|
|
1041
|
+
traversals: z.array(z.record(z.unknown())).optional().describe('Array of traversal JSON root nodes'),
|
|
1042
|
+
traversalPaths: z.array(z.string()).optional().describe('File paths to traversal JSON files (alternative to traversals — avoids large param)'),
|
|
904
1043
|
dataDir: z.string().describe('Path to blueprint data directory (e.g., .claude/blueprint/data/)'),
|
|
905
1044
|
outputPath: z.string().describe('File path to save spec.json (mapping.json saved alongside)'),
|
|
906
1045
|
resolutions: z.record(z.string()).optional().describe('Agent-decided mappings: { dsName: resolvedName }. Pass 2 only.'),
|
|
907
|
-
}, async ({ traversals, dataDir, outputPath, resolutions }) => {
|
|
1046
|
+
}, async ({ traversals, traversalPaths, dataDir, outputPath, resolutions }) => {
|
|
908
1047
|
try {
|
|
909
|
-
let tNodes = traversals;
|
|
1048
|
+
let tNodes = resolveTraversals(traversals, traversalPaths);
|
|
910
1049
|
// Step 0: Apply resolutions — replace _ds in traversal nodes
|
|
911
1050
|
if (resolutions && Object.keys(resolutions).length > 0) {
|
|
912
1051
|
// Deep clone to avoid mutating input
|
|
@@ -993,9 +1132,20 @@ If no reviewNeeded, completes in 1 pass.`, {
|
|
|
993
1132
|
}],
|
|
994
1133
|
};
|
|
995
1134
|
}
|
|
1135
|
+
// Step 3.5: Extract _repeat components
|
|
1136
|
+
// Components are placed before pages so builder creates them first
|
|
1137
|
+
const finalSpecs = [];
|
|
1138
|
+
const allComponents = [];
|
|
1139
|
+
const allPages = [];
|
|
1140
|
+
for (const spec of specs) {
|
|
1141
|
+
const { page, components } = extractRepeatComponents(spec);
|
|
1142
|
+
allComponents.push(...components);
|
|
1143
|
+
allPages.push(page);
|
|
1144
|
+
finalSpecs.push(...components, page);
|
|
1145
|
+
}
|
|
996
1146
|
// Step 4: Gate check each spec
|
|
997
1147
|
const gateResults = [];
|
|
998
|
-
for (const spec of
|
|
1148
|
+
for (const spec of finalSpecs) {
|
|
999
1149
|
gateResults.push(gateCheck(spec));
|
|
1000
1150
|
}
|
|
1001
1151
|
const allGatesPass = gateResults.every(g => g.pass);
|
|
@@ -1003,18 +1153,25 @@ If no reviewNeeded, completes in 1 pass.`, {
|
|
|
1003
1153
|
mkdirSync(dirname(outputPath), { recursive: true });
|
|
1004
1154
|
const trees = [];
|
|
1005
1155
|
const allStats = [];
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1156
|
+
// Also produce manifest + section files for incremental build
|
|
1157
|
+
const outputDir = dirname(outputPath);
|
|
1158
|
+
const manifests = [];
|
|
1159
|
+
for (const page of allPages) {
|
|
1160
|
+
const manifest = splitSections(page, allComponents, outputDir);
|
|
1161
|
+
manifests.push(manifest);
|
|
1162
|
+
}
|
|
1163
|
+
// Save full specs (legacy format) alongside manifest
|
|
1164
|
+
if (finalSpecs.length === 1) {
|
|
1165
|
+
writeFileSync(outputPath, JSON.stringify(finalSpecs[0], null, 2));
|
|
1166
|
+
trees.push(specToTree(finalSpecs[0]));
|
|
1167
|
+
allStats.push(countSpecNodes(finalSpecs[0]));
|
|
1010
1168
|
}
|
|
1011
1169
|
else {
|
|
1012
|
-
|
|
1013
|
-
for (let i = 0; i < specs.length; i++) {
|
|
1170
|
+
for (let i = 0; i < finalSpecs.length; i++) {
|
|
1014
1171
|
const specPath = outputPath.replace(/\.json$/, `_${i}.json`);
|
|
1015
|
-
writeFileSync(specPath, JSON.stringify(
|
|
1016
|
-
trees.push(specToTree(
|
|
1017
|
-
allStats.push(countSpecNodes(
|
|
1172
|
+
writeFileSync(specPath, JSON.stringify(finalSpecs[i], null, 2));
|
|
1173
|
+
trees.push(specToTree(finalSpecs[i]));
|
|
1174
|
+
allStats.push(countSpecNodes(finalSpecs[i]));
|
|
1018
1175
|
}
|
|
1019
1176
|
}
|
|
1020
1177
|
// Collect unresolved
|
|
@@ -1037,7 +1194,9 @@ If no reviewNeeded, completes in 1 pass.`, {
|
|
|
1037
1194
|
text: JSON.stringify({
|
|
1038
1195
|
success: allGatesPass,
|
|
1039
1196
|
savedTo: outputPath,
|
|
1197
|
+
manifestSavedTo: join(outputDir, 'manifest.json'),
|
|
1040
1198
|
mappingSavedTo: mappingPath,
|
|
1199
|
+
manifest: manifests.length === 1 ? manifests[0] : manifests,
|
|
1041
1200
|
tree: trees.length === 1 ? trees[0] : trees,
|
|
1042
1201
|
stats: allStats.length === 1 ? allStats[0] : allStats,
|
|
1043
1202
|
gateResult: gateResults.length === 1 ? gateResults[0] : gateResults,
|
|
@@ -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