@dkoul/auto-testid-core
Version:
Core AST parsing and transformation logic for React and Vue.js attribute generation
421 lines • 16.6 kB
JavaScript
;
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