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-experimental codemod to handle loaders #41633

Merged
merged 9 commits into from Oct 21, 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
2 changes: 1 addition & 1 deletion docs/advanced-features/codemods.md
Expand Up @@ -88,7 +88,7 @@ Dangerously migrates from `next/legacy/image` to the new `next/image` by adding
- Removes `objectPosition` prop and adds `style`
- Removes `lazyBoundary` prop
- Removes `lazyRoot` prop
- TODO: does not migrate the `loader` config. If you need it, you must manually add a `loader` prop.
- Changes next.config.js `loader` to "custom", removes `path`, and sets `loaderFile` to a new file.

#### Before: intrinsic

Expand Down
@@ -0,0 +1,6 @@
module.exports = {
images: {
loader: "akamai",
path: "https://example.com/",
},
}
@@ -0,0 +1,4 @@
const normalizeSrc = (src) => src[0] === '/' ? src.slice(1) : src
export default function akamaiLoader({ src, width, quality }) {
return 'https://example.com/' + normalizeSrc(src) + '?imwidth=' + width
}
@@ -0,0 +1,6 @@
module.exports = {
images: {
loader: "custom",
loaderFile: "./akamai-loader.js",
},
}
@@ -0,0 +1,6 @@
module.exports = {
images: {
loader: "cloudinary",
path: "https://example.com/",
},
}
@@ -0,0 +1,6 @@
const normalizeSrc = (src) => src[0] === '/' ? src.slice(1) : src
export default function cloudinaryLoader({ src, width, quality }) {
const params = ['f_auto', 'c_limit', 'w_' + width, 'q_' + (quality || 'auto')]
const paramsString = params.join(',') + '/'
return 'https://example.com/' + paramsString + normalizeSrc(src)
}
@@ -0,0 +1,6 @@
module.exports = {
images: {
loader: "custom",
loaderFile: "./cloudinary-loader.js",
},
}
@@ -0,0 +1,6 @@
module.exports = {
images: {
loader: "imgix",
path: "https://example.com/",
},
}
@@ -0,0 +1,10 @@
const normalizeSrc = (src) => src[0] === '/' ? src.slice(1) : src
export default function imgixLoader({ src, width, quality }) {
const url = new URL('https://example.com/' + 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
}
@@ -0,0 +1,6 @@
module.exports = {
images: {
loader: "custom",
loaderFile: "./imgix-loader.js",
},
}
@@ -0,0 +1,34 @@
/* global jest */
jest.autoMockOff()
const Runner = require('jscodeshift/dist/Runner');
const { cp, mkdir, rm, readdir, readFile } = require('fs/promises')
const { join } = require('path')

const fixtureDir = join(__dirname, '..', '__testfixtures__', 'next-image-experimental-loader')
const transform = join(__dirname, '..', 'next-image-experimental.js')
const opts = { recursive: true }

async function toObj(dir) {
const obj = {}
const files = await readdir(dir)
for (const file of files) {
obj[file] = await readFile(join(dir, file), 'utf8')
}
return obj
}
it.each(['imgix', 'cloudinary', 'akamai'])('should transform loader %s', async (loader) => {
try {
await mkdir(join(fixtureDir, 'tmp'), opts)
await cp(join(fixtureDir, loader, 'input'), join(fixtureDir, 'tmp'), opts)
process.chdir(join(fixtureDir, 'tmp'))
const result = await Runner.run(transform, [`.`], {})
expect(result.error).toBe(0)
expect(
await toObj(join(fixtureDir, 'tmp'))
).toStrictEqual(
await toObj(join(fixtureDir, loader, 'output'))
)
} finally {
await rm(join(fixtureDir, 'tmp'), opts)
}
})
106 changes: 106 additions & 0 deletions packages/next-codemod/transforms/next-image-experimental.ts
@@ -1,3 +1,4 @@
import { writeFileSync } from 'fs'
import type {
API,
Collection,
Expand Down Expand Up @@ -141,6 +142,100 @@ function findAndReplaceProps(
})
}

function nextConfigTransformer(j: JSCodeshift, root: Collection) {
let pathPrefix = ''
let loaderType = ''
root.find(j.ObjectExpression).forEach((o) => {
const [images] = o.value.properties || []
if (
images.type === 'Property' &&
images.key.type === 'Identifier' &&
images.key.name === 'images' &&
images.value.type === 'ObjectExpression' &&
images.value.properties
) {
const properties = images.value.properties.filter((p) => {
if (
p.type === 'Property' &&
p.key.type === 'Identifier' &&
p.key.name === 'loader' &&
'value' in p.value
) {
if (
p.value.value === 'imgix' ||
p.value.value === 'cloudinary' ||
p.value.value === 'akamai'
) {
loaderType = p.value.value
p.value.value = 'custom'
}
}
if (
p.type === 'Property' &&
p.key.type === 'Identifier' &&
p.key.name === 'path' &&
'value' in p.value
) {
pathPrefix = String(p.value.value)
return false
}
return true
})
if (loaderType && pathPrefix) {
let filename = `./${loaderType}-loader.js`
properties.push(
j.property('init', j.identifier('loaderFile'), j.literal(filename))
)
images.value.properties = properties
const normalizeSrc = `const normalizeSrc = (src) => src[0] === '/' ? src.slice(1) : src`
if (loaderType === 'imgix') {
writeFileSync(
filename,
`${normalizeSrc}
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
}`
.split('\n')
.map((l) => l.trim())
.join('\n')
)
} else if (loaderType === 'cloudinary') {
writeFileSync(
filename,
`${normalizeSrc}
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)
}`
.split('\n')
.map((l) => l.trim())
.join('\n')
)
} else if (loaderType === 'akamai') {
writeFileSync(
filename,
`${normalizeSrc}
export default function akamaiLoader({ src, width, quality }) {
return '${pathPrefix}' + normalizeSrc(src) + '?imwidth=' + width
}`
.split('\n')
.map((l) => l.trim())
.join('\n')
)
}
}
}
})
return root
}

export default function transformer(
file: FileInfo,
api: API,
Expand All @@ -149,6 +244,17 @@ export default function transformer(
const j = api.jscodeshift
const root = j(file.source)

const isConfig =
file.path === 'next.config.js' ||
file.path === 'next.config.ts' ||
file.path === 'next.config.mjs' ||
file.path === 'next.config.cjs'

if (isConfig) {
const result = nextConfigTransformer(j, root)
return result.toSource()
}

// Before: import Image from "next/legacy/image"
// After: import Image from "next/image"
root
Expand Down