@cooperco/component-library-mcp-server 0.1.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 +106 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +55 -0
- package/dist/parsers/passthrough-parser.d.ts +13 -0
- package/dist/parsers/passthrough-parser.js +115 -0
- package/dist/parsers/typescript-parser.d.ts +11 -0
- package/dist/parsers/typescript-parser.js +77 -0
- package/dist/parsers/vue-sfc-parser.d.ts +10 -0
- package/dist/parsers/vue-sfc-parser.js +61 -0
- package/dist/registry/component-descriptions.d.ts +1 -0
- package/dist/registry/component-descriptions.js +43 -0
- package/dist/registry/component-registry.d.ts +23 -0
- package/dist/registry/component-registry.js +256 -0
- package/dist/scaffolding/templates.d.ts +21 -0
- package/dist/scaffolding/templates.js +122 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.js +61 -0
- package/dist/tools/get-color-palettes.d.ts +14 -0
- package/dist/tools/get-color-palettes.js +68 -0
- package/dist/tools/get-component-props.d.ts +14 -0
- package/dist/tools/get-component-props.js +32 -0
- package/dist/tools/get-component.d.ts +14 -0
- package/dist/tools/get-component.js +109 -0
- package/dist/tools/get-passthrough-config.d.ts +14 -0
- package/dist/tools/get-passthrough-config.js +77 -0
- package/dist/tools/list-components.d.ts +7 -0
- package/dist/tools/list-components.js +21 -0
- package/dist/tools/scaffold-component.d.ts +29 -0
- package/dist/tools/scaffold-component.js +128 -0
- package/dist/tools/search-components.d.ts +7 -0
- package/dist/tools/search-components.js +21 -0
- package/dist/types.d.ts +43 -0
- package/dist/types.js +1 -0
- package/package.json +41 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { parseTypeScriptFile, findMainInterface, findPassthroughInterface, extractVariantsFromProp, } from '../parsers/typescript-parser.js';
|
|
4
|
+
import { parseVueSfc } from '../parsers/vue-sfc-parser.js';
|
|
5
|
+
import { parsePassthroughFile, parseDefaultPassthrough, findPassthroughForComponent, } from '../parsers/passthrough-parser.js';
|
|
6
|
+
import { getComponentDescription } from './component-descriptions.js';
|
|
7
|
+
// Fallback set for components that don't follow the *Module naming convention
|
|
8
|
+
// but should still be categorized as modules
|
|
9
|
+
const MODULE_COMPONENTS_OVERRIDE = new Set([
|
|
10
|
+
'ContentModule', // Doesn't end with "Module" in a unique way but is a module
|
|
11
|
+
]);
|
|
12
|
+
const FALLBACK_VARIANT_PROP_NAMES = ['variant', 'type', 'buttonType'];
|
|
13
|
+
/**
|
|
14
|
+
* Detects the variant prop by cross-referencing passthrough variant keys
|
|
15
|
+
* with props that have string literal union types.
|
|
16
|
+
* Falls back to checking known variant prop names.
|
|
17
|
+
*/
|
|
18
|
+
const detectVariantProp = (props, pt) => {
|
|
19
|
+
// Collect all props with string literal union values
|
|
20
|
+
const propsWithLiterals = props
|
|
21
|
+
.map((p) => ({ prop: p, values: extractVariantsFromProp(p) }))
|
|
22
|
+
.filter((entry) => entry.values.length > 0);
|
|
23
|
+
// If passthrough has variant keys, try to match them against prop values
|
|
24
|
+
if (pt?.variants) {
|
|
25
|
+
const ptVariantKeys = Object.keys(pt.variants);
|
|
26
|
+
const ptKeySet = new Set(ptVariantKeys.map((k) => k.toLowerCase()));
|
|
27
|
+
for (const { prop, values } of propsWithLiterals) {
|
|
28
|
+
const propValueSet = new Set(values.map((v) => v.toLowerCase()));
|
|
29
|
+
const overlap = [...ptKeySet].filter((k) => propValueSet.has(k));
|
|
30
|
+
// Every passthrough variant key must match a prop value
|
|
31
|
+
if (overlap.length > 0 && overlap.length >= ptKeySet.size) {
|
|
32
|
+
return {
|
|
33
|
+
detectedVariants: values,
|
|
34
|
+
detectedVariantPropName: prop.name,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// Fallback: check known variant prop names
|
|
40
|
+
for (const name of FALLBACK_VARIANT_PROP_NAMES) {
|
|
41
|
+
const prop = props.find((p) => p.name === name);
|
|
42
|
+
if (prop) {
|
|
43
|
+
const values = extractVariantsFromProp(prop);
|
|
44
|
+
if (values.length > 0) {
|
|
45
|
+
return {
|
|
46
|
+
detectedVariants: values,
|
|
47
|
+
detectedVariantPropName: prop.name,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return { detectedVariants: [], detectedVariantPropName: null };
|
|
53
|
+
};
|
|
54
|
+
export class ComponentRegistry {
|
|
55
|
+
cache = new Map();
|
|
56
|
+
discoveryCache = null;
|
|
57
|
+
discoveryMtime = 0;
|
|
58
|
+
passthroughCache = null;
|
|
59
|
+
passthroughMtime = 0;
|
|
60
|
+
projectRoot;
|
|
61
|
+
constructor(projectRoot) {
|
|
62
|
+
this.projectRoot = projectRoot;
|
|
63
|
+
}
|
|
64
|
+
async discoverComponents() {
|
|
65
|
+
const barrelPath = path.join(this.projectRoot, 'src/components/components.ts');
|
|
66
|
+
// Invalidate discovery cache if barrel file has changed
|
|
67
|
+
const barrelStat = await fs.stat(barrelPath);
|
|
68
|
+
if (this.discoveryCache && barrelStat.mtimeMs === this.discoveryMtime) {
|
|
69
|
+
return this.discoveryCache;
|
|
70
|
+
}
|
|
71
|
+
this.discoveryMtime = barrelStat.mtimeMs;
|
|
72
|
+
const source = await fs.readFile(barrelPath, 'utf-8');
|
|
73
|
+
const components = [];
|
|
74
|
+
const exportRegex = /export\s*\{\s*default\s+as\s+(\w+)\s*\}\s*from\s*'\.\/([^']+)'/g;
|
|
75
|
+
for (const match of source.matchAll(exportRegex)) {
|
|
76
|
+
const exportName = match[1];
|
|
77
|
+
const relativePath = match[2];
|
|
78
|
+
// Resolve the .vue file path
|
|
79
|
+
const absoluteVuePath = path.join(this.projectRoot, 'src/components', relativePath);
|
|
80
|
+
// Try to find the corresponding .ts type file
|
|
81
|
+
const vueParsed = path.parse(absoluteVuePath);
|
|
82
|
+
const tsPath = path.join(vueParsed.dir, `${vueParsed.name}.ts`);
|
|
83
|
+
let absoluteTypePath = null;
|
|
84
|
+
try {
|
|
85
|
+
await fs.access(tsPath);
|
|
86
|
+
absoluteTypePath = tsPath;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// No type file exists for this component
|
|
90
|
+
}
|
|
91
|
+
components.push({
|
|
92
|
+
exportName,
|
|
93
|
+
relativePath,
|
|
94
|
+
absoluteVuePath,
|
|
95
|
+
absoluteTypePath,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
this.discoveryCache = components;
|
|
99
|
+
return components;
|
|
100
|
+
}
|
|
101
|
+
async getComponent(name) {
|
|
102
|
+
const components = await this.discoverComponents();
|
|
103
|
+
const discovery = components.find((c) => c.exportName.toLowerCase() === name.toLowerCase());
|
|
104
|
+
if (!discovery)
|
|
105
|
+
return null;
|
|
106
|
+
// Check cache validity
|
|
107
|
+
const cached = this.cache.get(discovery.exportName);
|
|
108
|
+
if (cached) {
|
|
109
|
+
const vueStat = await fs.stat(discovery.absoluteVuePath);
|
|
110
|
+
const typeStat = discovery.absoluteTypePath
|
|
111
|
+
? await fs.stat(discovery.absoluteTypePath)
|
|
112
|
+
: null;
|
|
113
|
+
if (vueStat.mtimeMs === cached.vueMtime &&
|
|
114
|
+
(typeStat?.mtimeMs ?? 0) === cached.typeMtime) {
|
|
115
|
+
return cached.metadata;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return this.parseComponent(discovery);
|
|
119
|
+
}
|
|
120
|
+
async listComponents() {
|
|
121
|
+
const discoveries = await this.discoverComponents();
|
|
122
|
+
const results = [];
|
|
123
|
+
for (const discovery of discoveries) {
|
|
124
|
+
const metadata = await this.getComponent(discovery.exportName);
|
|
125
|
+
if (metadata)
|
|
126
|
+
results.push(metadata);
|
|
127
|
+
}
|
|
128
|
+
return results;
|
|
129
|
+
}
|
|
130
|
+
async searchComponents(query, category) {
|
|
131
|
+
const all = await this.listComponents();
|
|
132
|
+
const lowerQuery = query.toLowerCase();
|
|
133
|
+
return all.filter((c) => {
|
|
134
|
+
if (category && category !== 'all' && c.category !== category)
|
|
135
|
+
return false;
|
|
136
|
+
return (c.name.toLowerCase().includes(lowerQuery) ||
|
|
137
|
+
c.typename.toLowerCase().includes(lowerQuery) ||
|
|
138
|
+
c.description.toLowerCase().includes(lowerQuery) ||
|
|
139
|
+
c.props.some((p) => p.name.toLowerCase().includes(lowerQuery)));
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
async getPassthroughForComponent(componentName) {
|
|
143
|
+
const passthroughs = await this.loadPassthroughs();
|
|
144
|
+
const pt = findPassthroughForComponent(passthroughs, componentName);
|
|
145
|
+
if (!pt)
|
|
146
|
+
return null;
|
|
147
|
+
return { variants: pt.variants, keys: pt.keys };
|
|
148
|
+
}
|
|
149
|
+
async getColorPalettes() {
|
|
150
|
+
const palettePath = path.join(this.projectRoot, 'src/config/colorPalettes.ts');
|
|
151
|
+
return fs.readFile(palettePath, 'utf-8');
|
|
152
|
+
}
|
|
153
|
+
async getDefaultPassthrough() {
|
|
154
|
+
const ptPath = path.join(this.projectRoot, 'src/config/defaultPassthrough/index.ts');
|
|
155
|
+
const source = await fs.readFile(ptPath, 'utf-8');
|
|
156
|
+
return parseDefaultPassthrough(source);
|
|
157
|
+
}
|
|
158
|
+
async componentExists(name) {
|
|
159
|
+
const components = await this.discoverComponents();
|
|
160
|
+
return components.some((c) => c.exportName.toLowerCase() === name.toLowerCase());
|
|
161
|
+
}
|
|
162
|
+
async loadPassthroughs() {
|
|
163
|
+
const ptPath = path.join(this.projectRoot, 'src/config/defaultPassthrough/index.ts');
|
|
164
|
+
// Invalidate passthrough cache if file has changed
|
|
165
|
+
const ptStat = await fs.stat(ptPath);
|
|
166
|
+
if (this.passthroughCache && ptStat.mtimeMs === this.passthroughMtime) {
|
|
167
|
+
return this.passthroughCache;
|
|
168
|
+
}
|
|
169
|
+
this.passthroughMtime = ptStat.mtimeMs;
|
|
170
|
+
const source = await fs.readFile(ptPath, 'utf-8');
|
|
171
|
+
this.passthroughCache = parsePassthroughFile(source);
|
|
172
|
+
return this.passthroughCache;
|
|
173
|
+
}
|
|
174
|
+
async parseComponent(discovery) {
|
|
175
|
+
const vueSource = await fs.readFile(discovery.absoluteVuePath, 'utf-8');
|
|
176
|
+
const vueStat = await fs.stat(discovery.absoluteVuePath);
|
|
177
|
+
let typeSource = null;
|
|
178
|
+
let typeStat = null;
|
|
179
|
+
if (discovery.absoluteTypePath) {
|
|
180
|
+
typeSource = await fs.readFile(discovery.absoluteTypePath, 'utf-8');
|
|
181
|
+
typeStat = await fs.stat(discovery.absoluteTypePath);
|
|
182
|
+
}
|
|
183
|
+
// Parse Vue SFC
|
|
184
|
+
const sfcData = parseVueSfc(vueSource);
|
|
185
|
+
// Parse TypeScript interfaces
|
|
186
|
+
let props = [];
|
|
187
|
+
let extendsComponent = false;
|
|
188
|
+
let typename = discovery.exportName;
|
|
189
|
+
let passthroughInterfaceName = null;
|
|
190
|
+
let variants = [];
|
|
191
|
+
let variantPropName = null;
|
|
192
|
+
if (typeSource && discovery.absoluteTypePath) {
|
|
193
|
+
const interfaces = parseTypeScriptFile(typeSource, path.basename(discovery.absoluteTypePath));
|
|
194
|
+
const mainInterface = findMainInterface(interfaces, discovery.exportName);
|
|
195
|
+
if (mainInterface) {
|
|
196
|
+
props = mainInterface.props;
|
|
197
|
+
extendsComponent = mainInterface.extendsTypes.includes('Component');
|
|
198
|
+
typename = mainInterface.name;
|
|
199
|
+
// Extract __typename value
|
|
200
|
+
const typenameProp = mainInterface.props.find((p) => p.name === '__typename');
|
|
201
|
+
if (typenameProp) {
|
|
202
|
+
const tnMatch = typenameProp.type.match(/'([^']+)'/);
|
|
203
|
+
if (tnMatch)
|
|
204
|
+
typename = tnMatch[1];
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const ptInterface = findPassthroughInterface(interfaces);
|
|
208
|
+
if (ptInterface) {
|
|
209
|
+
passthroughInterfaceName = ptInterface.name;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
// Merge defaults from Vue SFC
|
|
213
|
+
for (const prop of props) {
|
|
214
|
+
if (sfcData.defaults[prop.name] !== undefined) {
|
|
215
|
+
prop.defaultValue = sfcData.defaults[prop.name];
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// Get passthrough keys
|
|
219
|
+
const passthroughs = await this.loadPassthroughs();
|
|
220
|
+
const pt = findPassthroughForComponent(passthroughs, discovery.exportName);
|
|
221
|
+
// Detect variant prop by cross-referencing passthrough variant keys with prop values
|
|
222
|
+
const detected = detectVariantProp(props, pt);
|
|
223
|
+
variants = detected.detectedVariants;
|
|
224
|
+
variantPropName = detected.detectedVariantPropName;
|
|
225
|
+
const directory = path.relative(this.projectRoot, path.dirname(discovery.absoluteVuePath));
|
|
226
|
+
const isModule = discovery.exportName.endsWith('Module') ||
|
|
227
|
+
MODULE_COMPONENTS_OVERRIDE.has(discovery.exportName);
|
|
228
|
+
const metadata = {
|
|
229
|
+
name: discovery.exportName,
|
|
230
|
+
category: isModule ? 'module' : 'component',
|
|
231
|
+
typename,
|
|
232
|
+
directory,
|
|
233
|
+
vueFile: path.relative(this.projectRoot, discovery.absoluteVuePath),
|
|
234
|
+
typeFile: discovery.absoluteTypePath
|
|
235
|
+
? path.relative(this.projectRoot, discovery.absoluteTypePath)
|
|
236
|
+
: null,
|
|
237
|
+
interfaceName: typename,
|
|
238
|
+
props,
|
|
239
|
+
emits: sfcData.emits,
|
|
240
|
+
slots: sfcData.slots,
|
|
241
|
+
variants,
|
|
242
|
+
variantPropName,
|
|
243
|
+
passthroughInterface: passthroughInterfaceName,
|
|
244
|
+
passthroughKeys: pt?.keys ?? [],
|
|
245
|
+
extendsComponent,
|
|
246
|
+
hasColorPalette: sfcData.usesColorPalette,
|
|
247
|
+
description: getComponentDescription(discovery.exportName),
|
|
248
|
+
};
|
|
249
|
+
this.cache.set(discovery.exportName, {
|
|
250
|
+
metadata,
|
|
251
|
+
vueMtime: vueStat.mtimeMs,
|
|
252
|
+
typeMtime: typeStat?.mtimeMs ?? 0,
|
|
253
|
+
});
|
|
254
|
+
return metadata;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
interface ScaffoldProp {
|
|
2
|
+
name: string;
|
|
3
|
+
type: string;
|
|
4
|
+
required?: boolean;
|
|
5
|
+
default?: string;
|
|
6
|
+
}
|
|
7
|
+
interface ScaffoldOptions {
|
|
8
|
+
name: string;
|
|
9
|
+
category: 'component' | 'module';
|
|
10
|
+
props?: ScaffoldProp[];
|
|
11
|
+
hasColorPalette?: boolean;
|
|
12
|
+
hasPassthrough?: boolean;
|
|
13
|
+
variants?: string[];
|
|
14
|
+
variantPropName?: string;
|
|
15
|
+
}
|
|
16
|
+
export declare const generateTypeFile: (options: ScaffoldOptions) => string;
|
|
17
|
+
export declare const generateVueFile: (options: ScaffoldOptions) => string;
|
|
18
|
+
export declare const generateStoryFile: (options: ScaffoldOptions) => string;
|
|
19
|
+
export declare const generateComponentExport: (name: string) => string;
|
|
20
|
+
export declare const generateTypeExport: (name: string) => string;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
export const generateTypeFile = (options) => {
|
|
2
|
+
const { name, props = [], hasPassthrough = true, variants } = options;
|
|
3
|
+
const lines = [];
|
|
4
|
+
lines.push("import { Component } from '../../types'");
|
|
5
|
+
if (hasPassthrough) {
|
|
6
|
+
lines.push("import { ComponentPassthrough } from '../../types'");
|
|
7
|
+
}
|
|
8
|
+
lines.push('');
|
|
9
|
+
if (hasPassthrough) {
|
|
10
|
+
lines.push(`export interface ${name}Passthrough extends ComponentPassthrough {`);
|
|
11
|
+
lines.push('\troot?: string');
|
|
12
|
+
lines.push('}');
|
|
13
|
+
lines.push('');
|
|
14
|
+
}
|
|
15
|
+
lines.push(`export interface ${name} extends Component {`);
|
|
16
|
+
lines.push(`\t__typename?: '${name}'`);
|
|
17
|
+
for (const prop of props) {
|
|
18
|
+
const optional = prop.required ? '' : '?';
|
|
19
|
+
lines.push(`\t${prop.name}${optional}: ${prop.type}`);
|
|
20
|
+
}
|
|
21
|
+
if (variants && variants.length > 0) {
|
|
22
|
+
const propName = options.variantPropName ?? 'variant';
|
|
23
|
+
const variantType = variants.map((v) => `'${v}'`).join(' | ');
|
|
24
|
+
lines.push(`\t${propName}?: ${variantType}`);
|
|
25
|
+
}
|
|
26
|
+
lines.push('\tbackgroundColor?: string');
|
|
27
|
+
lines.push('\ttextColor?: string');
|
|
28
|
+
if (hasPassthrough) {
|
|
29
|
+
lines.push(`\tpt?: ${name}Passthrough`);
|
|
30
|
+
}
|
|
31
|
+
lines.push('\tisChild?: boolean');
|
|
32
|
+
lines.push('}');
|
|
33
|
+
lines.push('');
|
|
34
|
+
return lines.join('\n');
|
|
35
|
+
};
|
|
36
|
+
export const generateVueFile = (options) => {
|
|
37
|
+
const { name, props = [], hasColorPalette = true, hasPassthrough = true, variants, } = options;
|
|
38
|
+
const lines = [];
|
|
39
|
+
lines.push('<script setup lang="ts">');
|
|
40
|
+
// Imports
|
|
41
|
+
const imports = [];
|
|
42
|
+
imports.push(`import { ${name} } from './${name}'`);
|
|
43
|
+
imports.push("import { computed } from 'vue'");
|
|
44
|
+
imports.push("import { twMerge } from 'tailwind-merge'");
|
|
45
|
+
if (hasColorPalette) {
|
|
46
|
+
imports.push("import { withColorPalette } from '../../config/colorPalettes'");
|
|
47
|
+
}
|
|
48
|
+
if (hasPassthrough) {
|
|
49
|
+
imports.push("import { combinePassthroughs } from '../../config/defaultPassthrough'");
|
|
50
|
+
}
|
|
51
|
+
lines.push(imports.join('\n'));
|
|
52
|
+
lines.push('');
|
|
53
|
+
// Props with defaults
|
|
54
|
+
const defaults = ['\tisChild: false,'];
|
|
55
|
+
if (variants && variants.length > 0) {
|
|
56
|
+
const propName = options.variantPropName ?? 'variant';
|
|
57
|
+
defaults.push(`\t${propName}: '${variants[0]}',`);
|
|
58
|
+
}
|
|
59
|
+
for (const prop of props) {
|
|
60
|
+
if (prop.default !== undefined) {
|
|
61
|
+
defaults.push(`\t${prop.name}: ${prop.default},`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
lines.push(`const props = withDefaults(defineProps<${name}>(), {`);
|
|
65
|
+
lines.push(defaults.join('\n'));
|
|
66
|
+
lines.push('})');
|
|
67
|
+
lines.push('');
|
|
68
|
+
if (hasColorPalette) {
|
|
69
|
+
lines.push('const { backgroundColor, palette } = withColorPalette(props)');
|
|
70
|
+
lines.push('');
|
|
71
|
+
}
|
|
72
|
+
lines.push('const computedClass = computed(() => {');
|
|
73
|
+
lines.push('\treturn twMerge(');
|
|
74
|
+
if (hasColorPalette) {
|
|
75
|
+
lines.push('\t\t`bg-${backgroundColor}`,');
|
|
76
|
+
}
|
|
77
|
+
lines.push("\t\tprops.class ?? ''");
|
|
78
|
+
lines.push('\t)');
|
|
79
|
+
lines.push('})');
|
|
80
|
+
lines.push('</script>');
|
|
81
|
+
lines.push('');
|
|
82
|
+
lines.push('<template>');
|
|
83
|
+
lines.push('\t<component :is="isChild ? \'div\' : \'section\'" :class="computedClass">');
|
|
84
|
+
lines.push(`\t\t<!-- ${name} content -->`);
|
|
85
|
+
lines.push('\t</component>');
|
|
86
|
+
lines.push('</template>');
|
|
87
|
+
lines.push('');
|
|
88
|
+
return lines.join('\n');
|
|
89
|
+
};
|
|
90
|
+
export const generateStoryFile = (options) => {
|
|
91
|
+
const { name, category } = options;
|
|
92
|
+
const storyCategory = category === 'module' ? 'Modules' : 'Components';
|
|
93
|
+
const importPath = `../../src/components/${name}/${name}.vue`;
|
|
94
|
+
const lines = [];
|
|
95
|
+
lines.push("import { Meta, StoryObj } from '@storybook/vue3'");
|
|
96
|
+
lines.push(`import ${name} from '${importPath}'`);
|
|
97
|
+
lines.push("import { DEFAULT_ARG_TYPES } from '..'");
|
|
98
|
+
lines.push('');
|
|
99
|
+
lines.push(`const meta: Meta<typeof ${name}> = {`);
|
|
100
|
+
lines.push(`\tcomponent: ${name},`);
|
|
101
|
+
lines.push('\targTypes: DEFAULT_ARG_TYPES,');
|
|
102
|
+
lines.push(`\ttitle: '${storyCategory}/${name}',`);
|
|
103
|
+
lines.push('}');
|
|
104
|
+
lines.push('');
|
|
105
|
+
lines.push(`type Story = StoryObj<typeof ${name}>`);
|
|
106
|
+
lines.push('');
|
|
107
|
+
lines.push('export default meta');
|
|
108
|
+
lines.push('');
|
|
109
|
+
lines.push('export const Default: Story = {');
|
|
110
|
+
lines.push('\targs: {');
|
|
111
|
+
lines.push("\t\tbackgroundColor: 'lily',");
|
|
112
|
+
lines.push('\t},');
|
|
113
|
+
lines.push('}');
|
|
114
|
+
lines.push('');
|
|
115
|
+
return lines.join('\n');
|
|
116
|
+
};
|
|
117
|
+
export const generateComponentExport = (name) => {
|
|
118
|
+
return `export { default as ${name} } from './${name}/${name}.vue'`;
|
|
119
|
+
};
|
|
120
|
+
export const generateTypeExport = (name) => {
|
|
121
|
+
return `export type { ${name} as ${name}Type } from './${name}/${name}'`;
|
|
122
|
+
};
|
package/dist/server.d.ts
ADDED
package/dist/server.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { handleListComponents } from './tools/list-components.js';
|
|
3
|
+
import { handleGetComponent } from './tools/get-component.js';
|
|
4
|
+
import { handleGetComponentProps } from './tools/get-component-props.js';
|
|
5
|
+
import { handleGetColorPalettes } from './tools/get-color-palettes.js';
|
|
6
|
+
import { handleGetPassthroughConfig } from './tools/get-passthrough-config.js';
|
|
7
|
+
import { handleSearchComponents } from './tools/search-components.js';
|
|
8
|
+
import { handleScaffoldComponent } from './tools/scaffold-component.js';
|
|
9
|
+
export const registerAllTools = (server, registry, projectRoot) => {
|
|
10
|
+
server.tool('list-components', 'List all available components in the Cooper Component Library with descriptions and variants', {}, async () => handleListComponents(registry));
|
|
11
|
+
server.tool('get-component', 'Get full documentation for a specific component including props, types, events, slots, passthrough config, and source code', {
|
|
12
|
+
name: z
|
|
13
|
+
.string()
|
|
14
|
+
.describe('Component name in PascalCase, e.g., "ContainerModule" or "CTA"'),
|
|
15
|
+
}, async ({ name }) => handleGetComponent(registry, name, projectRoot));
|
|
16
|
+
server.tool('get-component-props', 'Get detailed prop information for a component including types, defaults, and required status', { name: z.string().describe('Component name in PascalCase') }, async ({ name }) => handleGetComponentProps(registry, name));
|
|
17
|
+
server.tool('get-color-palettes', 'Get all available color palettes, their structure, usage patterns, and full source code', {}, async () => handleGetColorPalettes(registry));
|
|
18
|
+
server.tool('get-passthrough-config', 'Get the passthrough (style customization) configuration for a specific component, including default Tailwind classes', { name: z.string().describe('Component name in PascalCase') }, async ({ name }) => handleGetPassthroughConfig(registry, name));
|
|
19
|
+
server.tool('search-components', 'Search components by name, category, or prop names', {
|
|
20
|
+
query: z
|
|
21
|
+
.string()
|
|
22
|
+
.describe('Search term to match against component names, descriptions, and prop names'),
|
|
23
|
+
category: z
|
|
24
|
+
.enum(['component', 'module', 'all'])
|
|
25
|
+
.optional()
|
|
26
|
+
.describe('Filter by component category'),
|
|
27
|
+
}, async ({ query, category }) => handleSearchComponents(registry, query, category));
|
|
28
|
+
server.tool('scaffold-component', 'Generate a new component following library conventions. Creates .vue, .ts, and .stories.ts files, and updates barrel exports.', {
|
|
29
|
+
name: z
|
|
30
|
+
.string()
|
|
31
|
+
.describe('PascalCase component name, e.g., "BannerModule"'),
|
|
32
|
+
category: z
|
|
33
|
+
.enum(['component', 'module'])
|
|
34
|
+
.describe('Whether this is a basic component or a module'),
|
|
35
|
+
props: z
|
|
36
|
+
.array(z.object({
|
|
37
|
+
name: z.string(),
|
|
38
|
+
type: z.string(),
|
|
39
|
+
required: z.boolean().optional(),
|
|
40
|
+
default: z.string().optional(),
|
|
41
|
+
}))
|
|
42
|
+
.optional()
|
|
43
|
+
.describe('Initial props to include'),
|
|
44
|
+
hasColorPalette: z
|
|
45
|
+
.boolean()
|
|
46
|
+
.optional()
|
|
47
|
+
.describe('Include color palette integration (default: true)'),
|
|
48
|
+
hasPassthrough: z
|
|
49
|
+
.boolean()
|
|
50
|
+
.optional()
|
|
51
|
+
.describe('Include passthrough system (default: true)'),
|
|
52
|
+
variants: z
|
|
53
|
+
.array(z.string())
|
|
54
|
+
.optional()
|
|
55
|
+
.describe('Variant names, e.g., ["Default", "Hero"]'),
|
|
56
|
+
variantPropName: z
|
|
57
|
+
.string()
|
|
58
|
+
.optional()
|
|
59
|
+
.describe('Prop name for variants, e.g., "type" or "buttonType". Defaults to "variant".'),
|
|
60
|
+
}, async (args) => handleScaffoldComponent(registry, args, projectRoot));
|
|
61
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ComponentRegistry } from '../registry/component-registry.js';
|
|
2
|
+
export declare const handleGetColorPalettes: (registry: ComponentRegistry) => Promise<{
|
|
3
|
+
content: {
|
|
4
|
+
type: "text";
|
|
5
|
+
text: string;
|
|
6
|
+
}[];
|
|
7
|
+
isError: boolean;
|
|
8
|
+
} | {
|
|
9
|
+
content: {
|
|
10
|
+
type: "text";
|
|
11
|
+
text: string;
|
|
12
|
+
}[];
|
|
13
|
+
isError?: undefined;
|
|
14
|
+
}>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export const handleGetColorPalettes = async (registry) => {
|
|
2
|
+
let source;
|
|
3
|
+
try {
|
|
4
|
+
source = await registry.getColorPalettes();
|
|
5
|
+
}
|
|
6
|
+
catch {
|
|
7
|
+
return {
|
|
8
|
+
content: [
|
|
9
|
+
{
|
|
10
|
+
type: 'text',
|
|
11
|
+
text: 'Error: Could not read color palettes file. Ensure src/config/colorPalettes.ts exists.',
|
|
12
|
+
},
|
|
13
|
+
],
|
|
14
|
+
isError: true,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
// Extract palette names from the source
|
|
18
|
+
const paletteNames = [];
|
|
19
|
+
const paletteRegex = /(\w+)\s*:\s*\{[\s\S]*?headline\s*:/g;
|
|
20
|
+
for (const match of source.matchAll(paletteRegex)) {
|
|
21
|
+
if (match[1] !== 'cta' &&
|
|
22
|
+
match[1] !== 'tile' &&
|
|
23
|
+
match[1] !== 'label' &&
|
|
24
|
+
match[1] !== 'uiElement' &&
|
|
25
|
+
match[1] !== 'interactive') {
|
|
26
|
+
paletteNames.push(match[1]);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
let output = '# Color Palette System\n\n';
|
|
30
|
+
output += '## Available Palettes\n\n';
|
|
31
|
+
output +=
|
|
32
|
+
paletteNames.length > 0
|
|
33
|
+
? paletteNames.map((n) => `\`${n}\``).join(', ')
|
|
34
|
+
: 'See source file for palette names';
|
|
35
|
+
output += '\n\n';
|
|
36
|
+
output += '## Usage\n\n';
|
|
37
|
+
output += '```typescript\n';
|
|
38
|
+
output += "import { withColorPalette } from '@/config/colorPalettes'\n\n";
|
|
39
|
+
output += 'const { backgroundColor, palette } = withColorPalette(props)\n\n';
|
|
40
|
+
output += '// In template:\n';
|
|
41
|
+
output += '// <h2 :class="`text-${palette.headline}`">Title</h2>\n';
|
|
42
|
+
output += '// <p :class="`text-${palette.copy}`">Text</p>\n';
|
|
43
|
+
output += '```\n\n';
|
|
44
|
+
output += '## Override\n\n';
|
|
45
|
+
output += '```typescript\n';
|
|
46
|
+
output += "import { overridePalette } from '@/config/colorPalettes'\n";
|
|
47
|
+
output += "overridePalette(customPalettes, 'defaultColorName')\n";
|
|
48
|
+
output += '```\n\n';
|
|
49
|
+
output += '## Palette Structure\n\n';
|
|
50
|
+
output += 'Each palette provides these color roles:\n';
|
|
51
|
+
output += '- `headline` - Text color for headings\n';
|
|
52
|
+
output += '- `subheadline` - Text color for subheadings\n';
|
|
53
|
+
output += '- `copy` - Body text color\n';
|
|
54
|
+
output += '- `copyAccent` - Highlighted text color\n';
|
|
55
|
+
output += '- `bullet` - List bullet color\n';
|
|
56
|
+
output += '- `cta.fill` - Filled button colors (bg, copy)\n';
|
|
57
|
+
output += '- `cta.outline` - Outlined button colors (copy, border)\n';
|
|
58
|
+
output += '- `cta.link` - Link button color (copy)\n';
|
|
59
|
+
output += '- `cta.logo` - Logo button color (copy)\n';
|
|
60
|
+
output += '- `uiElement` - UI element colors (bg, copy, border)\n';
|
|
61
|
+
output += '- `tile` - Tile-specific colors\n';
|
|
62
|
+
output += '- `label` - Label background and text colors\n\n';
|
|
63
|
+
output += '## Full Source\n\n';
|
|
64
|
+
output += '```typescript\n';
|
|
65
|
+
output += source;
|
|
66
|
+
output += '\n```\n';
|
|
67
|
+
return { content: [{ type: 'text', text: output }] };
|
|
68
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ComponentRegistry } from '../registry/component-registry.js';
|
|
2
|
+
export declare const handleGetComponentProps: (registry: ComponentRegistry, name: string) => Promise<{
|
|
3
|
+
content: {
|
|
4
|
+
type: "text";
|
|
5
|
+
text: string;
|
|
6
|
+
}[];
|
|
7
|
+
isError: boolean;
|
|
8
|
+
} | {
|
|
9
|
+
content: {
|
|
10
|
+
type: "text";
|
|
11
|
+
text: string;
|
|
12
|
+
}[];
|
|
13
|
+
isError?: undefined;
|
|
14
|
+
}>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export const handleGetComponentProps = async (registry, name) => {
|
|
2
|
+
const metadata = await registry.getComponent(name);
|
|
3
|
+
if (!metadata) {
|
|
4
|
+
return {
|
|
5
|
+
content: [
|
|
6
|
+
{
|
|
7
|
+
type: 'text',
|
|
8
|
+
text: `Component "${name}" not found. Use list-components to see all available components.`,
|
|
9
|
+
},
|
|
10
|
+
],
|
|
11
|
+
isError: true,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
let output = `# ${metadata.name} Props\n\n`;
|
|
15
|
+
output += `**Interface:** ${metadata.interfaceName}\n`;
|
|
16
|
+
output += `**Extends Component:** ${metadata.extendsComponent ? 'Yes (inherits class?: string)' : 'No'}\n\n`;
|
|
17
|
+
if (metadata.props.length > 0) {
|
|
18
|
+
output += '| Prop | Type | Required | Default | Description |\n';
|
|
19
|
+
output += '|------|------|----------|---------|-------------|\n';
|
|
20
|
+
for (const prop of metadata.props) {
|
|
21
|
+
output += `| ${prop.name} | \`${prop.type}\` | ${prop.required ? 'Yes' : 'No'} | ${prop.defaultValue ?? '-'} | ${prop.description ?? '-'} |\n`;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
output += 'No props defined in type file.\n';
|
|
26
|
+
}
|
|
27
|
+
if (metadata.variants.length > 0) {
|
|
28
|
+
output += `\n## Variants\n\n`;
|
|
29
|
+
output += `Available variants: ${metadata.variants.map((v) => `\`${v}\``).join(', ')}\n`;
|
|
30
|
+
}
|
|
31
|
+
return { content: [{ type: 'text', text: output }] };
|
|
32
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ComponentRegistry } from '../registry/component-registry.js';
|
|
2
|
+
export declare const handleGetComponent: (registry: ComponentRegistry, name: string, projectRoot: string) => Promise<{
|
|
3
|
+
content: {
|
|
4
|
+
type: "text";
|
|
5
|
+
text: string;
|
|
6
|
+
}[];
|
|
7
|
+
isError: boolean;
|
|
8
|
+
} | {
|
|
9
|
+
content: {
|
|
10
|
+
type: "text";
|
|
11
|
+
text: string;
|
|
12
|
+
}[];
|
|
13
|
+
isError?: undefined;
|
|
14
|
+
}>;
|