rsnext/packages/next-codemod/transforms/new-link.ts
Zack Tanner e039cc72fc
enable verbatimModuleSyntax to make type imports/exports explicit (#56551)
This has come up in code review a few times, so enabling the tsconfig setting so it's more easily caught.
2023-10-07 15:09:54 +00:00

112 lines
3.3 KiB
TypeScript

import type { API, FileInfo } from 'jscodeshift'
export default function transformer(file: FileInfo, api: API) {
const j = api.jscodeshift.withParser('tsx')
const $j = j(file.source)
return $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)
const hasStylesJSX = $j.findJSXElements('style').some((stylePath) => {
const $style = j(stylePath)
const hasJSXProp =
$style.find(j.JSXAttribute, { name: { name: 'jsx' } }).size() !== 0
return hasJSXProp
})
linkElements.forEach((linkPath) => {
const $link = j(linkPath).filter((childPath) => {
// Exclude links with `legacybehavior` prop from modification
return (
j(childPath)
.find(j.JSXAttribute, { name: { name: 'legacyBehavior' } })
.size() === 0
)
})
if ($link.size() === 0) {
return
}
// If file has <style jsx> enable legacyBehavior
// and keep <a> to stay on the safe side
if (hasStylesJSX) {
$link
.get('attributes')
.push(j.jsxAttribute(j.jsxIdentifier('legacyBehavior')))
return
}
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'
)
})
// No <a> as child to <Link> so the old behavior is used
if ($childrenWithA.size() !== 1) {
$link
.get('attributes')
.push(j.jsxAttribute(j.jsxIdentifier('legacyBehavior')))
return
}
const props = $childrenWithA.get('attributes').value
const hasProps = props.length > 0
if (hasProps) {
// Add only unique props to <Link> (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 <a>
props.length = 0
}
const childrenProps = $childrenWithA.get('children')
$childrenWithA.replaceWith(childrenProps.value)
})
})
.toSource()
}