Skip to content

Commit

Permalink
Update next/image codemod to handle require() (#41345)
Browse files Browse the repository at this point in the history
Follow up to a comment here:

-
#41004 (review)
  • Loading branch information
styfle committed Oct 12, 2022
1 parent e1e64e0 commit 5d7d739
Show file tree
Hide file tree
Showing 6 changed files with 259 additions and 136 deletions.
@@ -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)
}

0 comments on commit 5d7d739

Please sign in to comment.