Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update next/image codemod to handle require() #41345

Merged
merged 1 commit into from Oct 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -0,0 +1,14 @@
const Image = require("next/legacy/image");
const Named = require("next/legacy/image");
const Foo = require("foo");

export default function Home() {
return (
<div>
<h1>Upgrade</h1>
<Image src="/test.jpg" width="200" height="300" layout="fixed" />
<Named src="/test.png" width="400" height="500" layout="fixed" />
<Foo />
</div>
);
}
@@ -0,0 +1,14 @@
const Image = require("next/image");
const Named = require("next/image");
const Foo = require("foo");

export default function Home() {
return (
<div>
<h1>Upgrade</h1>
<Image src="/test.jpg" width="200" height="300" />
<Named src="/test.png" width="400" height="500" />
<Foo />
</div>
);
}
@@ -0,0 +1,14 @@
export async function Home() {
const Image = require("next/image");
const Named = require("next/image");
const Foo = require("foo");
const Future1 = require("next/future/image");
const Future2 = require("next/future/image");
return (<div>
<h1>Both</h1>
<Image src="/test.jpg" width="200" height="300" />
<Named src="/test.png" width="500" height="400" />
<Future1 src="/test.webp" width="60" height="70" />
<Future2 src="/test.avif" width="80" height="90" />
</div>)
}
@@ -0,0 +1,14 @@
export async function Home() {
const Image = require("next/legacy/image");
const Named = require("next/legacy/image");
const Foo = require("foo");
const Future1 = require("next/image");
const Future2 = require("next/image");
return (<div>
<h1>Both</h1>
<Image src="/test.jpg" width="200" height="300" />
<Named src="/test.png" width="500" height="400" />
<Future1 src="/test.webp" width="60" height="70" />
<Future2 src="/test.avif" width="80" height="90" />
</div>)
}
303 changes: 167 additions & 136 deletions packages/next-codemod/transforms/next-image-experimental.ts
@@ -1,11 +1,147 @@
import type {
API,
Collection,
FileInfo,
ImportDefaultSpecifier,
JSCodeshift,
JSXAttribute,
Options,
} from 'jscodeshift'

function findAndReplaceProps(
j: JSCodeshift,
root: Collection,
tagName: string
) {
const layoutToStyle: Record<string, Record<string, string> | null> = {
intrinsic: { maxWidth: '100%', height: 'auto' },
responsive: { width: '100%', height: 'auto' },
fill: null,
fixed: null,
}
const layoutToSizes: Record<string, string | null> = {
intrinsic: null,
responsive: '100vw',
fill: '100vw',
fixed: null,
}
root
.find(j.JSXElement)
.filter(
(el) =>
el.value.openingElement.name &&
el.value.openingElement.name.type === 'JSXIdentifier' &&
el.value.openingElement.name.name === tagName
)
.forEach((el) => {
let layout = 'intrisic'
let objectFit = null
let objectPosition = null
let styleExpProps = []
let sizesAttr: JSXAttribute | null = null
const attributes = el.node.openingElement.attributes?.filter((a) => {
if (a.type !== 'JSXAttribute') {
return true
}
// TODO: hanlde case when not Literal
if (a.value?.type === 'Literal') {
if (a.name.name === 'layout') {
layout = String(a.value.value)
return false
}
if (a.name.name === 'objectFit') {
objectFit = String(a.value.value)
return false
}
if (a.name.name === 'objectPosition') {
objectPosition = String(a.value.value)
return false
}
}
if (a.name.name === 'style') {
if (
a.value?.type === 'JSXExpressionContainer' &&
a.value.expression.type === 'ObjectExpression'
) {
styleExpProps = a.value.expression.properties
} else if (
a.value?.type === 'JSXExpressionContainer' &&
a.value.expression.type === 'Identifier'
) {
styleExpProps = [
j.spreadElement(j.identifier(a.value.expression.name)),
]
} else {
console.warn('Unknown style attribute value detected', a.value)
}
return false
}
if (a.name.name === 'sizes') {
sizesAttr = a
return false
}
if (a.name.name === 'lazyBoundary') {
return false
}
if (a.name.name === 'lazyRoot') {
return false
}
return true
})

if (layout === 'fill') {
attributes.push(j.jsxAttribute(j.jsxIdentifier('fill')))
}

const sizes = layoutToSizes[layout]
if (sizes && !sizesAttr) {
sizesAttr = j.jsxAttribute(j.jsxIdentifier('sizes'), j.literal(sizes))
}

if (sizesAttr) {
attributes.push(sizesAttr)
}

let style = layoutToStyle[layout]
if (style || objectFit || objectPosition) {
if (!style) {
style = {}
}
if (objectFit) {
style.objectFit = objectFit
}
if (objectPosition) {
style.objectPosition = objectPosition
}
Object.entries(style).forEach(([key, value]) => {
styleExpProps.push(
j.objectProperty(j.identifier(key), j.stringLiteral(value))
)
})
const styleAttribute = j.jsxAttribute(
j.jsxIdentifier('style'),
j.jsxExpressionContainer(j.objectExpression(styleExpProps))
)
attributes.push(styleAttribute)
}

// TODO: should we add `alt=""` attribute?
// We should probably let the use it manually.

j(el).replaceWith(
j.jsxElement(
j.jsxOpeningElement(
el.node.openingElement.name,
attributes,
el.node.openingElement.selfClosing
),
el.node.closingElement,
el.node.children
)
)
})
}

export default function transformer(
file: FileInfo,
api: API,
Expand All @@ -24,145 +160,17 @@ export default function transformer(
const defaultSpecifier = imageImport.node.specifiers?.find(
(node) => node.type === 'ImportDefaultSpecifier'
) as ImportDefaultSpecifier | undefined
const defaultSpecifierName = defaultSpecifier?.local?.name
const tagName = defaultSpecifier?.local?.name

j(imageImport).replaceWith(
j.importDeclaration(
imageImport.node.specifiers,
j.stringLiteral('next/image')
if (tagName) {
j(imageImport).replaceWith(
j.importDeclaration(
imageImport.node.specifiers,
j.stringLiteral('next/image')
)
)
)

const layoutToStyle: Record<string, Record<string, string> | null> = {
intrinsic: { maxWidth: '100%', height: 'auto' },
responsive: { width: '100%', height: 'auto' },
fill: null,
fixed: null,
}
const layoutToSizes: Record<string, string | null> = {
intrinsic: null,
responsive: '100vw',
fill: '100vw',
fixed: null,
findAndReplaceProps(j, root, tagName)
}
root
.find(j.JSXElement)
.filter(
(el) =>
el.value.openingElement.name &&
el.value.openingElement.name.type === 'JSXIdentifier' &&
el.value.openingElement.name.name === defaultSpecifierName
)
.forEach((el) => {
let layout = 'intrisic'
let objectFit = null
let objectPosition = null
let styleExpProps = []
let sizesAttr: JSXAttribute | null = null
const attributes = el.node.openingElement.attributes?.filter((a) => {
if (a.type !== 'JSXAttribute') {
return true
}
// TODO: hanlde case when not Literal
if (a.value?.type === 'Literal') {
if (a.name.name === 'layout') {
layout = String(a.value.value)
return false
}
if (a.name.name === 'objectFit') {
objectFit = String(a.value.value)
return false
}
if (a.name.name === 'objectPosition') {
objectPosition = String(a.value.value)
return false
}
}
if (a.name.name === 'style') {
if (
a.value?.type === 'JSXExpressionContainer' &&
a.value.expression.type === 'ObjectExpression'
) {
styleExpProps = a.value.expression.properties
} else if (
a.value?.type === 'JSXExpressionContainer' &&
a.value.expression.type === 'Identifier'
) {
styleExpProps = [
j.spreadElement(j.identifier(a.value.expression.name)),
]
} else {
console.warn('Unknown style attribute value detected', a.value)
}
return false
}
if (a.name.name === 'sizes') {
sizesAttr = a
return false
}
if (a.name.name === 'lazyBoundary') {
return false
}
if (a.name.name === 'lazyRoot') {
return false
}
return true
})

if (layout === 'fill') {
attributes.push(j.jsxAttribute(j.jsxIdentifier('fill')))
}

const sizes = layoutToSizes[layout]
if (sizes && !sizesAttr) {
sizesAttr = j.jsxAttribute(
j.jsxIdentifier('sizes'),
j.literal(sizes)
)
}

if (sizesAttr) {
attributes.push(sizesAttr)
}

let style = layoutToStyle[layout]
if (style || objectFit || objectPosition) {
if (!style) {
style = {}
}
if (objectFit) {
style.objectFit = objectFit
}
if (objectPosition) {
style.objectPosition = objectPosition
}
Object.entries(style).forEach(([key, value]) => {
styleExpProps.push(
j.objectProperty(j.identifier(key), j.stringLiteral(value))
)
})
const styleAttribute = j.jsxAttribute(
j.jsxIdentifier('style'),
j.jsxExpressionContainer(j.objectExpression(styleExpProps))
)
attributes.push(styleAttribute)
}

// TODO: should we add `alt=""` attribute?
// We should probably let the use it manually.

j(el).replaceWith(
j.jsxElement(
j.jsxOpeningElement(
el.node.openingElement.name,
attributes,
el.node.openingElement.selfClosing
),
el.node.closingElement,
el.node.children
)
)
})
})
// Before: const Image = await import("next/legacy/image")
// After: const Image = await import("next/image")
Expand All @@ -175,6 +183,29 @@ export default function transformer(
j.importExpression(j.stringLiteral('next/image'))
)
})

// Before: const Image = require("next/legacy/image")
// After: const Image = require("next/image")
root.find(j.CallExpression).forEach((requireExp) => {
if (
requireExp?.value?.callee?.type === 'Identifier' &&
requireExp.value.callee.name === 'require'
) {
let firstArg = requireExp.value.arguments[0]
if (
firstArg &&
firstArg.type === 'Literal' &&
firstArg.value === 'next/legacy/image'
) {
const tagName = requireExp?.parentPath?.value?.id?.name
if (tagName) {
requireExp.value.arguments[0] = j.literal('next/image')
findAndReplaceProps(j, root, tagName)
}
}
}
})

// TODO: do the same transforms for dynamic imports
return root.toSource(options)
}