@zeroheight/mcp-server 2.1.0 → 2.1.1
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/CHANGELOG.md +4 -0
- package/README.md +12 -4
- package/dist/api/api.js +31 -6
- package/dist/api/api.js.map +1 -1
- package/dist/api/page.js +8 -11
- package/dist/api/page.js.map +1 -1
- package/dist/api/styleguide.js +6 -6
- package/dist/api/styleguide.js.map +1 -1
- package/dist/api/token.js +15 -0
- package/dist/api/token.js.map +1 -0
- package/dist/api/types/token.js +5 -0
- package/dist/api/types/token.js.map +1 -0
- package/dist/auth/introspection.js +36 -0
- package/dist/auth/introspection.js.map +1 -0
- package/dist/common/credentials.js +27 -9
- package/dist/common/credentials.js.map +1 -1
- package/dist/common/formatters/token.js +10 -0
- package/dist/common/formatters/token.js.map +1 -0
- package/dist/lint/color.js +43 -0
- package/dist/lint/color.js.map +1 -0
- package/dist/lint/dimension.js +90 -0
- package/dist/lint/dimension.js.map +1 -0
- package/dist/lint/index.js +122 -0
- package/dist/lint/index.js.map +1 -0
- package/dist/lint/parser.js +462 -0
- package/dist/lint/parser.js.map +1 -0
- package/dist/lint/tokens.js +33 -0
- package/dist/lint/tokens.js.map +1 -0
- package/dist/logging.js +129 -0
- package/dist/logging.js.map +1 -0
- package/dist/mcp-server.js +9 -4
- package/dist/mcp-server.js.map +1 -1
- package/dist/tools/lint.js +74 -0
- package/dist/tools/lint.js.map +1 -0
- package/dist/tools/page.js +56 -25
- package/dist/tools/page.js.map +1 -1
- package/dist/tools/styleguide.js +22 -11
- package/dist/tools/styleguide.js.map +1 -1
- package/dist/tools/token.js +73 -0
- package/dist/tools/token.js.map +1 -0
- package/dist/types/server.js +5 -0
- package/dist/types/server.js.map +1 -0
- package/package.json +10 -3
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
|
|
2
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="7bbccfe5-434b-51a1-b7c0-ec2d1cbea607")}catch(e){}}();
|
|
3
|
+
import { flattenTokensByType } from "./tokens.js";
|
|
4
|
+
/** Parse a dimension string (e.g., "16px", "1.5rem") into value and unit. */
|
|
5
|
+
export function parseDimension(value) {
|
|
6
|
+
if (typeof value !== "string")
|
|
7
|
+
return null;
|
|
8
|
+
const trimmed = value.trim();
|
|
9
|
+
// Match number (int or float, optional negative) followed by unit
|
|
10
|
+
const match = trimmed.match(/^(-?\d+(?:\.\d+)?)(px|rem|em|%|vw|vh|vmin|vmax|ch|ex|pt|pc|in|cm|mm)$/i);
|
|
11
|
+
if (!match)
|
|
12
|
+
return null;
|
|
13
|
+
return {
|
|
14
|
+
value: parseFloat(match[1]),
|
|
15
|
+
unit: match[2].toLowerCase(),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
const PROPERTY_TO_CATEGORY = {
|
|
19
|
+
// Spacing properties
|
|
20
|
+
padding: "spacing",
|
|
21
|
+
"padding-top": "spacing",
|
|
22
|
+
"padding-right": "spacing",
|
|
23
|
+
"padding-bottom": "spacing",
|
|
24
|
+
"padding-left": "spacing",
|
|
25
|
+
margin: "spacing",
|
|
26
|
+
"margin-top": "spacing",
|
|
27
|
+
"margin-right": "spacing",
|
|
28
|
+
"margin-bottom": "spacing",
|
|
29
|
+
"margin-left": "spacing",
|
|
30
|
+
gap: "spacing",
|
|
31
|
+
"row-gap": "spacing",
|
|
32
|
+
"column-gap": "spacing",
|
|
33
|
+
// Border radius properties
|
|
34
|
+
"border-radius": "borderRadius",
|
|
35
|
+
"border-top-left-radius": "borderRadius",
|
|
36
|
+
"border-top-right-radius": "borderRadius",
|
|
37
|
+
"border-bottom-left-radius": "borderRadius",
|
|
38
|
+
"border-bottom-right-radius": "borderRadius",
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Find the closest dimension token by unit and numeric value.
|
|
42
|
+
* When property is provided, prefers tokens in the semantic category (e.g., padding -> spacing).
|
|
43
|
+
*/
|
|
44
|
+
export function findClosestDimensionToken(dim, tokens, property) {
|
|
45
|
+
const parsed = parseDimension(dim);
|
|
46
|
+
if (!parsed)
|
|
47
|
+
return null;
|
|
48
|
+
const dimensionTokens = flattenTokensByType(tokens, "dimension");
|
|
49
|
+
const preferredCategory = property ? PROPERTY_TO_CATEGORY[property] : null;
|
|
50
|
+
let closest = null;
|
|
51
|
+
let closestDiff = Infinity;
|
|
52
|
+
let foundInCategory = false;
|
|
53
|
+
for (const [name, tokenValue] of dimensionTokens) {
|
|
54
|
+
const tokenParsed = parseDimension(tokenValue);
|
|
55
|
+
if (!tokenParsed)
|
|
56
|
+
continue;
|
|
57
|
+
// Only match same unit
|
|
58
|
+
if (tokenParsed.unit !== parsed.unit)
|
|
59
|
+
continue;
|
|
60
|
+
const diff = Math.abs(tokenParsed.value - parsed.value);
|
|
61
|
+
// If we have a preferred category, prioritize tokens in that category
|
|
62
|
+
if (preferredCategory) {
|
|
63
|
+
const isInCategory = name.startsWith(preferredCategory);
|
|
64
|
+
// If we haven't found a token in the preferred category yet,
|
|
65
|
+
// accept any match from that category
|
|
66
|
+
if (!foundInCategory && isInCategory) {
|
|
67
|
+
closest = { name, value: tokenValue };
|
|
68
|
+
closestDiff = diff;
|
|
69
|
+
foundInCategory = true;
|
|
70
|
+
}
|
|
71
|
+
// If we're in the preferred category, keep the closest match
|
|
72
|
+
else if (foundInCategory && isInCategory && diff < closestDiff) {
|
|
73
|
+
closestDiff = diff;
|
|
74
|
+
closest = { name, value: tokenValue };
|
|
75
|
+
}
|
|
76
|
+
// Skip tokens outside the preferred category if we've found one inside
|
|
77
|
+
else if (foundInCategory && !isInCategory) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// No property context or not in category yet - accept any closest match
|
|
82
|
+
if (!foundInCategory && diff < closestDiff) {
|
|
83
|
+
closestDiff = diff;
|
|
84
|
+
closest = { name, value: tokenValue };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return closest;
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=dimension.js.map
|
|
90
|
+
//# debugId=7bbccfe5-434b-51a1-b7c0-ec2d1cbea607
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dimension.js","sources":["lint/dimension.ts"],"sourceRoot":"/","sourcesContent":["import type { TokenGroup } from \"../api/types/token.js\";\nimport { flattenTokensByType } from \"./tokens.js\";\n\nexport interface ParsedDimension {\n value: number;\n unit: string;\n}\n\nexport interface DimensionTokenMatch {\n name: string;\n value: string;\n}\n\n/** Parse a dimension string (e.g., \"16px\", \"1.5rem\") into value and unit. */\nexport function parseDimension(value: string): ParsedDimension | null {\n if (typeof value !== \"string\") return null;\n\n const trimmed = value.trim();\n // Match number (int or float, optional negative) followed by unit\n const match = trimmed.match(\n /^(-?\\d+(?:\\.\\d+)?)(px|rem|em|%|vw|vh|vmin|vmax|ch|ex|pt|pc|in|cm|mm)$/i,\n );\n\n if (!match) return null;\n\n return {\n value: parseFloat(match[1]),\n unit: match[2].toLowerCase(),\n };\n}\n\nconst PROPERTY_TO_CATEGORY: Record<string, string> = {\n // Spacing properties\n padding: \"spacing\",\n \"padding-top\": \"spacing\",\n \"padding-right\": \"spacing\",\n \"padding-bottom\": \"spacing\",\n \"padding-left\": \"spacing\",\n margin: \"spacing\",\n \"margin-top\": \"spacing\",\n \"margin-right\": \"spacing\",\n \"margin-bottom\": \"spacing\",\n \"margin-left\": \"spacing\",\n gap: \"spacing\",\n \"row-gap\": \"spacing\",\n \"column-gap\": \"spacing\",\n // Border radius properties\n \"border-radius\": \"borderRadius\",\n \"border-top-left-radius\": \"borderRadius\",\n \"border-top-right-radius\": \"borderRadius\",\n \"border-bottom-left-radius\": \"borderRadius\",\n \"border-bottom-right-radius\": \"borderRadius\",\n};\n\n/**\n * Find the closest dimension token by unit and numeric value.\n * When property is provided, prefers tokens in the semantic category (e.g., padding -> spacing).\n */\nexport function findClosestDimensionToken(\n dim: string,\n tokens: TokenGroup,\n property?: string,\n): DimensionTokenMatch | null {\n const parsed = parseDimension(dim);\n if (!parsed) return null;\n\n const dimensionTokens = flattenTokensByType(tokens, \"dimension\");\n const preferredCategory = property ? PROPERTY_TO_CATEGORY[property] : null;\n\n let closest: DimensionTokenMatch | null = null;\n let closestDiff = Infinity;\n let foundInCategory = false;\n\n for (const [name, tokenValue] of dimensionTokens) {\n const tokenParsed = parseDimension(tokenValue);\n if (!tokenParsed) continue;\n\n // Only match same unit\n if (tokenParsed.unit !== parsed.unit) continue;\n\n const diff = Math.abs(tokenParsed.value - parsed.value);\n\n // If we have a preferred category, prioritize tokens in that category\n if (preferredCategory) {\n const isInCategory = name.startsWith(preferredCategory);\n\n // If we haven't found a token in the preferred category yet,\n // accept any match from that category\n if (!foundInCategory && isInCategory) {\n closest = { name, value: tokenValue };\n closestDiff = diff;\n foundInCategory = true;\n }\n // If we're in the preferred category, keep the closest match\n else if (foundInCategory && isInCategory && diff < closestDiff) {\n closestDiff = diff;\n closest = { name, value: tokenValue };\n }\n // Skip tokens outside the preferred category if we've found one inside\n else if (foundInCategory && !isInCategory) {\n continue;\n }\n }\n\n // No property context or not in category yet - accept any closest match\n if (!foundInCategory && diff < closestDiff) {\n closestDiff = diff;\n closest = { name, value: tokenValue };\n }\n }\n\n return closest;\n}\n"],"names":[],"mappings":"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","debug_id":"7bbccfe5-434b-51a1-b7c0-ec2d1cbea607"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
|
|
2
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="2994b90b-3bef-5ecc-ae24-88f8a636b349")}catch(e){}}();
|
|
3
|
+
import { findClosestColorToken } from "./color.js";
|
|
4
|
+
import { findClosestDimensionToken, parseDimension } from "./dimension.js";
|
|
5
|
+
import { parseCss, parseJs, parseJsx, parseScss, parseTs, parseTsx, } from "./parser.js";
|
|
6
|
+
/**
|
|
7
|
+
* Lint code against design tokens.
|
|
8
|
+
*
|
|
9
|
+
* @param code - Source code to lint.
|
|
10
|
+
* @param language - Language of the source code.
|
|
11
|
+
* @param tokens - Design tokens to validate against.
|
|
12
|
+
* @param threshold - Color distance threshold for suggestions (default: 10).
|
|
13
|
+
* @returns Array of violations found.
|
|
14
|
+
*/
|
|
15
|
+
export function lint(code, language, tokens, threshold = 10) {
|
|
16
|
+
const violations = [];
|
|
17
|
+
const parsed = (() => {
|
|
18
|
+
switch (language) {
|
|
19
|
+
case "tsx":
|
|
20
|
+
return parseTsx(code);
|
|
21
|
+
case "ts":
|
|
22
|
+
return parseTs(code);
|
|
23
|
+
case "jsx":
|
|
24
|
+
return parseJsx(code);
|
|
25
|
+
case "js":
|
|
26
|
+
return parseJs(code);
|
|
27
|
+
case "scss":
|
|
28
|
+
return parseScss(code);
|
|
29
|
+
case "css":
|
|
30
|
+
default:
|
|
31
|
+
return parseCss(code);
|
|
32
|
+
}
|
|
33
|
+
})();
|
|
34
|
+
for (const match of parsed.colors) {
|
|
35
|
+
const violation = checkColor(match, tokens, threshold);
|
|
36
|
+
if (violation) {
|
|
37
|
+
violations.push(violation);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
for (const match of parsed.dimensions) {
|
|
41
|
+
const violation = checkDimension(match, tokens);
|
|
42
|
+
if (violation) {
|
|
43
|
+
violations.push(violation);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return violations;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Check a color match against tokens and return a violation if not using a token.
|
|
50
|
+
*/
|
|
51
|
+
function checkColor(match, tokens, threshold) {
|
|
52
|
+
const closest = findClosestColorToken(match.value, tokens, threshold);
|
|
53
|
+
if (!closest) {
|
|
54
|
+
return {
|
|
55
|
+
line: match.line,
|
|
56
|
+
column: match.column,
|
|
57
|
+
endLine: match.endLine,
|
|
58
|
+
endColumn: match.endColumn,
|
|
59
|
+
value: match.value,
|
|
60
|
+
type: "color",
|
|
61
|
+
message: `Color '${match.value}' is not a design token.`,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const isExactMatch = closest.distance === 0;
|
|
65
|
+
const message = isExactMatch
|
|
66
|
+
? `Hardcoded color '${match.value}' should use token '${closest.name}'.`
|
|
67
|
+
: `Color '${match.value}' is not a design token. Did you mean '${closest.name}' (${closest.value})?`;
|
|
68
|
+
return {
|
|
69
|
+
line: match.line,
|
|
70
|
+
column: match.column,
|
|
71
|
+
endLine: match.endLine,
|
|
72
|
+
endColumn: match.endColumn,
|
|
73
|
+
value: match.value,
|
|
74
|
+
type: "color",
|
|
75
|
+
message,
|
|
76
|
+
suggestion: {
|
|
77
|
+
tokenName: closest.name,
|
|
78
|
+
tokenValue: closest.value,
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Check a dimension match against tokens and return a violation if not using a token.
|
|
84
|
+
*/
|
|
85
|
+
function checkDimension(match, tokens) {
|
|
86
|
+
const closest = findClosestDimensionToken(match.value, tokens);
|
|
87
|
+
if (!closest) {
|
|
88
|
+
return {
|
|
89
|
+
line: match.line,
|
|
90
|
+
column: match.column,
|
|
91
|
+
endLine: match.endLine,
|
|
92
|
+
endColumn: match.endColumn,
|
|
93
|
+
value: match.value,
|
|
94
|
+
type: "dimension",
|
|
95
|
+
message: `Dimension '${match.value}' is not a design token.`,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const parsed = parseDimension(match.value);
|
|
99
|
+
const tokenParsed = parseDimension(closest.value);
|
|
100
|
+
const isExactMatch = parsed &&
|
|
101
|
+
tokenParsed &&
|
|
102
|
+
parsed.value === tokenParsed.value &&
|
|
103
|
+
parsed.unit === tokenParsed.unit;
|
|
104
|
+
const message = isExactMatch
|
|
105
|
+
? `Hardcoded dimension '${match.value}' should use token '${closest.name}'.`
|
|
106
|
+
: `Dimension '${match.value}' is not a design token. Did you mean '${closest.name}' (${closest.value})?`;
|
|
107
|
+
return {
|
|
108
|
+
line: match.line,
|
|
109
|
+
column: match.column,
|
|
110
|
+
endLine: match.endLine,
|
|
111
|
+
endColumn: match.endColumn,
|
|
112
|
+
value: match.value,
|
|
113
|
+
type: "dimension",
|
|
114
|
+
message,
|
|
115
|
+
suggestion: {
|
|
116
|
+
tokenName: closest.name,
|
|
117
|
+
tokenValue: closest.value,
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=index.js.map
|
|
122
|
+
//# debugId=2994b90b-3bef-5ecc-ae24-88f8a636b349
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":["lint/index.ts"],"sourceRoot":"/","sourcesContent":["import type { TokenGroup } from \"../api/types/token.js\";\nimport { findClosestColorToken } from \"./color.js\";\nimport { findClosestDimensionToken, parseDimension } from \"./dimension.js\";\nimport {\n parseCss,\n parseJs,\n parseJsx,\n parseScss,\n parseTs,\n parseTsx,\n type Match,\n} from \"./parser.js\";\n\n/**\n * Represents a lint violation found in the code.\n */\nexport interface Violation {\n line: number;\n column: number;\n endLine: number;\n endColumn: number;\n value: string;\n type: \"color\" | \"dimension\";\n message: string;\n suggestion?: {\n tokenName: string;\n tokenValue: string;\n };\n}\n\nexport type Language = \"css\" | \"scss\" | \"tsx\" | \"ts\" | \"js\" | \"jsx\";\n\n/**\n * Lint code against design tokens.\n *\n * @param code - Source code to lint.\n * @param language - Language of the source code.\n * @param tokens - Design tokens to validate against.\n * @param threshold - Color distance threshold for suggestions (default: 10).\n * @returns Array of violations found.\n */\nexport function lint(\n code: string,\n language: Language,\n tokens: TokenGroup,\n threshold: number = 10,\n): Violation[] {\n const violations: Violation[] = [];\n\n const parsed = (() => {\n switch (language) {\n case \"tsx\":\n return parseTsx(code);\n case \"ts\":\n return parseTs(code);\n case \"jsx\":\n return parseJsx(code);\n case \"js\":\n return parseJs(code);\n case \"scss\":\n return parseScss(code);\n case \"css\":\n default:\n return parseCss(code);\n }\n })();\n\n for (const match of parsed.colors) {\n const violation = checkColor(match, tokens, threshold);\n if (violation) {\n violations.push(violation);\n }\n }\n\n for (const match of parsed.dimensions) {\n const violation = checkDimension(match, tokens);\n if (violation) {\n violations.push(violation);\n }\n }\n\n return violations;\n}\n\n/**\n * Check a color match against tokens and return a violation if not using a token.\n */\nfunction checkColor(\n match: Match,\n tokens: TokenGroup,\n threshold: number,\n): Violation | null {\n const closest = findClosestColorToken(match.value, tokens, threshold);\n\n if (!closest) {\n return {\n line: match.line,\n column: match.column,\n endLine: match.endLine,\n endColumn: match.endColumn,\n value: match.value,\n type: \"color\",\n message: `Color '${match.value}' is not a design token.`,\n };\n }\n\n const isExactMatch = closest.distance === 0;\n const message = isExactMatch\n ? `Hardcoded color '${match.value}' should use token '${closest.name}'.`\n : `Color '${match.value}' is not a design token. Did you mean '${closest.name}' (${closest.value})?`;\n\n return {\n line: match.line,\n column: match.column,\n endLine: match.endLine,\n endColumn: match.endColumn,\n value: match.value,\n type: \"color\",\n message,\n suggestion: {\n tokenName: closest.name,\n tokenValue: closest.value,\n },\n };\n}\n\n/**\n * Check a dimension match against tokens and return a violation if not using a token.\n */\nfunction checkDimension(match: Match, tokens: TokenGroup): Violation | null {\n const closest = findClosestDimensionToken(match.value, tokens);\n\n if (!closest) {\n return {\n line: match.line,\n column: match.column,\n endLine: match.endLine,\n endColumn: match.endColumn,\n value: match.value,\n type: \"dimension\",\n message: `Dimension '${match.value}' is not a design token.`,\n };\n }\n\n const parsed = parseDimension(match.value);\n const tokenParsed = parseDimension(closest.value);\n const isExactMatch =\n parsed &&\n tokenParsed &&\n parsed.value === tokenParsed.value &&\n parsed.unit === tokenParsed.unit;\n\n const message = isExactMatch\n ? `Hardcoded dimension '${match.value}' should use token '${closest.name}'.`\n : `Dimension '${match.value}' is not a design token. Did you mean '${closest.name}' (${closest.value})?`;\n\n return {\n line: match.line,\n column: match.column,\n endLine: match.endLine,\n endColumn: match.endColumn,\n value: match.value,\n type: \"dimension\",\n message,\n suggestion: {\n tokenName: closest.name,\n tokenValue: closest.value,\n },\n };\n}\n"],"names":[],"mappings":"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","debug_id":"2994b90b-3bef-5ecc-ae24-88f8a636b349"}
|