Skip to content

Commit ada60dc

Browse files
authored
[codemod] upgrade to nearest minor by default (#87004)
1 parent 585126e commit ada60dc

File tree

2 files changed

+67
-15
lines changed

2 files changed

+67
-15
lines changed

packages/next-codemod/bin/next-codemod.ts

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -58,18 +58,9 @@ program
5858
.description(
5959
'Upgrade Next.js apps to desired versions with a single command.'
6060
)
61-
// The CLI interface must be backwards compatible at all times so that older
62-
// versions of Next.js can still run the codemod successfully.
6361
.argument(
6462
'[revision]',
65-
'Specify the target Next.js version using an NPM dist tag (e.g. "latest", "canary", "rc", "beta") or an exact version number (e.g. "15.0.0").',
66-
packageJson.version.includes('-canary.')
67-
? 'canary'
68-
: packageJson.version.includes('-rc.')
69-
? 'rc'
70-
: packageJson.version.includes('-beta.')
71-
? 'beta'
72-
: 'latest'
63+
'Specify the upgrade type ("patch", "minor", "major"), an NPM dist tag (e.g. "latest", "canary", "rc"), or an exact version (e.g. "15.0.0"). Defaults to "minor".'
7364
)
7465
.usage('[revision] [options]')
7566
.option('--verbose', 'Verbose output', false)

packages/next-codemod/bin/upgrade.ts

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
satisfies as satisfiesVersionRange,
66
compare as compareVersions,
77
major,
8+
minor,
89
} from 'semver'
910
import { execSync } from 'child_process'
1011
import path from 'path'
@@ -81,6 +82,33 @@ function endMessage(targetNextVersion: string) {
8182

8283
const cwd = process.cwd()
8384

85+
/**
86+
* Resolves semantic version keywords (patch, minor, major) to npm version queries.
87+
* - patch: latest patch within current minor (e.g., 15.0.x -> 15.0.y)
88+
* - minor: latest minor within current major (e.g., 15.0.x -> 15.1.x)
89+
* - major: latest stable version (equivalent to "latest")
90+
*/
91+
function resolveSemanticRevision(
92+
revision: string,
93+
installedVersion: string
94+
): string {
95+
const installedMajor = major(installedVersion)
96+
const installedMinor = minor(installedVersion)
97+
98+
switch (revision) {
99+
case 'patch':
100+
// ~15.0.0 matches >=15.0.0 <15.1.0
101+
return `~${installedMajor}.${installedMinor}.0`
102+
case 'minor':
103+
// ^15.0.0 matches >=15.0.0 <16.0.0
104+
return `^${installedMajor}.0.0`
105+
case 'major':
106+
return 'latest'
107+
default:
108+
return revision
109+
}
110+
}
111+
84112
export async function runUpgrade(
85113
revision: string | undefined,
86114
options: { verbose: boolean }
@@ -89,18 +117,53 @@ export async function runUpgrade(
89117
const appPackageJsonPath = path.resolve(cwd, 'package.json')
90118
let appPackageJson = JSON.parse(fs.readFileSync(appPackageJsonPath, 'utf8'))
91119

120+
const installedNextVersion = getInstalledNextVersion()
121+
122+
// Resolve semantic keywords to npm version queries
123+
const resolvedRevision = resolveSemanticRevision(
124+
revision ?? 'minor',
125+
installedNextVersion
126+
)
127+
128+
if (options.verbose) {
129+
console.log(` Resolved upgrade target: ${resolvedRevision}`)
130+
}
131+
92132
let targetNextPackageJson: {
93133
version: string
94134
peerDependencies: Record<string, string>
95135
}
96136

97137
try {
138+
// First, find the highest matching version
139+
const versionsJSON = execSync(
140+
`npm --silent view "next@${resolvedRevision}" --json --field version`,
141+
{ encoding: 'utf-8' }
142+
)
143+
const versionOrVersions = JSON.parse(versionsJSON)
144+
let targetVersion: string
145+
if (Array.isArray(versionOrVersions)) {
146+
versionOrVersions.sort(compareVersions)
147+
targetVersion = versionOrVersions[versionOrVersions.length - 1]
148+
} else {
149+
targetVersion = versionOrVersions
150+
}
151+
152+
if (options.verbose) {
153+
console.log(` Target version: ${targetVersion}`)
154+
}
155+
156+
// Then fetch the full package info for that specific version
98157
const targetNextPackage = execSync(
99-
`npm --silent view "next@${revision}" --json`,
158+
`npm --silent view "next@${targetVersion}" --json`,
100159
{ encoding: 'utf-8' }
101160
)
102161
targetNextPackageJson = JSON.parse(targetNextPackage)
103-
} catch {}
162+
} catch (e) {
163+
if (options.verbose) {
164+
console.error(' Error fetching package info:', e)
165+
}
166+
}
104167

105168
const validRevision =
106169
targetNextPackageJson !== null &&
@@ -109,12 +172,10 @@ export async function runUpgrade(
109172
'peerDependencies' in targetNextPackageJson
110173
if (!validRevision) {
111174
throw new BadInput(
112-
`Invalid revision provided: "${revision}". Please provide a valid Next.js version or dist-tag (e.g. "latest", "canary", "beta", "rc", or "15.0.0").\nCheck available versions at https://www.npmjs.com/package/next?activeTab=versions.`
175+
`Invalid revision provided: "${revision ?? 'minor'}" (resolved to "${resolvedRevision}"). Please provide a valid Next.js version, dist-tag (e.g. "latest", "canary", "rc"), or upgrade type ("patch", "minor", "major").\nCheck available versions at https://www.npmjs.com/package/next?activeTab=versions.`
113176
)
114177
}
115178

116-
const installedNextVersion = getInstalledNextVersion()
117-
118179
const targetNextVersion = targetNextPackageJson.version
119180

120181
if (compareVersions(installedNextVersion, targetNextVersion) === 0) {

0 commit comments

Comments
 (0)