2023-02-18 03:35:41 +01:00
|
|
|
import { join, parse } from 'path'
|
2022-10-22 00:26:42 +02:00
|
|
|
import { writeFileSync } from 'fs'
|
2022-10-11 22:44:36 +02:00
|
|
|
import type {
|
|
|
|
API,
|
2022-10-12 03:12:02 +02:00
|
|
|
Collection,
|
2022-10-11 22:44:36 +02:00
|
|
|
FileInfo,
|
|
|
|
ImportDefaultSpecifier,
|
2022-10-12 03:12:02 +02:00
|
|
|
JSCodeshift,
|
2022-10-11 22:44:36 +02:00
|
|
|
JSXAttribute,
|
|
|
|
Options,
|
|
|
|
} from 'jscodeshift'
|
|
|
|
|
2022-10-12 03:12:02 +02:00
|
|
|
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) => {
|
2022-10-17 16:41:35 +02:00
|
|
|
let layout = 'intrinsic'
|
2022-10-12 03:12:02 +02:00
|
|
|
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
|
|
|
|
}
|
2022-10-17 16:41:35 +02:00
|
|
|
|
|
|
|
if (a.name.name === 'layout' && 'value' in a.value) {
|
|
|
|
layout = String(a.value.value)
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
if (a.name.name === 'objectFit' && 'value' in a.value) {
|
|
|
|
objectFit = String(a.value.value)
|
|
|
|
return false
|
2022-10-12 03:12:02 +02:00
|
|
|
}
|
2022-10-17 16:41:35 +02:00
|
|
|
if (a.name.name === 'objectPosition' && 'value' in a.value) {
|
|
|
|
objectPosition = String(a.value.value)
|
|
|
|
return false
|
|
|
|
}
|
2022-10-27 18:17:28 +02:00
|
|
|
if (a.name.name === 'lazyBoundary') {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
if (a.name.name === 'lazyRoot') {
|
|
|
|
return false
|
|
|
|
}
|
2022-10-17 16:41:35 +02:00
|
|
|
|
2022-10-12 03:12:02 +02:00
|
|
|
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
|
|
|
|
)
|
|
|
|
)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-02-18 03:35:41 +01:00
|
|
|
function nextConfigTransformer(
|
|
|
|
j: JSCodeshift,
|
|
|
|
root: Collection,
|
|
|
|
appDir: string
|
|
|
|
) {
|
2022-10-22 00:26:42 +02:00
|
|
|
let pathPrefix = ''
|
|
|
|
let loaderType = ''
|
|
|
|
root.find(j.ObjectExpression).forEach((o) => {
|
2023-02-18 03:35:41 +01:00
|
|
|
;(o.value.properties || []).forEach((images) => {
|
|
|
|
if (
|
|
|
|
images.type === 'ObjectProperty' &&
|
|
|
|
images.key.type === 'Identifier' &&
|
|
|
|
images.key.name === 'images' &&
|
|
|
|
images.value.type === 'ObjectExpression' &&
|
|
|
|
images.value.properties
|
|
|
|
) {
|
|
|
|
const properties = images.value.properties.filter((p) => {
|
2022-10-22 00:26:42 +02:00
|
|
|
if (
|
2023-02-18 03:35:41 +01:00
|
|
|
p.type === 'ObjectProperty' &&
|
|
|
|
p.key.type === 'Identifier' &&
|
|
|
|
p.key.name === 'loader' &&
|
|
|
|
'value' in p.value
|
2022-10-22 00:26:42 +02:00
|
|
|
) {
|
2023-02-18 03:35:41 +01:00
|
|
|
if (
|
|
|
|
p.value.value === 'imgix' ||
|
|
|
|
p.value.value === 'cloudinary' ||
|
|
|
|
p.value.value === 'akamai'
|
|
|
|
) {
|
|
|
|
loaderType = p.value.value
|
|
|
|
p.value.value = 'custom'
|
|
|
|
}
|
2022-10-22 00:26:42 +02:00
|
|
|
}
|
2023-02-18 03:35:41 +01:00
|
|
|
if (
|
|
|
|
p.type === 'ObjectProperty' &&
|
|
|
|
p.key.type === 'Identifier' &&
|
|
|
|
p.key.name === 'path' &&
|
|
|
|
'value' in p.value
|
|
|
|
) {
|
|
|
|
pathPrefix = String(p.value.value)
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
})
|
|
|
|
if (loaderType && pathPrefix) {
|
|
|
|
const importSpecifier = `./${loaderType}-loader.js`
|
|
|
|
const filePath = join(appDir, importSpecifier)
|
|
|
|
properties.push(
|
|
|
|
j.property(
|
|
|
|
'init',
|
|
|
|
j.identifier('loaderFile'),
|
|
|
|
j.literal(importSpecifier)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
images.value.properties = properties
|
|
|
|
const normalizeSrc = `const normalizeSrc = (src) => src[0] === '/' ? src.slice(1) : src`
|
|
|
|
if (loaderType === 'imgix') {
|
|
|
|
writeFileSync(
|
|
|
|
filePath,
|
|
|
|
`${normalizeSrc}
|
2022-10-22 00:26:42 +02:00
|
|
|
export default function imgixLoader({ src, width, quality }) {
|
|
|
|
const url = new URL('${pathPrefix}' + normalizeSrc(src))
|
|
|
|
const params = url.searchParams
|
|
|
|
params.set('auto', params.getAll('auto').join(',') || 'format')
|
|
|
|
params.set('fit', params.get('fit') || 'max')
|
|
|
|
params.set('w', params.get('w') || width.toString())
|
|
|
|
if (quality) { params.set('q', quality.toString()) }
|
|
|
|
return url.href
|
|
|
|
}`
|
2023-02-18 03:35:41 +01:00
|
|
|
.split('\n')
|
|
|
|
.map((l) => l.trim())
|
|
|
|
.join('\n')
|
|
|
|
)
|
|
|
|
} else if (loaderType === 'cloudinary') {
|
|
|
|
writeFileSync(
|
|
|
|
filePath,
|
|
|
|
`${normalizeSrc}
|
2022-10-22 00:26:42 +02:00
|
|
|
export default function cloudinaryLoader({ src, width, quality }) {
|
|
|
|
const params = ['f_auto', 'c_limit', 'w_' + width, 'q_' + (quality || 'auto')]
|
|
|
|
const paramsString = params.join(',') + '/'
|
|
|
|
return '${pathPrefix}' + paramsString + normalizeSrc(src)
|
|
|
|
}`
|
2023-02-18 03:35:41 +01:00
|
|
|
.split('\n')
|
|
|
|
.map((l) => l.trim())
|
|
|
|
.join('\n')
|
|
|
|
)
|
|
|
|
} else if (loaderType === 'akamai') {
|
|
|
|
writeFileSync(
|
|
|
|
filePath,
|
|
|
|
`${normalizeSrc}
|
2022-10-22 00:26:42 +02:00
|
|
|
export default function akamaiLoader({ src, width, quality }) {
|
|
|
|
return '${pathPrefix}' + normalizeSrc(src) + '?imwidth=' + width
|
|
|
|
}`
|
2023-02-18 03:35:41 +01:00
|
|
|
.split('\n')
|
|
|
|
.map((l) => l.trim())
|
|
|
|
.join('\n')
|
|
|
|
)
|
|
|
|
}
|
2022-10-22 00:26:42 +02:00
|
|
|
}
|
|
|
|
}
|
2023-02-18 03:35:41 +01:00
|
|
|
})
|
2022-10-22 00:26:42 +02:00
|
|
|
})
|
|
|
|
return root
|
|
|
|
}
|
|
|
|
|
2022-10-11 22:44:36 +02:00
|
|
|
export default function transformer(
|
|
|
|
file: FileInfo,
|
|
|
|
api: API,
|
|
|
|
options: Options
|
|
|
|
) {
|
2023-02-17 02:36:09 +01:00
|
|
|
const j = api.jscodeshift.withParser('tsx')
|
2022-10-11 22:44:36 +02:00
|
|
|
const root = j(file.source)
|
|
|
|
|
2023-02-18 03:35:41 +01:00
|
|
|
const parsed = parse(file.path || '/')
|
2022-10-22 00:26:42 +02:00
|
|
|
const isConfig =
|
2023-02-18 03:35:41 +01:00
|
|
|
parsed.base === 'next.config.js' ||
|
|
|
|
parsed.base === 'next.config.ts' ||
|
|
|
|
parsed.base === 'next.config.mjs' ||
|
|
|
|
parsed.base === 'next.config.cjs'
|
2022-10-22 00:26:42 +02:00
|
|
|
|
|
|
|
if (isConfig) {
|
2023-02-18 03:35:41 +01:00
|
|
|
const result = nextConfigTransformer(j, root, parsed.dir)
|
2022-10-22 00:26:42 +02:00
|
|
|
return result.toSource()
|
|
|
|
}
|
|
|
|
|
2022-10-11 22:44:36 +02:00
|
|
|
// Before: import Image from "next/legacy/image"
|
|
|
|
// After: import Image from "next/image"
|
|
|
|
root
|
|
|
|
.find(j.ImportDeclaration, {
|
|
|
|
source: { value: 'next/legacy/image' },
|
|
|
|
})
|
|
|
|
.forEach((imageImport) => {
|
|
|
|
const defaultSpecifier = imageImport.node.specifiers?.find(
|
|
|
|
(node) => node.type === 'ImportDefaultSpecifier'
|
|
|
|
) as ImportDefaultSpecifier | undefined
|
2022-10-12 03:12:02 +02:00
|
|
|
const tagName = defaultSpecifier?.local?.name
|
2023-02-27 22:36:26 +01:00
|
|
|
imageImport.node.source = j.stringLiteral('next/image')
|
2022-10-12 03:12:02 +02:00
|
|
|
if (tagName) {
|
|
|
|
findAndReplaceProps(j, root, tagName)
|
2022-10-11 22:44:36 +02:00
|
|
|
}
|
|
|
|
})
|
|
|
|
// Before: const Image = await import("next/legacy/image")
|
|
|
|
// After: const Image = await import("next/image")
|
2023-02-17 02:36:09 +01:00
|
|
|
root.find(j.AwaitExpression).forEach((awaitExp) => {
|
|
|
|
const arg = awaitExp.value.argument
|
|
|
|
if (arg?.type === 'CallExpression' && arg.callee.type === 'Import') {
|
|
|
|
if (
|
|
|
|
arg.arguments[0].type === 'StringLiteral' &&
|
|
|
|
arg.arguments[0].value === 'next/legacy/image'
|
|
|
|
) {
|
|
|
|
arg.arguments[0] = j.stringLiteral('next/image')
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
2022-10-12 03:12:02 +02:00
|
|
|
|
|
|
|
// 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 &&
|
2023-02-17 02:36:09 +01:00
|
|
|
firstArg.type === 'StringLiteral' &&
|
2022-10-12 03:12:02 +02:00
|
|
|
firstArg.value === 'next/legacy/image'
|
|
|
|
) {
|
|
|
|
const tagName = requireExp?.parentPath?.value?.id?.name
|
|
|
|
if (tagName) {
|
2023-02-17 02:36:09 +01:00
|
|
|
requireExp.value.arguments[0] = j.stringLiteral('next/image')
|
2022-10-12 03:12:02 +02:00
|
|
|
findAndReplaceProps(j, root, tagName)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
2022-10-11 22:44:36 +02:00
|
|
|
// TODO: do the same transforms for dynamic imports
|
|
|
|
return root.toSource(options)
|
|
|
|
}
|