@sangheepark/figma-ds-mcp 0.2.6 → 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.
@@ -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:hidden → layout.clip-content 변환 (6-A 필터링 전에 처리)
239
- if (node.style && node.style['overflow'] === 'hidden') {
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'];
@@ -541,6 +546,53 @@ function extractRepeatComponents(spec, defaultCount = 3) {
541
546
  walk(page);
542
547
  return { page, components: Array.from(extracted.values()) };
543
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
+ }
544
596
  // fontWeight name → CSS number (reverse of display names in data files)
545
597
  const FONT_WEIGHT_NAME_TO_NUM = {
546
598
  thin: '100', hairline: '100',
@@ -912,16 +964,28 @@ Run after validate_traversal. If outputPath is provided, spec is saved to file a
912
964
  }],
913
965
  };
914
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
+ }
915
977
  // generate_mapping — D5
916
978
  server.tool('generate_mapping', `Generate mapping.json from traversal JSON + blueprint data files. Zero LLM involvement.
917
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).
918
980
  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'),
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)'),
920
983
  dataDir: z.string().describe('Path to blueprint data directory (e.g., .claude/blueprint/data/)'),
921
984
  outputPath: z.string().describe('File path to save mapping.json'),
922
- }, async ({ traversals, dataDir, outputPath }) => {
985
+ }, async ({ traversals, traversalPaths, dataDir, outputPath }) => {
923
986
  try {
924
- const result = generateMapping(traversals, dataDir);
987
+ const tNodes = resolveTraversals(traversals, traversalPaths);
988
+ const result = generateMapping(tNodes, dataDir);
925
989
  mkdirSync(dirname(outputPath), { recursive: true });
926
990
  writeFileSync(outputPath, JSON.stringify(result, null, 2));
927
991
  // Summary for context (not the full mapping)
@@ -933,7 +997,7 @@ Saves result to outputPath. Replaces ds-data-lookup sub-agent.`, {
933
997
  allDs.add(node._ds);
934
998
  node.children?.forEach(collectDs);
935
999
  }
936
- traversals.forEach(collectDs);
1000
+ tNodes.forEach(collectDs);
937
1001
  const unmatchedDs = [];
938
1002
  for (const ds of allDs) {
939
1003
  if (!matchedDs.has(ds) && !reviewDs.has(ds))
@@ -974,13 +1038,14 @@ Saves result to outputPath. Replaces ds-data-lookup sub-agent.`, {
974
1038
  server.tool('run_pipeline', `Run the full spec pipeline: generate_mapping → validate → enrich → gate → tree in one call.
975
1039
  Supports 2-pass pattern: Pass 1 returns reviewNeeded (ambiguous matches). Agent decides, then Pass 2 with resolutions auto-applies decisions.
976
1040
  If no reviewNeeded, completes in 1 pass.`, {
977
- 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)'),
978
1043
  dataDir: z.string().describe('Path to blueprint data directory (e.g., .claude/blueprint/data/)'),
979
1044
  outputPath: z.string().describe('File path to save spec.json (mapping.json saved alongside)'),
980
1045
  resolutions: z.record(z.string()).optional().describe('Agent-decided mappings: { dsName: resolvedName }. Pass 2 only.'),
981
- }, async ({ traversals, dataDir, outputPath, resolutions }) => {
1046
+ }, async ({ traversals, traversalPaths, dataDir, outputPath, resolutions }) => {
982
1047
  try {
983
- let tNodes = traversals;
1048
+ let tNodes = resolveTraversals(traversals, traversalPaths);
984
1049
  // Step 0: Apply resolutions — replace _ds in traversal nodes
985
1050
  if (resolutions && Object.keys(resolutions).length > 0) {
986
1051
  // Deep clone to avoid mutating input
@@ -1070,8 +1135,12 @@ If no reviewNeeded, completes in 1 pass.`, {
1070
1135
  // Step 3.5: Extract _repeat components
1071
1136
  // Components are placed before pages so builder creates them first
1072
1137
  const finalSpecs = [];
1138
+ const allComponents = [];
1139
+ const allPages = [];
1073
1140
  for (const spec of specs) {
1074
1141
  const { page, components } = extractRepeatComponents(spec);
1142
+ allComponents.push(...components);
1143
+ allPages.push(page);
1075
1144
  finalSpecs.push(...components, page);
1076
1145
  }
1077
1146
  // Step 4: Gate check each spec
@@ -1084,13 +1153,20 @@ If no reviewNeeded, completes in 1 pass.`, {
1084
1153
  mkdirSync(dirname(outputPath), { recursive: true });
1085
1154
  const trees = [];
1086
1155
  const allStats = [];
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
1087
1164
  if (finalSpecs.length === 1) {
1088
1165
  writeFileSync(outputPath, JSON.stringify(finalSpecs[0], null, 2));
1089
1166
  trees.push(specToTree(finalSpecs[0]));
1090
1167
  allStats.push(countSpecNodes(finalSpecs[0]));
1091
1168
  }
1092
1169
  else {
1093
- // Multiple specs → save each with index
1094
1170
  for (let i = 0; i < finalSpecs.length; i++) {
1095
1171
  const specPath = outputPath.replace(/\.json$/, `_${i}.json`);
1096
1172
  writeFileSync(specPath, JSON.stringify(finalSpecs[i], null, 2));
@@ -1118,7 +1194,9 @@ If no reviewNeeded, completes in 1 pass.`, {
1118
1194
  text: JSON.stringify({
1119
1195
  success: allGatesPass,
1120
1196
  savedTo: outputPath,
1197
+ manifestSavedTo: join(outputDir, 'manifest.json'),
1121
1198
  mappingSavedTo: mappingPath,
1199
+ manifest: manifests.length === 1 ? manifests[0] : manifests,
1122
1200
  tree: trees.length === 1 ? trees[0] : trees,
1123
1201
  stats: allStats.length === 1 ? allStats[0] : allStats,
1124
1202
  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.7",
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",