transforms/update-react-imports.js (306 lines of code) (raw):

/** * (c) Facebook, Inc. and its affiliates. Confidential and proprietary. * * @format */ 'use strict'; module.exports = function(file, api, options) { const j = api.jscodeshift; const printOptions = options.printOptions || {}; const root = j(file.source); const destructureNamespaceImports = options.destructureNamespaceImports; // https://github.com/facebook/jscodeshift/blob/master/recipes/retain-first-comment.md function getFirstNode() { return root.find(j.Program).get('body', 0).node; } // Save the comments attached to the first node const firstNode = getFirstNode(); const { comments } = firstNode; function isVariableDeclared(variable) { return ( root .find(j.Identifier, { name: variable, }) .filter( path => path.parent.value.type !== 'MemberExpression' && path.parent.value.type !== 'QualifiedTypeIdentifier' && path.parent.value.type !== 'JSXMemberExpression', ) .size() > 0 ); } // Get all paths that import from React const reactImportPaths = root .find(j.ImportDeclaration, { type: 'ImportDeclaration', }) .filter(path => { return ( ( path.value.source.type === 'Literal' || path.value.source.type === 'StringLiteral' ) && ( path.value.source.value === 'React' || path.value.source.value === 'react' ) ); }); // get all namespace/default React imports const reactPaths = reactImportPaths.filter(path => { return ( path.value.specifiers.length > 0 && path.value.importKind === 'value' && path.value.specifiers.some(specifier => specifier.local.name === 'React') ); }); if (reactPaths.size() > 1) { throw Error( 'There should only be one React import. Please remove the duplicate import and try again.', ); } if (reactPaths.size() === 0) { return null; } const reactPath = reactPaths.paths()[0]; // Reuse the node so that we can preserve quoting style. const reactLiteral = reactPath.value.source; const isDefaultImport = reactPath.value.specifiers.some( specifier => specifier.type === 'ImportDefaultSpecifier' && specifier.local.name === 'React', ); // Check to see if we should keep the React import const isReactImportUsed = root .find(j.Identifier, { name: 'React', }) .filter(path => { return path.parent.parent.value.type !== 'ImportDeclaration'; }) .size() > 0; // local: imported const reactIdentifiers = {}; const reactTypeIdentifiers = {}; let canDestructureReactVariable = false; if (isReactImportUsed && (isDefaultImport || destructureNamespaceImports)) { // Checks to see if the react variable is used itself (rather than used to access its properties) canDestructureReactVariable = root .find(j.Identifier, { name: 'React', }) .filter(path => { return path.parent.parent.value.type !== 'ImportDeclaration'; }) .filter( path => !( path.parent.value.type === 'MemberExpression' && path.parent.value.object.name === 'React' ) && !( path.parent.value.type === 'QualifiedTypeIdentifier' && path.parent.value.qualification.name === 'React' ) && !( path.parent.value.type === 'JSXMemberExpression' && path.parent.value.object.name === 'React' ), ) .size() === 0; if (canDestructureReactVariable) { // Add React identifiers to separate object so we can destructure the imports // later if we can. If a type variable that we are trying to import has already // been declared, do not try to destructure imports // (ex. Element is declared and we are using React.Element) root .find(j.QualifiedTypeIdentifier, { qualification: { type: 'Identifier', name: 'React', }, }) .forEach(path => { const id = path.value.id.name; if (path.parent.parent.value.type === 'TypeofTypeAnnotation') { // This is a typeof import so it isn't actually a type reactIdentifiers[id] = id; if (reactTypeIdentifiers[id]) { canDestructureReactVariable = false; } } else { reactTypeIdentifiers[id] = id; if (reactIdentifiers[id]) { canDestructureReactVariable = false; } } if (isVariableDeclared(id)) { canDestructureReactVariable = false; } }); // Add React identifiers to separate object so we can destructure the imports // later if we can. If a variable that we are trying to import has already // been declared, do not try to destructure imports // (ex. createElement is declared and we are using React.createElement) root .find(j.MemberExpression, { object: { type: 'Identifier', name: 'React', }, }) .forEach(path => { const property = path.value.property.name; reactIdentifiers[property] = property; if (isVariableDeclared(property) || reactTypeIdentifiers[property]) { canDestructureReactVariable = false; } }); // Add React identifiers to separate object so we can destructure the imports // later if we can. If a JSX variable that we are trying to import has already // been declared, do not try to destructure imports // (ex. Fragment is declared and we are using React.Fragment) root .find(j.JSXMemberExpression, { object: { type: 'JSXIdentifier', name: 'React', }, }) .forEach(path => { const property = path.value.property.name; reactIdentifiers[property] = property; if (isVariableDeclared(property) || reactTypeIdentifiers[property]) { canDestructureReactVariable = false; } }); } } if (canDestructureReactVariable) { // replace react identifiers root .find(j.QualifiedTypeIdentifier, { qualification: { type: 'Identifier', name: 'React', }, }) .forEach(path => { const id = path.value.id.name; j(path).replaceWith(j.identifier(id)); }); root .find(j.MemberExpression, { object: { type: 'Identifier', name: 'React', }, }) .forEach(path => { const property = path.value.property.name; j(path).replaceWith(j.identifier(property)); }); root .find(j.JSXMemberExpression, { object: { type: 'JSXIdentifier', name: 'React', }, }) .forEach(path => { const property = path.value.property.name; j(path).replaceWith(j.jsxIdentifier(property)); }); // Add exisiting React imports to map reactImportPaths.forEach(path => { const specifiers = path.value.specifiers; for (let i = 0; i < specifiers.length; i++) { const specifier = specifiers[i]; // get all type and regular imports that are imported // from React if (specifier.type === 'ImportSpecifier') { if ( path.value.importKind === 'type' || specifier.importKind === 'type' ) { reactTypeIdentifiers[specifier.local.name] = specifier.imported.name; } else { reactIdentifiers[specifier.local.name] = specifier.imported.name; } } } }); const regularImports = []; Object.keys(reactIdentifiers).forEach(local => { const imported = reactIdentifiers[local]; regularImports.push( j.importSpecifier(j.identifier(imported), j.identifier(local)), ); }); const typeImports = []; Object.keys(reactTypeIdentifiers).forEach(local => { const imported = reactTypeIdentifiers[local]; typeImports.push( j.importSpecifier(j.identifier(imported), j.identifier(local)), ); }); if (regularImports.length > 0) { j(reactPath).insertAfter( j.importDeclaration(regularImports, reactLiteral), ); } if (typeImports.length > 0) { j(reactPath).insertAfter( j.importDeclaration(typeImports, reactLiteral, 'type'), ); } // remove all old react imports reactImportPaths.forEach(path => { // This is for import type React from 'react' which shouldn't // be removed if ( path.value.specifiers.some( specifier => specifier.type === 'ImportDefaultSpecifier' && specifier.local.name === 'React' && (specifier.importKind === 'type' || path.value.importKind === 'type'), ) ) { j(path).insertAfter( j.importDeclaration( [j.importDefaultSpecifier(j.identifier('React'))], reactLiteral, 'type', ), ); } j(path).remove(); }); } else { // Remove the import because it's not being used // If we should keep the React import, just convert // default imports to named imports let isImportRemoved = false; const specifiers = reactPath.value.specifiers; for (let i = 0; i < specifiers.length; i++) { const specifier = specifiers[i]; if (specifier.type === 'ImportNamespaceSpecifier') { if (!isReactImportUsed) { isImportRemoved = true; j(reactPath).remove(); } } else if (specifier.type === 'ImportDefaultSpecifier') { if (isReactImportUsed) { j(reactPath).insertAfter( j.importDeclaration( [j.importNamespaceSpecifier(j.identifier('React'))], reactLiteral, ), ); } if (specifiers.length > 1) { const typeImports = []; const regularImports = []; for (let x = 0; x < specifiers.length; x++) { if (specifiers[x].type !== 'ImportDefaultSpecifier') { if (specifiers[x].importKind === 'type') { typeImports.push(specifiers[x]); } else { regularImports.push(specifiers[x]); } } } if (regularImports.length > 0) { j(reactPath).insertAfter( j.importDeclaration(regularImports, reactLiteral), ); } if (typeImports.length > 0) { j(reactPath).insertAfter( j.importDeclaration(typeImports, reactLiteral, 'type'), ); } } isImportRemoved = true; j(reactPath).remove(); } } if (!isImportRemoved) { return null; } } // If the first node has been modified or deleted, reattach the comments const firstNode2 = getFirstNode(); if (firstNode2 !== firstNode) { firstNode2.comments = comments; } return root.toSource(printOptions); };