@ncds/ui-admin-mcp 1.0.0-alpha.10

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.
Files changed (94) hide show
  1. package/README.md +113 -0
  2. package/bin/components.bundle.js +1 -0
  3. package/bin/definitions/compliance-rules.json +64 -0
  4. package/bin/definitions/instructions.md +164 -0
  5. package/bin/definitions/js-api.json +165 -0
  6. package/bin/definitions/rules.json +59 -0
  7. package/bin/definitions/token-descriptions.json +27 -0
  8. package/bin/definitions/tool-definitions.json +58 -0
  9. package/bin/server.d.ts +1 -0
  10. package/bin/server.js +217 -0
  11. package/bin/server.mjs +8 -0
  12. package/bin/tools/getComponentProps.d.ts +4 -0
  13. package/bin/tools/getComponentProps.js +20 -0
  14. package/bin/tools/getDesignTokens.d.ts +13 -0
  15. package/bin/tools/getDesignTokens.js +20 -0
  16. package/bin/tools/listComponents.d.ts +16 -0
  17. package/bin/tools/listComponents.js +24 -0
  18. package/bin/tools/listIcons.d.ts +22 -0
  19. package/bin/tools/listIcons.js +23 -0
  20. package/bin/tools/ping.d.ts +17 -0
  21. package/bin/tools/ping.js +20 -0
  22. package/bin/tools/renderToHtml.d.ts +23 -0
  23. package/bin/tools/renderToHtml.js +267 -0
  24. package/bin/tools/searchComponent.d.ts +4 -0
  25. package/bin/tools/searchComponent.js +33 -0
  26. package/bin/tools/searchIcon.d.ts +7 -0
  27. package/bin/tools/searchIcon.js +19 -0
  28. package/bin/tools/validateHtml.d.ts +18 -0
  29. package/bin/tools/validateHtml.js +85 -0
  30. package/bin/types.d.ts +123 -0
  31. package/bin/types.js +5 -0
  32. package/bin/utils/bemValidator.d.ts +36 -0
  33. package/bin/utils/bemValidator.js +198 -0
  34. package/bin/utils/compliance.d.ts +52 -0
  35. package/bin/utils/compliance.js +199 -0
  36. package/bin/utils/dataLoader.d.ts +35 -0
  37. package/bin/utils/dataLoader.js +192 -0
  38. package/bin/utils/domEnvironment.d.ts +9 -0
  39. package/bin/utils/domEnvironment.js +75 -0
  40. package/bin/utils/fuzzyMatch.d.ts +21 -0
  41. package/bin/utils/fuzzyMatch.js +110 -0
  42. package/bin/utils/logger.d.ts +18 -0
  43. package/bin/utils/logger.js +27 -0
  44. package/bin/utils/response.d.ts +28 -0
  45. package/bin/utils/response.js +39 -0
  46. package/bin/utils/tokenValidator.d.ts +24 -0
  47. package/bin/utils/tokenValidator.js +162 -0
  48. package/bin/version.d.ts +4 -0
  49. package/bin/version.js +7 -0
  50. package/data/_icons.json +12401 -0
  51. package/data/_meta.json +12 -0
  52. package/data/_tokens.json +661 -0
  53. package/data/badge-group.json +295 -0
  54. package/data/badge.json +246 -0
  55. package/data/bread-crumb.json +87 -0
  56. package/data/button-group.json +94 -0
  57. package/data/button.json +259 -0
  58. package/data/carousel-arrow.json +87 -0
  59. package/data/carousel-number-group.json +87 -0
  60. package/data/checkbox.json +99 -0
  61. package/data/combo-box.json +173 -0
  62. package/data/date-picker.json +123 -0
  63. package/data/divider.json +91 -0
  64. package/data/dot.json +103 -0
  65. package/data/dropdown.json +258 -0
  66. package/data/empty-state.json +227 -0
  67. package/data/featured-icon.json +139 -0
  68. package/data/file-input.json +315 -0
  69. package/data/horizontal-tab.json +329 -0
  70. package/data/image-file-input.json +339 -0
  71. package/data/input-base.json +299 -0
  72. package/data/modal.json +143 -0
  73. package/data/notification.json +194 -0
  74. package/data/number-input.json +295 -0
  75. package/data/pagination.json +101 -0
  76. package/data/password-input.json +263 -0
  77. package/data/progress-bar.json +109 -0
  78. package/data/progress-circle.json +96 -0
  79. package/data/radio.json +86 -0
  80. package/data/range-date-picker-with-buttons.json +273 -0
  81. package/data/range-date-picker.json +259 -0
  82. package/data/select-box.json +193 -0
  83. package/data/select.json +132 -0
  84. package/data/slider.json +100 -0
  85. package/data/spinner.json +93 -0
  86. package/data/switch.json +156 -0
  87. package/data/tag.json +159 -0
  88. package/data/textarea.json +96 -0
  89. package/data/toggle.json +102 -0
  90. package/data/tooltip.json +185 -0
  91. package/data/vertical-tab.json +314 -0
  92. package/package.json +61 -0
  93. package/templates/.mcp.json.example +8 -0
  94. package/templates/README.md +30 -0
@@ -0,0 +1,9 @@
1
+ /**
2
+ * DOM + React 런타임 초기화 — jsdom 환경 + createPortal mock + React SSR
3
+ *
4
+ * Action (부수효과): globalThis를 변조한다. server.ts 또는 테스트 beforeAll에서 1회 호출.
5
+ * 이 파일로 추출하여 server.ts/테스트 간 코드 중복을 제거한다 (§ 3.9).
6
+ */
7
+ import type { ReactRuntime } from '../types.js';
8
+ /** jsdom + React SSR 환경을 초기화하고 ReactRuntime을 반환 */
9
+ export declare const setupDomEnvironment: () => ReactRuntime;
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.setupDomEnvironment = void 0;
4
+ /**
5
+ * monorepo에서 패키지가 여러 node_modules에 설치될 수 있음.
6
+ * 번들의 external require가 MCP 로컬 node_modules의 버전을 사용하도록 통일한다.
7
+ *
8
+ * 문제: esbuild 번들은 ui-admin cwd에서 빌드되므로, external require가
9
+ * 루트 node_modules(오래된 버전)를 resolve할 수 있다.
10
+ * 해결: MCP 패키지 기준 require.resolve로 경로를 고정한다.
11
+ */
12
+ const unifyModuleResolution = () => {
13
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
14
+ const Module = require('module');
15
+ const overrides = {};
16
+ const pin = (name) => {
17
+ try {
18
+ overrides[name] = require.resolve(name);
19
+ }
20
+ catch {
21
+ /* 미설치 패키지는 무시 */
22
+ }
23
+ };
24
+ // React 인스턴스 통일 (Hook dispatcher 일치 보장)
25
+ pin('react');
26
+ pin('react-dom');
27
+ pin('react/jsx-runtime');
28
+ // external 패키지 버전 통일 (monorepo hoisting 불일치 방지)
29
+ pin('@ncds/ui-admin-icon');
30
+ pin('swiper/react');
31
+ pin('swiper');
32
+ const origResolve = Module._resolveFilename;
33
+ Module._resolveFilename = function (request, parent, isMain, options) {
34
+ if (request in overrides)
35
+ return overrides[request];
36
+ return origResolve.call(this, request, parent, isMain, options);
37
+ };
38
+ };
39
+ /** jsdom + React SSR 환경을 초기화하고 ReactRuntime을 반환 */
40
+ const setupDomEnvironment = () => {
41
+ unifyModuleResolution();
42
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
43
+ const { JSDOM } = require('jsdom');
44
+ const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>');
45
+ if (typeof globalThis.document === 'undefined')
46
+ globalThis.document = dom.window.document;
47
+ if (typeof globalThis.window === 'undefined')
48
+ globalThis.window = dom.window;
49
+ if (typeof globalThis.navigator === 'undefined') {
50
+ Object.defineProperty(globalThis, 'navigator', { value: dom.window.navigator, writable: true });
51
+ }
52
+ // Swiper 등 외부 라이브러리가 HTMLElement를 직접 참조 — jsdom에서 전역 노출
53
+ for (const key of [
54
+ 'HTMLElement',
55
+ 'HTMLDivElement',
56
+ 'HTMLSpanElement',
57
+ 'Element',
58
+ 'Node',
59
+ 'Event',
60
+ 'CustomEvent',
61
+ ]) {
62
+ if (typeof globalThis[key] === 'undefined') {
63
+ globalThis[key] = dom.window[key];
64
+ }
65
+ }
66
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
67
+ const ReactDOM = require('react-dom');
68
+ ReactDOM.createPortal = (children) => children;
69
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
70
+ const React = require('react');
71
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
72
+ const { renderToStaticMarkup } = require('react-dom/server');
73
+ return { createElement: React.createElement, renderToStaticMarkup };
74
+ };
75
+ exports.setupDomEnvironment = setupDomEnvironment;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * 한국어/영어 퍼지 매칭 유틸
3
+ *
4
+ * - 공백 제거 후 비교 ("약관동의" ↔ "약관 동의")
5
+ * - 부분 포함 ("비번" ⊂ "비밀번호"는 안 되지만, "로딩" ⊂ "로딩중"은 됨)
6
+ * - 편집 거리(Levenshtein) 기반 오타 허용 ("셀랙트" ↔ "셀렉트", "buttn" ↔ "button")
7
+ */
8
+ export interface FuzzyResult {
9
+ score: number;
10
+ matchType: 'exact' | 'contains' | 'fuzzy';
11
+ }
12
+ /**
13
+ * query가 target에 매칭되는지 퍼지 검사
14
+ *
15
+ * @returns null이면 매칭 실패, FuzzyResult면 매칭 성공
16
+ */
17
+ export declare const fuzzyMatch: (query: string, target: string) => FuzzyResult | null;
18
+ /**
19
+ * query를 여러 targets에 대해 퍼지 매칭하고 최고 점수를 반환
20
+ */
21
+ export declare const bestFuzzyMatch: (query: string, targets: string[]) => FuzzyResult | null;
@@ -0,0 +1,110 @@
1
+ "use strict";
2
+ /**
3
+ * 한국어/영어 퍼지 매칭 유틸
4
+ *
5
+ * - 공백 제거 후 비교 ("약관동의" ↔ "약관 동의")
6
+ * - 부분 포함 ("비번" ⊂ "비밀번호"는 안 되지만, "로딩" ⊂ "로딩중"은 됨)
7
+ * - 편집 거리(Levenshtein) 기반 오타 허용 ("셀랙트" ↔ "셀렉트", "buttn" ↔ "button")
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.bestFuzzyMatch = exports.fuzzyMatch = void 0;
11
+ // ── 상수 ──────────────────────────────────────────────────────────
12
+ /** 포함 매칭 시 기본 점수 */
13
+ const CONTAINS_BASE_SCORE = 0.7;
14
+ /** 포함 매칭 시 길이 비율 보너스 가중치 */
15
+ const CONTAINS_BONUS_MULTIPLIER = 0.2;
16
+ /** 이 길이 이하의 쿼리는 "짧은 쿼리"로 분류 (typo threshold = 1) */
17
+ const SHORT_QUERY_LENGTH = 2;
18
+ /** 긴 쿼리의 오타 허용 비율 (ceil(length * 이 값)) */
19
+ const TYPO_THRESHOLD_RATIO = 0.3;
20
+ /** target이 query보다 이만큼 이상 길면 부분 문자열 매칭 시도 */
21
+ const LENGTH_DIFF_FOR_SUBSTRING = 2;
22
+ /** 부분 문자열 매칭을 시도할 target 최대 길이 */
23
+ const SUBSTRING_TARGET_LIMIT = 100;
24
+ /** 부분 문자열 퍼지 매칭 시 기본 점수 */
25
+ const SUBSTRING_BASE_SCORE = 0.5;
26
+ /** 부분 문자열 퍼지 매칭 시 유사도 보너스 가중치 */
27
+ const SUBSTRING_BONUS_MULTIPLIER = 0.2;
28
+ // ── 순수 함수 ────────────────────────────────────────────────────
29
+ /** 공백·하이픈 제거 후 소문자 */
30
+ const normalize = (s) => s.toLowerCase().replace(/[\s\-_]/g, '');
31
+ /** Levenshtein 편집 거리 */
32
+ const editDistance = (a, b) => {
33
+ const m = a.length;
34
+ const n = b.length;
35
+ if (m === 0)
36
+ return n;
37
+ if (n === 0)
38
+ return m;
39
+ // 짧은 배열만 유지 (메모리 최적화)
40
+ let prev = Array.from({ length: n + 1 }, (_, i) => i);
41
+ let curr = new Array(n + 1);
42
+ for (let i = 1; i <= m; i++) {
43
+ curr[0] = i;
44
+ for (let j = 1; j <= n; j++) {
45
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
46
+ curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
47
+ }
48
+ [prev, curr] = [curr, prev];
49
+ }
50
+ return prev[n];
51
+ };
52
+ /**
53
+ * query가 target에 매칭되는지 퍼지 검사
54
+ *
55
+ * @returns null이면 매칭 실패, FuzzyResult면 매칭 성공
56
+ */
57
+ const fuzzyMatch = (query, target) => {
58
+ const q = normalize(query);
59
+ const t = normalize(target);
60
+ if (!q || !t)
61
+ return null;
62
+ // 1. 정확 일치 (공백 제거 후)
63
+ if (t === q)
64
+ return { score: 1.0, matchType: 'exact' };
65
+ // 2. 포함 매칭 (공백 제거 후)
66
+ if (t.includes(q) || q.includes(t)) {
67
+ const shorter = Math.min(q.length, t.length);
68
+ const longer = Math.max(q.length, t.length);
69
+ return { score: CONTAINS_BASE_SCORE + CONTAINS_BONUS_MULTIPLIER * (shorter / longer), matchType: 'contains' };
70
+ }
71
+ // 3. 편집 거리 기반 퍼지 매칭
72
+ const isShortQuery = q.length <= SHORT_QUERY_LENGTH;
73
+ const typoThreshold = isShortQuery ? 1 : Math.ceil(q.length * TYPO_THRESHOLD_RATIO);
74
+ const dist = editDistance(q, t);
75
+ // target이 query보다 훨씬 길면, 부분 문자열 슬라이딩 윈도우로 비교
76
+ const targetIsLonger = t.length > q.length + LENGTH_DIFF_FOR_SUBSTRING;
77
+ const queryIsLongEnough = q.length >= SHORT_QUERY_LENGTH;
78
+ const targetWithinLimit = t.length <= SUBSTRING_TARGET_LIMIT;
79
+ const shouldTrySubstringMatch = targetIsLonger && queryIsLongEnough && targetWithinLimit;
80
+ if (shouldTrySubstringMatch) {
81
+ for (let i = 0; i <= t.length - q.length; i++) {
82
+ const sub = t.substring(i, i + q.length);
83
+ const subDist = editDistance(q, sub);
84
+ if (subDist <= typoThreshold) {
85
+ const subSimilarity = q.length === 0 ? 1 : 1 - subDist / q.length;
86
+ return { score: SUBSTRING_BASE_SCORE + SUBSTRING_BONUS_MULTIPLIER * subSimilarity, matchType: 'fuzzy' };
87
+ }
88
+ }
89
+ }
90
+ if (dist <= typoThreshold) {
91
+ const maxLen = Math.max(q.length, t.length);
92
+ return { score: maxLen === 0 ? 1 : 1 - dist / maxLen, matchType: 'fuzzy' };
93
+ }
94
+ return null;
95
+ };
96
+ exports.fuzzyMatch = fuzzyMatch;
97
+ /**
98
+ * query를 여러 targets에 대해 퍼지 매칭하고 최고 점수를 반환
99
+ */
100
+ const bestFuzzyMatch = (query, targets) => {
101
+ let best = null;
102
+ for (const target of targets) {
103
+ const result = (0, exports.fuzzyMatch)(query, target);
104
+ if (result && (!best || result.score > best.score)) {
105
+ best = result;
106
+ }
107
+ }
108
+ return best;
109
+ };
110
+ exports.bestFuzzyMatch = bestFuzzyMatch;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * stderr 전용 logger 유틸리티 — 부수효과 함수 (stderr I/O)
3
+ *
4
+ * ⚠️ console.log / console.warn / console.error 직접 사용 금지
5
+ *
6
+ * MCP stdio 프로토콜은 채널 역할이 엄격히 분리됩니다.
7
+ * stdin → 클라이언트 → 서버 요청(JSON-RPC)
8
+ * stdout → 서버 → 클라이언트 응답(JSON-RPC) ← 프로토콜 전용 채널
9
+ * stderr → 로그 전용 (프로토콜 밖)
10
+ *
11
+ * console.log는 stdout에 출력되어 JSON-RPC 스트림을 오염시킵니다.
12
+ * 모든 로깅은 반드시 이 logger를 통해 stderr로 출력해야 합니다.
13
+ */
14
+ export declare const logger: {
15
+ info: (msg: string) => void;
16
+ warn: (msg: string) => void;
17
+ error: (msg: string) => void;
18
+ };
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.logger = void 0;
4
+ /**
5
+ * stderr 전용 logger 유틸리티 — 부수효과 함수 (stderr I/O)
6
+ *
7
+ * ⚠️ console.log / console.warn / console.error 직접 사용 금지
8
+ *
9
+ * MCP stdio 프로토콜은 채널 역할이 엄격히 분리됩니다.
10
+ * stdin → 클라이언트 → 서버 요청(JSON-RPC)
11
+ * stdout → 서버 → 클라이언트 응답(JSON-RPC) ← 프로토콜 전용 채널
12
+ * stderr → 로그 전용 (프로토콜 밖)
13
+ *
14
+ * console.log는 stdout에 출력되어 JSON-RPC 스트림을 오염시킵니다.
15
+ * 모든 로깅은 반드시 이 logger를 통해 stderr로 출력해야 합니다.
16
+ */
17
+ exports.logger = {
18
+ info: (msg) => {
19
+ process.stderr.write(`[INFO] ${msg}\n`);
20
+ },
21
+ warn: (msg) => {
22
+ process.stderr.write(`[WARN] ${msg}\n`);
23
+ },
24
+ error: (msg) => {
25
+ process.stderr.write(`[ERROR] ${msg}\n`);
26
+ },
27
+ };
@@ -0,0 +1,28 @@
1
+ /**
2
+ * MCP tool 응답 헬퍼 — 순수 함수
3
+ *
4
+ * 모든 tool이 동일한 응답 구조를 사용하도록 통일합니다.
5
+ * AI가 tool 코드를 읽을 때 응답 구조를 이 파일 하나로 파악할 수 있습니다.
6
+ */
7
+ import type { McpErrorCode } from '../types.js';
8
+ /** MCP tool 응답 타입 — registerTool 콜백 반환 타입과 호환 */
9
+ export type McpToolResponse = {
10
+ [key: string]: unknown;
11
+ content: Array<{
12
+ type: 'text';
13
+ text: string;
14
+ }>;
15
+ isError?: true;
16
+ };
17
+ /** 성공 응답 — data를 JSON.stringify하여 반환 */
18
+ export declare const successResponse: (data: unknown) => McpToolResponse;
19
+ /** 오류 응답 — code + message + suggestion + optional warnings 구조로 isError: true 반환 */
20
+ export declare const errorResponse: (code: McpErrorCode, message: string, suggestion: string, warnings?: string[]) => McpToolResponse;
21
+ /** COMPONENT_NOT_FOUND 오류 — 여러 tool에서 공통 사용 */
22
+ export declare const componentNotFoundResponse: (name: string) => McpToolResponse;
23
+ /** ping 미호출 시 응답에 핵심 rules를 주입하는 래퍼 */
24
+ export declare const appendPingReminder: (response: McpToolResponse, reminder: string) => McpToolResponse;
25
+ /** 입력 정규화 — name.trim().toLowerCase() */
26
+ export declare const normalizeName: (name: string) => string;
27
+ /** catch 블록에서 에러 메시지를 안전하게 추출 */
28
+ export declare const toErrorMessage: (err: unknown) => string;
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toErrorMessage = exports.normalizeName = exports.appendPingReminder = exports.componentNotFoundResponse = exports.errorResponse = exports.successResponse = void 0;
4
+ /** 성공 응답 — data를 JSON.stringify하여 반환 */
5
+ const successResponse = (data) => ({
6
+ content: [{ type: 'text', text: JSON.stringify(data) }],
7
+ });
8
+ exports.successResponse = successResponse;
9
+ /** 오류 응답 — code + message + suggestion + optional warnings 구조로 isError: true 반환 */
10
+ const errorResponse = (code, message, suggestion, warnings) => ({
11
+ content: [
12
+ {
13
+ type: 'text',
14
+ text: JSON.stringify({
15
+ code,
16
+ message,
17
+ suggestion,
18
+ ...(warnings && warnings.length > 0 && { warnings }),
19
+ }),
20
+ },
21
+ ],
22
+ isError: true,
23
+ });
24
+ exports.errorResponse = errorResponse;
25
+ /** COMPONENT_NOT_FOUND 오류 — 여러 tool에서 공통 사용 */
26
+ const componentNotFoundResponse = (name) => (0, exports.errorResponse)('COMPONENT_NOT_FOUND', `Component '${name}' not found.`, 'Use search_component to find similar components, or list_components to see all available.');
27
+ exports.componentNotFoundResponse = componentNotFoundResponse;
28
+ /** ping 미호출 시 응답에 핵심 rules를 주입하는 래퍼 */
29
+ const appendPingReminder = (response, reminder) => {
30
+ const text = response.content[0].text + '\n\n' + reminder;
31
+ return { ...response, content: [{ type: 'text', text }] };
32
+ };
33
+ exports.appendPingReminder = appendPingReminder;
34
+ /** 입력 정규화 — name.trim().toLowerCase() */
35
+ const normalizeName = (name) => name.trim().toLowerCase();
36
+ exports.normalizeName = normalizeName;
37
+ /** catch 블록에서 에러 메시지를 안전하게 추출 */
38
+ const toErrorMessage = (err) => (err instanceof Error ? err.message : String(err));
39
+ exports.toErrorMessage = toErrorMessage;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * 토큰 검증 순수 함수 — 하드코딩 색상 감지 + 존재하지 않는 토큰 감지
3
+ *
4
+ * compliance.ts에서 분리. 모든 함수는 순수 함수(Calculation).
5
+ */
6
+ import type { parse } from 'node-html-parser';
7
+ import type { ComplianceError, TokenData } from '../types.js';
8
+ /** hex 값(lowercase) → 토큰 CSS 변수명 배열 역방향 매핑 빌드 — server.ts에서 1회 호출 */
9
+ export declare const buildTokenValueMap: (tokenData: TokenData) => Map<string, string[]>;
10
+ /** inline style + <style> 블록의 CSS 텍스트를 하나로 합침 */
11
+ export declare const extractAllCss: (root: ReturnType<typeof parse>) => string;
12
+ /** 토큰 미사용 + 무효 토큰 감지 */
13
+ export declare const detectTokenIssues: (params: {
14
+ root: ReturnType<typeof parse>;
15
+ tokenData: TokenData;
16
+ tokenValueMap: Map<string, string[]>;
17
+ }) => {
18
+ errors: ComplianceError[];
19
+ tokenUsage: {
20
+ correct: number;
21
+ missing: number;
22
+ invalid: number;
23
+ };
24
+ };
@@ -0,0 +1,162 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.detectTokenIssues = exports.extractAllCss = exports.buildTokenValueMap = void 0;
4
+ // ── server.ts 사전 계산용 ───────────────────────────────────────────────
5
+ /** hex 값(lowercase) → 토큰 CSS 변수명 배열 역방향 매핑 빌드 — server.ts에서 1회 호출 */
6
+ const buildTokenValueMap = (tokenData) => {
7
+ const map = new Map();
8
+ for (const group of tokenData.groups) {
9
+ if (group.category !== 'color')
10
+ continue;
11
+ for (const token of group.tokens) {
12
+ const hex = token.value.toLowerCase();
13
+ const existing = map.get(hex);
14
+ if (existing)
15
+ existing.push(token.name);
16
+ else
17
+ map.set(hex, [token.name]);
18
+ }
19
+ }
20
+ return map;
21
+ };
22
+ exports.buildTokenValueMap = buildTokenValueMap;
23
+ // ── 상수 ─────────────────────────────────────────────────────────────────
24
+ const HEX_PATTERN_SOURCE = /#([0-9a-fA-F]{3,8})\b/g.source;
25
+ const RGB_PATTERN_SOURCE = /rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/g.source;
26
+ const VAR_PATTERN_SOURCE = /var\(\s*(--[a-zA-Z0-9-]+)(?:\s*,\s*[^)]+)?\s*\)/g.source;
27
+ // ── 순수 헬퍼 ────────────────────────────────────────────────────────────
28
+ /** rgb(r,g,b) → #rrggbb hex 변환 */
29
+ const rgbToHex = (r, g, b) => `#${[r, g, b].map((v) => v.toString(16).padStart(2, '0')).join('')}`;
30
+ /** 3자리 hex를 6자리로 정규화 */
31
+ const normalizeHex = (hex) => {
32
+ const h = hex.toLowerCase().replace('#', '');
33
+ if (h.length === 3)
34
+ return `#${h[0]}${h[0]}${h[1]}${h[1]}${h[2]}${h[2]}`;
35
+ return `#${h.slice(0, 6)}`;
36
+ };
37
+ /** var()의 prefix가 토큰 시스템에 속하는지 확인 */
38
+ const hasKnownTokenPrefix = (varName, prefixSet) => {
39
+ for (const prefix of prefixSet) {
40
+ if (varName.startsWith(prefix))
41
+ return true;
42
+ }
43
+ return false;
44
+ };
45
+ /** tokenData에 등록된 토큰명 prefix Set — 커스텀 변수 제외용 */
46
+ const buildTokenPrefixSet = (tokenData) => {
47
+ const prefixes = new Set();
48
+ for (const group of tokenData.groups) {
49
+ for (const token of group.tokens) {
50
+ const stripped = token.name.replace(/^--/, '');
51
+ const segments = stripped.split('-');
52
+ if (segments.length >= 2)
53
+ prefixes.add(`--${segments.slice(0, 2).join('-')}`);
54
+ if (segments.length >= 1)
55
+ prefixes.add(`--${segments[0]}`);
56
+ }
57
+ }
58
+ return prefixes;
59
+ };
60
+ /** 모든 토큰명을 Set으로 수집 */
61
+ const buildTokenNameSet = (tokenData) => {
62
+ const names = new Set();
63
+ for (const group of tokenData.groups) {
64
+ for (const token of group.tokens)
65
+ names.add(token.name);
66
+ }
67
+ return names;
68
+ };
69
+ /** inline style + <style> 블록의 CSS 텍스트를 하나로 합침 */
70
+ const extractAllCss = (root) => {
71
+ const parts = [];
72
+ for (const el of root.querySelectorAll('[style]')) {
73
+ const style = el.getAttribute('style');
74
+ if (style)
75
+ parts.push(style);
76
+ }
77
+ for (const styleEl of root.querySelectorAll('style')) {
78
+ parts.push(styleEl.textContent ?? '');
79
+ }
80
+ return parts.join(' ');
81
+ };
82
+ exports.extractAllCss = extractAllCss;
83
+ /** CSS 텍스트에서 하드코딩된 색상값(hex/rgb)을 추출 */
84
+ const collectHardcodedColors = (css) => {
85
+ const colors = [];
86
+ let match;
87
+ const hexPattern = new RegExp(HEX_PATTERN_SOURCE, 'g');
88
+ while ((match = hexPattern.exec(css)) !== null) {
89
+ colors.push({ raw: match[0], normalized: normalizeHex(match[0]) });
90
+ }
91
+ const rgbPattern = new RegExp(RGB_PATTERN_SOURCE, 'g');
92
+ while ((match = rgbPattern.exec(css)) !== null) {
93
+ colors.push({
94
+ raw: match[0],
95
+ normalized: rgbToHex(parseInt(match[1], 10), parseInt(match[2], 10), parseInt(match[3], 10)),
96
+ });
97
+ }
98
+ return colors;
99
+ };
100
+ /** token_not_used suggestion 생성 */
101
+ const buildTokenSuggestion = (raw, tokenNames) => tokenNames.length === 1
102
+ ? `Use var(${tokenNames[0]}) instead of ${raw}`
103
+ : `Possible tokens: ${tokenNames.map((t) => `var(${t})`).join(', ')}`;
104
+ // ── 검증 함수 ────────────────────────────────────────────────────────────
105
+ /** 하드코딩 색상 → token_not_used 에러 수집 */
106
+ const collectTokenNotUsedErrors = (css, tokenValueMap) => {
107
+ const errors = [];
108
+ let missing = 0;
109
+ for (const color of collectHardcodedColors(css)) {
110
+ const tokenNames = tokenValueMap.get(color.normalized);
111
+ if (!tokenNames)
112
+ continue;
113
+ missing++;
114
+ errors.push({
115
+ type: 'token_not_used',
116
+ target: color.raw,
117
+ component: '(inline-style)',
118
+ message: `Hardcoded color '${color.raw}' should use a design token.`,
119
+ suggestion: buildTokenSuggestion(color.raw, tokenNames),
120
+ });
121
+ }
122
+ return { errors, missing };
123
+ };
124
+ /** var() 참조 → invalid_token 에러 수집 + correct 카운트 */
125
+ const collectInvalidTokenErrors = (css, tokenNameSet, tokenPrefixSet) => {
126
+ const errors = [];
127
+ let correct = 0;
128
+ let invalid = 0;
129
+ let varMatch;
130
+ const varPattern = new RegExp(VAR_PATTERN_SOURCE, 'g');
131
+ while ((varMatch = varPattern.exec(css)) !== null) {
132
+ const varName = varMatch[1];
133
+ if (!hasKnownTokenPrefix(varName, tokenPrefixSet))
134
+ continue;
135
+ if (tokenNameSet.has(varName)) {
136
+ correct++;
137
+ }
138
+ else {
139
+ invalid++;
140
+ errors.push({
141
+ type: 'invalid_token',
142
+ target: `var(${varName})`,
143
+ component: '(inline-style)',
144
+ message: `Token '${varName}' does not exist in the design system.`,
145
+ suggestion: "Call get_design_tokens('color') to see available tokens.",
146
+ });
147
+ }
148
+ }
149
+ return { errors, correct, invalid };
150
+ };
151
+ /** 토큰 미사용 + 무효 토큰 감지 */
152
+ const detectTokenIssues = (params) => {
153
+ const { root, tokenData, tokenValueMap } = params;
154
+ const allCss = (0, exports.extractAllCss)(root);
155
+ const notUsed = collectTokenNotUsedErrors(allCss, tokenValueMap);
156
+ const invalidResult = collectInvalidTokenErrors(allCss, buildTokenNameSet(tokenData), buildTokenPrefixSet(tokenData));
157
+ return {
158
+ errors: [...notUsed.errors, ...invalidResult.errors],
159
+ tokenUsage: { correct: invalidResult.correct, missing: notUsed.missing, invalid: invalidResult.invalid },
160
+ };
161
+ };
162
+ exports.detectTokenIssues = detectTokenIssues;
@@ -0,0 +1,4 @@
1
+ /**
2
+ * version — 빌드 시 scripts/build-npm.js가 자동 생성 (수동 편집 금지)
3
+ */
4
+ export declare const VERSION = "1.6.3";
package/bin/version.js ADDED
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.VERSION = void 0;
4
+ /**
5
+ * version — 빌드 시 scripts/build-npm.js가 자동 생성 (수동 편집 금지)
6
+ */
7
+ exports.VERSION = '1.6.3';