Skip to content

Commit

Permalink
Update next-image-experimental codemod to handle loaders (#41633)
Browse files Browse the repository at this point in the history
As a follow up to #41585, this PR updates the `next-image-experimental`
codemod to change `loader` to `loaderFile`.
  • Loading branch information
styfle committed Oct 21, 2022
1 parent 14c9376 commit 6fb9a19
Show file tree
Hide file tree
Showing 12 changed files with 197 additions and 1 deletion.
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

0 comments on commit 6fb9a19

Please sign in to comment.