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

Support codemods in @next/upgrade #70270

Open
wants to merge 1 commit into
base: 09-10-_next_upgrade_package
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions packages/next-upgrade/codemods.ts
Original file line number Diff line number Diff line change
@@ -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 <a> 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',
},
],
},
]
114 changes: 98 additions & 16 deletions packages/next-upgrade/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -42,6 +43,8 @@ async function run(): Promise<void> {
}
}

const installedNextVersion = await getInstalledNextVersion()

if (!targetNextPackageJson) {
let nextPackageJson: { [key: string]: any } = {}
try {
Expand Down Expand Up @@ -87,7 +90,16 @@ async function run(): Promise<void> {
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(
{
Expand All @@ -104,23 +116,25 @@ async function run(): Promise<void> {
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)
}

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) {
Expand Down Expand Up @@ -149,6 +163,8 @@ async function run(): Promise<void> {
stdio: 'inherit',
})

await suggestCodemods(installedNextVersion, targetNextVersion)

console.log(
`\n${chalk.green('✔')} Your Next.js project has been upgraded successfully. ${chalk.bold('Time to ship! 🚢')}`
)
Expand Down Expand Up @@ -180,35 +196,37 @@ async function detectWorkspace(appPackageJson: any): Promise<void> {
}
}

/*
* 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<number> {
async function getInstalledNextVersion(): Promise<string> {
const require = createRequire(import.meta.url)
const installedNextPackageJsonDir = require.resolve('next/package.json', {
paths: [process.cwd()],
})
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<number> {
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<PackageManager> {
Expand Down Expand Up @@ -365,4 +383,68 @@ async function suggestTurbopack(packageJson: any): Promise<void> {
responseCustomDevScript.customDevScript || devScript
}

async function suggestCodemods(
initialNextVersion: string,
targetNextVersion: string
): Promise<void> {
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)
Loading