@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.
@@ -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:hiddenlayout.clip-content 변환 (6-A 필터링 전에 처리)
239
- if (node.style && node.style['overflow'] === 'hidden') {
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 result = generateMapping(traversals, dataDir);
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
- traversals.forEach(collectDs);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sangheepark/figma-ds-mcp",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "description": "MCP server for Code to Figma Bridge — bridges Claude Code to Figma plugin via WebSocket",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",