Skip to main content
Glama
vue.ts17.5 kB
/** * Vue language handler for the Code-Map Generator tool. * This file contains the language handler for Vue single-file components. */ import { BaseLanguageHandler } from './base.js'; import { SyntaxNode } from '../parser.js'; import { FunctionExtractionOptions } from '../types.js'; import { getNodeText } from '../astAnalyzer.js'; import logger from '../../../logger.js'; import path from 'path'; /** * Language handler for Vue. * Provides enhanced function name detection for Vue single-file components. */ export class VueHandler extends BaseLanguageHandler { /** * Options for the handler. */ protected options?: { filePath?: string }; /** * Gets the query patterns for function detection. */ protected getFunctionQueryPatterns(): string[] { return [ 'script_element', 'element', 'method_definition', 'function_declaration', 'arrow_function', 'property' ]; } /** * Gets the query patterns for class detection. */ protected getClassQueryPatterns(): string[] { return [ 'document', 'script_element', 'template_element', 'style_element' ]; } /** * Gets the query patterns for import detection. */ protected getImportQueryPatterns(): string[] { return [ 'import_statement', 'import_declaration', 'element' ]; } /** * Extracts the function name from an AST node. */ protected extractFunctionName( node: SyntaxNode, sourceCode: string, _options?: FunctionExtractionOptions ): string { try { // Handle script elements if (node.type === 'script_element') { return 'script_section'; } // Handle template elements if (node.type === 'element' && this.isTemplateElement(node, sourceCode)) { return 'template_section'; } // Handle style elements if (node.type === 'element' && this.isStyleElement(node, sourceCode)) { return 'style_section'; } // Handle method definitions in script section if (node.type === 'method_definition') { const nameNode = node.childForFieldName('name'); if (nameNode) { const name = getNodeText(nameNode, sourceCode); // Check for Vue lifecycle hooks if (this.isVueLifecycleHook(name)) { return `lifecycle_${name}`; } // Check for Vue computed properties if (this.isInComputedSection(node, sourceCode)) { return `computed_${name}`; } // Check for Vue watchers if (this.isInWatchSection(node, sourceCode)) { return `watch_${name}`; } // Check for Vue methods if (this.isInMethodsSection(node, sourceCode)) { return `method_${name}`; } return name; } } // Handle function declarations if (node.type === 'function_declaration') { const nameNode = node.childForFieldName('name'); if (nameNode) { return getNodeText(nameNode, sourceCode); } } // Handle arrow functions if (node.type === 'arrow_function') { // Check if assigned to a property if (node.parent?.type === 'pair') { const keyNode = node.parent.childForFieldName('key'); if (keyNode) { const key = getNodeText(keyNode, sourceCode); // Check for Vue lifecycle hooks if (this.isVueLifecycleHook(key)) { return `lifecycle_${key}`; } // Check for Vue computed properties if (this.isInComputedSection(node.parent, sourceCode)) { return `computed_${key}`; } // Check for Vue watchers if (this.isInWatchSection(node.parent, sourceCode)) { return `watch_${key}`; } // Check for Vue methods if (this.isInMethodsSection(node.parent, sourceCode)) { return `method_${key}`; } return key; } } return 'arrow_function'; } // Handle properties (for Vue options API) if (node.type === 'property') { const keyNode = node.childForFieldName('key'); if (keyNode) { const key = getNodeText(keyNode, sourceCode); // Check for Vue special properties if (['data', 'computed', 'methods', 'watch', 'props', 'components'].includes(key)) { return `vue_${key}`; } return key; } } return 'vue_element'; } catch (error) { logger.warn({ err: error, nodeType: node.type }, 'Error extracting Vue function name'); return 'vue_element'; } } /** * Checks if a node is a template element. */ private isTemplateElement(node: SyntaxNode, sourceCode: string): boolean { try { const tagNameNode = node.childForFieldName('tag_name'); return tagNameNode ? getNodeText(tagNameNode, sourceCode) === 'template' : false; } catch (error) { logger.warn({ err: error, nodeType: node.type }, 'Error checking if node is a template element'); return false; } } /** * Checks if a node is a style element. */ private isStyleElement(node: SyntaxNode, sourceCode: string): boolean { try { const tagNameNode = node.childForFieldName('tag_name'); return tagNameNode ? getNodeText(tagNameNode, sourceCode) === 'style' : false; } catch (error) { logger.warn({ err: error, nodeType: node.type }, 'Error checking if node is a style element'); return false; } } /** * Checks if a name is a Vue lifecycle hook. */ private isVueLifecycleHook(name: string): boolean { const lifecycleHooks = [ 'beforeCreate', 'created', 'beforeMount', 'mounted', 'beforeUpdate', 'updated', 'activated', 'deactivated', 'beforeDestroy', 'destroyed', 'beforeUnmount', 'unmounted', 'errorCaptured', 'renderTracked', 'renderTriggered', 'serverPrefetch' ]; return lifecycleHooks.includes(name); } /** * Checks if a node is in the computed section. */ private isInComputedSection(node: SyntaxNode, sourceCode: string): boolean { try { let current = node; while (current.parent) { current = current.parent; if (current.type === 'pair') { const keyNode = current.childForFieldName('key'); if (keyNode && getNodeText(keyNode, sourceCode) === 'computed') { return true; } } } return false; } catch (error) { logger.warn({ err: error, nodeType: node.type }, 'Error checking if node is in computed section'); return false; } } /** * Checks if a node is in the watch section. */ private isInWatchSection(node: SyntaxNode, sourceCode: string): boolean { try { let current = node; while (current.parent) { current = current.parent; if (current.type === 'pair') { const keyNode = current.childForFieldName('key'); if (keyNode && getNodeText(keyNode, sourceCode) === 'watch') { return true; } } } return false; } catch (error) { logger.warn({ err: error, nodeType: node.type }, 'Error checking if node is in watch section'); return false; } } /** * Checks if a node is in the methods section. */ private isInMethodsSection(node: SyntaxNode, sourceCode: string): boolean { try { let current = node; while (current.parent) { current = current.parent; if (current.type === 'pair') { const keyNode = current.childForFieldName('key'); if (keyNode && getNodeText(keyNode, sourceCode) === 'methods') { return true; } } } return false; } catch (error) { logger.warn({ err: error, nodeType: node.type }, 'Error checking if node is in methods section'); return false; } } /** * Extracts the class name from an AST node. */ protected extractClassName(node: SyntaxNode, sourceCode: string): string { try { if (node.type === 'document') { // Try to get the component name from the script section const componentName = this.extractComponentName(node, sourceCode); if (componentName) { return `Vue_${componentName}`; } // Default to the filename without extension if (this.options?.filePath) { const filename = path.basename(this.options.filePath, path.extname(this.options.filePath)); return `Vue_${filename}`; } } else if (node.type === 'script_element') { return 'Vue_Script'; } else if (node.type === 'element') { if (this.isTemplateElement(node, sourceCode)) { return 'Vue_Template'; } else if (this.isStyleElement(node, sourceCode)) { return 'Vue_Style'; } } return 'Vue_Component'; } catch (error) { logger.warn({ err: error, nodeType: node.type }, 'Error extracting Vue class name'); return 'Vue_Component'; } } /** * Extracts the component name from a Vue file. */ private extractComponentName(node: SyntaxNode, sourceCode: string): string | null { try { // Find the script element let scriptNode = null; for (let i = 0; i < node.childCount; i++) { const child = node.child(i); if (child?.type === 'script_element') { scriptNode = child; break; } } if (scriptNode) { // Look for the name property in the component options const scriptContent = getNodeText(scriptNode, sourceCode); // Check for Vue 2 style component name const nameMatch = scriptContent.match(/name\s*:\s*['"]([^'"]+)['"]/); if (nameMatch && nameMatch[1]) { return nameMatch[1]; } // Check for Vue 3 defineComponent with name const defineComponentMatch = scriptContent.match(/defineComponent\s*\(\s*\{\s*name\s*:\s*['"]([^'"]+)['"]/); if (defineComponentMatch && defineComponentMatch[1]) { return defineComponentMatch[1]; } // Check for export default class style const classMatch = scriptContent.match(/export\s+default\s+class\s+(\w+)/); if (classMatch && classMatch[1]) { return classMatch[1]; } } return null; } catch (error) { logger.warn({ err: error, nodeType: node.type }, 'Error extracting Vue component name'); return null; } } /** * Extracts the import path from an AST node. */ protected extractImportPath(node: SyntaxNode, sourceCode: string): string { try { if (node.type === 'import_statement' || node.type === 'import_declaration') { const sourceNode = node.childForFieldName('source'); if (sourceNode) { return getNodeText(sourceNode, sourceCode).replace(/^["']|["']$/g, ''); } } else if (node.type === 'element') { // Check for src attribute in script or style tags if (this.isScriptElement(node, sourceCode) || this.isStyleElement(node, sourceCode)) { const startTagNode = node.childForFieldName('start_tag'); if (startTagNode) { const attributesNode = startTagNode.childForFieldName('attributes'); if (attributesNode) { for (let i = 0; i < attributesNode.childCount; i++) { const attr = attributesNode.child(i); if (attr?.type === 'attribute') { const nameNode = attr.childForFieldName('name'); if (nameNode && getNodeText(nameNode, sourceCode) === 'src') { const valueNode = attr.childForFieldName('value'); if (valueNode) { return getNodeText(valueNode, sourceCode).replace(/^["']|["']$/g, ''); } } } } } } } } return 'unknown'; } catch (error) { logger.warn({ err: error, nodeType: node.type }, 'Error extracting Vue import path'); return 'unknown'; } } /** * Checks if a node is a script element. */ private isScriptElement(node: SyntaxNode, sourceCode: string): boolean { try { const tagNameNode = node.childForFieldName('tag_name'); return tagNameNode ? getNodeText(tagNameNode, sourceCode) === 'script' : false; } catch (error) { logger.warn({ err: error, nodeType: node.type }, 'Error checking if node is a script element'); return false; } } /** * Finds the script node in a Vue document. */ private findScriptNode(node: SyntaxNode, sourceCode: string): SyntaxNode | null { try { // Find the script element for (let i = 0; i < node.childCount; i++) { const child = node.child(i); if (child && this.isScriptElement(child, sourceCode)) { return child; } } return null; } catch (error) { logger.warn({ err: error, nodeType: node.type }, 'Error finding script node'); return null; } } /** * Extracts the function comment from an AST node. */ protected extractFunctionComment(node: SyntaxNode, _sourceCode: string): string | undefined { try { // Look for JSDoc comments before the node const current = node; let prev = current.previousNamedSibling; while (prev && prev.type !== 'comment') { prev = prev.previousNamedSibling; } if (prev && prev.type === 'comment') { // Check if it's a JSDoc comment if (prev.text.startsWith('/**')) { // Remove comment markers and asterisks return prev.text .replace(/^\/\*\*\s*|\s*\*\/$/g, '') .replace(/^\s*\*\s*/mg, '') .trim(); } // Regular comment return prev.text .replace(/^\/\/\s*/mg, '') .trim(); } return undefined; } catch (error) { logger.warn({ err: error, nodeType: node.type }, 'Error extracting Vue function comment'); return undefined; } } /** * Extracts the class comment from an AST node. */ protected extractClassComment(node: SyntaxNode, sourceCode: string): string | undefined { try { if (node.type === 'document') { // Try to get the component description from the script section const scriptNode = this.findScriptNode(node, sourceCode); if (scriptNode) { // Look for a comment before the export default const scriptContent = getNodeText(scriptNode, sourceCode); const lines = scriptContent.split('\n'); for (let i = 0; i < lines.length; i++) { if (lines[i].includes('export default')) { // Check for comments above the export default const commentLines = []; let j = i - 1; // Skip empty lines while (j >= 0 && lines[j].trim() === '') { j--; } // Collect comment lines while (j >= 0 && (lines[j].trim().startsWith('//') || lines[j].trim().startsWith('*'))) { const commentLine = lines[j].trim() .replace(/^\/\/\s*/, '') .replace(/^\*\s*/, '') .trim(); if (commentLine) { commentLines.unshift(commentLine); } j--; } if (commentLines.length > 0) { return commentLines.join(' '); } break; } } } } return undefined; } catch (error) { logger.warn({ err: error, nodeType: node.type }, 'Error extracting Vue class comment'); return undefined; } } /** * Detects the framework used in the source code. */ detectFramework(sourceCode: string): string | null { try { // Vue 3 Composition API detection if (sourceCode.includes('defineComponent') || sourceCode.includes('setup()') || sourceCode.includes('ref(') || sourceCode.includes('reactive(')) { return 'vue3-composition'; } // Vue 3 Options API detection if (sourceCode.includes('Vue.createApp') || sourceCode.includes('createApp(')) { return 'vue3-options'; } // Vue 2 detection if (sourceCode.includes('Vue.component') || sourceCode.includes('new Vue(') || sourceCode.includes('Vue.extend')) { return 'vue2'; } // Vuex detection if (sourceCode.includes('Vuex') || sourceCode.includes('createStore') || sourceCode.includes('mapState') || sourceCode.includes('mapGetters')) { return 'vuex'; } // Vue Router detection if (sourceCode.includes('Vue-Router') || sourceCode.includes('createRouter') || sourceCode.includes('useRouter')) { return 'vue-router'; } // Default to Vue return 'vue'; } catch (error) { logger.warn({ err: error }, 'Error detecting Vue framework'); return 'vue'; } } }

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/freshtechbro/vibe-coder-mcp'

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