@monoharada/wcf-mcp 0.9.1 → 0.10.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 +23 -3
- package/core/constants.mjs +1 -1
- package/core/plugins.mjs +184 -3
- package/core/register.mjs +1007 -145
- package/core/tokens.mjs +70 -17
- package/core.mjs +13 -0
- package/data/design-tokens.json +932 -2
- package/data/guidelines-index.json +88 -24
- package/examples/plugins/custom-validation-plugin.mjs +61 -0
- package/package.json +1 -1
- package/runtime-data.mjs +41 -5
- package/server.mjs +5 -0
- package/validator.mjs +209 -31
package/core/tokens.mjs
CHANGED
|
@@ -9,6 +9,19 @@ const TOKEN_MISUSE_ALLOWED_TYPES = Object.freeze(new Set(['color', 'spacing']));
|
|
|
9
9
|
const TOKEN_THEMES = Object.freeze(new Set(['light', 'dark', 'all']));
|
|
10
10
|
const WCAG_LEVELS = Object.freeze(new Set(['A', 'AA', 'AAA', 'all']));
|
|
11
11
|
|
|
12
|
+
function getThemeConfig(designTokensData) {
|
|
13
|
+
const available = Array.isArray(designTokensData?.themes?.available) && designTokensData.themes.available.length > 0
|
|
14
|
+
? [...new Set(designTokensData.themes.available.map((theme) => String(theme).trim().toLowerCase()).filter(Boolean))]
|
|
15
|
+
: ['light'];
|
|
16
|
+
const defaultTheme = typeof designTokensData?.themes?.default === 'string' && available.includes(String(designTokensData.themes.default).toLowerCase())
|
|
17
|
+
? String(designTokensData.themes.default).toLowerCase()
|
|
18
|
+
: available[0];
|
|
19
|
+
return {
|
|
20
|
+
defaultTheme,
|
|
21
|
+
available,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
12
25
|
export function normalizeTokenValue(value) {
|
|
13
26
|
if (typeof value === 'string') return value.trim().toLowerCase().replace(/\s+/g, ' ');
|
|
14
27
|
if (typeof value === 'number') return String(value);
|
|
@@ -54,7 +67,7 @@ export function normalizeTokenIdentifier(value) {
|
|
|
54
67
|
return `--${raw.replace(/^[-]+/, '')}`;
|
|
55
68
|
}
|
|
56
69
|
|
|
57
|
-
export function resolveTokenTheme(theme) {
|
|
70
|
+
export function resolveTokenTheme(theme, designTokensData) {
|
|
58
71
|
const requested = String(theme ?? 'light').trim().toLowerCase() || 'light';
|
|
59
72
|
if (!TOKEN_THEMES.has(requested)) {
|
|
60
73
|
return {
|
|
@@ -63,29 +76,46 @@ export function resolveTokenTheme(theme) {
|
|
|
63
76
|
message: `Unsupported theme: ${requested}. Allowed values are light, dark, all.`,
|
|
64
77
|
};
|
|
65
78
|
}
|
|
66
|
-
|
|
79
|
+
|
|
80
|
+
const themeConfig = getThemeConfig(designTokensData);
|
|
81
|
+
if (requested === 'all') {
|
|
82
|
+
return {
|
|
83
|
+
ok: true,
|
|
84
|
+
requested,
|
|
85
|
+
resolved: themeConfig.defaultTheme,
|
|
86
|
+
available: themeConfig.available,
|
|
87
|
+
mode: 'all',
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!themeConfig.available.includes(requested)) {
|
|
67
92
|
return {
|
|
68
93
|
ok: false,
|
|
69
94
|
errorCode: 'INVALID_THEME',
|
|
70
|
-
message: `Theme "${requested}" is not
|
|
95
|
+
message: `Theme "${requested}" is not supported. Available themes in current data: ${themeConfig.available.join(', ')}.`,
|
|
71
96
|
};
|
|
72
97
|
}
|
|
98
|
+
|
|
73
99
|
return {
|
|
74
100
|
ok: true,
|
|
75
101
|
requested,
|
|
76
|
-
resolved:
|
|
77
|
-
available:
|
|
102
|
+
resolved: requested,
|
|
103
|
+
available: themeConfig.available,
|
|
104
|
+
mode: 'single',
|
|
78
105
|
};
|
|
79
106
|
}
|
|
80
107
|
|
|
81
108
|
export function extractReferencedTokenNames(value) {
|
|
82
109
|
if (typeof value !== 'string') return [];
|
|
83
110
|
const refs = [];
|
|
84
|
-
const re = /var\(
|
|
111
|
+
const re = /var\(([^)]*)\)/g;
|
|
85
112
|
let match;
|
|
86
113
|
while ((match = re.exec(value))) {
|
|
87
|
-
const
|
|
88
|
-
|
|
114
|
+
const candidates = String(match[1] ?? '').match(/--[^,\s)]+/g) ?? [];
|
|
115
|
+
for (const candidate of candidates) {
|
|
116
|
+
const tokenName = normalizeTokenIdentifier(candidate);
|
|
117
|
+
if (tokenName) refs.push(tokenName);
|
|
118
|
+
}
|
|
89
119
|
}
|
|
90
120
|
return [...new Set(refs)];
|
|
91
121
|
}
|
|
@@ -141,12 +171,10 @@ export function buildTokenRelationshipIndex(designTokensData) {
|
|
|
141
171
|
/**
|
|
142
172
|
* Extract which components reference which design tokens via var() in CEM cssProperties.
|
|
143
173
|
* Returns Map<tokenName, Set<componentTagName>>.
|
|
144
|
-
* DD-25: var(--token, fallback) fallback values are not extracted (known limitation).
|
|
145
174
|
*/
|
|
146
175
|
export function buildComponentTokenReferencedBy(manifest) {
|
|
147
176
|
const result = new Map();
|
|
148
177
|
const modules = Array.isArray(manifest?.modules) ? manifest.modules : [];
|
|
149
|
-
const varRe = /var\((--[\w-]+)/g;
|
|
150
178
|
for (const mod of modules) {
|
|
151
179
|
const declarations = Array.isArray(mod?.declarations) ? mod.declarations : [];
|
|
152
180
|
for (const decl of declarations) {
|
|
@@ -155,9 +183,8 @@ export function buildComponentTokenReferencedBy(manifest) {
|
|
|
155
183
|
const cssProps = Array.isArray(decl?.cssProperties) ? decl.cssProperties : [];
|
|
156
184
|
for (const prop of cssProps) {
|
|
157
185
|
const defaultVal = typeof prop?.default === 'string' ? prop.default : '';
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const tokenName = normalizeTokenIdentifier(m[1]);
|
|
186
|
+
for (const tokenRef of extractReferencedTokenNames(defaultVal)) {
|
|
187
|
+
const tokenName = normalizeTokenIdentifier(tokenRef);
|
|
161
188
|
if (!tokenName) continue;
|
|
162
189
|
if (!result.has(tokenName)) result.set(tokenName, new Set());
|
|
163
190
|
result.get(tokenName).add(tag);
|
|
@@ -235,6 +262,30 @@ function buildUsageExamples(token) {
|
|
|
235
262
|
return [`.example { --token-value: ${cssVar}; }`];
|
|
236
263
|
}
|
|
237
264
|
|
|
265
|
+
function resolveTokenValueForTheme(token, themeInfo) {
|
|
266
|
+
if (token?.themeValues && typeof token.themeValues === 'object') {
|
|
267
|
+
const themedValue = token.themeValues[themeInfo.resolved];
|
|
268
|
+
if (typeof themedValue === 'string' && themedValue.trim() !== '') {
|
|
269
|
+
return themedValue;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return token?.value;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function serializeTokenForTheme(token, themeInfo, { includeThemeValues = false } = {}) {
|
|
276
|
+
const serialized = {
|
|
277
|
+
...toTokenSummary({
|
|
278
|
+
...token,
|
|
279
|
+
value: resolveTokenValueForTheme(token, themeInfo),
|
|
280
|
+
}),
|
|
281
|
+
};
|
|
282
|
+
if (token?.group !== undefined) serialized.group = token.group;
|
|
283
|
+
if (includeThemeValues && token?.themeValues && typeof token.themeValues === 'object') {
|
|
284
|
+
serialized.themeValues = token.themeValues;
|
|
285
|
+
}
|
|
286
|
+
return serialized;
|
|
287
|
+
}
|
|
288
|
+
|
|
238
289
|
function buildTokenErrorPayload(code, message, extra = {}) {
|
|
239
290
|
return {
|
|
240
291
|
isError: true,
|
|
@@ -253,7 +304,7 @@ export function buildDesignTokenDetailPayload(designTokensData, name, theme) {
|
|
|
253
304
|
);
|
|
254
305
|
}
|
|
255
306
|
|
|
256
|
-
const themeInfo = resolveTokenTheme(theme);
|
|
307
|
+
const themeInfo = resolveTokenTheme(theme, designTokensData);
|
|
257
308
|
if (!themeInfo.ok) {
|
|
258
309
|
return buildTokenErrorPayload(themeInfo.errorCode, themeInfo.message);
|
|
259
310
|
}
|
|
@@ -294,7 +345,7 @@ export function buildDesignTokenDetailPayload(designTokensData, name, theme) {
|
|
|
294
345
|
isError: false,
|
|
295
346
|
payload: {
|
|
296
347
|
token: {
|
|
297
|
-
...
|
|
348
|
+
...serializeTokenForTheme(token, themeInfo, { includeThemeValues: true }),
|
|
298
349
|
group: token?.group ?? null,
|
|
299
350
|
},
|
|
300
351
|
references,
|
|
@@ -318,7 +369,7 @@ export function buildDesignTokensPayload(designTokensData, { type, category, que
|
|
|
318
369
|
);
|
|
319
370
|
}
|
|
320
371
|
|
|
321
|
-
const themeInfo = resolveTokenTheme(theme);
|
|
372
|
+
const themeInfo = resolveTokenTheme(theme, designTokensData);
|
|
322
373
|
if (!themeInfo.ok) {
|
|
323
374
|
return buildTokenErrorPayload(themeInfo.errorCode, themeInfo.message);
|
|
324
375
|
}
|
|
@@ -335,7 +386,9 @@ export function buildDesignTokensPayload(designTokensData, { type, category, que
|
|
|
335
386
|
isError: false,
|
|
336
387
|
payload: {
|
|
337
388
|
total: tokens.length,
|
|
338
|
-
tokens
|
|
389
|
+
tokens: tokens.map((token) =>
|
|
390
|
+
serializeTokenForTheme(token, themeInfo, { includeThemeValues: themeInfo.requested === 'all' })
|
|
391
|
+
),
|
|
339
392
|
summary: designTokensData.summary,
|
|
340
393
|
theme: {
|
|
341
394
|
requested: themeInfo.requested,
|
package/core.mjs
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* Actual logic lives in core/ sub-modules (DD-02, DD-09).
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import fs from 'node:fs/promises';
|
|
11
12
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
12
13
|
import { registerAll } from './core/register.mjs';
|
|
13
14
|
|
|
@@ -118,6 +119,9 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
|
|
|
118
119
|
const loadTextData = typeof options?.loadTextData === 'function'
|
|
119
120
|
? options.loadTextData
|
|
120
121
|
: null;
|
|
122
|
+
const loadTextDataFromPath = typeof options?.loadTextDataFromPath === 'function'
|
|
123
|
+
? options.loadTextDataFromPath
|
|
124
|
+
: null;
|
|
121
125
|
|
|
122
126
|
const loadJson = async (fileName) => {
|
|
123
127
|
const override = pluginDataSourceMap.get(fileName);
|
|
@@ -128,6 +132,13 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
|
|
|
128
132
|
return loadJsonDataFromPath(override.path, fileName, override.pluginName);
|
|
129
133
|
};
|
|
130
134
|
const loadText = async (fileName) => {
|
|
135
|
+
const override = pluginDataSourceMap.get(fileName);
|
|
136
|
+
if (override) {
|
|
137
|
+
if (loadTextDataFromPath) {
|
|
138
|
+
return loadTextDataFromPath(override.path, fileName, override.pluginName);
|
|
139
|
+
}
|
|
140
|
+
return fs.readFile(override.path, 'utf8');
|
|
141
|
+
}
|
|
131
142
|
if (!loadTextData) throw new Error(`Text data loader not configured for ${fileName}`);
|
|
132
143
|
return loadTextData(fileName);
|
|
133
144
|
};
|
|
@@ -145,6 +156,7 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
|
|
|
145
156
|
buildSlotNameMap = () => new Map(),
|
|
146
157
|
detectInvalidSlotName = () => [],
|
|
147
158
|
detectMissingRequiredAttributes = () => [],
|
|
159
|
+
detectDuplicateIdsInMarkup = () => [],
|
|
148
160
|
detectOrphanedChildComponents = () => [],
|
|
149
161
|
detectEmptyInteractiveElement = () => [],
|
|
150
162
|
detectNonLowercaseAttributes = () => [],
|
|
@@ -226,6 +238,7 @@ export async function createMcpServer(loadJsonData, loadValidator, options = {})
|
|
|
226
238
|
buildSlotNameMap,
|
|
227
239
|
detectInvalidSlotName,
|
|
228
240
|
detectMissingRequiredAttributes,
|
|
241
|
+
detectDuplicateIdsInMarkup,
|
|
229
242
|
detectOrphanedChildComponents,
|
|
230
243
|
detectEmptyInteractiveElement,
|
|
231
244
|
detectNonLowercaseAttributes,
|