// It might insert extra parens for JSX components // x-ref: https://github.com/facebook/jscodeshift/issues/534 import type { API, Collection, FileInfo, JSXElement } from 'jscodeshift' import { createParserFromPath } from '../lib/parser' import { NEXT_CODEMOD_ERROR_PREFIX } from './lib/async-request-api/utils' export default function transformer(file: FileInfo, _api: API) { const j = createParserFromPath(file.path) const $j = j(file.source) let hasChanges = false $j.find(j.ImportDeclaration, { source: { value: 'next/link' } }).forEach( (path) => { const defaultImport = j(path).find(j.ImportDefaultSpecifier) if (defaultImport.size() === 0) { return } const variableName = j(path) .find(j.ImportDefaultSpecifier) .find(j.Identifier) .get('name').value if (!variableName) { return } const linkElements = $j.findJSXElements(variableName) linkElements.forEach((linkPath) => { const $link: Collection = j(linkPath) if ($link.size() === 0) { return } const $legacyBehaviorProps = $link.find(j.JSXAttribute, { name: { type: 'JSXIdentifier', name: 'legacyBehavior' }, }) $legacyBehaviorProps.remove() hasChanges ||= $legacyBehaviorProps.size() > 0 const $passHrefProps = $link.find(j.JSXAttribute, { name: { type: 'JSXIdentifier', name: 'passHref' }, }) $passHrefProps.remove() hasChanges ||= $passHrefProps.size() > 0 const linkChildrenNodes = $link.get('children') // Text-only link children are already correct with the new behavior // `next/link` would previously auto-wrap typeof 'string' children already if ( linkChildrenNodes.value && linkChildrenNodes.value.length === 1 && linkChildrenNodes.value[0].type === 'JSXText' ) { return } // Direct child elements referenced const $childrenElements = $link.childElements() const $childrenWithA = $childrenElements.filter((childPath) => { return ( j(childPath).find(j.JSXOpeningElement).get('name').get('name') .value === 'a' ) }) if ($childrenWithA.length === 0) { if ($legacyBehaviorProps.length > 0) { linkPath.node.children.unshift( j.jsxText('\n'), j.jsxExpressionContainer.from({ expression: j.jsxEmptyExpression.from({ comments: [ j.commentBlock.from({ value: ` ${NEXT_CODEMOD_ERROR_PREFIX} This Link previously used the now removed \`legacyBehavior\` prop, and has a child that might not be an anchor. The codemod bailed out of lifting the child props to the Link. Check that the child component does not render an anchor, and potentially move the props manually to Link. `, }), ], }), }) ) hasChanges = true } } else { const props = $childrenWithA.get('attributes').value const hasProps = props.length > 0 if (hasProps) { // Add only unique props to (skip duplicate props) const linkPropNames = $link .get('attributes') .value.map((linkProp) => linkProp?.name?.name) const uniqueProps = [] props.forEach((anchorProp) => { if (!linkPropNames.includes(anchorProp?.name?.name)) { uniqueProps.push(anchorProp) } }) $link.get('attributes').value.push(...uniqueProps) // Remove props from props.length = 0 hasChanges = true } const childrenProps = $childrenWithA.get('children') $childrenWithA.replaceWith(childrenProps.value) hasChanges = true } }) } ) if (hasChanges) { return $j.toSource() } return file.source }