From 99b9447eab63e6d3869b32656b5060a4578e266a Mon Sep 17 00:00:00 2001 From: LichuAcu Date: Thu, 19 Sep 2024 16:02:22 -0700 Subject: [PATCH] Support codemods in @next/upgrade --- packages/next-upgrade/codemods.ts | 96 +++++++++++++++++++++++++ packages/next-upgrade/index.ts | 114 +++++++++++++++++++++++++----- 2 files changed, 194 insertions(+), 16 deletions(-) create mode 100644 packages/next-upgrade/codemods.ts diff --git a/packages/next-upgrade/codemods.ts b/packages/next-upgrade/codemods.ts new file mode 100644 index 0000000000000..d5fef8e63affe --- /dev/null +++ b/packages/next-upgrade/codemods.ts @@ -0,0 +1,96 @@ +type Codemod = { + title: string + value: string +} + +type VersionCodemods = { + version: string + codemods: Codemod[] +} + +export const availableCodemods: VersionCodemods[] = [ + { + version: '6', + codemods: [ + { + title: 'Use withRouter', + value: 'url-to-withrouter', + }, + ], + }, + { + version: '8', + codemods: [ + { + title: 'Transform AMP HOC into page config', + value: 'withamp-to-config', + }, + ], + }, + { + version: '9', + codemods: [ + { + title: 'Transform Anonymous Components into Named Components', + value: 'name-default-component', + }, + ], + }, + { + version: '10', + codemods: [ + { + title: 'Add React Import', + value: 'add-missing-react-import', + }, + ], + }, + { + version: '11', + codemods: [ + { + title: 'Migrate from CRA', + value: 'cra-to-next', + }, + ], + }, + { + version: '13.0', + codemods: [ + { + title: 'Remove Tags From Link Components', + value: 'new-link', + }, + { + title: 'Migrate to the New Image Component', + value: 'next-image-experimental', + }, + { + title: 'Rename Next Image Imports', + value: 'next-image-to-legacy-image', + }, + ], + }, + { + version: '13.2', + codemods: [ + { + title: 'Use Built-in Font', + value: 'built-in-next-font', + }, + ], + }, + { + version: '14.0', + codemods: [ + { + title: 'Migrate ImageResponse imports', + value: 'next-og-import', + }, + { + title: 'Use viewport export', + value: 'metadata-to-viewport-export', + }, + ], + }, +] diff --git a/packages/next-upgrade/index.ts b/packages/next-upgrade/index.ts index fde92bf9bf1f8..361d130ae63c2 100644 --- a/packages/next-upgrade/index.ts +++ b/packages/next-upgrade/index.ts @@ -8,6 +8,7 @@ import { compareVersions } from 'compare-versions' import chalk from 'chalk' import which from 'which' import { createRequire } from 'node:module' +import { availableCodemods } from './codemods.js' type StandardVersionSpecifier = 'canary' | 'rc' | 'latest' type CustomVersionSpecifier = string @@ -42,6 +43,8 @@ async function run(): Promise { } } + const installedNextVersion = await getInstalledNextVersion() + if (!targetNextPackageJson) { let nextPackageJson: { [key: string]: any } = {} try { @@ -87,7 +90,16 @@ async function run(): Promise { description: `Production-ready release (${nextPackageJson['latest'].version})`, }) - const initialVersionSpecifierIdx = await processCurrentVersion(showRc) + if (installedNextVersion) { + console.log( + `You are currently using ${chalk.blue('Next.js ' + installedNextVersion)}` + ) + } + + const initialVersionSpecifierIdx = await getVersionSpecifierIdx( + installedNextVersion, + showRc + ) const response: Response = await prompts( { @@ -104,9 +116,11 @@ async function run(): Promise { targetVersionSpecifier = response.version } + const targetNextVersion = targetNextPackageJson.version + if ( - targetNextPackageJson.version && - compareVersions(targetNextPackageJson.version, '15.0.0-canary') >= 0 + targetNextVersion && + compareVersions(targetNextVersion, '15.0.0-canary') >= 0 ) { await suggestTurbopack(appPackageJson) } @@ -114,13 +128,13 @@ async function run(): Promise { fs.writeFileSync(appPackageJsonPath, JSON.stringify(appPackageJson, null, 2)) const packageManager: PackageManager = await getPackageManager(appPackageJson) + const nextDependency = `next@${targetNextVersion}` const reactDependencies = [ `react@${targetNextPackageJson.peerDependencies['react']}`, `@types/react@${targetNextPackageJson.devDependencies['@types/react']}`, `react-dom@${targetNextPackageJson.peerDependencies['react-dom']}`, `@types/react-dom@${targetNextPackageJson.devDependencies['@types/react-dom']}`, ] - const nextDependency = `next@${targetNextPackageJson.version}` let updateCommand switch (packageManager) { @@ -149,6 +163,8 @@ async function run(): Promise { stdio: 'inherit', }) + await suggestCodemods(installedNextVersion, targetNextVersion) + console.log( `\n${chalk.green('✔')} Your Next.js project has been upgraded successfully. ${chalk.bold('Time to ship! 🚢')}` ) @@ -180,11 +196,7 @@ async function detectWorkspace(appPackageJson: any): Promise { } } -/* - * Logs the current version and returns the index of the current version specifier - * in the array ['canary', 'rc', 'latest'] - */ -async function processCurrentVersion(showRc: boolean): Promise { +async function getInstalledNextVersion(): Promise { const require = createRequire(import.meta.url) const installedNextPackageJsonDir = require.resolve('next/package.json', { paths: [process.cwd()], @@ -192,23 +204,29 @@ async function processCurrentVersion(showRc: boolean): Promise { const installedNextPackageJson = JSON.parse( fs.readFileSync(installedNextPackageJsonDir, 'utf8') ) - let installedNextVersion = installedNextPackageJson.version + return installedNextPackageJson.version +} + +/* + * Returns the index of the current version's specifier in the + * array ['canary', 'rc', 'latest'] or ['canary', 'latest'] + */ +async function getVersionSpecifierIdx( + installedNextVersion: string, + showRc: boolean +): Promise { if (installedNextVersion == null) { return 0 } - console.log( - `You are currently using ${chalk.blue('Next.js ' + installedNextVersion)}` - ) - if (installedNextVersion.includes('canary')) { return 0 } if (installedNextVersion.includes('rc')) { - return 1 // If rc is not available, will default to latest's index + return 1 } - return showRc ? 2 : 1 // "latest" is 1 or 2 depending on if rc is shown as an option + return showRc ? 2 : 1 } async function getPackageManager(_packageJson: any): Promise { @@ -365,4 +383,68 @@ async function suggestTurbopack(packageJson: any): Promise { responseCustomDevScript.customDevScript || devScript } +async function suggestCodemods( + initialNextVersion: string, + targetNextVersion: string +): Promise { + const initialVersionIndex = availableCodemods.findIndex( + (versionCodemods) => + compareVersions(versionCodemods.version, initialNextVersion) > 0 + ) + if (initialVersionIndex === -1) { + return + } + + let targetVersionIndex = availableCodemods.findIndex( + (versionCodemods) => + compareVersions(versionCodemods.version, targetNextVersion) > 0 + ) + if (targetVersionIndex === -1) { + targetVersionIndex = availableCodemods.length + } + + const relevantCodemods = availableCodemods + .slice(initialVersionIndex, targetVersionIndex) + .flatMap((versionCodemods) => versionCodemods.codemods) + + if (relevantCodemods.length === 0) { + return + } + + let codemodsString = `\nThe following ${chalk.blue('codemods')} are available for your upgrade:` + relevantCodemods.forEach((codemod) => { + codemodsString += `\n- ${codemod.title} ${chalk.gray(`(${codemod.value})`)}` + }) + codemodsString += '\n' + + console.log(codemodsString) + + const responseCodemods = await prompts( + { + type: 'confirm', + name: 'apply', + message: `Do you want to apply these codemods?`, + initial: true, + }, + { + onCancel: () => { + process.exit(0) + }, + } + ) + + if (!responseCodemods.apply) { + return + } + + for (const codemod of relevantCodemods) { + execSync( + `npx @next/codemod@latest ${codemod.value} ${process.cwd()} --force`, + { + stdio: 'inherit', + } + ) + } +} + run().catch(console.error)