canicode 0.7.0 → 0.8.0
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/README.md +2 -0
- package/dist/cli/index.js +23 -9
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +16 -3
- package/dist/index.js +256 -5
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +264 -3
- package/dist/mcp/server.js.map +1 -1
- package/package.json +1 -1
package/dist/mcp/server.js
CHANGED
|
@@ -959,6 +959,211 @@ async function loadFromApi(fileKey, nodeId, token) {
|
|
|
959
959
|
};
|
|
960
960
|
}
|
|
961
961
|
|
|
962
|
+
// src/core/adapters/tailwind-parser.ts
|
|
963
|
+
var SPACING_SCALE = {
|
|
964
|
+
"0": 0,
|
|
965
|
+
"px": 1,
|
|
966
|
+
"0.5": 2,
|
|
967
|
+
"1": 4,
|
|
968
|
+
"1.5": 6,
|
|
969
|
+
"2": 8,
|
|
970
|
+
"2.5": 10,
|
|
971
|
+
"3": 12,
|
|
972
|
+
"3.5": 14,
|
|
973
|
+
"4": 16,
|
|
974
|
+
"5": 20,
|
|
975
|
+
"6": 24,
|
|
976
|
+
"7": 28,
|
|
977
|
+
"8": 32,
|
|
978
|
+
"9": 36,
|
|
979
|
+
"10": 40,
|
|
980
|
+
"11": 44,
|
|
981
|
+
"12": 48,
|
|
982
|
+
"14": 56,
|
|
983
|
+
"16": 64,
|
|
984
|
+
"20": 80,
|
|
985
|
+
"24": 96,
|
|
986
|
+
"28": 112,
|
|
987
|
+
"32": 128,
|
|
988
|
+
"36": 144,
|
|
989
|
+
"40": 160,
|
|
990
|
+
"44": 176,
|
|
991
|
+
"48": 192,
|
|
992
|
+
"52": 208,
|
|
993
|
+
"56": 224,
|
|
994
|
+
"60": 240,
|
|
995
|
+
"64": 256,
|
|
996
|
+
"72": 288,
|
|
997
|
+
"80": 320,
|
|
998
|
+
"96": 384
|
|
999
|
+
};
|
|
1000
|
+
function resolveSpacing(value) {
|
|
1001
|
+
const arbMatch = value.match(/^\[(\d+(?:\.\d+)?)px\]$/);
|
|
1002
|
+
if (arbMatch?.[1]) return parseFloat(arbMatch[1]);
|
|
1003
|
+
return SPACING_SCALE[value];
|
|
1004
|
+
}
|
|
1005
|
+
function extractStylesFromClasses(classes) {
|
|
1006
|
+
const styles = {};
|
|
1007
|
+
const tokens = classes.split(/\s+/);
|
|
1008
|
+
for (const token of tokens) {
|
|
1009
|
+
if (token === "flex" || token === "flex-row") {
|
|
1010
|
+
styles.layoutMode = "HORIZONTAL";
|
|
1011
|
+
} else if (token === "flex-col") {
|
|
1012
|
+
styles.layoutMode = "VERTICAL";
|
|
1013
|
+
} else if (token === "absolute") {
|
|
1014
|
+
styles.layoutPositioning = "ABSOLUTE";
|
|
1015
|
+
} else if (token === "relative" || token === "static") {
|
|
1016
|
+
styles.layoutPositioning = "AUTO";
|
|
1017
|
+
} else if (token === "w-full") {
|
|
1018
|
+
styles.layoutSizingHorizontal = "FILL";
|
|
1019
|
+
} else if (token === "h-full") {
|
|
1020
|
+
styles.layoutSizingVertical = "FILL";
|
|
1021
|
+
} else if (token === "w-fit") {
|
|
1022
|
+
styles.layoutSizingHorizontal = "HUG";
|
|
1023
|
+
} else if (token === "h-fit") {
|
|
1024
|
+
styles.layoutSizingVertical = "HUG";
|
|
1025
|
+
} else if (token.match(/^w-\[.+\]$/) || token.match(/^w-\d/)) {
|
|
1026
|
+
styles.layoutSizingHorizontal = "FIXED";
|
|
1027
|
+
} else if (token.match(/^h-\[.+\]$/) || token.match(/^h-\d/)) {
|
|
1028
|
+
styles.layoutSizingVertical = "FIXED";
|
|
1029
|
+
} else if (token.startsWith("gap-")) {
|
|
1030
|
+
const val = token.slice(4);
|
|
1031
|
+
const px = resolveSpacing(val);
|
|
1032
|
+
if (px !== void 0) styles.itemSpacing = px;
|
|
1033
|
+
} else if (token.startsWith("p-") && !token.startsWith("pl-") && !token.startsWith("pr-") && !token.startsWith("pt-") && !token.startsWith("pb-") && !token.startsWith("px-") && !token.startsWith("py-")) {
|
|
1034
|
+
const val = token.slice(2);
|
|
1035
|
+
const px = resolveSpacing(val);
|
|
1036
|
+
if (px !== void 0) {
|
|
1037
|
+
styles.paddingLeft = px;
|
|
1038
|
+
styles.paddingRight = px;
|
|
1039
|
+
styles.paddingTop = px;
|
|
1040
|
+
styles.paddingBottom = px;
|
|
1041
|
+
}
|
|
1042
|
+
} else if (token.startsWith("px-")) {
|
|
1043
|
+
const px = resolveSpacing(token.slice(3));
|
|
1044
|
+
if (px !== void 0) {
|
|
1045
|
+
styles.paddingLeft = px;
|
|
1046
|
+
styles.paddingRight = px;
|
|
1047
|
+
}
|
|
1048
|
+
} else if (token.startsWith("py-")) {
|
|
1049
|
+
const px = resolveSpacing(token.slice(3));
|
|
1050
|
+
if (px !== void 0) {
|
|
1051
|
+
styles.paddingTop = px;
|
|
1052
|
+
styles.paddingBottom = px;
|
|
1053
|
+
}
|
|
1054
|
+
} else if (token.startsWith("pl-")) {
|
|
1055
|
+
const px = resolveSpacing(token.slice(3));
|
|
1056
|
+
if (px !== void 0) styles.paddingLeft = px;
|
|
1057
|
+
} else if (token.startsWith("pr-")) {
|
|
1058
|
+
const px = resolveSpacing(token.slice(3));
|
|
1059
|
+
if (px !== void 0) styles.paddingRight = px;
|
|
1060
|
+
} else if (token.startsWith("pt-")) {
|
|
1061
|
+
const px = resolveSpacing(token.slice(3));
|
|
1062
|
+
if (px !== void 0) styles.paddingTop = px;
|
|
1063
|
+
} else if (token.startsWith("pb-")) {
|
|
1064
|
+
const px = resolveSpacing(token.slice(3));
|
|
1065
|
+
if (px !== void 0) styles.paddingBottom = px;
|
|
1066
|
+
} else if (token.startsWith("bg-[")) {
|
|
1067
|
+
const colorMatch = token.match(/^bg-\[(.+)\]$/);
|
|
1068
|
+
if (colorMatch?.[1]) {
|
|
1069
|
+
const color = colorMatch[1];
|
|
1070
|
+
if (!styles.fills) styles.fills = [];
|
|
1071
|
+
if (color.startsWith("var(")) {
|
|
1072
|
+
styles.fills.push({ type: "SOLID", color: {}, boundVariable: color });
|
|
1073
|
+
} else {
|
|
1074
|
+
styles.fills.push({ type: "SOLID", color: parseHexColor(color) });
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
} else if (token === "shadow" || token.startsWith("shadow-")) {
|
|
1078
|
+
if (!styles.effects) styles.effects = [];
|
|
1079
|
+
styles.effects.push({ type: "DROP_SHADOW" });
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
return styles;
|
|
1083
|
+
}
|
|
1084
|
+
function parseHexColor(hex) {
|
|
1085
|
+
const clean = hex.replace("#", "");
|
|
1086
|
+
if (clean.length === 6 || clean.length === 8) {
|
|
1087
|
+
return {
|
|
1088
|
+
r: parseInt(clean.slice(0, 2), 16) / 255,
|
|
1089
|
+
g: parseInt(clean.slice(2, 4), 16) / 255,
|
|
1090
|
+
b: parseInt(clean.slice(4, 6), 16) / 255,
|
|
1091
|
+
a: clean.length === 8 ? parseInt(clean.slice(6, 8), 16) / 255 : 1
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
return { r: 0, g: 0, b: 0, a: 1 };
|
|
1095
|
+
}
|
|
1096
|
+
function extractRootClasses(code) {
|
|
1097
|
+
const match = code.match(/className="([^"]+)"/);
|
|
1098
|
+
return match?.[1] ?? "";
|
|
1099
|
+
}
|
|
1100
|
+
function extractAllClasses(code) {
|
|
1101
|
+
const classes = [];
|
|
1102
|
+
const regex = /className="([^"]+)"/g;
|
|
1103
|
+
let match;
|
|
1104
|
+
while ((match = regex.exec(code)) !== null) {
|
|
1105
|
+
if (match[1]) classes.push(match[1]);
|
|
1106
|
+
}
|
|
1107
|
+
return classes;
|
|
1108
|
+
}
|
|
1109
|
+
function parseDesignContextCode(code) {
|
|
1110
|
+
const rootClasses = extractRootClasses(code);
|
|
1111
|
+
const rootStyles = extractStylesFromClasses(rootClasses);
|
|
1112
|
+
const allClasses = extractAllClasses(code);
|
|
1113
|
+
const aggregatedFills = [...rootStyles.fills ?? []];
|
|
1114
|
+
const aggregatedEffects = [...rootStyles.effects ?? []];
|
|
1115
|
+
for (const cls of allClasses) {
|
|
1116
|
+
const styles = extractStylesFromClasses(cls);
|
|
1117
|
+
if (styles.fills) {
|
|
1118
|
+
for (const fill of styles.fills) aggregatedFills.push(fill);
|
|
1119
|
+
}
|
|
1120
|
+
if (styles.effects) {
|
|
1121
|
+
for (const effect of styles.effects) aggregatedEffects.push(effect);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
return {
|
|
1125
|
+
...rootStyles,
|
|
1126
|
+
...aggregatedFills.length > 0 ? { fills: aggregatedFills } : {},
|
|
1127
|
+
...aggregatedEffects.length > 0 ? { effects: aggregatedEffects } : {}
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
function parseCodeHeader(code) {
|
|
1131
|
+
const headerMatch = code.match(/\/\*\s*(.+?)\s*\*\//);
|
|
1132
|
+
if (!headerMatch?.[1]) return {};
|
|
1133
|
+
const header = headerMatch[1];
|
|
1134
|
+
const result = {};
|
|
1135
|
+
const nameParts = header.split(" \u2014 ");
|
|
1136
|
+
if (nameParts[0]) result.name = nameParts[0].trim();
|
|
1137
|
+
const dimMatch = header.match(/(\d+)x(\d+)/);
|
|
1138
|
+
if (dimMatch?.[1] && dimMatch[2]) {
|
|
1139
|
+
result.width = parseInt(dimMatch[1]);
|
|
1140
|
+
result.height = parseInt(dimMatch[2]);
|
|
1141
|
+
}
|
|
1142
|
+
const typeMatch = header.match(/\b(COMPONENT|FRAME|INSTANCE|GROUP|SECTION)\b/);
|
|
1143
|
+
if (typeMatch?.[1]) result.type = typeMatch[1];
|
|
1144
|
+
if (header.includes("no auto-layout")) {
|
|
1145
|
+
result.hasAutoLayout = false;
|
|
1146
|
+
} else if (header.includes("auto-layout")) {
|
|
1147
|
+
result.hasAutoLayout = true;
|
|
1148
|
+
if (header.includes("vertical auto-layout")) result.layoutDirection = "VERTICAL";
|
|
1149
|
+
else if (header.includes("horizontal auto-layout")) result.layoutDirection = "HORIZONTAL";
|
|
1150
|
+
}
|
|
1151
|
+
return result;
|
|
1152
|
+
}
|
|
1153
|
+
function enrichNodeWithStyles(node, styles) {
|
|
1154
|
+
if (styles.layoutMode && !node.layoutMode) node.layoutMode = styles.layoutMode;
|
|
1155
|
+
if (styles.layoutPositioning && !node.layoutPositioning) node.layoutPositioning = styles.layoutPositioning;
|
|
1156
|
+
if (styles.layoutSizingHorizontal && !node.layoutSizingHorizontal) node.layoutSizingHorizontal = styles.layoutSizingHorizontal;
|
|
1157
|
+
if (styles.layoutSizingVertical && !node.layoutSizingVertical) node.layoutSizingVertical = styles.layoutSizingVertical;
|
|
1158
|
+
if (styles.itemSpacing !== void 0 && node.itemSpacing === void 0) node.itemSpacing = styles.itemSpacing;
|
|
1159
|
+
if (styles.paddingLeft !== void 0 && node.paddingLeft === void 0) node.paddingLeft = styles.paddingLeft;
|
|
1160
|
+
if (styles.paddingRight !== void 0 && node.paddingRight === void 0) node.paddingRight = styles.paddingRight;
|
|
1161
|
+
if (styles.paddingTop !== void 0 && node.paddingTop === void 0) node.paddingTop = styles.paddingTop;
|
|
1162
|
+
if (styles.paddingBottom !== void 0 && node.paddingBottom === void 0) node.paddingBottom = styles.paddingBottom;
|
|
1163
|
+
if (styles.fills && !node.fills) node.fills = styles.fills;
|
|
1164
|
+
if (styles.effects && !node.effects) node.effects = styles.effects;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
962
1167
|
// src/core/adapters/figma-mcp-adapter.ts
|
|
963
1168
|
var TAG_TYPE_MAP = {
|
|
964
1169
|
canvas: "CANVAS",
|
|
@@ -1089,6 +1294,54 @@ function parseMcpMetadataXml(xml, fileKey, fileName) {
|
|
|
1089
1294
|
styles: {}
|
|
1090
1295
|
};
|
|
1091
1296
|
}
|
|
1297
|
+
function enrichWithDesignContext(file, designContextCode, targetNodeId) {
|
|
1298
|
+
const header = parseCodeHeader(designContextCode);
|
|
1299
|
+
const styles = parseDesignContextCode(designContextCode);
|
|
1300
|
+
if (header.hasAutoLayout === true && header.layoutDirection) {
|
|
1301
|
+
if (!styles.layoutMode) styles.layoutMode = header.layoutDirection;
|
|
1302
|
+
} else if (header.hasAutoLayout === false) {
|
|
1303
|
+
delete styles.layoutMode;
|
|
1304
|
+
}
|
|
1305
|
+
if (targetNodeId) {
|
|
1306
|
+
const node = findNodeById2(file.document, targetNodeId);
|
|
1307
|
+
if (node) {
|
|
1308
|
+
enrichNodeWithStyles(node, styles);
|
|
1309
|
+
enrichChildrenFromCode(node, designContextCode);
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
enrichNodeWithStyles(file.document, styles);
|
|
1314
|
+
enrichChildrenFromCode(file.document, designContextCode);
|
|
1315
|
+
}
|
|
1316
|
+
function enrichChildrenFromCode(parent, code) {
|
|
1317
|
+
if (!parent.children) return;
|
|
1318
|
+
for (const child of parent.children) {
|
|
1319
|
+
const escapedName = child.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1320
|
+
const commentPattern = new RegExp(
|
|
1321
|
+
`\\{/\\*\\s*${escapedName}(?:\\s*\u2014[^*]*)?\\s*\\*/\\}\\s*\\n\\s*<\\w+[^>]*className="([^"]*)"`
|
|
1322
|
+
);
|
|
1323
|
+
const match = code.match(commentPattern);
|
|
1324
|
+
if (match?.[1]) {
|
|
1325
|
+
const childStyles = parseDesignContextCode(
|
|
1326
|
+
`<div className="${match[1]}">`
|
|
1327
|
+
);
|
|
1328
|
+
enrichNodeWithStyles(child, childStyles);
|
|
1329
|
+
}
|
|
1330
|
+
if (child.children) {
|
|
1331
|
+
enrichChildrenFromCode(child, code);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
function findNodeById2(node, id) {
|
|
1336
|
+
if (node.id === id) return node;
|
|
1337
|
+
if (node.children) {
|
|
1338
|
+
for (const child of node.children) {
|
|
1339
|
+
const found = findNodeById2(child, id);
|
|
1340
|
+
if (found) return found;
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
return void 0;
|
|
1344
|
+
}
|
|
1092
1345
|
|
|
1093
1346
|
// src/core/engine/design-data-parser.ts
|
|
1094
1347
|
function parseDesignData(data, fileKey, fileName) {
|
|
@@ -3127,16 +3380,21 @@ Two ways to provide design data:
|
|
|
3127
3380
|
|
|
3128
3381
|
Typical flow with Figma MCP (recommended, no token needed):
|
|
3129
3382
|
Step 1: Call the official Figma MCP's get_metadata tool to get the node tree
|
|
3130
|
-
Step 2:
|
|
3383
|
+
Step 2: Call the official Figma MCP's get_design_context tool on the same node to get style data
|
|
3384
|
+
Step 3: Pass get_metadata result as designData and get_design_context code as designContext to this tool
|
|
3385
|
+
|
|
3386
|
+
The designContext parameter enriches analysis with style information (colors, layout, spacing, effects)
|
|
3387
|
+
that get_metadata alone cannot provide. Without it, token and layout rules may not fire.
|
|
3131
3388
|
|
|
3132
3389
|
IMPORTANT \u2014 Before calling this tool, check which data source is available:
|
|
3133
|
-
- If the official Figma MCP (https://mcp.figma.com/mcp) is connected: use get_metadata \u2192 designData flow. No token needed.
|
|
3390
|
+
- If the official Figma MCP (https://mcp.figma.com/mcp) is connected: use get_metadata + get_design_context \u2192 designData + designContext flow. No token needed.
|
|
3134
3391
|
- If Figma MCP is NOT connected: use the input parameter with a Figma URL. This requires a FIGMA_TOKEN.
|
|
3135
3392
|
Tell the user: "The official Figma MCP server is not connected. To use without a token, set it up:
|
|
3136
3393
|
claude mcp add -s project -t http figma https://mcp.figma.com/mcp
|
|
3137
3394
|
Otherwise, provide a Figma API token via FIGMA_TOKEN env var or the token parameter."`,
|
|
3138
3395
|
{
|
|
3139
3396
|
designData: z.string().optional().describe("Figma node data from Figma MCP get_metadata (XML or JSON). Pass this instead of input when using Figma MCP."),
|
|
3397
|
+
designContext: z.string().optional().describe("Code output from Figma MCP get_design_context. Enriches designData with style info (colors, layout, spacing, effects). Highly recommended alongside designData."),
|
|
3140
3398
|
input: z.string().optional().describe("Figma URL. Used when designData is not provided. Requires FIGMA_TOKEN."),
|
|
3141
3399
|
fileKey: z.string().optional().describe("Figma file key (used with designData to generate deep links)"),
|
|
3142
3400
|
fileName: z.string().optional().describe("Figma file name (used with designData for display)"),
|
|
@@ -3152,13 +3410,16 @@ IMPORTANT \u2014 Before calling this tool, check which data source is available:
|
|
|
3152
3410
|
openWorldHint: true,
|
|
3153
3411
|
title: "Analyze Figma Design"
|
|
3154
3412
|
},
|
|
3155
|
-
async ({ designData, input, fileKey, fileName, token, preset, targetNodeId, configPath, customRulesPath }) => {
|
|
3413
|
+
async ({ designData, designContext, input, fileKey, fileName, token, preset, targetNodeId, configPath, customRulesPath }) => {
|
|
3156
3414
|
trackEvent(EVENTS.MCP_TOOL_CALLED, { tool: "analyze" });
|
|
3157
3415
|
try {
|
|
3158
3416
|
let file;
|
|
3159
3417
|
let nodeId;
|
|
3160
3418
|
if (designData) {
|
|
3161
3419
|
file = parseDesignData(designData, fileKey ?? "unknown", fileName);
|
|
3420
|
+
if (designContext) {
|
|
3421
|
+
enrichWithDesignContext(file, designContext, targetNodeId);
|
|
3422
|
+
}
|
|
3162
3423
|
} else if (input) {
|
|
3163
3424
|
const loaded = await loadFile(input, token);
|
|
3164
3425
|
file = loaded.file;
|