UNPKG

@dkoul/auto-testid-core

Version:

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

421 lines 16.6 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.reactParser = exports.ReactParser = void 0; const parser_1 = require("@babel/parser"); const traverse_1 = __importDefault(require("@babel/traverse")); const generator_1 = __importDefault(require("@babel/generator")); const t = __importStar(require("@babel/types")); const logger_1 = require("../utils/logger"); const validation_1 = require("../utils/validation"); class ReactParser { constructor() { this.logger = new logger_1.Logger('ReactParser'); } canParse(filePath) { const supportedExtensions = ['.jsx', '.tsx', '.js', '.ts']; return supportedExtensions.some(ext => filePath.endsWith(ext)); } parse(content, filePath) { const elements = []; const errors = []; this.logger.debug(`Parsing React file: ${filePath}`); try { // Parse with Babel, supporting JSX and TypeScript const ast = (0, parser_1.parse)(content, { sourceType: 'module', plugins: [ 'jsx', 'typescript', 'decorators-legacy', 'classProperties', 'dynamicImport', 'exportDefaultFrom', 'exportNamespaceFrom', 'functionBind', 'nullishCoalescingOperator', 'objectRestSpread', 'optionalChaining', ], }); // Traverse AST to find JSX elements (0, traverse_1.default)(ast, { JSXElement: (path) => { try { const element = this.extractElementFromJSX(path, filePath); if (element) { elements.push(element); } } catch (error) { errors.push({ message: `Failed to extract JSX element: ${error}`, position: this.getPositionFromPath(path), severity: 'error', }); } }, JSXFragment: (path) => { // Handle React fragments - traverse children try { this.extractElementsFromFragment(path, elements, filePath); } catch (error) { errors.push({ message: `Failed to process JSX fragment: ${error}`, position: this.getPositionFromPath(path), severity: 'warning', }); } }, }); const metadata = { framework: 'react', 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 React file ${filePath}: ${error}`); return { elements: [], errors: [{ message: `Parse error: ${error}`, severity: 'error', }], metadata: { framework: 'react', filePath, sourceLength: content.length, elementsCount: 0, }, }; } } transform(content, transformations) { const errors = []; try { this.logger.debug(`Applying ${transformations.length} transformations to React code`); // Parse the source code const ast = (0, parser_1.parse)(content, { sourceType: 'module', plugins: [ 'jsx', 'typescript', 'decorators-legacy', 'classProperties', 'dynamicImport', 'exportDefaultFrom', 'exportNamespaceFrom', 'functionBind', 'nullishCoalescingOperator', 'objectRestSpread', 'optionalChaining', ], }); // Group transformations by position for efficient processing const transformationMap = new Map(); transformations.forEach(transformation => { const key = `${transformation.position.line}:${transformation.position.column}`; if (!transformationMap.has(key)) { transformationMap.set(key, []); } transformationMap.get(key).push(transformation); }); // Apply transformations (0, traverse_1.default)(ast, { JSXOpeningElement: (path) => { const position = this.getPositionFromPath(path); if (!position) return; const key = `${position.line}:${position.column}`; const elementTransformations = transformationMap.get(key); if (elementTransformations) { elementTransformations.forEach(transformation => { try { this.applyTransformationToJSX(path, transformation); } catch (error) { errors.push({ message: `Failed to apply transformation: ${error}`, position, severity: 'error', }); } }); } }, }); // Generate the transformed code const result = (0, generator_1.default)(ast, { retainLines: true, sourceMaps: true, }); this.logger.info(`Successfully applied ${transformations.length} transformations`); return { code: result.code, sourceMap: JSON.stringify(result.map), 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', }], }; } } extractElementFromJSX(path, filePath) { const openingElement = path.node.openingElement; if (!t.isJSXIdentifier(openingElement.name)) { // Handle JSXMemberExpression (e.g., React.Component) return null; } const tagName = openingElement.name.name; const attributes = {}; // Extract attributes openingElement.attributes.forEach(attr => { if (t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name)) { const attrName = attr.name.name; let attrValue = ''; if (attr.value) { if (t.isStringLiteral(attr.value)) { attrValue = attr.value.value; } else if (t.isJSXExpressionContainer(attr.value) && !t.isJSXEmptyExpression(attr.value.expression)) { // Handle expression containers (e.g., {variable}) attrValue = this.extractExpressionValue(attr.value.expression); } } attributes[attrName] = attrValue; } }); // Extract text content let content = ''; path.node.children.forEach(child => { if (t.isJSXText(child)) { content += child.value.trim(); } }); const position = this.getPositionFromPath(path); const element = { tag: tagName, attributes, content: content || undefined, position, framework: 'react', }; // Validate the element const validation = validation_1.ValidationUtils.validateElement(element); if (!validation.valid) { this.logger.warn(`Invalid element found: ${validation.errors[0]?.message}`); return null; } return element; } extractElementsFromFragment(path, elements, filePath) { // React fragments don't need test IDs themselves, // but we need to process their children path.traverse({ JSXElement: (childPath) => { try { const element = this.extractElementFromJSX(childPath, filePath); if (element) { elements.push(element); } } catch (error) { this.logger.warn(`Failed to extract element from fragment: ${error}`); } }, }); } extractExpressionValue(expression) { try { if (t.isStringLiteral(expression)) { return expression.value; } if (t.isNumericLiteral(expression)) { return expression.value.toString(); } if (t.isBooleanLiteral(expression)) { return expression.value.toString(); } if (t.isIdentifier(expression)) { return `{${expression.name}}`; } if (t.isTemplateLiteral(expression)) { // Simple template literal extraction return expression.quasis.map(q => q.value.cooked || '').join('${...}'); } // For complex expressions, return a placeholder return '{...}'; } catch (error) { this.logger.debug(`Could not extract expression value: ${error}`); return ''; } } getPositionFromPath(path) { const loc = path.node.loc; if (!loc) return undefined; return { line: loc.start.line, column: loc.start.column, index: path.node.start || 0, }; } applyTransformationToJSX(path, transformation) { if (transformation.type !== 'add-attribute') { throw new Error(`Unsupported transformation type: ${transformation.type}`); } // Check if attribute already exists const existingAttr = path.node.attributes.find(attr => t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name) && attr.name.name === transformation.attribute); if (existingAttr) { // Update existing attribute if (t.isJSXAttribute(existingAttr)) { existingAttr.value = t.stringLiteral(transformation.value); } } else { // Add new attribute const newAttribute = t.jsxAttribute(t.jsxIdentifier(transformation.attribute), t.stringLiteral(transformation.value)); path.node.attributes.push(newAttribute); } this.logger.debug(`Applied transformation: ${transformation.attribute}="${transformation.value}"`); } // Helper method for debugging - serialize AST node serialize(ast) { try { const result = (0, generator_1.default)(ast, { retainLines: false, }); return result.code; } catch (error) { this.logger.error(`Failed to serialize AST: ${error}`); return ''; } } // Utility method to check if content contains JSX static containsJSX(content) { // Simple heuristic - look for JSX-like patterns const jsxPatterns = [ /<[A-Z][a-zA-Z0-9]*\s*[^>]*>/, // Component tags /<[a-z]+\s+[^>]*>/, // HTML tags with attributes /React\./, // React imports/usage /import.*from\s+['"]react['"]/, // React imports ]; return jsxPatterns.some(pattern => pattern.test(content)); } // Utility method to detect if file is TypeScript static isTypeScript(filePath) { return filePath.endsWith('.ts') || filePath.endsWith('.tsx'); } // Extract component name from file path or AST extractComponentName(content, filePath) { try { const ast = (0, parser_1.parse)(content, { sourceType: 'module', plugins: ['jsx', 'typescript', 'decorators-legacy'], }); let componentName = null; // Look for function components (0, traverse_1.default)(ast, { FunctionDeclaration: (path) => { if (this.isReactComponent(path)) { componentName = path.node.id?.name || null; } }, VariableDeclarator: (path) => { if (t.isIdentifier(path.node.id) && (t.isArrowFunctionExpression(path.node.init) || t.isFunctionExpression(path.node.init))) { // Check if this looks like a React component const name = path.node.id.name; if (/^[A-Z]/.test(name)) { componentName = name; } } }, ClassDeclaration: (path) => { if (this.isReactComponent(path)) { componentName = path.node.id?.name || null; } }, }); // Fallback to filename if (!componentName) { const baseName = filePath.split('/').pop()?.replace(/\.(jsx?|tsx?)$/, ''); componentName = baseName || null; } return componentName; } catch (error) { this.logger.debug(`Could not extract component name: ${error}`); return null; } } isReactComponent(path) { // Simple heuristic to detect React components // Look for JSX elements in the function/class body let hasJSX = false; path.traverse({ JSXElement: () => { hasJSX = true; path.stop(); // Stop traversing once we find JSX }, }); return hasJSX; } } exports.ReactParser = ReactParser; exports.reactParser = new ReactParser(); //# sourceMappingURL=react-parser.js.map