UNPKG

@dkoul/auto-testid-core

Version:

Core AST parsing and transformation logic for React and Vue.js attribute generation

351 lines 14.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.vueParser = exports.VueParser = void 0; const logger_1 = require("../utils/logger"); const validation_1 = require("../utils/validation"); class VueParser { constructor() { this.logger = new logger_1.Logger('VueParser'); } canParse(filePath) { return filePath.endsWith('.vue'); } parse(content, filePath) { const elements = []; const errors = []; this.logger.debug(`Parsing Vue file: ${filePath}`); try { // Parse Vue Single File Component structure const sections = this.parseSFC(content); if (!sections.template) { this.logger.warn(`No template section found in ${filePath}`); return { elements: [], errors: [{ message: 'No template section found in Vue Single File Component', severity: 'warning', }], metadata: { framework: 'vue', filePath, sourceLength: content.length, elementsCount: 0, }, }; } // Parse the template section for HTML elements const templateElements = this.parseTemplate(sections.template.content, sections.template.startLine); elements.push(...templateElements); const metadata = { framework: 'vue', filePath, sourceLength: content.length, elementsCount: elements.length, }; this.logger.info(`Parsed ${elements.length} elements from ${filePath}`); return { elements, errors, metadata }; } catch (error) { this.logger.error(`Failed to parse Vue file ${filePath}: ${error}`); return { elements: [], errors: [{ message: `Parse error: ${error}`, severity: 'error', }], metadata: { framework: 'vue', filePath, sourceLength: content.length, elementsCount: 0, }, }; } } transform(content, transformations) { const errors = []; try { this.logger.debug(`Applying ${transformations.length} transformations to Vue file`); // Parse the SFC structure const sections = this.parseSFC(content); if (!sections.template) { return { code: content, transformations: [], errors: [{ message: 'No template section found for transformation', severity: 'error', }], }; } // Apply transformations to the template section let modifiedTemplate = sections.template.content; const templateStartLine = sections.template.startLine; // Sort transformations by line number (descending) to avoid position shifts const sortedTransformations = [...transformations].sort((a, b) => { const aLine = a.position.line - templateStartLine; const bLine = b.position.line - templateStartLine; return bLine - aLine; }); for (const transformation of sortedTransformations) { try { // Adjust line number relative to template section const relativeLineNumber = transformation.position.line - templateStartLine; modifiedTemplate = this.applyTransformationToTemplate(modifiedTemplate, transformation, relativeLineNumber); } catch (error) { errors.push({ message: `Failed to apply transformation: ${error}`, position: transformation.position, severity: 'error', }); } } // Reconstruct the full SFC with modified template const modifiedContent = this.reconstructSFC(content, sections, modifiedTemplate); this.logger.info(`Successfully applied ${transformations.length} transformations`); return { code: modifiedContent, transformations, errors, }; } catch (error) { this.logger.error(`Transform error: ${error}`); return { code: content, // Return original on error transformations: [], errors: [{ message: `Transform error: ${error}`, severity: 'error', }], }; } } parseSFC(content) { const lines = content.split('\n'); const sections = { template: null, script: null, style: null, }; let currentSection = null; let currentContent = []; let startLine = 0; for (let i = 0; i < lines.length; i++) { const line = lines[i]; const trimmedLine = line.trim(); // Check for section start tags if (trimmedLine.startsWith('<template')) { if (currentSection) { // Save previous section this.saveSection(sections, currentSection, currentContent, startLine); } currentSection = 'template'; currentContent = []; startLine = i + 1; // Content starts on next line } else if (trimmedLine.startsWith('<script')) { if (currentSection) { this.saveSection(sections, currentSection, currentContent, startLine); } currentSection = 'script'; currentContent = []; startLine = i + 1; } else if (trimmedLine.startsWith('<style')) { if (currentSection) { this.saveSection(sections, currentSection, currentContent, startLine); } currentSection = 'style'; currentContent = []; startLine = i + 1; } else if (trimmedLine.startsWith('</template>') || trimmedLine.startsWith('</script>') || trimmedLine.startsWith('</style>')) { // End of current section if (currentSection) { this.saveSection(sections, currentSection, currentContent, startLine); currentSection = null; currentContent = []; } } else if (currentSection) { // Add content line to current section currentContent.push(line); } } // Save the last section if it exists if (currentSection) { this.saveSection(sections, currentSection, currentContent, startLine); } return sections; } saveSection(sections, sectionName, content, startLine) { sections[sectionName] = { content: content.join('\n'), startLine, endLine: startLine + content.length - 1, }; } parseTemplate(templateContent, baseLineNumber) { const elements = []; try { // Use a simple HTML-like parser for Vue templates const lines = templateContent.split('\n'); for (let i = 0; i < lines.length; i++) { const line = lines[i]; const trimmedLine = line.trim(); // Skip empty lines and comments if (!trimmedLine || trimmedLine.startsWith('<!--')) { continue; } // Find HTML-like opening tags const tagMatches = line.matchAll(/<([a-zA-Z][a-zA-Z0-9-]*)\s*([^>]*)>/g); for (const match of tagMatches) { const [fullMatch, tagName, attributesString] = match; const startIndex = match.index || 0; // Skip self-closing tags and closing tags if (fullMatch.endsWith('/>') || tagName.startsWith('/')) { continue; } // Parse attributes const attributes = this.parseAttributes(attributesString); // Extract content if it's a simple single-line element let content; const contentMatch = line.match(new RegExp(`<${tagName}[^>]*>([^<]*)</${tagName}>`)); if (contentMatch) { content = contentMatch[1].trim(); } const position = { line: baseLineNumber + i + 1, // +1 for 1-based line numbering column: startIndex + 1, // +1 for 1-based column numbering index: startIndex, }; const element = { tag: tagName, attributes, content: content || undefined, position, framework: 'vue', }; // Validate the element const validation = validation_1.ValidationUtils.validateElement(element); if (validation.valid) { elements.push(element); } else { this.logger.debug(`Invalid element skipped: ${validation.errors[0]?.message}`); } } } } catch (error) { this.logger.error(`Error parsing template: ${error}`); } return elements; } parseAttributes(attributesString) { const attributes = {}; if (!attributesString.trim()) { return attributes; } // Simple attribute parsing - handles most common cases // This regex handles: attr="value", attr='value', attr, :attr="value", @event="handler" const attrMatches = attributesString.matchAll(/([:\w-@]+)(?:=["']([^"']*)["'])?/g); for (const match of attrMatches) { const [, attrName, attrValue = ''] = match; attributes[attrName] = attrValue; } return attributes; } applyTransformationToTemplate(templateContent, transformation, relativeLineNumber) { if (transformation.type !== 'add-attribute') { throw new Error(`Unsupported transformation type: ${transformation.type}`); } const lines = templateContent.split('\n'); if (relativeLineNumber < 0 || relativeLineNumber >= lines.length) { throw new Error(`Invalid line number: ${relativeLineNumber}`); } const line = lines[relativeLineNumber]; const tagName = transformation.element.tag; // Find the opening tag in the line const tagRegex = new RegExp(`<${tagName}(\\s[^>]*)?>`); const tagMatch = line.match(tagRegex); if (!tagMatch) { throw new Error(`Could not find opening tag <${tagName}> in line`); } // Check if attribute already exists const existingAttrRegex = new RegExp(`\\s${transformation.attribute}=`); if (existingAttrRegex.test(tagMatch[0])) { // Update existing attribute const updatedTag = tagMatch[0].replace(new RegExp(`(${transformation.attribute}=)(["'])([^"']*)(["'])`), `$1$2${transformation.value}$4`); lines[relativeLineNumber] = line.replace(tagMatch[0], updatedTag); } else { // Add new attribute const insertionPoint = tagMatch[0].endsWith('>') ? tagMatch[0].length - 1 // Before the closing > : tagMatch[0].length; // At the end const beforeClosing = tagMatch[0].substring(0, insertionPoint); const closing = tagMatch[0].substring(insertionPoint); const newAttribute = ` ${transformation.attribute}="${transformation.value}"`; const updatedTag = beforeClosing + newAttribute + closing; lines[relativeLineNumber] = line.replace(tagMatch[0], updatedTag); } this.logger.debug(`Applied transformation: ${transformation.attribute}="${transformation.value}"`); return lines.join('\n'); } reconstructSFC(originalContent, sections, newTemplateContent) { if (!sections.template) { return originalContent; } const lines = originalContent.split('\n'); // Replace the template section content const templateStartLine = sections.template.startLine; const templateEndLine = sections.template.endLine; const newTemplateLines = newTemplateContent.split('\n'); // Create new content array const newLines = [ ...lines.slice(0, templateStartLine), // Before template ...newTemplateLines, // New template content ...lines.slice(templateEndLine + 1) // After template ]; return newLines.join('\n'); } // Static utility method to check if content is a Vue SFC static isVueSFC(content) { const vuePatterns = [ /<template[^>]*>/i, /<script[^>]*>/i, /export\s+default\s*{/, /Vue\.component/, ]; return vuePatterns.some(pattern => pattern.test(content)); } // Extract component name from Vue SFC extractComponentName(content, filePath) { try { // Try to extract from script section const sections = this.parseSFC(content); if (sections.script) { // Look for export default { name: 'ComponentName' } const nameMatch = sections.script.content.match(/name\s*:\s*['"`]([^'"`]+)['"`]/); if (nameMatch) { return nameMatch[1]; } } // Fallback to filename const baseName = filePath.split('/').pop()?.replace(/\.vue$/, ''); return baseName || null; } catch (error) { this.logger.debug(`Could not extract component name: ${error}`); return null; } } } exports.VueParser = VueParser; exports.vueParser = new VueParser(); //# sourceMappingURL=vue-parser.js.map