Skip to main content
Glama
grafana
by grafana
component-parser.ts9.55 kB
/** * Component parser for Grafana UI TypeScript components * Extracts metadata, props, imports, exports, and dependencies from component source code */ export interface ComponentMetadata { name: string; description?: string; props: PropDefinition[]; exports: ExportDefinition[]; imports: ImportDefinition[]; dependencies: string[]; hasTests: boolean; hasStories: boolean; hasDocumentation: boolean; } export interface PropDefinition { name: string; type: string; required: boolean; description?: string; defaultValue?: string; } export interface ExportDefinition { name: string; type: "component" | "type" | "function" | "const"; isDefault?: boolean; } export interface ImportDefinition { module: string; imports: string[]; isDefault?: boolean; isNamespace?: boolean; } /** * Parse TypeScript component code and extract all metadata * @param componentName Name of the component * @param code TypeScript source code * @returns ComponentMetadata object */ export function parseComponentMetadata( componentName: string, code: string, ): ComponentMetadata { const imports = extractImportsFromCode(code); const exports = extractExportsFromCode(code); const dependencies = extractDependencies(code); // Try to find props interface - look for ComponentProps pattern const propsInterfaceName = findPropsInterface(code, componentName); const props = propsInterfaceName ? extractPropsFromCode(code, propsInterfaceName) : []; // Extract description from JSDoc comments const description = extractComponentDescription(code, componentName); return { name: componentName, description, props, exports: parseExports(exports), imports: parseImports(code), dependencies, hasTests: false, // Will be set by caller based on file structure hasStories: false, // Will be set by caller based on file structure hasDocumentation: false, // Will be set by caller based on file structure }; } /** * Extract external dependencies from import statements * @param code TypeScript source code * @returns Array of external dependency names */ export function extractImportsFromCode(code: string): string[] { const dependencies: string[] = []; // Match import statements const importRegex = /import\s+.*?\s+from\s+['"]([@\w\/\-\.]+)['"]/g; let match; while ((match = importRegex.exec(code)) !== null) { const dep = match[1]; // Only include external dependencies (not relative imports) if ( !dep.startsWith("./") && !dep.startsWith("../") && !dep.startsWith("@/") ) { dependencies.push(dep); } } return [...new Set(dependencies)]; // Remove duplicates } /** * Extract exported identifiers from code * @param code TypeScript source code * @returns Array of exported names */ export function extractExportsFromCode(code: string): string[] { const exports: string[] = []; // Match export statements const exportRegexes = [ /export\s+(?:type|interface)\s+(\w+)/g, /export\s+(?:const|let|var)\s+(\w+)/g, /export\s+(?:function)\s+(\w+)/g, /export\s+(?:class)\s+(\w+)/g, /export\s+\{([^}]+)\}/g, // Named exports ]; exportRegexes.forEach((regex) => { let match; while ((match = regex.exec(code)) !== null) { if (regex.source.includes("\\{")) { // Handle named exports const namedExports = match[1] .split(",") .map((exp) => exp.trim().split(" as ")[0]); exports.push(...namedExports); } else { exports.push(match[1]); } } }); return [...new Set(exports)]; // Remove duplicates } /** * Extract props from a TypeScript interface or type definition * @param code TypeScript source code * @param interfaceName Name of the interface to extract props from * @returns Array of prop definitions */ export function extractPropsFromCode( code: string, interfaceName: string, ): PropDefinition[] { const props: PropDefinition[] = []; // Find the interface definition const interfaceRegex = new RegExp( `(?:type|interface)\\s+${interfaceName}\\s*=?\\s*\\{([^}]*)\\}`, "s", ); const match = code.match(interfaceRegex); if (!match) { return props; } const interfaceBody = match[1]; // Extract each property const propRegex = /(\/\*\*\s*(.*?)\s*\*\/\s*)?(\w+)(\?)?:\s*([^;,\n]+)/g; let propMatch; while ((propMatch = propRegex.exec(interfaceBody)) !== null) { const [, , comment, name, optional, type] = propMatch; props.push({ name, type: type.trim(), required: !optional, description: comment ? comment.trim() : undefined, defaultValue: undefined, // Could be extracted from default props or function parameters }); } // Also try simpler property extraction without JSDoc if (props.length === 0) { const simplePropRegex = /(\w+)(\?)?:\s*([^;,\n]+)/g; let simplePropMatch; while ((simplePropMatch = simplePropRegex.exec(interfaceBody)) !== null) { const [, name, optional, type] = simplePropMatch; // Check if there's a comment above this property const lines = interfaceBody.split("\n"); const propLineIndex = lines.findIndex((line) => line.includes(`${name}:`), ); let description: string | undefined; if (propLineIndex > 0) { const prevLine = lines[propLineIndex - 1].trim(); if ( prevLine.startsWith("/**") || prevLine.startsWith("*") || prevLine.startsWith("//") ) { description = prevLine.replace(/\/\*\*|\*\/|\*|\/\//g, "").trim(); } } props.push({ name, type: type.trim(), required: !optional, description, defaultValue: undefined, }); } } return props; } /** * Find the props interface name for a component * @param code TypeScript source code * @param componentName Name of the component * @returns Props interface name if found */ function findPropsInterface( code: string, componentName: string, ): string | null { // Common patterns for props interfaces const patterns = [ `${componentName}Props`, `I${componentName}Props`, `${componentName}Properties`, "CommonProps", // Fallback for some components ]; for (const pattern of patterns) { if ( code.includes(`type ${pattern}`) || code.includes(`interface ${pattern}`) ) { return pattern; } } return null; } /** * Extract component description from JSDoc comments * @param code TypeScript source code * @param componentName Name of the component * @returns Component description if found */ function extractComponentDescription( code: string, componentName: string, ): string | undefined { // Look for JSDoc comment before component definition const componentRegex = new RegExp( `/\\*\\*([^*]|\\*(?!/))*\\*/\\s*export\\s+const\\s+${componentName}`, "s", ); const match = code.match(componentRegex); if (match) { return match[0] .replace(/\/\*\*|\*\/|\*|/g, "") .trim() .split("\n")[0] .trim(); } return undefined; } /** * Parse imports into structured format * @param code TypeScript source code * @returns Array of import definitions */ function parseImports(code: string): ImportDefinition[] { const imports: ImportDefinition[] = []; const importRegex = /import\s+((?:\w+,\s*)?(?:\{[^}]*\}|\*\s+as\s+\w+|\w+))\s+from\s+['"]([^'"]+)['"]/g; let match; while ((match = importRegex.exec(code)) !== null) { const [, importSpec, module] = match; if (importSpec.includes("* as ")) { // Namespace import const namespaceMatch = importSpec.match(/\*\s+as\s+(\w+)/); if (namespaceMatch) { imports.push({ module, imports: [namespaceMatch[1]], isNamespace: true, }); } } else if (importSpec.includes("{")) { // Named imports const namedImports = importSpec .replace(/[{}]/g, "") .split(",") .map((imp) => imp.trim()) .filter(Boolean); imports.push({ module, imports: namedImports, }); } else { // Default import imports.push({ module, imports: [importSpec.trim()], isDefault: true, }); } } return imports; } /** * Parse exports into structured format * @param exportNames Array of export names * @returns Array of export definitions */ function parseExports(exportNames: string[]): ExportDefinition[] { return exportNames.map((name) => ({ name, type: inferExportType(name), // Simple heuristic-based type inference isDefault: false, // We don't handle default exports in this simple version })); } /** * Infer export type based on naming conventions * @param name Export name * @returns Inferred export type */ function inferExportType(name: string): ExportDefinition["type"] { if ( name.endsWith("Props") || name.endsWith("Type") || name.endsWith("Interface") ) { return "type"; } if (name[0] === name[0].toUpperCase() && !name.includes("_")) { return "component"; } if (name.includes("_") || name.toUpperCase() === name) { return "const"; } return "function"; } /** * Extract external dependencies for the component * @param code TypeScript source code * @returns Array of external dependency names */ function extractDependencies(code: string): string[] { return extractImportsFromCode(code); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/grafana/grafana-ui-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server