@ncds/ui-admin-mcp 1.0.0-alpha.2 → 1.0.0-alpha.20
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/bin/components.bundle.js +15 -19
- package/bin/definitions/external/editor.d.ts +50 -0
- package/bin/definitions/external/editor.js +53 -0
- package/bin/definitions/external/step-guide.d.ts +61 -0
- package/bin/definitions/external/step-guide.js +52 -0
- package/bin/definitions/instructions.md +194 -10
- package/bin/definitions/js-api.json +352 -0
- package/bin/definitions/rules.json +36 -4
- package/bin/definitions/tool-definitions.json +33 -9
- package/bin/overrides/composition.json +2500 -0
- package/bin/server.d.ts +5 -0
- package/bin/server.js +97 -33
- package/bin/server.mjs +0 -0
- package/bin/tools/external/editor.d.ts +18 -0
- package/bin/tools/external/editor.js +88 -0
- package/bin/tools/external/step-guide.d.ts +19 -0
- package/bin/tools/external/step-guide.js +79 -0
- package/bin/tools/getComponentProps.d.ts +3 -0
- package/bin/tools/getComponentProps.js +12 -3
- package/bin/tools/listCompositionOverrides.d.ts +61 -0
- package/bin/tools/listCompositionOverrides.js +156 -0
- package/bin/tools/ping.d.ts +1 -1
- package/bin/tools/renderToHtml.d.ts +38 -7
- package/bin/tools/renderToHtml.js +785 -110
- package/bin/tools/searchComponent.d.ts +5 -0
- package/bin/tools/searchComponent.js +3 -3
- package/bin/tools/validateHtml.d.ts +8 -6
- package/bin/tools/validateHtml.js +74 -6
- package/bin/types.d.ts +60 -1
- package/bin/utils/bemValidator.d.ts +16 -8
- package/bin/utils/bemValidator.js +16 -4
- package/bin/utils/compliance.d.ts +7 -6
- package/bin/utils/compliance.js +8 -4
- package/bin/utils/dataLoader.d.ts +43 -14
- package/bin/utils/dataLoader.js +125 -22
- package/bin/utils/domEnvironment.js +51 -0
- package/bin/utils/fuzzyMatch.d.ts +4 -0
- package/bin/utils/fuzzyMatch.js +13 -3
- package/bin/utils/logger.d.ts +5 -5
- package/bin/utils/logger.js +5 -5
- package/bin/utils/response.d.ts +4 -2
- package/bin/utils/response.js +15 -4
- package/bin/utils/tokenValidator.d.ts +4 -3
- package/bin/utils/tokenValidator.js +13 -11
- package/bin/version.d.ts +4 -2
- package/bin/version.js +4 -2
- package/data/_icons.json +357 -2
- package/data/_meta.json +4 -5
- package/data/_tokens.json +8 -8
- package/data/badge-group.json +181 -4
- package/data/badge.json +146 -14
- package/data/block-container.json +95 -0
- package/data/block-header.json +208 -0
- package/data/bread-crumb.json +38 -2
- package/data/button-group.json +59 -0
- package/data/button.json +124 -1
- package/data/carousel-arrow.json +6 -11
- package/data/carousel-number-group.json +2 -12
- package/data/checkbox.json +1 -1
- package/data/combo-box.json +32 -8
- package/data/data-grid.json +240 -0
- package/data/date-picker.json +22 -2
- package/data/divider.json +1 -1
- package/data/dot.json +2 -2
- package/data/dropdown.json +187 -20
- package/data/editor.json +85 -0
- package/data/empty-state.json +168 -3
- package/data/featured-icon.json +20 -5
- package/data/file-input.json +176 -10
- package/data/horizontal-tab.json +219 -3
- package/data/image-file-input.json +176 -10
- package/data/input-base.json +165 -4
- package/data/modal.json +266 -5
- package/data/notification.json +56 -40
- package/data/number-input.json +164 -4
- package/data/page-title.json +135 -0
- package/data/pagination.json +8 -4
- package/data/password-input.json +252 -13
- package/data/progress-bar.json +28 -8
- package/data/progress-circle.json +9 -6
- package/data/radio.json +4 -3
- package/data/range-date-picker-with-buttons.json +187 -7
- package/data/range-date-picker.json +186 -6
- package/data/select-box.json +48 -16
- package/data/select.json +35 -25
- package/data/slider.json +1 -1
- package/data/spinner.json +3 -4
- package/data/step-guide.json +130 -0
- package/data/switch.json +66 -6
- package/data/table.json +293 -0
- package/data/tag.json +68 -6
- package/data/textarea.json +1 -1
- package/data/toggle.json +2 -2
- package/data/tooltip.json +16 -3
- package/data/vertical-tab.json +220 -3
- package/package.json +27 -25
- package/bin/instructions.d.ts +0 -1
- package/bin/instructions.js +0 -14
- package/bin/tools/getComponentHtml.d.ts +0 -3
- package/bin/tools/getComponentHtml.js +0 -30
package/bin/utils/dataLoader.js
CHANGED
|
@@ -3,57 +3,142 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.
|
|
6
|
+
exports.loadTokenData = exports.loadMeta = exports.loadJsApi = exports.loadInstructions = exports.loadIconData = exports.loadComponentsFromDir = exports.loadCompositionOverrides = exports.loadComplianceRules = exports.getComponent = exports.getAllComponents = exports.DEFAULT_DATA_DIR = void 0;
|
|
7
7
|
/**
|
|
8
8
|
* dataLoader — mcp/data/*.json + definitions/ 파일을 읽어 반환
|
|
9
9
|
*
|
|
10
10
|
* 함수 분류:
|
|
11
|
-
* loadComponentsFromDir, loadIconData, loadCdnMeta, loadInstructions → 부수효과 (fs I/O)
|
|
11
|
+
* loadComponentsFromDir, loadCompositionOverrides, loadIconData, loadCdnMeta, loadInstructions → 부수효과 (fs I/O)
|
|
12
12
|
* getComponent, getAllComponents → 순수 함수 (Map 조회)
|
|
13
13
|
*
|
|
14
|
-
* Map 소유권은 server.ts가 가진다. 이 파일은 상태를 보유하지 않는다.
|
|
14
|
+
* Map + 메타데이터 소유권은 server.ts가 가진다. 이 파일은 상태를 보유하지 않는다.
|
|
15
15
|
*/
|
|
16
16
|
const fs_1 = __importDefault(require("fs"));
|
|
17
17
|
const path_1 = __importDefault(require("path"));
|
|
18
18
|
const logger_js_1 = require("./logger.js");
|
|
19
19
|
const response_js_1 = require("./response.js");
|
|
20
|
-
|
|
20
|
+
const DEFAULT_DATA_DIR = path_1.default.resolve(__dirname, '../../data');
|
|
21
|
+
exports.DEFAULT_DATA_DIR = DEFAULT_DATA_DIR;
|
|
22
|
+
/**
|
|
23
|
+
* P8: Composition Overrides 위치.
|
|
24
|
+
* extract-mcp-data.ts 가 만드는 data/*.json 위에 컴포넌트 디렉토리를 건드리지 않고
|
|
25
|
+
* allowedChildren / allowedParents / canonicalExample / canonicalExamples / bemClassesExtra 를
|
|
26
|
+
* 추가하는 단일 채널.
|
|
27
|
+
*
|
|
28
|
+
* 빌드 산출물(bin/) 에서는 src/overrides 가 build:server 단계에서 복사되지 않을 수 있으므로
|
|
29
|
+
* src 와 bin 양쪽을 fallback 으로 본다.
|
|
30
|
+
*/
|
|
31
|
+
const COMPOSITION_OVERRIDES_CANDIDATES = [
|
|
32
|
+
path_1.default.resolve(__dirname, '../../src/overrides/composition.json'),
|
|
33
|
+
path_1.default.resolve(__dirname, '../overrides/composition.json'),
|
|
34
|
+
];
|
|
35
|
+
/** composition.json 파일에서 메타 키를 제거하여 CompositionEntry 객체만 수집 */
|
|
36
|
+
const filterCompositionEntries = (parsed) => {
|
|
37
|
+
if (typeof parsed !== 'object' || !parsed)
|
|
38
|
+
return {};
|
|
39
|
+
const entries = {};
|
|
40
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
41
|
+
if (key.startsWith('_'))
|
|
42
|
+
continue;
|
|
43
|
+
if (value && typeof value === 'object' && !Array.isArray(value))
|
|
44
|
+
entries[key] = value;
|
|
45
|
+
}
|
|
46
|
+
return entries;
|
|
47
|
+
};
|
|
48
|
+
const loadCompositionOverrides = () => {
|
|
49
|
+
for (const candidate of COMPOSITION_OVERRIDES_CANDIDATES) {
|
|
50
|
+
try {
|
|
51
|
+
const raw = fs_1.default.readFileSync(candidate, 'utf-8');
|
|
52
|
+
const parsed = JSON.parse(raw);
|
|
53
|
+
const entries = filterCompositionEntries(parsed);
|
|
54
|
+
logger_js_1.logger.info(`composition overrides 로딩: ${Object.keys(entries).length}개 (${path_1.default.basename(candidate)})`);
|
|
55
|
+
return entries;
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
// ENOENT: 파일 없으면 다음 후보 시도 (TOCTOU 회피)
|
|
59
|
+
if (err instanceof Error && 'code' in err && err.code === 'ENOENT') {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
logger_js_1.logger.warn(`composition overrides 파싱 실패 (${candidate}): ${(0, response_js_1.toErrorMessage)(err)}`);
|
|
63
|
+
return {};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return {};
|
|
67
|
+
};
|
|
68
|
+
exports.loadCompositionOverrides = loadCompositionOverrides;
|
|
69
|
+
/** P8: `_` 접두 메타 키를 객체에서 제거 (얕은 필터). canonicalExamples 같은 중첩 맵에서 _note 누출 방지. */
|
|
70
|
+
const stripMetaKeys = (obj) => {
|
|
71
|
+
if (!obj)
|
|
72
|
+
return obj;
|
|
73
|
+
const cleaned = {};
|
|
74
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
75
|
+
if (key.startsWith('_'))
|
|
76
|
+
continue;
|
|
77
|
+
cleaned[key] = value;
|
|
78
|
+
}
|
|
79
|
+
return cleaned;
|
|
80
|
+
};
|
|
81
|
+
/** ComponentData 에 composition overrides 항목을 in-place 병합 — bemClasses 는 union, 나머지는 추가 필드. */
|
|
82
|
+
const mergeCompositionEntry = (component, entry) => {
|
|
83
|
+
if (entry.bemClassesExtra && entry.bemClassesExtra.length > 0) {
|
|
84
|
+
const merged = new Set([...component.bemClasses, ...entry.bemClassesExtra]);
|
|
85
|
+
component.bemClasses = [...merged].sort();
|
|
86
|
+
}
|
|
87
|
+
if (entry.allowedChildren)
|
|
88
|
+
component.allowedChildren = entry.allowedChildren;
|
|
89
|
+
if (entry.allowedParents)
|
|
90
|
+
component.allowedParents = entry.allowedParents;
|
|
91
|
+
if (entry.canonicalExample) {
|
|
92
|
+
// canonicalExample 내부 _note / _comment 같은 메타 키 제거 — 응답에 누출되지 않도록
|
|
93
|
+
component.canonicalExample = stripMetaKeys(entry.canonicalExample);
|
|
94
|
+
}
|
|
95
|
+
if (entry.canonicalExamples) {
|
|
96
|
+
// canonicalExamples 내부 _note / _comment 같은 메타 키 제거 — 응답에 누출되지 않도록
|
|
97
|
+
component.canonicalExamples = stripMetaKeys(entry.canonicalExamples);
|
|
98
|
+
}
|
|
99
|
+
// P13: description 끝에 append — list_components 응답에서 컴포넌트 용도 명시
|
|
100
|
+
if (entry.descriptionExtra) {
|
|
101
|
+
component.description = `${component.description}\n\n${entry.descriptionExtra}`;
|
|
102
|
+
}
|
|
103
|
+
// P13: aliases union — search_component 별칭 보강 (중복 제거)
|
|
104
|
+
if (entry.aliasesExtra && entry.aliasesExtra.length > 0) {
|
|
105
|
+
const set = new Set([...(component.aliases ?? []), ...entry.aliasesExtra]);
|
|
106
|
+
component.aliases = [...set];
|
|
107
|
+
}
|
|
108
|
+
};
|
|
21
109
|
// ── 검증 헬퍼 (§ 3.8 복잡한 조건을 의도가 드러나는 이름으로 분리) ──────────
|
|
22
110
|
/** JSON.parse 결과가 name: string 필드를 가진 컴포넌트 객체인지 검증 */
|
|
23
111
|
const isValidComponentJson = (data) => !!data && typeof data === 'object' && 'name' in data && typeof data.name === 'string';
|
|
24
112
|
/** JSON.parse 결과가 icons 배열을 가진 아이콘 데이터인지 검증 */
|
|
25
113
|
const isValidIconJson = (data) => !!data && typeof data === 'object' && 'icons' in data && Array.isArray(data.icons);
|
|
26
114
|
// ── 부수효과 함수 (fs I/O) ────────────────────────────────────────────────
|
|
27
|
-
/** data/ 디렉토리의 JSON 파일을 읽어 componentMap
|
|
115
|
+
/** data/ 디렉토리의 JSON 파일을 읽어 componentMap + compositionOverrides로 반환 */
|
|
28
116
|
const loadComponentsFromDir = (dataDir) => {
|
|
29
117
|
if (!fs_1.default.existsSync(dataDir)) {
|
|
30
|
-
|
|
31
|
-
process.exit(1);
|
|
118
|
+
throw new Error(`mcp/data/ 디렉토리가 없습니다: ${dataDir}`);
|
|
32
119
|
}
|
|
33
120
|
const jsonFiles = fs_1.default.readdirSync(dataDir).filter((f) => f.endsWith('.json') && !f.startsWith('_'));
|
|
34
121
|
if (jsonFiles.length === 0) {
|
|
35
|
-
|
|
36
|
-
process.exit(1);
|
|
122
|
+
throw new Error(`mcp/data/ 디렉토리에 JSON 파일이 없습니다: ${dataDir}`);
|
|
37
123
|
}
|
|
38
124
|
const map = new Map();
|
|
125
|
+
// P8: composition overrides (composition.json) 를 한 번만 로딩해 각 컴포넌트에 병합 — 호출자에게도 반환
|
|
126
|
+
const compositionOverrides = loadCompositionOverrides();
|
|
39
127
|
for (const file of jsonFiles) {
|
|
40
128
|
const filePath = path_1.default.join(dataDir, file);
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
throw new Error('name 필드가 없거나 올바르지 않습니다');
|
|
46
|
-
}
|
|
47
|
-
const component = parsed;
|
|
48
|
-
map.set(component.name, component);
|
|
49
|
-
}
|
|
50
|
-
catch (err) {
|
|
51
|
-
logger_js_1.logger.error(`JSON 파싱 실패 (${file}): ${(0, response_js_1.toErrorMessage)(err)}`);
|
|
52
|
-
process.exit(1);
|
|
129
|
+
const raw = fs_1.default.readFileSync(filePath, 'utf-8');
|
|
130
|
+
const parsed = JSON.parse(raw);
|
|
131
|
+
if (!isValidComponentJson(parsed)) {
|
|
132
|
+
throw new Error(`JSON 파싱 실패 (${file}): name 필드가 없거나 올바르지 않습니다`);
|
|
53
133
|
}
|
|
134
|
+
const component = parsed;
|
|
135
|
+
const compositionOverridesEntry = compositionOverrides[component.name];
|
|
136
|
+
if (compositionOverridesEntry)
|
|
137
|
+
mergeCompositionEntry(component, compositionOverridesEntry);
|
|
138
|
+
map.set(component.name, component);
|
|
54
139
|
}
|
|
55
140
|
logger_js_1.logger.info(`컴포넌트 ${map.size}개 로딩 완료`);
|
|
56
|
-
return map;
|
|
141
|
+
return { map, compositionOverrides };
|
|
57
142
|
};
|
|
58
143
|
exports.loadComponentsFromDir = loadComponentsFromDir;
|
|
59
144
|
/** data/_icons.json을 읽어 IconData로 반환 (파일 없으면 빈 데이터) */
|
|
@@ -165,6 +250,24 @@ const loadInstructions = (definitionsDir) => {
|
|
|
165
250
|
return fs_1.default.readFileSync(instructionsPath, 'utf-8').trim();
|
|
166
251
|
};
|
|
167
252
|
exports.loadInstructions = loadInstructions;
|
|
253
|
+
/** definitions/js-api.json을 로드하여 exportName→JsApiInfo Map 반환 */
|
|
254
|
+
const loadJsApi = (definitionsDir) => {
|
|
255
|
+
const filePath = path_1.default.resolve(definitionsDir, 'js-api.json');
|
|
256
|
+
if (!fs_1.default.existsSync(filePath)) {
|
|
257
|
+
logger_js_1.logger.info('js-api.json이 없습니다 — JS API 힌트 비활성화');
|
|
258
|
+
return new Map();
|
|
259
|
+
}
|
|
260
|
+
try {
|
|
261
|
+
const raw = fs_1.default.readFileSync(filePath, 'utf-8');
|
|
262
|
+
const data = JSON.parse(raw);
|
|
263
|
+
return new Map(Object.entries(data));
|
|
264
|
+
}
|
|
265
|
+
catch (err) {
|
|
266
|
+
logger_js_1.logger.error(`js-api.json 파싱 실패: ${(0, response_js_1.toErrorMessage)(err)}`);
|
|
267
|
+
return new Map();
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
exports.loadJsApi = loadJsApi;
|
|
168
271
|
// ── 순수 함수 (외부 상태 없음, Map을 파라미터로 받음) ──────────────────────
|
|
169
272
|
/** componentMap에서 이름으로 단일 컴포넌트 조회 */
|
|
170
273
|
const getComponent = (map, name) => map.get(name);
|
|
@@ -1,8 +1,44 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
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
|
+
};
|
|
4
39
|
/** jsdom + React SSR 환경을 초기화하고 ReactRuntime을 반환 */
|
|
5
40
|
const setupDomEnvironment = () => {
|
|
41
|
+
unifyModuleResolution();
|
|
6
42
|
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
|
|
7
43
|
const { JSDOM } = require('jsdom');
|
|
8
44
|
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>');
|
|
@@ -10,9 +46,24 @@ const setupDomEnvironment = () => {
|
|
|
10
46
|
globalThis.document = dom.window.document;
|
|
11
47
|
if (typeof globalThis.window === 'undefined')
|
|
12
48
|
globalThis.window = dom.window;
|
|
49
|
+
/* v8 ignore next 3 -- Node.js 환경에서 navigator 미존재 시 설정. 테스트 실행 순서에 따라 이미 설정된 경우 스킵 */
|
|
13
50
|
if (typeof globalThis.navigator === 'undefined') {
|
|
14
51
|
Object.defineProperty(globalThis, 'navigator', { value: dom.window.navigator, writable: true });
|
|
15
52
|
}
|
|
53
|
+
// Swiper 등 외부 라이브러리가 HTMLElement를 직접 참조 — jsdom에서 전역 노출
|
|
54
|
+
for (const key of [
|
|
55
|
+
'HTMLElement',
|
|
56
|
+
'HTMLDivElement',
|
|
57
|
+
'HTMLSpanElement',
|
|
58
|
+
'Element',
|
|
59
|
+
'Node',
|
|
60
|
+
'Event',
|
|
61
|
+
'CustomEvent',
|
|
62
|
+
]) {
|
|
63
|
+
if (typeof globalThis[key] === 'undefined') {
|
|
64
|
+
globalThis[key] = dom.window[key];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
16
67
|
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
|
|
17
68
|
const ReactDOM = require('react-dom');
|
|
18
69
|
ReactDOM.createPortal = (children) => children;
|
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
* - 부분 포함 ("비번" ⊂ "비밀번호"는 안 되지만, "로딩" ⊂ "로딩중"은 됨)
|
|
6
6
|
* - 편집 거리(Levenshtein) 기반 오타 허용 ("셀랙트" ↔ "셀렉트", "buttn" ↔ "button")
|
|
7
7
|
*/
|
|
8
|
+
/** Levenshtein 편집 거리 */
|
|
9
|
+
export declare const editDistance: (a: string, b: string) => number;
|
|
10
|
+
/** 편집 거리 기반 유사도 (0~1, 1이 완전 일치) */
|
|
11
|
+
export declare const similarity: (a: string, b: string) => number;
|
|
8
12
|
export interface FuzzyResult {
|
|
9
13
|
score: number;
|
|
10
14
|
matchType: 'exact' | 'contains' | 'fuzzy';
|
package/bin/utils/fuzzyMatch.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* - 편집 거리(Levenshtein) 기반 오타 허용 ("셀랙트" ↔ "셀렉트", "buttn" ↔ "button")
|
|
8
8
|
*/
|
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
-
exports.bestFuzzyMatch = exports.fuzzyMatch = void 0;
|
|
10
|
+
exports.bestFuzzyMatch = exports.fuzzyMatch = exports.similarity = exports.editDistance = void 0;
|
|
11
11
|
// ── 상수 ──────────────────────────────────────────────────────────
|
|
12
12
|
/** 포함 매칭 시 기본 점수 */
|
|
13
13
|
const CONTAINS_BASE_SCORE = 0.7;
|
|
@@ -49,11 +49,21 @@ const editDistance = (a, b) => {
|
|
|
49
49
|
}
|
|
50
50
|
return prev[n];
|
|
51
51
|
};
|
|
52
|
+
exports.editDistance = editDistance;
|
|
53
|
+
/** 편집 거리 기반 유사도 (0~1, 1이 완전 일치) */
|
|
54
|
+
const similarity = (a, b) => {
|
|
55
|
+
const maxLen = Math.max(a.length, b.length);
|
|
56
|
+
if (maxLen === 0)
|
|
57
|
+
return 1;
|
|
58
|
+
return 1 - (0, exports.editDistance)(a, b) / maxLen;
|
|
59
|
+
};
|
|
60
|
+
exports.similarity = similarity;
|
|
52
61
|
/**
|
|
53
62
|
* query가 target에 매칭되는지 퍼지 검사
|
|
54
63
|
*
|
|
55
64
|
* @returns null이면 매칭 실패, FuzzyResult면 매칭 성공
|
|
56
65
|
*/
|
|
66
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: 정확/포함/퍼지 단계별 매칭 분기 — 알고리즘 본질, 별 리팩토링 거리
|
|
57
67
|
const fuzzyMatch = (query, target) => {
|
|
58
68
|
const q = normalize(query);
|
|
59
69
|
const t = normalize(target);
|
|
@@ -71,7 +81,7 @@ const fuzzyMatch = (query, target) => {
|
|
|
71
81
|
// 3. 편집 거리 기반 퍼지 매칭
|
|
72
82
|
const isShortQuery = q.length <= SHORT_QUERY_LENGTH;
|
|
73
83
|
const typoThreshold = isShortQuery ? 1 : Math.ceil(q.length * TYPO_THRESHOLD_RATIO);
|
|
74
|
-
const dist = editDistance(q, t);
|
|
84
|
+
const dist = (0, exports.editDistance)(q, t);
|
|
75
85
|
// target이 query보다 훨씬 길면, 부분 문자열 슬라이딩 윈도우로 비교
|
|
76
86
|
const targetIsLonger = t.length > q.length + LENGTH_DIFF_FOR_SUBSTRING;
|
|
77
87
|
const queryIsLongEnough = q.length >= SHORT_QUERY_LENGTH;
|
|
@@ -80,7 +90,7 @@ const fuzzyMatch = (query, target) => {
|
|
|
80
90
|
if (shouldTrySubstringMatch) {
|
|
81
91
|
for (let i = 0; i <= t.length - q.length; i++) {
|
|
82
92
|
const sub = t.substring(i, i + q.length);
|
|
83
|
-
const subDist = editDistance(q, sub);
|
|
93
|
+
const subDist = (0, exports.editDistance)(q, sub);
|
|
84
94
|
if (subDist <= typoThreshold) {
|
|
85
95
|
const subSimilarity = q.length === 0 ? 1 : 1 - subDist / q.length;
|
|
86
96
|
return { score: SUBSTRING_BASE_SCORE + SUBSTRING_BONUS_MULTIPLIER * subSimilarity, matchType: 'fuzzy' };
|
package/bin/utils/logger.d.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* stderr 전용 logger 유틸리티 — 부수효과 함수 (stderr I/O)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* console.log / console.warn / console.error 직접 사용 금지.
|
|
5
5
|
*
|
|
6
|
-
* MCP stdio 프로토콜은 채널 역할이 엄격히
|
|
6
|
+
* MCP stdio 프로토콜은 채널 역할이 엄격히 분리된다.
|
|
7
7
|
* stdin → 클라이언트 → 서버 요청(JSON-RPC)
|
|
8
|
-
* stdout → 서버 → 클라이언트 응답(JSON-RPC)
|
|
8
|
+
* stdout → 서버 → 클라이언트 응답(JSON-RPC). 프로토콜 전용 채널
|
|
9
9
|
* stderr → 로그 전용 (프로토콜 밖)
|
|
10
10
|
*
|
|
11
|
-
* console.log는 stdout
|
|
12
|
-
* 모든 로깅은
|
|
11
|
+
* console.log는 stdout을 오염시켜 JSON-RPC 스트림을 깨뜨린다.
|
|
12
|
+
* 모든 로깅은 이 logger를 통해 stderr로 출력한다.
|
|
13
13
|
*/
|
|
14
14
|
export declare const logger: {
|
|
15
15
|
info: (msg: string) => void;
|
package/bin/utils/logger.js
CHANGED
|
@@ -4,15 +4,15 @@ exports.logger = void 0;
|
|
|
4
4
|
/**
|
|
5
5
|
* stderr 전용 logger 유틸리티 — 부수효과 함수 (stderr I/O)
|
|
6
6
|
*
|
|
7
|
-
*
|
|
7
|
+
* console.log / console.warn / console.error 직접 사용 금지.
|
|
8
8
|
*
|
|
9
|
-
* MCP stdio 프로토콜은 채널 역할이 엄격히
|
|
9
|
+
* MCP stdio 프로토콜은 채널 역할이 엄격히 분리된다.
|
|
10
10
|
* stdin → 클라이언트 → 서버 요청(JSON-RPC)
|
|
11
|
-
* stdout → 서버 → 클라이언트 응답(JSON-RPC)
|
|
11
|
+
* stdout → 서버 → 클라이언트 응답(JSON-RPC). 프로토콜 전용 채널
|
|
12
12
|
* stderr → 로그 전용 (프로토콜 밖)
|
|
13
13
|
*
|
|
14
|
-
* console.log는 stdout
|
|
15
|
-
* 모든 로깅은
|
|
14
|
+
* console.log는 stdout을 오염시켜 JSON-RPC 스트림을 깨뜨린다.
|
|
15
|
+
* 모든 로깅은 이 logger를 통해 stderr로 출력한다.
|
|
16
16
|
*/
|
|
17
17
|
exports.logger = {
|
|
18
18
|
info: (msg) => {
|
package/bin/utils/response.d.ts
CHANGED
|
@@ -16,10 +16,12 @@ export type McpToolResponse = {
|
|
|
16
16
|
};
|
|
17
17
|
/** 성공 응답 — data를 JSON.stringify하여 반환 */
|
|
18
18
|
export declare const successResponse: (data: unknown) => McpToolResponse;
|
|
19
|
-
/** 오류 응답 — code + message + suggestion 구조로 isError: true 반환 */
|
|
20
|
-
export declare const errorResponse: (code: McpErrorCode, message: string, suggestion: string) => McpToolResponse;
|
|
19
|
+
/** 오류 응답 — code + message + suggestion + optional warnings 구조로 isError: true 반환 */
|
|
20
|
+
export declare const errorResponse: (code: McpErrorCode, message: string, suggestion: string, warnings?: string[]) => McpToolResponse;
|
|
21
21
|
/** COMPONENT_NOT_FOUND 오류 — 여러 tool에서 공통 사용 */
|
|
22
22
|
export declare const componentNotFoundResponse: (name: string) => McpToolResponse;
|
|
23
|
+
/** ping 미호출 시 응답에 핵심 rules를 주입하는 래퍼 */
|
|
24
|
+
export declare const appendPingReminder: (response: McpToolResponse, reminder: string) => McpToolResponse;
|
|
23
25
|
/** 입력 정규화 — name.trim().toLowerCase() */
|
|
24
26
|
export declare const normalizeName: (name: string) => string;
|
|
25
27
|
/** catch 블록에서 에러 메시지를 안전하게 추출 */
|
package/bin/utils/response.js
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.toErrorMessage = exports.normalizeName = exports.componentNotFoundResponse = exports.errorResponse = exports.successResponse = void 0;
|
|
3
|
+
exports.toErrorMessage = exports.normalizeName = exports.appendPingReminder = exports.componentNotFoundResponse = exports.errorResponse = exports.successResponse = void 0;
|
|
4
4
|
/** 성공 응답 — data를 JSON.stringify하여 반환 */
|
|
5
5
|
const successResponse = (data) => ({
|
|
6
6
|
content: [{ type: 'text', text: JSON.stringify(data) }],
|
|
7
7
|
});
|
|
8
8
|
exports.successResponse = successResponse;
|
|
9
|
-
/** 오류 응답 — code + message + suggestion 구조로 isError: true 반환 */
|
|
10
|
-
const errorResponse = (code, message, suggestion) => ({
|
|
9
|
+
/** 오류 응답 — code + message + suggestion + optional warnings 구조로 isError: true 반환 */
|
|
10
|
+
const errorResponse = (code, message, suggestion, warnings) => ({
|
|
11
11
|
content: [
|
|
12
12
|
{
|
|
13
13
|
type: 'text',
|
|
14
|
-
text: JSON.stringify({
|
|
14
|
+
text: JSON.stringify({
|
|
15
|
+
code,
|
|
16
|
+
message,
|
|
17
|
+
suggestion,
|
|
18
|
+
...(warnings && warnings.length > 0 && { warnings }),
|
|
19
|
+
}),
|
|
15
20
|
},
|
|
16
21
|
],
|
|
17
22
|
isError: true,
|
|
@@ -20,6 +25,12 @@ exports.errorResponse = errorResponse;
|
|
|
20
25
|
/** COMPONENT_NOT_FOUND 오류 — 여러 tool에서 공통 사용 */
|
|
21
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.');
|
|
22
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;
|
|
23
34
|
/** 입력 정규화 — name.trim().toLowerCase() */
|
|
24
35
|
const normalizeName = (name) => name.trim().toLowerCase();
|
|
25
36
|
exports.normalizeName = normalizeName;
|
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
import type { parse } from 'node-html-parser';
|
|
7
7
|
import type { ComplianceError, TokenData } from '../types.js';
|
|
8
8
|
/** hex 값(lowercase) → 토큰 CSS 변수명 배열 역방향 매핑 빌드 — server.ts에서 1회 호출 */
|
|
9
|
-
|
|
9
|
+
declare const buildTokenValueMap: (tokenData: TokenData) => Map<string, string[]>;
|
|
10
10
|
/** inline style + <style> 블록의 CSS 텍스트를 하나로 합침 */
|
|
11
|
-
|
|
11
|
+
declare const extractAllCss: (root: ReturnType<typeof parse>) => string;
|
|
12
12
|
/** 토큰 미사용 + 무효 토큰 감지 */
|
|
13
|
-
|
|
13
|
+
declare const detectTokenIssues: (params: {
|
|
14
14
|
root: ReturnType<typeof parse>;
|
|
15
15
|
tokenData: TokenData;
|
|
16
16
|
tokenValueMap: Map<string, string[]>;
|
|
@@ -22,3 +22,4 @@ export declare const detectTokenIssues: (params: {
|
|
|
22
22
|
invalid: number;
|
|
23
23
|
};
|
|
24
24
|
};
|
|
25
|
+
export { buildTokenValueMap, detectTokenIssues, extractAllCss };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.extractAllCss = exports.detectTokenIssues = exports.buildTokenValueMap = void 0;
|
|
4
4
|
// ── server.ts 사전 계산용 ───────────────────────────────────────────────
|
|
5
5
|
/** hex 값(lowercase) → 토큰 CSS 변수명 배열 역방향 매핑 빌드 — server.ts에서 1회 호출 */
|
|
6
6
|
const buildTokenValueMap = (tokenData) => {
|
|
@@ -25,14 +25,18 @@ const HEX_PATTERN_SOURCE = /#([0-9a-fA-F]{3,8})\b/g.source;
|
|
|
25
25
|
const RGB_PATTERN_SOURCE = /rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/g.source;
|
|
26
26
|
const VAR_PATTERN_SOURCE = /var\(\s*(--[a-zA-Z0-9-]+)(?:\s*,\s*[^)]+)?\s*\)/g.source;
|
|
27
27
|
// ── 순수 헬퍼 ────────────────────────────────────────────────────────────
|
|
28
|
+
const HEX_RADIX = 16;
|
|
29
|
+
const HEX_BYTE_LENGTH = 2;
|
|
30
|
+
const HEX_SHORT_LENGTH = 3;
|
|
31
|
+
const HEX_FULL_LENGTH = 6;
|
|
28
32
|
/** rgb(r,g,b) → #rrggbb hex 변환 */
|
|
29
|
-
const rgbToHex = (r, g, b) => `#${[r, g, b].map((v) => v.toString(
|
|
33
|
+
const rgbToHex = (r, g, b) => `#${[r, g, b].map((v) => v.toString(HEX_RADIX).padStart(HEX_BYTE_LENGTH, '0')).join('')}`;
|
|
30
34
|
/** 3자리 hex를 6자리로 정규화 */
|
|
31
35
|
const normalizeHex = (hex) => {
|
|
32
36
|
const h = hex.toLowerCase().replace('#', '');
|
|
33
|
-
if (h.length ===
|
|
37
|
+
if (h.length === HEX_SHORT_LENGTH)
|
|
34
38
|
return `#${h[0]}${h[0]}${h[1]}${h[1]}${h[2]}${h[2]}`;
|
|
35
|
-
return `#${h.slice(0,
|
|
39
|
+
return `#${h.slice(0, HEX_FULL_LENGTH)}`;
|
|
36
40
|
};
|
|
37
41
|
/** var()의 prefix가 토큰 시스템에 속하는지 확인 */
|
|
38
42
|
const hasKnownTokenPrefix = (varName, prefixSet) => {
|
|
@@ -83,16 +87,15 @@ exports.extractAllCss = extractAllCss;
|
|
|
83
87
|
/** CSS 텍스트에서 하드코딩된 색상값(hex/rgb)을 추출 */
|
|
84
88
|
const collectHardcodedColors = (css) => {
|
|
85
89
|
const colors = [];
|
|
86
|
-
let match;
|
|
87
90
|
const hexPattern = new RegExp(HEX_PATTERN_SOURCE, 'g');
|
|
88
|
-
|
|
91
|
+
for (const match of css.matchAll(hexPattern)) {
|
|
89
92
|
colors.push({ raw: match[0], normalized: normalizeHex(match[0]) });
|
|
90
93
|
}
|
|
91
94
|
const rgbPattern = new RegExp(RGB_PATTERN_SOURCE, 'g');
|
|
92
|
-
|
|
95
|
+
for (const match of css.matchAll(rgbPattern)) {
|
|
93
96
|
colors.push({
|
|
94
97
|
raw: match[0],
|
|
95
|
-
normalized: rgbToHex(parseInt(match[1], 10), parseInt(match[2], 10), parseInt(match[3], 10)),
|
|
98
|
+
normalized: rgbToHex(Number.parseInt(match[1], 10), Number.parseInt(match[2], 10), Number.parseInt(match[3], 10)),
|
|
96
99
|
});
|
|
97
100
|
}
|
|
98
101
|
return colors;
|
|
@@ -126,9 +129,8 @@ const collectInvalidTokenErrors = (css, tokenNameSet, tokenPrefixSet) => {
|
|
|
126
129
|
const errors = [];
|
|
127
130
|
let correct = 0;
|
|
128
131
|
let invalid = 0;
|
|
129
|
-
let varMatch;
|
|
130
132
|
const varPattern = new RegExp(VAR_PATTERN_SOURCE, 'g');
|
|
131
|
-
|
|
133
|
+
for (const varMatch of css.matchAll(varPattern)) {
|
|
132
134
|
const varName = varMatch[1];
|
|
133
135
|
if (!hasKnownTokenPrefix(varName, tokenPrefixSet))
|
|
134
136
|
continue;
|
|
@@ -151,7 +153,7 @@ const collectInvalidTokenErrors = (css, tokenNameSet, tokenPrefixSet) => {
|
|
|
151
153
|
/** 토큰 미사용 + 무효 토큰 감지 */
|
|
152
154
|
const detectTokenIssues = (params) => {
|
|
153
155
|
const { root, tokenData, tokenValueMap } = params;
|
|
154
|
-
const allCss =
|
|
156
|
+
const allCss = extractAllCss(root);
|
|
155
157
|
const notUsed = collectTokenNotUsedErrors(allCss, tokenValueMap);
|
|
156
158
|
const invalidResult = collectInvalidTokenErrors(allCss, buildTokenNameSet(tokenData), buildTokenPrefixSet(tokenData));
|
|
157
159
|
return {
|
package/bin/version.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* NCDS UI Admin MCP 서버가 노출하는 ui-admin 정합 기준 버전.
|
|
3
|
+
* scripts/generate-version-ts.ts 가 ui-admin/package.json 의 version 으로부터 자동 생성한다.
|
|
4
|
+
* 수동 편집 금지 — 빌드(`pnpm generate:version-ts` 또는 `pnpm build`) 시 덮어쓰여진다.
|
|
3
5
|
*/
|
|
4
|
-
export declare const VERSION = "1.
|
|
6
|
+
export declare const VERSION = "1.8.7";
|
package/bin/version.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.VERSION = void 0;
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* NCDS UI Admin MCP 서버가 노출하는 ui-admin 정합 기준 버전.
|
|
6
|
+
* scripts/generate-version-ts.ts 가 ui-admin/package.json 의 version 으로부터 자동 생성한다.
|
|
7
|
+
* 수동 편집 금지 — 빌드(`pnpm generate:version-ts` 또는 `pnpm build`) 시 덮어쓰여진다.
|
|
6
8
|
*/
|
|
7
|
-
exports.VERSION = '1.
|
|
9
|
+
exports.VERSION = '1.8.7';
|