UNPKG

move-prop-types

Version:

"Help quickly replace the prop type checker in older react projects to map to prop-types"

229 lines 8.36 kB
/** * AST-based PropTypes transformation using Babel * * This module provides a more robust and accurate transformation compared to regex-based approach. * Benefits: * - Understands JavaScript/TypeScript syntax correctly * - Handles complex cases and edge cases better * - Preserves code formatting and comments where possible * - More maintainable and less error-prone */ import { parse } from '@babel/parser'; import traverse from '@babel/traverse'; import generate from '@babel/generator'; import * as t from '@babel/types'; /** * Parse JavaScript/TypeScript file using Babel */ function parseCode(code, filename) { const isTypeScript = filename.endsWith('.tsx') || filename.endsWith('.ts'); return parse(code, { sourceType: 'module', allowImportExportEverywhere: false, allowReturnOutsideFunction: false, plugins: [ 'jsx', 'asyncGenerators', 'bigInt', 'classProperties', 'decorators-legacy', 'doExpressions', 'dynamicImport', 'exportDefaultFrom', 'exportNamespaceFrom', 'functionBind', 'functionSent', 'importMeta', 'nullishCoalescingOperator', 'numericSeparator', 'objectRestSpread', 'optionalCatchBinding', 'optionalChaining', 'throwExpressions', 'topLevelAwait', // 'trailingFunctionCommas', ...(isTypeScript ? ['typescript'] : []) ] }); } /** * Analyze the code to determine PropTypes usage patterns */ function analyzePropTypesUsage(ast) { let hasES6Import = false; let hasReactPropTypesUsage = false; traverse(ast, { // Check for ES6 imports: import { PropTypes } from 'react' ImportDeclaration(path) { if ((path.node.source.value === 'react' || path.node.source.value === 'React') && path.node.specifiers.some((spec) => t.isImportSpecifier(spec) && (t.isIdentifier(spec.imported) ? spec.imported.name : spec.imported.value) === 'PropTypes')) { hasES6Import = true; } }, // Check for React.PropTypes usage MemberExpression(path) { if (t.isMemberExpression(path.node) && t.isIdentifier(path.node.object, { name: 'React' }) && t.isIdentifier(path.node.property, { name: 'PropTypes' })) { hasReactPropTypesUsage = true; } } }); return { hasES6Import, hasReactPropTypesUsage, needsTransformation: hasES6Import || hasReactPropTypesUsage }; } /** * Transform ES6 PropTypes imports * Removes PropTypes from React import and adds separate PropTypes import */ function transformES6Imports(ast) { let modified = false; let hasAddedPropTypesImport = false; traverse(ast, { ImportDeclaration(path) { // Handle React imports that include PropTypes if (path.node.source.value === 'react' || path.node.source.value === 'React') { const propTypesSpecifier = path.node.specifiers.find((spec) => t.isImportSpecifier(spec) && (t.isIdentifier(spec.imported) ? spec.imported.name : spec.imported.value) === 'PropTypes'); if (propTypesSpecifier) { // Remove PropTypes from the React import path.node.specifiers = path.node.specifiers.filter((spec) => spec !== propTypesSpecifier); modified = true; // Add PropTypes import after the React import if not already added if (!hasAddedPropTypesImport) { const propTypesImport = t.importDeclaration([t.importDefaultSpecifier(t.identifier('PropTypes'))], t.stringLiteral('prop-types')); path.insertAfter(propTypesImport); hasAddedPropTypesImport = true; } // If React import becomes empty (only had PropTypes), remove it if (path.node.specifiers.length === 0) { path.remove(); } } } } }); return modified; } /** * Transform React.PropTypes usage to PropTypes */ function transformReactPropTypesUsage(ast) { let modified = false; traverse(ast, { MemberExpression(path) { // Transform React.PropTypes.* to PropTypes.* if (t.isMemberExpression(path.node) && t.isIdentifier(path.node.object, { name: 'React' }) && t.isIdentifier(path.node.property, { name: 'PropTypes' })) { // Check if this is part of a longer chain like React.PropTypes.string const { parent } = path; if (t.isMemberExpression(parent) && parent.object === path.node) { // Replace React.PropTypes with just PropTypes path.replaceWith(t.identifier('PropTypes')); modified = true; } } } }); return modified; } /** * Add PropTypes import at the top of the file if needed */ function addPropTypesImportIfNeeded(ast, hasReactPropTypesUsage) { if (!hasReactPropTypesUsage) { return false; } // Check if PropTypes import already exists let hasExistingImport = false; traverse(ast, { ImportDeclaration(path) { if (path.node.source.value === 'prop-types') { hasExistingImport = true; } } }); if (hasExistingImport) { return false; } // Find the best place to insert the import (after React import or at the top) const { program } = ast; let insertIndex = 0; // Look for React import to insert after it for (let i = 0; i < program.body.length; i++) { const node = program.body[i]; if (t.isImportDeclaration(node) && (node.source.value === 'react' || node.source.value === 'React')) { insertIndex = i + 1; break; } } // Create PropTypes import const propTypesImport = t.importDeclaration([t.importDefaultSpecifier(t.identifier('PropTypes'))], t.stringLiteral('prop-types')); // Insert the import program.body.splice(insertIndex, 0, propTypesImport); return true; } /** * Main transformation function using AST */ export function transformWithAST(code, filename = 'unknown.js') { try { // Parse the code into AST const ast = parseCode(code, filename); // Analyze PropTypes usage const usage = analyzePropTypesUsage(ast); // If no PropTypes usage found, return original code if (!usage.needsTransformation) { return { code, modified: false }; } let modified = false; // Transform ES6 imports if (usage.hasES6Import) { modified = transformES6Imports(ast) || modified; } // Transform React.PropTypes usage if (usage.hasReactPropTypesUsage) { modified = transformReactPropTypesUsage(ast) || modified; modified = addPropTypesImportIfNeeded(ast, true) || modified; } // Generate the transformed code const result = generate(ast, { retainLines: false, compact: false, concise: false, jsescOption: { quotes: 'single' } }); return { code: result.code, modified }; } catch (error) { // If AST parsing fails, return original code console.warn(`AST transformation failed for ${filename}:`, error instanceof Error ? error.message : String(error)); return { code, modified: false }; } } /** * Check if file needs PropTypes transformation using AST analysis */ export function needsTransformation(code, filename = 'unknown.js') { try { const ast = parseCode(code, filename); const usage = analyzePropTypesUsage(ast); return usage.needsTransformation; } catch { // If parsing fails, fall back to conservative approach return false; } } //# sourceMappingURL=ast-transformer.js.map