From 52e2cede97e70b3326262ff353fc534c880000bf Mon Sep 17 00:00:00 2001 From: Steven Date: Fri, 17 Feb 2023 09:55:33 -0500 Subject: [PATCH 1/4] fix: codemod `next/image` within monorepo --- .../monorepo/input/app1/next.config.js | 6 ++++++ .../monorepo/input/app2/next.config.js | 6 ++++++ .../monorepo/output/app1/imgix-loader.js | 10 ++++++++++ .../monorepo/output/app1/next.config.js | 6 ++++++ .../monorepo/output/app2/cloudinary-loader.js | 6 ++++++ .../monorepo/output/app2/next.config.js | 6 ++++++ .../transforms/next-image-experimental.ts | 20 ++++++++++++------- 7 files changed, 53 insertions(+), 7 deletions(-) create mode 100644 packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/monorepo/input/app1/next.config.js create mode 100644 packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/monorepo/input/app2/next.config.js create mode 100644 packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/monorepo/output/app1/imgix-loader.js create mode 100644 packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/monorepo/output/app1/next.config.js create mode 100644 packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/monorepo/output/app2/cloudinary-loader.js create mode 100644 packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/monorepo/output/app2/next.config.js diff --git a/packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/monorepo/input/app1/next.config.js b/packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/monorepo/input/app1/next.config.js new file mode 100644 index 0000000000000..0a5810c97c6b3 --- /dev/null +++ b/packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/monorepo/input/app1/next.config.js @@ -0,0 +1,6 @@ +module.exports = { + images: { + loader: "imgix", + path: "https://example.com/" + }, +} \ No newline at end of file diff --git a/packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/monorepo/input/app2/next.config.js b/packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/monorepo/input/app2/next.config.js new file mode 100644 index 0000000000000..eba596451330a --- /dev/null +++ b/packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/monorepo/input/app2/next.config.js @@ -0,0 +1,6 @@ +module.exports = { + images: { + loader: "cloudinary", + path: "https://example.com/" + }, +} \ No newline at end of file diff --git a/packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/monorepo/output/app1/imgix-loader.js b/packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/monorepo/output/app1/imgix-loader.js new file mode 100644 index 0000000000000..22468f5a87f5d --- /dev/null +++ b/packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/monorepo/output/app1/imgix-loader.js @@ -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 +} \ No newline at end of file diff --git a/packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/monorepo/output/app1/next.config.js b/packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/monorepo/output/app1/next.config.js new file mode 100644 index 0000000000000..28ec52518cef4 --- /dev/null +++ b/packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/monorepo/output/app1/next.config.js @@ -0,0 +1,6 @@ +module.exports = { + images: { + loader: "custom", + loaderFile: "./imgix-loader.js" + }, +} \ No newline at end of file diff --git a/packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/monorepo/output/app2/cloudinary-loader.js b/packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/monorepo/output/app2/cloudinary-loader.js new file mode 100644 index 0000000000000..f0ac0c3209d6b --- /dev/null +++ b/packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/monorepo/output/app2/cloudinary-loader.js @@ -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) +} \ No newline at end of file diff --git a/packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/monorepo/output/app2/next.config.js b/packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/monorepo/output/app2/next.config.js new file mode 100644 index 0000000000000..28175db3e45c9 --- /dev/null +++ b/packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/monorepo/output/app2/next.config.js @@ -0,0 +1,6 @@ +module.exports = { + images: { + loader: "custom", + loaderFile: "./cloudinary-loader.js" + }, +} \ No newline at end of file diff --git a/packages/next-codemod/transforms/next-image-experimental.ts b/packages/next-codemod/transforms/next-image-experimental.ts index c26d471001b22..838463b997cfa 100644 --- a/packages/next-codemod/transforms/next-image-experimental.ts +++ b/packages/next-codemod/transforms/next-image-experimental.ts @@ -1,3 +1,4 @@ +import { join, parse } from 'path' import { writeFileSync } from 'fs' import type { API, @@ -148,7 +149,11 @@ function findAndReplaceProps( }) } -function nextConfigTransformer(j: JSCodeshift, root: Collection) { +function nextConfigTransformer( + j: JSCodeshift, + root: Collection, + appDir: string +) { let pathPrefix = '' let loaderType = '' root.find(j.ObjectExpression).forEach((o) => { @@ -196,7 +201,7 @@ function nextConfigTransformer(j: JSCodeshift, root: Collection) { const normalizeSrc = `const normalizeSrc = (src) => src[0] === '/' ? src.slice(1) : src` if (loaderType === 'imgix') { writeFileSync( - filename, + join(appDir, filename), `${normalizeSrc} export default function imgixLoader({ src, width, quality }) { const url = new URL('${pathPrefix}' + normalizeSrc(src)) @@ -250,14 +255,15 @@ export default function transformer( const j = api.jscodeshift.withParser('tsx') const root = j(file.source) + const parsed = parse(file.path) const isConfig = - file.path === 'next.config.js' || - file.path === 'next.config.ts' || - file.path === 'next.config.mjs' || - file.path === 'next.config.cjs' + parsed.base === 'next.config.js' || + parsed.base === 'next.config.ts' || + parsed.base === 'next.config.mjs' || + parsed.base === 'next.config.cjs' if (isConfig) { - const result = nextConfigTransformer(j, root) + const result = nextConfigTransformer(j, root, parsed.dir) return result.toSource() } From 03fed32bb6f6350c805f2542a4534053571f278c Mon Sep 17 00:00:00 2001 From: Steven Date: Fri, 17 Feb 2023 11:28:23 -0500 Subject: [PATCH 2/4] Update tests to handle tmpdir --- .../next-image-experimental-loader.test.js | 18 +++++++++++------- .../transforms/next-image-experimental.ts | 17 +++++++++++------ 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/packages/next-codemod/transforms/__tests__/next-image-experimental-loader.test.js b/packages/next-codemod/transforms/__tests__/next-image-experimental-loader.test.js index a80f8c5bc1b94..edee80d6e0171 100644 --- a/packages/next-codemod/transforms/__tests__/next-image-experimental-loader.test.js +++ b/packages/next-codemod/transforms/__tests__/next-image-experimental-loader.test.js @@ -2,12 +2,13 @@ jest.autoMockOff() const Runner = require('jscodeshift/dist/Runner'); const { cp, mkdir, rm, readdir, readFile } = require('fs/promises') -const { readdirSync } = require('fs') +const { mkdtempSync, readdirSync } = require('fs') +const { tmpdir } = require('os') 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 } +const opts = { recursive: true, force: true } async function toObj(dir) { const obj = {} @@ -19,18 +20,21 @@ async function toObj(dir) { } it.each(readdirSync(fixtureDir))('should transform loader %s', async (loader) => { + const tmp = mkdtempSync(join(tmpdir(), `next-image-experimental-${loader}-`)) + const originalCwd = process.cwd() try { - await mkdir(join(fixtureDir, 'tmp'), opts) - await cp(join(fixtureDir, loader, 'input'), join(fixtureDir, 'tmp'), opts) - process.chdir(join(fixtureDir, 'tmp')) + await mkdir(tmp, opts) + await cp(join(fixtureDir, loader, 'input'), tmp, opts) + process.chdir(tmp) const result = await Runner.run(transform, [`.`], {}) expect(result.error).toBe(0) expect( - await toObj(join(fixtureDir, 'tmp')) + await toObj(tmp) ).toStrictEqual( await toObj(join(fixtureDir, loader, 'output')) ) } finally { - await rm(join(fixtureDir, 'tmp'), opts) + await rm(tmp, opts) + process.chdir(originalCwd) } }) \ No newline at end of file diff --git a/packages/next-codemod/transforms/next-image-experimental.ts b/packages/next-codemod/transforms/next-image-experimental.ts index 838463b997cfa..7bf6e76ba6cd9 100644 --- a/packages/next-codemod/transforms/next-image-experimental.ts +++ b/packages/next-codemod/transforms/next-image-experimental.ts @@ -193,15 +193,20 @@ function nextConfigTransformer( return true }) if (loaderType && pathPrefix) { - let filename = `./${loaderType}-loader.js` + const importSpecifier = `./${loaderType}-loader.js` + const filePath = join(appDir, importSpecifier) properties.push( - j.property('init', j.identifier('loaderFile'), j.literal(filename)) + 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( - join(appDir, filename), + filePath, `${normalizeSrc} export default function imgixLoader({ src, width, quality }) { const url = new URL('${pathPrefix}' + normalizeSrc(src)) @@ -218,7 +223,7 @@ function nextConfigTransformer( ) } else if (loaderType === 'cloudinary') { writeFileSync( - filename, + filePath, `${normalizeSrc} export default function cloudinaryLoader({ src, width, quality }) { const params = ['f_auto', 'c_limit', 'w_' + width, 'q_' + (quality || 'auto')] @@ -231,7 +236,7 @@ function nextConfigTransformer( ) } else if (loaderType === 'akamai') { writeFileSync( - filename, + filePath, `${normalizeSrc} export default function akamaiLoader({ src, width, quality }) { return '${pathPrefix}' + normalizeSrc(src) + '?imwidth=' + width @@ -255,7 +260,7 @@ export default function transformer( const j = api.jscodeshift.withParser('tsx') const root = j(file.source) - const parsed = parse(file.path) + const parsed = parse(file.path || '/') const isConfig = parsed.base === 'next.config.js' || parsed.base === 'next.config.ts' || From 2900f8033f5828876a4c6709fb0839fb5d8afd1e Mon Sep 17 00:00:00 2001 From: Steven Date: Fri, 17 Feb 2023 11:34:31 -0500 Subject: [PATCH 3/4] Make `toObj()` recursive --- .../next-image-experimental-loader.test.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/next-codemod/transforms/__tests__/next-image-experimental-loader.test.js b/packages/next-codemod/transforms/__tests__/next-image-experimental-loader.test.js index edee80d6e0171..cb1bb262b5594 100644 --- a/packages/next-codemod/transforms/__tests__/next-image-experimental-loader.test.js +++ b/packages/next-codemod/transforms/__tests__/next-image-experimental-loader.test.js @@ -1,8 +1,8 @@ /* global jest */ jest.autoMockOff() const Runner = require('jscodeshift/dist/Runner'); -const { cp, mkdir, rm, readdir, readFile } = require('fs/promises') -const { mkdtempSync, readdirSync } = require('fs') +const { cp, mkdir, mkdtemp, rm, readdir, readFile, stat } = require('fs/promises') +const { readdirSync } = require('fs') const { tmpdir } = require('os') const { join } = require('path') @@ -14,13 +14,19 @@ async function toObj(dir) { const obj = {} const files = await readdir(dir) for (const file of files) { - obj[file] = await readFile(join(dir, file), 'utf8') + const filePath = join(dir, file) + const s = await stat(filePath) + if (s.isDirectory()) { + obj[file] = await toObj(filePath) + } else { + obj[file] = await readFile(filePath, 'utf8') + } } return obj } it.each(readdirSync(fixtureDir))('should transform loader %s', async (loader) => { - const tmp = mkdtempSync(join(tmpdir(), `next-image-experimental-${loader}-`)) + const tmp = await mkdtemp(join(tmpdir(), `next-image-experimental-${loader}-`)) const originalCwd = process.cwd() try { await mkdir(tmp, opts) From bb67101701d447be6235c06cdc9dc09e69116ead Mon Sep 17 00:00:00 2001 From: Steven Date: Fri, 17 Feb 2023 15:33:33 -0500 Subject: [PATCH 4/4] Fix case for multiple keys --- .../many-keys/input/next.config.js | 21 +++ .../many-keys/output/cloudinary-loader.js | 6 + .../many-keys/output/next.config.js | 21 +++ .../transforms/next-image-experimental.ts | 137 +++++++++--------- 4 files changed, 117 insertions(+), 68 deletions(-) create mode 100644 packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/many-keys/input/next.config.js create mode 100644 packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/many-keys/output/cloudinary-loader.js create mode 100644 packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/many-keys/output/next.config.js diff --git a/packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/many-keys/input/next.config.js b/packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/many-keys/input/next.config.js new file mode 100644 index 0000000000000..06217557737ac --- /dev/null +++ b/packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/many-keys/input/next.config.js @@ -0,0 +1,21 @@ +/** + * @type {import('next').NextConfig} + */ +module.exports = { + reactStrictMode: true, + + images: { + loader: "cloudinary", + path: "https://example.com/" + }, + + async redirects() { + return [ + { + source: '/source', + destination: '/dest', + permanent: true, + }, + ]; + }, +} diff --git a/packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/many-keys/output/cloudinary-loader.js b/packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/many-keys/output/cloudinary-loader.js new file mode 100644 index 0000000000000..f0ac0c3209d6b --- /dev/null +++ b/packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/many-keys/output/cloudinary-loader.js @@ -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) +} \ No newline at end of file diff --git a/packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/many-keys/output/next.config.js b/packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/many-keys/output/next.config.js new file mode 100644 index 0000000000000..9430e62d4e6e0 --- /dev/null +++ b/packages/next-codemod/transforms/__testfixtures__/next-image-experimental-loader/many-keys/output/next.config.js @@ -0,0 +1,21 @@ +/** + * @type {import('next').NextConfig} + */ +module.exports = { + reactStrictMode: true, + + images: { + loader: "custom", + loaderFile: "./cloudinary-loader.js" + }, + + async redirects() { + return [ + { + source: '/source', + destination: '/dest', + permanent: true, + }, + ]; + }, +} diff --git a/packages/next-codemod/transforms/next-image-experimental.ts b/packages/next-codemod/transforms/next-image-experimental.ts index 7bf6e76ba6cd9..96bdd384f1ca9 100644 --- a/packages/next-codemod/transforms/next-image-experimental.ts +++ b/packages/next-codemod/transforms/next-image-experimental.ts @@ -157,57 +157,57 @@ function nextConfigTransformer( let pathPrefix = '' let loaderType = '' root.find(j.ObjectExpression).forEach((o) => { - const [images] = o.value.properties || [] - 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) => { - if ( - p.type === 'ObjectProperty' && - p.key.type === 'Identifier' && - p.key.name === 'loader' && - 'value' in p.value - ) { + ;(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) => { if ( - p.value.value === 'imgix' || - p.value.value === 'cloudinary' || - p.value.value === 'akamai' + p.type === 'ObjectProperty' && + p.key.type === 'Identifier' && + p.key.name === 'loader' && + 'value' in p.value ) { - loaderType = p.value.value - p.value.value = 'custom' + if ( + p.value.value === 'imgix' || + p.value.value === 'cloudinary' || + p.value.value === 'akamai' + ) { + loaderType = p.value.value + p.value.value = 'custom' + } } - } - 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) + 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} + images.value.properties = properties + const normalizeSrc = `const normalizeSrc = (src) => src[0] === '/' ? src.slice(1) : src` + if (loaderType === 'imgix') { + writeFileSync( + filePath, + `${normalizeSrc} export default function imgixLoader({ src, width, quality }) { const url = new URL('${pathPrefix}' + normalizeSrc(src)) const params = url.searchParams @@ -217,37 +217,38 @@ function nextConfigTransformer( if (quality) { params.set('q', quality.toString()) } return url.href }` - .split('\n') - .map((l) => l.trim()) - .join('\n') - ) - } else if (loaderType === 'cloudinary') { - writeFileSync( - filePath, - `${normalizeSrc} + .split('\n') + .map((l) => l.trim()) + .join('\n') + ) + } else if (loaderType === 'cloudinary') { + writeFileSync( + filePath, + `${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( - filePath, - `${normalizeSrc} + .split('\n') + .map((l) => l.trim()) + .join('\n') + ) + } else if (loaderType === 'akamai') { + writeFileSync( + filePath, + `${normalizeSrc} export default function akamaiLoader({ src, width, quality }) { return '${pathPrefix}' + normalizeSrc(src) + '?imwidth=' + width }` - .split('\n') - .map((l) => l.trim()) - .join('\n') - ) + .split('\n') + .map((l) => l.trim()) + .join('\n') + ) + } } } - } + }) }) return root }