@sangheepark/figma-ds-mcp 0.2.6 → 0.2.8
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 +122 -10
- package/package.json +1 -1
|
@@ -234,13 +234,23 @@ 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 → clip-content 변환 (style 또는 layout 어디에 있든 처리)
|
|
243
|
+
const styleOverflow = node.style && node.style['overflow'];
|
|
244
|
+
if (styleOverflow === 'hidden' || styleOverflow === 'scroll' || styleOverflow === 'auto') {
|
|
240
245
|
node.layout = node.layout || {};
|
|
241
246
|
node.layout['clip-content'] = true;
|
|
242
247
|
delete node.style['overflow'];
|
|
243
248
|
}
|
|
249
|
+
const layoutOverflow = node.layout && node.layout['overflow'];
|
|
250
|
+
if (layoutOverflow === 'hidden' || layoutOverflow === 'scroll' || layoutOverflow === 'auto') {
|
|
251
|
+
node.layout['clip-content'] = true;
|
|
252
|
+
delete node.layout['overflow'];
|
|
253
|
+
}
|
|
244
254
|
// 6-A: CSS-only 속성 필터링 (Figma가 지원하지 않는 CSS 속성 제거)
|
|
245
255
|
if (node.style) {
|
|
246
256
|
for (const key of Object.keys(node.style)) {
|
|
@@ -249,6 +259,25 @@ function enrichSpec(traversal, mapping) {
|
|
|
249
259
|
}
|
|
250
260
|
}
|
|
251
261
|
}
|
|
262
|
+
// 6-A2: layout에서 CSS position 키 → Figma x/y 변환 후 non-layout 키 제거
|
|
263
|
+
if (node.layout) {
|
|
264
|
+
const lay = node.layout;
|
|
265
|
+
// CSS top/left → Figma y/x (absolute positioning만)
|
|
266
|
+
if (lay['positioning'] === 'absolute') {
|
|
267
|
+
if (lay['top'] !== undefined && lay['y'] === undefined) {
|
|
268
|
+
lay['y'] = String(lay['top']);
|
|
269
|
+
}
|
|
270
|
+
if (lay['left'] !== undefined && lay['x'] === undefined) {
|
|
271
|
+
lay['x'] = String(lay['left']);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
// non-layout 키 제거 (top, bottom, left, right, aspect-ratio, overflow 잔존 등)
|
|
275
|
+
for (const key of Object.keys(lay)) {
|
|
276
|
+
if (!LAYOUT_KEYS.has(key)) {
|
|
277
|
+
delete lay[key];
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
252
281
|
// 6-B: direction 기본값 column (children 있는 frame/component에 direction 없으면)
|
|
253
282
|
if ((node.type === 'frame' || node.type === 'component') && node.children && node.children.length > 0) {
|
|
254
283
|
if (!node.layout?.direction) {
|
|
@@ -541,6 +570,53 @@ function extractRepeatComponents(spec, defaultCount = 3) {
|
|
|
541
570
|
walk(page);
|
|
542
571
|
return { page, components: Array.from(extracted.values()) };
|
|
543
572
|
}
|
|
573
|
+
function countNodes(node) {
|
|
574
|
+
let count = 1;
|
|
575
|
+
if (node.children) {
|
|
576
|
+
for (const child of node.children)
|
|
577
|
+
count += countNodes(child);
|
|
578
|
+
}
|
|
579
|
+
return count;
|
|
580
|
+
}
|
|
581
|
+
function splitSections(pageSpec, components, outputDir) {
|
|
582
|
+
mkdirSync(join(outputDir, 'components'), { recursive: true });
|
|
583
|
+
mkdirSync(join(outputDir, 'sections'), { recursive: true });
|
|
584
|
+
// Save components
|
|
585
|
+
const compEntries = [];
|
|
586
|
+
for (const comp of components) {
|
|
587
|
+
const name = comp.name || `Component_${compEntries.length}`;
|
|
588
|
+
const relPath = `components/${name}.json`;
|
|
589
|
+
writeFileSync(join(outputDir, relPath), JSON.stringify(comp, null, 2));
|
|
590
|
+
compEntries.push({ name, specPath: relPath, nodeCount: countNodes(comp) });
|
|
591
|
+
}
|
|
592
|
+
// Split page into sections: use _section markers or top-level children
|
|
593
|
+
const sectionEntries = [];
|
|
594
|
+
const rootLayout = { ...(pageSpec.layout || {}) };
|
|
595
|
+
const rootStyle = pageSpec.style ? { ...pageSpec.style } : undefined;
|
|
596
|
+
const pageName = pageSpec.name || 'Page';
|
|
597
|
+
if (pageSpec.children && pageSpec.children.length > 0) {
|
|
598
|
+
for (let i = 0; i < pageSpec.children.length; i++) {
|
|
599
|
+
const child = pageSpec.children[i];
|
|
600
|
+
const name = child.name || `Section_${i}`;
|
|
601
|
+
const relPath = `sections/${name}.json`;
|
|
602
|
+
writeFileSync(join(outputDir, relPath), JSON.stringify(child, null, 2));
|
|
603
|
+
sectionEntries.push({ name, specPath: relPath, nodeCount: countNodes(child), order: i });
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
let totalNodes = 1; // root
|
|
607
|
+
for (const c of compEntries)
|
|
608
|
+
totalNodes += c.nodeCount;
|
|
609
|
+
for (const s of sectionEntries)
|
|
610
|
+
totalNodes += s.nodeCount;
|
|
611
|
+
const manifest = {
|
|
612
|
+
page: { name: pageName, rootLayout, rootStyle },
|
|
613
|
+
components: compEntries,
|
|
614
|
+
sections: sectionEntries,
|
|
615
|
+
stats: { totalNodes, components: compEntries.length, sections: sectionEntries.length },
|
|
616
|
+
};
|
|
617
|
+
writeFileSync(join(outputDir, 'manifest.json'), JSON.stringify(manifest, null, 2));
|
|
618
|
+
return manifest;
|
|
619
|
+
}
|
|
544
620
|
// fontWeight name → CSS number (reverse of display names in data files)
|
|
545
621
|
const FONT_WEIGHT_NAME_TO_NUM = {
|
|
546
622
|
thin: '100', hairline: '100',
|
|
@@ -912,16 +988,28 @@ Run after validate_traversal. If outputPath is provided, spec is saved to file a
|
|
|
912
988
|
}],
|
|
913
989
|
};
|
|
914
990
|
});
|
|
991
|
+
// Helper: resolve traversals from either JSON param or file paths
|
|
992
|
+
function resolveTraversals(traversals, traversalPaths) {
|
|
993
|
+
if (traversalPaths && traversalPaths.length > 0) {
|
|
994
|
+
return traversalPaths.map(p => JSON.parse(readFileSync(p, 'utf-8')));
|
|
995
|
+
}
|
|
996
|
+
if (traversals && traversals.length > 0) {
|
|
997
|
+
return traversals;
|
|
998
|
+
}
|
|
999
|
+
throw new Error('Either traversals or traversalPaths must be provided');
|
|
1000
|
+
}
|
|
915
1001
|
// generate_mapping — D5
|
|
916
1002
|
server.tool('generate_mapping', `Generate mapping.json from traversal JSON + blueprint data files. Zero LLM involvement.
|
|
917
1003
|
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).
|
|
918
1004
|
Saves result to outputPath. Replaces ds-data-lookup sub-agent.`, {
|
|
919
|
-
traversals: z.array(z.record(z.unknown())).describe('Array of traversal JSON root nodes'),
|
|
1005
|
+
traversals: z.array(z.record(z.unknown())).optional().describe('Array of traversal JSON root nodes'),
|
|
1006
|
+
traversalPaths: z.array(z.string()).optional().describe('File paths to traversal JSON files (alternative to traversals — avoids large param)'),
|
|
920
1007
|
dataDir: z.string().describe('Path to blueprint data directory (e.g., .claude/blueprint/data/)'),
|
|
921
1008
|
outputPath: z.string().describe('File path to save mapping.json'),
|
|
922
|
-
}, async ({ traversals, dataDir, outputPath }) => {
|
|
1009
|
+
}, async ({ traversals, traversalPaths, dataDir, outputPath }) => {
|
|
923
1010
|
try {
|
|
924
|
-
const
|
|
1011
|
+
const tNodes = resolveTraversals(traversals, traversalPaths);
|
|
1012
|
+
const result = generateMapping(tNodes, dataDir);
|
|
925
1013
|
mkdirSync(dirname(outputPath), { recursive: true });
|
|
926
1014
|
writeFileSync(outputPath, JSON.stringify(result, null, 2));
|
|
927
1015
|
// Summary for context (not the full mapping)
|
|
@@ -933,7 +1021,7 @@ Saves result to outputPath. Replaces ds-data-lookup sub-agent.`, {
|
|
|
933
1021
|
allDs.add(node._ds);
|
|
934
1022
|
node.children?.forEach(collectDs);
|
|
935
1023
|
}
|
|
936
|
-
|
|
1024
|
+
tNodes.forEach(collectDs);
|
|
937
1025
|
const unmatchedDs = [];
|
|
938
1026
|
for (const ds of allDs) {
|
|
939
1027
|
if (!matchedDs.has(ds) && !reviewDs.has(ds))
|
|
@@ -974,13 +1062,14 @@ Saves result to outputPath. Replaces ds-data-lookup sub-agent.`, {
|
|
|
974
1062
|
server.tool('run_pipeline', `Run the full spec pipeline: generate_mapping → validate → enrich → gate → tree in one call.
|
|
975
1063
|
Supports 2-pass pattern: Pass 1 returns reviewNeeded (ambiguous matches). Agent decides, then Pass 2 with resolutions auto-applies decisions.
|
|
976
1064
|
If no reviewNeeded, completes in 1 pass.`, {
|
|
977
|
-
traversals: z.array(z.record(z.unknown())).describe('Array of traversal JSON root nodes'),
|
|
1065
|
+
traversals: z.array(z.record(z.unknown())).optional().describe('Array of traversal JSON root nodes'),
|
|
1066
|
+
traversalPaths: z.array(z.string()).optional().describe('File paths to traversal JSON files (alternative to traversals — avoids large param)'),
|
|
978
1067
|
dataDir: z.string().describe('Path to blueprint data directory (e.g., .claude/blueprint/data/)'),
|
|
979
1068
|
outputPath: z.string().describe('File path to save spec.json (mapping.json saved alongside)'),
|
|
980
1069
|
resolutions: z.record(z.string()).optional().describe('Agent-decided mappings: { dsName: resolvedName }. Pass 2 only.'),
|
|
981
|
-
}, async ({ traversals, dataDir, outputPath, resolutions }) => {
|
|
1070
|
+
}, async ({ traversals, traversalPaths, dataDir, outputPath, resolutions }) => {
|
|
982
1071
|
try {
|
|
983
|
-
let tNodes = traversals;
|
|
1072
|
+
let tNodes = resolveTraversals(traversals, traversalPaths);
|
|
984
1073
|
// Step 0: Apply resolutions — replace _ds in traversal nodes
|
|
985
1074
|
if (resolutions && Object.keys(resolutions).length > 0) {
|
|
986
1075
|
// Deep clone to avoid mutating input
|
|
@@ -1070,8 +1159,12 @@ If no reviewNeeded, completes in 1 pass.`, {
|
|
|
1070
1159
|
// Step 3.5: Extract _repeat components
|
|
1071
1160
|
// Components are placed before pages so builder creates them first
|
|
1072
1161
|
const finalSpecs = [];
|
|
1162
|
+
const allComponents = [];
|
|
1163
|
+
const allPages = [];
|
|
1073
1164
|
for (const spec of specs) {
|
|
1074
1165
|
const { page, components } = extractRepeatComponents(spec);
|
|
1166
|
+
allComponents.push(...components);
|
|
1167
|
+
allPages.push(page);
|
|
1075
1168
|
finalSpecs.push(...components, page);
|
|
1076
1169
|
}
|
|
1077
1170
|
// Step 4: Gate check each spec
|
|
@@ -1084,13 +1177,30 @@ If no reviewNeeded, completes in 1 pass.`, {
|
|
|
1084
1177
|
mkdirSync(dirname(outputPath), { recursive: true });
|
|
1085
1178
|
const trees = [];
|
|
1086
1179
|
const allStats = [];
|
|
1180
|
+
// Also produce manifest + section files for incremental build
|
|
1181
|
+
const outputDir = dirname(outputPath);
|
|
1182
|
+
const manifests = [];
|
|
1183
|
+
if (allPages.length === 1) {
|
|
1184
|
+
const manifest = splitSections(allPages[0], allComponents, outputDir);
|
|
1185
|
+
manifests.push(manifest);
|
|
1186
|
+
}
|
|
1187
|
+
else {
|
|
1188
|
+
for (let i = 0; i < allPages.length; i++) {
|
|
1189
|
+
const pageDir = join(outputDir, `page_${i}`);
|
|
1190
|
+
mkdirSync(pageDir, { recursive: true });
|
|
1191
|
+
const manifest = splitSections(allPages[i], allComponents, pageDir);
|
|
1192
|
+
manifests.push(manifest);
|
|
1193
|
+
}
|
|
1194
|
+
// Save combined manifest at root level
|
|
1195
|
+
writeFileSync(join(outputDir, 'manifest.json'), JSON.stringify(manifests, null, 2));
|
|
1196
|
+
}
|
|
1197
|
+
// Save full specs (legacy format) alongside manifest
|
|
1087
1198
|
if (finalSpecs.length === 1) {
|
|
1088
1199
|
writeFileSync(outputPath, JSON.stringify(finalSpecs[0], null, 2));
|
|
1089
1200
|
trees.push(specToTree(finalSpecs[0]));
|
|
1090
1201
|
allStats.push(countSpecNodes(finalSpecs[0]));
|
|
1091
1202
|
}
|
|
1092
1203
|
else {
|
|
1093
|
-
// Multiple specs → save each with index
|
|
1094
1204
|
for (let i = 0; i < finalSpecs.length; i++) {
|
|
1095
1205
|
const specPath = outputPath.replace(/\.json$/, `_${i}.json`);
|
|
1096
1206
|
writeFileSync(specPath, JSON.stringify(finalSpecs[i], null, 2));
|
|
@@ -1118,7 +1228,9 @@ If no reviewNeeded, completes in 1 pass.`, {
|
|
|
1118
1228
|
text: JSON.stringify({
|
|
1119
1229
|
success: allGatesPass,
|
|
1120
1230
|
savedTo: outputPath,
|
|
1231
|
+
manifestSavedTo: join(outputDir, 'manifest.json'),
|
|
1121
1232
|
mappingSavedTo: mappingPath,
|
|
1233
|
+
manifest: manifests.length === 1 ? manifests[0] : manifests,
|
|
1122
1234
|
tree: trees.length === 1 ? trees[0] : trees,
|
|
1123
1235
|
stats: allStats.length === 1 ? allStats[0] : allStats,
|
|
1124
1236
|
gateResult: gateResults.length === 1 ? gateResults[0] : gateResults,
|
package/package.json
CHANGED