transforms/create-element-to-jsx.js (232 lines of code) (raw):

/** * Copyright 2015-present, Facebook, Inc. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * */ 'use strict'; module.exports = function(file, api, options) { const j = api.jscodeshift; const printOptions = options.printOptions || {}; const root = j(file.source); const ReactUtils = require('./utils/ReactUtils')(j); const encodeJSXTextValue = value => value.replace(/</g, '&lt;').replace(/>/g, '&gt;'); const canLiteralBePropString = node => node.raw.indexOf('\\') === -1 && node.value.indexOf('"') === -1; const convertExpressionToJSXAttributes = expression => { if (!expression) { return { attributes: [], extraComments: [] }; } const isReactSpread = expression.type === 'CallExpression' && expression.callee.type === 'MemberExpression' && expression.callee.object.name === 'React' && expression.callee.property.name === '__spread'; const isObjectAssign = expression.type === 'CallExpression' && expression.callee.type === 'MemberExpression' && expression.callee.object.name === 'Object' && expression.callee.property.name === 'assign'; const validSpreadTypes = [ 'Identifier', 'MemberExpression', 'CallExpression' ]; if (isReactSpread || isObjectAssign) { const resultAttributes = []; const resultExtraComments = expression.comments || []; const { callee } = expression; for (const node of [callee, callee.object, callee.property]) { resultExtraComments.push(...(node.comments || [])); } expression.arguments.forEach(expression => { const { attributes, extraComments } = convertExpressionToJSXAttributes( expression ); resultAttributes.push(...attributes); resultExtraComments.push(...extraComments); }); return { attributes: resultAttributes, extraComments: resultExtraComments }; } else if (validSpreadTypes.indexOf(expression.type) != -1) { return { attributes: [j.jsxSpreadAttribute(expression)], extraComments: [] }; } else if (expression.type === 'ObjectExpression') { const attributes = expression.properties.map(property => { if (property.type === 'SpreadProperty') { const spreadAttribute = j.jsxSpreadAttribute(property.argument); spreadAttribute.comments = property.comments; return spreadAttribute; } else if (property.type === 'Property') { const propertyValueType = property.value.type; let value; if ( propertyValueType === 'Literal' && typeof property.value.value === 'string' && canLiteralBePropString(property.value) ) { value = j.literal(property.value.value); value.comments = property.value.comments; } else { value = j.jsxExpressionContainer(property.value); } let jsxIdentifier; if (property.key.type === 'Literal') { jsxIdentifier = j.jsxIdentifier(property.key.value); } else { jsxIdentifier = j.jsxIdentifier(property.key.name); } jsxIdentifier.comments = property.key.comments; const jsxAttribute = j.jsxAttribute(jsxIdentifier, value); jsxAttribute.comments = property.comments; return jsxAttribute; } return null; }); return { attributes, extraComments: expression.comments || [] }; } else if (expression.type === 'Literal' && expression.value === null) { return { attributes: [], extraComments: expression.comments || [] }; } else { throw new Error(`Unexpected attribute of type "${expression.type}"`); } }; const canConvertToJSXIdentifier = node => (node.type === 'Literal' && typeof node.value === 'string') || node.type === 'Identifier' || (node.type === 'MemberExpression' && !node.computed && canConvertToJSXIdentifier(node.object) && canConvertToJSXIdentifier(node.property)); const jsxIdentifierFor = node => { let identifier; let comments = node.comments || []; if (node.type === 'Literal') { identifier = j.jsxIdentifier(node.value); } else if (node.type === 'MemberExpression') { let { identifier: objectIdentifier, comments: objectComments } = jsxIdentifierFor(node.object); let { identifier: propertyIdentifier, comments: propertyComments } = jsxIdentifierFor(node.property); identifier = j.jsxMemberExpression(objectIdentifier, propertyIdentifier); comments.push(...objectComments, ...propertyComments); } else { identifier = j.jsxIdentifier(node.name); } return { identifier, comments }; }; const isCapitalizationInvalid = node => (node.type === 'Literal' && !/^[a-z]/.test(node.value)) || (node.type === 'Identifier' && /^[a-z]/.test(node.name)); const convertNodeToJSX = node => { const comments = node.value.comments || []; const { callee } = node.value; for (const calleeNode of [callee, callee.object, callee.property]) { for (const comment of calleeNode.comments || []) { comment.leading = true; comment.trailing = false; comments.push(comment); } } const args = node.value.arguments; if ( isCapitalizationInvalid(args[0]) || !canConvertToJSXIdentifier(args[0]) ) { return node.value; } const { identifier: jsxIdentifier, comments: identifierComments } = jsxIdentifierFor(args[0]); const props = args[1]; const { attributes, extraComments } = convertExpressionToJSXAttributes( props ); for (const comment of [...identifierComments, ...extraComments]) { comment.leading = false; comment.trailing = true; comments.push(comment); } const children = args.slice(2).map((child, index) => { if ( child.type === 'Literal' && typeof child.value === 'string' && !child.comments && child.value !== '' && child.value.trim() === child.value ) { return j.jsxText(encodeJSXTextValue(child.value)); } else if ( child.type === 'CallExpression' && child.callee.object && child.callee.object.name === 'React' && child.callee.property.name === 'createElement' ) { const jsxChild = convertNodeToJSX(node.get('arguments', index + 2)); if ( jsxChild.type !== 'JSXElement' || (jsxChild.comments || []).length > 0 ) { return j.jsxExpressionContainer(jsxChild); } else { return jsxChild; } } else if (child.type === 'SpreadElement') { return j.jsxExpressionContainer(child.argument); } else { return j.jsxExpressionContainer(child); } }); const openingElement = j.jsxOpeningElement(jsxIdentifier, attributes); if (children.length) { const endIdentifier = Object.assign({}, jsxIdentifier, { comments: [] }); // Add text newline nodes between elements so recast formats one child per // line instead of all children on one line. const paddedChildren = [j.jsxText('\n')]; for (const child of children) { paddedChildren.push(child, j.jsxText('\n')); } const element = j.jsxElement( openingElement, j.jsxClosingElement(endIdentifier), paddedChildren ); element.comments = comments; return element; } else { openingElement.selfClosing = true; const element = j.jsxElement(openingElement); element.comments = comments; return element; } }; if (options['explicit-require'] === false || ReactUtils.hasReact(root)) { const mutations = root .find(j.CallExpression, { callee: { object: { name: 'React' }, property: { name: 'createElement' } } }) .replaceWith(convertNodeToJSX) .size(); if (mutations) { return root.toSource(printOptions); } } return null; };