@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
package/README.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# @cooperco/component-library-mcp-server
|
|
2
|
+
|
|
3
|
+
MCP (Model Context Protocol) server for the Cooper Component Library. Gives AI assistants like Claude Code access to component documentation, prop types, color palettes, passthrough configs, and scaffolding tools.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @cooperco/component-library-mcp-server
|
|
9
|
+
# or use npx (no install needed)
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Configure Claude Code
|
|
13
|
+
|
|
14
|
+
Add to your project's `.claude/settings.json`:
|
|
15
|
+
|
|
16
|
+
```json
|
|
17
|
+
{
|
|
18
|
+
"mcpServers": {
|
|
19
|
+
"cooper-components": {
|
|
20
|
+
"command": "npx",
|
|
21
|
+
"args": ["-y", "@cooperco/component-library-mcp-server"]
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The server auto-detects the project root by walking up from the working directory looking for `src/components/components.ts`.
|
|
28
|
+
|
|
29
|
+
### Explicit project path
|
|
30
|
+
|
|
31
|
+
If auto-detection doesn't work (e.g., you're in a monorepo), pass the path explicitly:
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"mcpServers": {
|
|
36
|
+
"cooper-components": {
|
|
37
|
+
"command": "npx",
|
|
38
|
+
"args": ["-y", "@cooperco/component-library-mcp-server", "/path/to/Cordblood-Component-Library"]
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Or use an environment variable:
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"mcpServers": {
|
|
49
|
+
"cooper-components": {
|
|
50
|
+
"command": "npx",
|
|
51
|
+
"args": ["-y", "@cooperco/component-library-mcp-server"],
|
|
52
|
+
"env": {
|
|
53
|
+
"COOPER_PROJECT_ROOT": "/path/to/Cordblood-Component-Library"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Local development (within this repo)
|
|
61
|
+
|
|
62
|
+
When working in the component library repo itself, use the local build:
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{
|
|
66
|
+
"mcpServers": {
|
|
67
|
+
"cooper-components": {
|
|
68
|
+
"command": "node",
|
|
69
|
+
"args": ["mcp/dist/index.js"]
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Tools
|
|
76
|
+
|
|
77
|
+
| Tool | Description |
|
|
78
|
+
|------|-------------|
|
|
79
|
+
| `list-components` | List all components with categories, variants, and descriptions |
|
|
80
|
+
| `get-component` | Full docs for a component: props, events, slots, passthrough, and source code |
|
|
81
|
+
| `get-component-props` | Focused prop table with types, defaults, and required status |
|
|
82
|
+
| `get-color-palettes` | Color palette system: available palettes, structure, and usage |
|
|
83
|
+
| `get-passthrough-config` | Passthrough keys and default Tailwind classes for a component |
|
|
84
|
+
| `search-components` | Search components by name, category, or prop names |
|
|
85
|
+
| `scaffold-component` | Generate a new component with .vue, .ts, and .stories.ts files |
|
|
86
|
+
|
|
87
|
+
## Examples
|
|
88
|
+
|
|
89
|
+
Once configured, Claude Code can use the tools automatically:
|
|
90
|
+
|
|
91
|
+
- *"What components are available?"* → calls `list-components`
|
|
92
|
+
- *"Show me the props for ContainerModule"* → calls `get-component`
|
|
93
|
+
- *"What color palettes can I use?"* → calls `get-color-palettes`
|
|
94
|
+
- *"Create a new BannerModule component"* → calls `scaffold-component`
|
|
95
|
+
|
|
96
|
+
## How it works
|
|
97
|
+
|
|
98
|
+
The server reads your component library source files at runtime:
|
|
99
|
+
|
|
100
|
+
- Parses TypeScript interfaces from `ComponentName.ts` files using the TypeScript Compiler API
|
|
101
|
+
- Extracts `withDefaults`, `defineEmits`, and `<slot>` usage from `.vue` files
|
|
102
|
+
- Reads passthrough configurations from `src/config/defaultPassthrough/index.ts`
|
|
103
|
+
- Reads color palettes from `src/config/colorPalettes.ts`
|
|
104
|
+
- Discovers components from `src/components/components.ts` barrel exports
|
|
105
|
+
|
|
106
|
+
Results are cached in memory and invalidated when source files change, so documentation is always current.
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { ComponentRegistry } from './registry/component-registry.js';
|
|
5
|
+
import { registerAllTools } from './server.js';
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { createRequire } from 'module';
|
|
9
|
+
const resolveProjectRoot = () => {
|
|
10
|
+
// 1. Explicit env var override
|
|
11
|
+
if (process.env.COOPER_PROJECT_ROOT) {
|
|
12
|
+
return path.resolve(process.env.COOPER_PROJECT_ROOT);
|
|
13
|
+
}
|
|
14
|
+
// 2. CLI argument: node dist/index.js /path/to/project
|
|
15
|
+
const cliArg = process.argv[2];
|
|
16
|
+
if (cliArg && !cliArg.startsWith('-')) {
|
|
17
|
+
return path.resolve(cliArg);
|
|
18
|
+
}
|
|
19
|
+
// 3. Walk up from cwd looking for src/components/components.ts
|
|
20
|
+
let dir = process.cwd();
|
|
21
|
+
while (dir !== path.dirname(dir)) {
|
|
22
|
+
if (fs.existsSync(path.join(dir, 'src/components/components.ts'))) {
|
|
23
|
+
return dir;
|
|
24
|
+
}
|
|
25
|
+
dir = path.dirname(dir);
|
|
26
|
+
}
|
|
27
|
+
// 4. Fall back to cwd
|
|
28
|
+
return process.cwd();
|
|
29
|
+
};
|
|
30
|
+
const main = async () => {
|
|
31
|
+
const projectRoot = resolveProjectRoot();
|
|
32
|
+
// Validate the project root has the expected structure
|
|
33
|
+
const barrelPath = path.join(projectRoot, 'src/components/components.ts');
|
|
34
|
+
if (!fs.existsSync(barrelPath)) {
|
|
35
|
+
process.stderr.write(`Error: Could not find component library at ${projectRoot}\n` +
|
|
36
|
+
`Expected to find: ${barrelPath}\n\n` +
|
|
37
|
+
`Set COOPER_PROJECT_ROOT env var or pass the path as an argument:\n` +
|
|
38
|
+
` npx @cooperco/component-library-mcp-server /path/to/Cordblood-Component-Library\n`);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
const require = createRequire(import.meta.url);
|
|
42
|
+
const { version } = require('../package.json');
|
|
43
|
+
const server = new McpServer({
|
|
44
|
+
name: 'cooper-component-library',
|
|
45
|
+
version,
|
|
46
|
+
});
|
|
47
|
+
const registry = new ComponentRegistry(projectRoot);
|
|
48
|
+
registerAllTools(server, registry, projectRoot);
|
|
49
|
+
const transport = new StdioServerTransport();
|
|
50
|
+
await server.connect(transport);
|
|
51
|
+
};
|
|
52
|
+
main().catch((error) => {
|
|
53
|
+
process.stderr.write(`Fatal error: ${error}\n`);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { PassthroughKeyInfo } from '../types.js';
|
|
2
|
+
interface ParsedPassthrough {
|
|
3
|
+
name: string;
|
|
4
|
+
variants: Record<string, PassthroughKeyInfo[]> | null;
|
|
5
|
+
keys: PassthroughKeyInfo[];
|
|
6
|
+
}
|
|
7
|
+
export declare const parsePassthroughFile: (source: string) => ParsedPassthrough[];
|
|
8
|
+
/**
|
|
9
|
+
* Extracts the DEFAULT_PASSTHROUGH key-value pairs from the passthrough index source.
|
|
10
|
+
*/
|
|
11
|
+
export declare const parseDefaultPassthrough: (source: string) => PassthroughKeyInfo[];
|
|
12
|
+
export declare const findPassthroughForComponent: (passthroughs: ParsedPassthrough[], componentName: string) => ParsedPassthrough | null;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
export const parsePassthroughFile = (source) => {
|
|
2
|
+
const results = [];
|
|
3
|
+
// Find all exported const declarations
|
|
4
|
+
const exportMatches = source.matchAll(/export const (\w+Pt\w*)\s*(?::\s*\w+\s*)?=\s*/g);
|
|
5
|
+
for (const match of exportMatches) {
|
|
6
|
+
const name = match[1];
|
|
7
|
+
const startIndex = (match.index ?? 0) + match[0].length;
|
|
8
|
+
// Determine if this is a variant-based passthrough (has nested keys like { Centered: ..., Hero: ... })
|
|
9
|
+
// or a flat passthrough
|
|
10
|
+
const rest = source.slice(startIndex);
|
|
11
|
+
if (rest.startsWith('{') && isVariantObject(rest)) {
|
|
12
|
+
const variants = parseVariantPassthrough(rest);
|
|
13
|
+
results.push({ name, variants, keys: [] });
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
const keys = parseFlatPassthrough(rest);
|
|
17
|
+
results.push({ name, variants: null, keys });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return results;
|
|
21
|
+
};
|
|
22
|
+
const isVariantObject = (source) => {
|
|
23
|
+
// Variant objects have keys like List:, Tile:, Centered:, Hero:, link:, fill: at the top level
|
|
24
|
+
// Supports both capitalized (List, Hero) and lowercase (link, fill) variant keys
|
|
25
|
+
const variantPattern = /^\{\s*\n\s*(\w+)\s*:\s*(?:\{|combinePassthroughs)/;
|
|
26
|
+
return variantPattern.test(source);
|
|
27
|
+
};
|
|
28
|
+
const extractBalancedBraces = (source, start) => {
|
|
29
|
+
let depth = 0;
|
|
30
|
+
for (let i = start; i < source.length; i++) {
|
|
31
|
+
if (source[i] === '{')
|
|
32
|
+
depth++;
|
|
33
|
+
else if (source[i] === '}') {
|
|
34
|
+
depth--;
|
|
35
|
+
if (depth === 0)
|
|
36
|
+
return source.slice(start + 1, i);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return '';
|
|
40
|
+
};
|
|
41
|
+
const parseVariantPassthrough = (source) => {
|
|
42
|
+
const variants = {};
|
|
43
|
+
// First, extract only the outer object's content using balanced braces
|
|
44
|
+
const outerContent = extractBalancedBraces(source, 0);
|
|
45
|
+
if (!outerContent)
|
|
46
|
+
return variants;
|
|
47
|
+
// Match variant keys at the first indentation level (tab or spaces)
|
|
48
|
+
const variantKeyPattern = /(?:^|\n)\s+(\w+)\s*:\s*/g;
|
|
49
|
+
for (const match of outerContent.matchAll(variantKeyPattern)) {
|
|
50
|
+
const variantName = match[1];
|
|
51
|
+
const afterKey = (match.index ?? 0) + match[0].length;
|
|
52
|
+
const rest = outerContent.slice(afterKey);
|
|
53
|
+
if (rest.startsWith('combinePassthroughs(')) {
|
|
54
|
+
// Extract the override object (second argument)
|
|
55
|
+
const overrideMatch = rest.match(/combinePassthroughs\(\s*(?:\{[^}]*\}|[^,]+),\s*\{([^}]*)\}/s);
|
|
56
|
+
if (overrideMatch) {
|
|
57
|
+
variants[variantName] = parseKeyValuePairs(overrideMatch[1]);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
variants[variantName] = [];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else if (rest.startsWith('{')) {
|
|
64
|
+
// Use balanced brace extraction for nested objects
|
|
65
|
+
const block = extractBalancedBraces(rest, 0);
|
|
66
|
+
variants[variantName] = parseKeyValuePairs(block);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return variants;
|
|
70
|
+
};
|
|
71
|
+
const parseFlatPassthrough = (source) => {
|
|
72
|
+
// Check if it starts with combinePassthroughs(
|
|
73
|
+
if (source.startsWith('combinePassthroughs(')) {
|
|
74
|
+
// Extract the override object (second argument)
|
|
75
|
+
const overrideMatch = source.match(/combinePassthroughs\(\s*(?:\{[^}]*\}|[^,]+),\s*\{([^}]*)\}/s);
|
|
76
|
+
if (overrideMatch) {
|
|
77
|
+
return parseKeyValuePairs(overrideMatch[1]);
|
|
78
|
+
}
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
// Direct object literal
|
|
82
|
+
const objectMatch = source.match(/^\{([^}]*)\}/s);
|
|
83
|
+
if (objectMatch) {
|
|
84
|
+
return parseKeyValuePairs(objectMatch[1]);
|
|
85
|
+
}
|
|
86
|
+
return [];
|
|
87
|
+
};
|
|
88
|
+
const parseKeyValuePairs = (block) => {
|
|
89
|
+
const keys = [];
|
|
90
|
+
// Match key: 'value' or key: `value` or key: "value"
|
|
91
|
+
const pairs = block.matchAll(/(\w+)\s*:\s*(?:'([^']*)'|`([^`]*)`|"([^"]*)")/g);
|
|
92
|
+
for (const pair of pairs) {
|
|
93
|
+
const key = pair[1];
|
|
94
|
+
const value = pair[2] ?? pair[3] ?? pair[4] ?? '';
|
|
95
|
+
keys.push({ key, defaultClasses: value.trim() });
|
|
96
|
+
}
|
|
97
|
+
return keys;
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* Extracts the DEFAULT_PASSTHROUGH key-value pairs from the passthrough index source.
|
|
101
|
+
*/
|
|
102
|
+
export const parseDefaultPassthrough = (source) => {
|
|
103
|
+
const match = source.match(/export const DEFAULT_PASSTHROUGH\s*(?::\s*\w+\s*)?=\s*\{([^}]+)\}/s);
|
|
104
|
+
if (!match)
|
|
105
|
+
return [];
|
|
106
|
+
return parseKeyValuePairs(match[1]);
|
|
107
|
+
};
|
|
108
|
+
export const findPassthroughForComponent = (passthroughs, componentName) => {
|
|
109
|
+
// Try exact match first (e.g., "ContainerModulePt")
|
|
110
|
+
const exact = passthroughs.find((pt) => pt.name === `${componentName}Pt`);
|
|
111
|
+
if (exact)
|
|
112
|
+
return exact;
|
|
113
|
+
// Try partial match
|
|
114
|
+
return (passthroughs.find((pt) => pt.name.toLowerCase().startsWith(componentName.toLowerCase())) ?? null);
|
|
115
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { PropInfo } from '../types.js';
|
|
2
|
+
interface ParsedInterface {
|
|
3
|
+
name: string;
|
|
4
|
+
extendsTypes: string[];
|
|
5
|
+
props: PropInfo[];
|
|
6
|
+
}
|
|
7
|
+
export declare const parseTypeScriptFile: (source: string, filename: string) => ParsedInterface[];
|
|
8
|
+
export declare const findMainInterface: (interfaces: ParsedInterface[], componentName: string) => ParsedInterface | null;
|
|
9
|
+
export declare const findPassthroughInterface: (interfaces: ParsedInterface[]) => ParsedInterface | null;
|
|
10
|
+
export declare const extractVariantsFromProp: (prop: PropInfo) => string[];
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
export const parseTypeScriptFile = (source, filename) => {
|
|
3
|
+
const sourceFile = ts.createSourceFile(filename, source, ts.ScriptTarget.Latest, true);
|
|
4
|
+
const interfaces = [];
|
|
5
|
+
ts.forEachChild(sourceFile, (node) => {
|
|
6
|
+
if (ts.isInterfaceDeclaration(node)) {
|
|
7
|
+
const name = node.name.text;
|
|
8
|
+
const extendsTypes = [];
|
|
9
|
+
if (node.heritageClauses) {
|
|
10
|
+
for (const clause of node.heritageClauses) {
|
|
11
|
+
for (const type of clause.types) {
|
|
12
|
+
extendsTypes.push(type.expression.getText(sourceFile));
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const props = [];
|
|
17
|
+
for (const member of node.members) {
|
|
18
|
+
if (ts.isPropertySignature(member) && member.name) {
|
|
19
|
+
const propName = member.name.getText(sourceFile);
|
|
20
|
+
const isOptional = !!member.questionToken;
|
|
21
|
+
const typeText = member.type
|
|
22
|
+
? member.type.getText(sourceFile)
|
|
23
|
+
: 'unknown';
|
|
24
|
+
let description;
|
|
25
|
+
const jsDocTags = ts.getJSDocTags(member);
|
|
26
|
+
if (jsDocTags.length > 0) {
|
|
27
|
+
description = jsDocTags
|
|
28
|
+
.map((tag) => tag.comment?.toString() ?? '')
|
|
29
|
+
.filter(Boolean)
|
|
30
|
+
.join(' ');
|
|
31
|
+
}
|
|
32
|
+
// Also check for leading JSDoc comments
|
|
33
|
+
const fullText = member.getFullText(sourceFile);
|
|
34
|
+
const jsDocMatch = fullText.match(/\/\*\*\s*\n?\s*\*?\s*(.+?)\s*\*?\s*\*\//);
|
|
35
|
+
if (jsDocMatch && !description) {
|
|
36
|
+
description = jsDocMatch[1].trim();
|
|
37
|
+
}
|
|
38
|
+
props.push({
|
|
39
|
+
name: propName,
|
|
40
|
+
type: typeText,
|
|
41
|
+
required: !isOptional,
|
|
42
|
+
description,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
interfaces.push({ name, extendsTypes, props });
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
return interfaces;
|
|
50
|
+
};
|
|
51
|
+
export const findMainInterface = (interfaces, componentName) => {
|
|
52
|
+
// First try exact match
|
|
53
|
+
const exact = interfaces.find((i) => i.name === componentName);
|
|
54
|
+
if (exact)
|
|
55
|
+
return exact;
|
|
56
|
+
// Then try matching interfaces that extend Component
|
|
57
|
+
const extending = interfaces.find((i) => i.extendsTypes.includes('Component'));
|
|
58
|
+
if (extending)
|
|
59
|
+
return extending;
|
|
60
|
+
// Fall back to first interface that isn't a passthrough
|
|
61
|
+
return (interfaces.find((i) => !i.name.toLowerCase().includes('passthrough') &&
|
|
62
|
+
!i.name.toLowerCase().includes('pt')) ?? null);
|
|
63
|
+
};
|
|
64
|
+
export const findPassthroughInterface = (interfaces) => {
|
|
65
|
+
return (interfaces.find((i) => i.name.toLowerCase().includes('passthrough') &&
|
|
66
|
+
(i.extendsTypes.includes('ComponentPassthrough') ||
|
|
67
|
+
i.extendsTypes.includes('GenericComponentPassthrough') ||
|
|
68
|
+
i.extendsTypes.length > 0)) ?? null);
|
|
69
|
+
};
|
|
70
|
+
export const extractVariantsFromProp = (prop) => {
|
|
71
|
+
// Match union literal types like "'Centered' | 'Hero'"
|
|
72
|
+
const matches = prop.type.match(/'([^']+)'/g);
|
|
73
|
+
if (matches) {
|
|
74
|
+
return matches.map((m) => m.replace(/'/g, ''));
|
|
75
|
+
}
|
|
76
|
+
return [];
|
|
77
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { EmitInfo, SlotInfo } from '../types.js';
|
|
2
|
+
interface VueSfcData {
|
|
3
|
+
interfaceName: string | null;
|
|
4
|
+
defaults: Record<string, string>;
|
|
5
|
+
emits: EmitInfo[];
|
|
6
|
+
slots: SlotInfo[];
|
|
7
|
+
usesColorPalette: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare const parseVueSfc: (source: string) => VueSfcData;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export const parseVueSfc = (source) => {
|
|
2
|
+
const interfaceName = extractInterfaceName(source);
|
|
3
|
+
const defaults = extractDefaults(source);
|
|
4
|
+
const emits = extractEmits(source);
|
|
5
|
+
const slots = extractSlots(source);
|
|
6
|
+
const usesColorPalette = source.includes('withColorPalette');
|
|
7
|
+
return { interfaceName, defaults, emits, slots, usesColorPalette };
|
|
8
|
+
};
|
|
9
|
+
const extractInterfaceName = (source) => {
|
|
10
|
+
const match = source.match(/defineProps<(\w+)>/);
|
|
11
|
+
return match ? match[1] : null;
|
|
12
|
+
};
|
|
13
|
+
const extractDefaults = (source) => {
|
|
14
|
+
const defaults = {};
|
|
15
|
+
// Match withDefaults block - handle multi-line
|
|
16
|
+
const match = source.match(/withDefaults\(defineProps<\w+>\(\),\s*\{([^}]+)\}\)/s);
|
|
17
|
+
if (!match)
|
|
18
|
+
return defaults;
|
|
19
|
+
const block = match[1];
|
|
20
|
+
// Match each key: value pair
|
|
21
|
+
const pairs = block.matchAll(/(\w+)\s*:\s*(.+?)(?:,\s*$|\s*$)/gm);
|
|
22
|
+
for (const pair of pairs) {
|
|
23
|
+
const key = pair[1].trim();
|
|
24
|
+
const value = pair[2].trim().replace(/,$/, '').trim();
|
|
25
|
+
if (key && value) {
|
|
26
|
+
defaults[key] = value;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return defaults;
|
|
30
|
+
};
|
|
31
|
+
const extractEmits = (source) => {
|
|
32
|
+
const emits = [];
|
|
33
|
+
// Match defineEmits(['name1', 'name2'])
|
|
34
|
+
const match = source.match(/defineEmits\(\[([^\]]+)\]\)/);
|
|
35
|
+
if (match) {
|
|
36
|
+
const names = match[1].matchAll(/'([^']+)'/g);
|
|
37
|
+
for (const name of names) {
|
|
38
|
+
emits.push({ name: name[1] });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return emits;
|
|
42
|
+
};
|
|
43
|
+
const extractSlots = (source) => {
|
|
44
|
+
const slots = [];
|
|
45
|
+
const seen = new Set();
|
|
46
|
+
// Extract <template> section
|
|
47
|
+
const templateMatch = source.match(/<template>([\s\S]*)<\/template>/);
|
|
48
|
+
if (!templateMatch)
|
|
49
|
+
return slots;
|
|
50
|
+
const template = templateMatch[1];
|
|
51
|
+
// Match <slot /> and <slot name="xxx" />
|
|
52
|
+
const slotMatches = template.matchAll(/<slot\s*(?:name="([\w-]+)")?\s*\/?>/g);
|
|
53
|
+
for (const match of slotMatches) {
|
|
54
|
+
const name = match[1] ?? 'default';
|
|
55
|
+
if (!seen.has(name)) {
|
|
56
|
+
seen.add(name);
|
|
57
|
+
slots.push({ name });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return slots;
|
|
61
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const getComponentDescription: (componentName: string) => string;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const COMPONENT_DESCRIPTIONS = {
|
|
2
|
+
Accordion: 'Expandable content sections with List and Tile variants',
|
|
3
|
+
AccordionItem: 'Individual accordion panel with header and collapsible body',
|
|
4
|
+
AccordionListItem: 'List-style accordion item with bordered layout',
|
|
5
|
+
AccordionTileItem: 'Tile-style accordion item with card layout',
|
|
6
|
+
CarouselModule: 'Carousel/slider for content collections with navigation controls',
|
|
7
|
+
ContainerModule: 'Layout container with start/end content slots (Centered/Hero variants)',
|
|
8
|
+
ContentModule: 'Rich text content block with headline, body copy, and CTAs',
|
|
9
|
+
ContainerCollectionModule: 'Collection of ContainerModule components',
|
|
10
|
+
CTA: 'Call-to-action button/link (fill, outline, link, logo types)',
|
|
11
|
+
FooterNavigation: 'Footer navigation with grouped links',
|
|
12
|
+
FooterCopyright: 'Footer copyright text and legal links',
|
|
13
|
+
SocialMediaRef: 'Social media icon link',
|
|
14
|
+
Image: 'Responsive image with mobile variant and animation support',
|
|
15
|
+
LogoCollectionModule: 'Grid of logo images',
|
|
16
|
+
NavigationElement: 'Navigation link item with optional icon',
|
|
17
|
+
PrimaryNavigation: 'Top-level navigation bar',
|
|
18
|
+
SplitModule: 'Three-column layout with start/center/end slots',
|
|
19
|
+
TestimonialModule: 'Testimonial with quote, author, and media',
|
|
20
|
+
TileCollectionModule: 'Grid/collection of tile components',
|
|
21
|
+
TileContentIconTile: 'Tile with icon, headline, and body',
|
|
22
|
+
TileContentImageTile: 'Tile with image, headline, and body',
|
|
23
|
+
TileContentTextTile: 'Text-only tile with headline and body',
|
|
24
|
+
TileContentVideoTile: 'Tile with embedded video',
|
|
25
|
+
TileContentImageStackedAnimatedTile: 'Animated stacked image tile',
|
|
26
|
+
Video: 'Video player component with thumbnail',
|
|
27
|
+
AnimationWrapper: 'Wrapper that adds scroll-triggered animations',
|
|
28
|
+
HelloBar: 'Top-of-page announcement banner',
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Splits a PascalCase name into human-readable words.
|
|
32
|
+
* E.g., "CarouselModule" -> "Carousel module", "CTA" -> "CTA"
|
|
33
|
+
*/
|
|
34
|
+
const humanizePascalCase = (name) => {
|
|
35
|
+
const words = name.replace(/([a-z])([A-Z])/g, '$1 $2').split(' ');
|
|
36
|
+
return words
|
|
37
|
+
.map((word, index) => (index === 0 ? word : word.toLowerCase()))
|
|
38
|
+
.join(' ');
|
|
39
|
+
};
|
|
40
|
+
export const getComponentDescription = (componentName) => {
|
|
41
|
+
return (COMPONENT_DESCRIPTIONS[componentName] ??
|
|
42
|
+
`${humanizePascalCase(componentName)} component`);
|
|
43
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ComponentMetadata, ComponentDiscovery, PassthroughKeyInfo } from '../types.js';
|
|
2
|
+
export declare class ComponentRegistry {
|
|
3
|
+
private cache;
|
|
4
|
+
private discoveryCache;
|
|
5
|
+
private discoveryMtime;
|
|
6
|
+
private passthroughCache;
|
|
7
|
+
private passthroughMtime;
|
|
8
|
+
private projectRoot;
|
|
9
|
+
constructor(projectRoot: string);
|
|
10
|
+
discoverComponents(): Promise<ComponentDiscovery[]>;
|
|
11
|
+
getComponent(name: string): Promise<ComponentMetadata | null>;
|
|
12
|
+
listComponents(): Promise<ComponentMetadata[]>;
|
|
13
|
+
searchComponents(query: string, category?: 'component' | 'module' | 'all'): Promise<ComponentMetadata[]>;
|
|
14
|
+
getPassthroughForComponent(componentName: string): Promise<{
|
|
15
|
+
variants: Record<string, PassthroughKeyInfo[]> | null;
|
|
16
|
+
keys: PassthroughKeyInfo[];
|
|
17
|
+
} | null>;
|
|
18
|
+
getColorPalettes(): Promise<string>;
|
|
19
|
+
getDefaultPassthrough(): Promise<PassthroughKeyInfo[]>;
|
|
20
|
+
componentExists(name: string): Promise<boolean>;
|
|
21
|
+
private loadPassthroughs;
|
|
22
|
+
private parseComponent;
|
|
23
|
+
}
|