Skip to content

Commit 90c0a76

Browse files
next-codemod(upgrade): warn peer dependencies not met (#71693)
### Why? After the upgrade, there might be an incompatibility with the `peerDependencies` of the existing dependencies. Therefore, we run a check for it to warn the user at the end of the upgrade. ## Testing Plan ### Unmet Peer Dependencies with Range and Prerelease ``` pnpm test:upgrade-fixture bin/__testfixtures__/peer-dep-out-of-range ``` CLI output includes: ``` ⚠ Found 2 dependencies that seem incompatible with the upgraded package versions. You may have to update these packages to their latest version or file an issue to ask for support of the upgraded libraries. unmet-prerelease 0.0.1 ├── incompatible with [email protected] └── incompatible with [email protected] unmet-range 0.0.1 ├── incompatible with [email protected] └── incompatible with [email protected] ``` Closes NDX-382 --------- Co-authored-by: Sebastian "Sebbie" Silbermann <[email protected]>
1 parent a5811a7 commit 90c0a76

File tree

8 files changed

+155
-4
lines changed

8 files changed

+155
-4
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
default should print
2+
3+
```bash
4+
⚠ Found 2 dependencies that seem incompatible with the upgraded package versions.
5+
You may have to update these packages to their latest version or file an issue to ask for support of the upgraded libraries.
6+
unmet-prerelease 0.0.1
7+
├── ✕ unmet peer react@"^18.2.0 || 19.0.0-rc-aaaaaaaa-20240101": found 19.0.0-rc-7c8e5e7a-20241101
8+
└── ✕ unmet peer react-dom@"^18.2.0 || 19.0.0-rc-aaaaaaaa-20240101": found 19.0.0-rc-7c8e5e7a-20241101
9+
unmet-range 0.0.1
10+
├── ✕ unmet peer react@"^18.0.0 || ^19.0.0": found 19.0.0-rc-7c8e5e7a-20241101
11+
└── ✕ unmet peer react-dom@"< 19": found 19.0.0-rc-7c8e5e7a-20241101
12+
```
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "met-range",
3+
"peerDependencies": {
4+
"react": "^18.0.0 || ^19.0.0-rc.0",
5+
"react-dom": "^19.0.0-rc.0"
6+
}
7+
}

packages/next-codemod/bin/__testfixtures__/peer-dep-out-of-range/met-range/pnpm-workspace.yaml

Whitespace-only changes.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"name": "peer-dep-out-of-range",
3+
"scripts": {
4+
"dev": "next dev --turbopack"
5+
},
6+
"dependencies": {
7+
"next": "15.0.0-canary.0",
8+
"react": "19.0.0-rc-f994737d14-20240522",
9+
"react-dom": "19.0.0-rc-f994737d14-20240522",
10+
"unmet-prerelease": "file:./unmet-prerelease",
11+
"unmet-range": "file:./unmet-range"
12+
}
13+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"name": "unmet-prerelease",
3+
"version": "0.0.1",
4+
"peerDependencies": {
5+
"react": "^18.2.0 || 19.0.0-rc-aaaaaaaa-20240101",
6+
"react-dom": "^18.2.0 || 19.0.0-rc-aaaaaaaa-20240101"
7+
}
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"name": "unmet-range",
3+
"version": "0.0.1",
4+
"peerDependencies": {
5+
"react": "^18.0.0 || ^19.0.0",
6+
"react-dom": "< 19"
7+
}
8+
}

packages/next-codemod/bin/upgrade.ts

Lines changed: 102 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import * as os from 'os'
22
import prompts from 'prompts'
33
import fs from 'fs'
4-
import compareVersions from 'semver/functions/compare'
4+
import {
5+
satisfies as satisfiesVersionRange,
6+
compare as compareVersions,
7+
} from 'semver'
58
import { execSync } from 'child_process'
69
import path from 'path'
710
import pc from 'picocolors'
@@ -306,7 +309,7 @@ export async function runUpgrade(
306309
}
307310

308311
console.log(
309-
`Upgrading your project to ${pc.blue('Next.js ' + targetNextVersion)}...\n`
312+
`Upgrading your project to ${pc.blue('Next.js ' + targetNextVersion)}...`
310313
)
311314

312315
for (const [dep, version] of dependenciesToInstall) {
@@ -323,7 +326,7 @@ export async function runUpgrade(
323326
os.EOL
324327
)
325328

326-
runInstallation(packageManager)
329+
runInstallation(packageManager, { cwd })
327330

328331
for (const codemod of codemods) {
329332
await runTransform(codemod, cwd, { force: true, verbose })
@@ -353,6 +356,9 @@ export async function runUpgrade(
353356
if (codemods.length > 0) {
354357
console.log(`${pc.green('✔')} Codemods have been applied successfully.`)
355358
}
359+
360+
warnDependenciesOutOfRange(appPackageJson, versionMapping)
361+
356362
endMessage()
357363
}
358364

@@ -633,3 +639,96 @@ function writeOverridesField(
633639
}
634640
}
635641
}
642+
643+
function warnDependenciesOutOfRange(
644+
appPackageJson: any,
645+
versionMapping: Record<string, { version: string; required: boolean }>
646+
) {
647+
const allDirectDependencies = {
648+
...appPackageJson.dependencies,
649+
...appPackageJson.devDependencies,
650+
}
651+
652+
const dependenciesOutOfRange = new Map<
653+
string,
654+
{
655+
[dependency: string]: {
656+
currentVersion: string
657+
expectedVersionRange: string
658+
}
659+
}
660+
>()
661+
662+
const resolvedDependencyVersions = new Map<string, string>()
663+
for (const dependency of Object.keys(allDirectDependencies)) {
664+
let pkgJson
665+
666+
// TODO: Asking package manager for the installed version is most robust e.g. `pnpm why ${dependency}`
667+
// require.resolve(`${dependency}/package.json`, { paths: [cwd] }) results in previously installed version being used in PNPM
668+
let pkgJsonFromNodeModules
669+
try {
670+
pkgJsonFromNodeModules = path.join(
671+
cwd,
672+
'node_modules',
673+
dependency,
674+
'package.json'
675+
)
676+
677+
pkgJson = JSON.parse(fs.readFileSync(pkgJsonFromNodeModules, 'utf8'))
678+
} catch {
679+
console.warn(
680+
`${pc.yellow('⚠')} Could not find package.json for dependency "${dependency}" at "${pkgJsonFromNodeModules}". This may affect peer dependency checks.`
681+
)
682+
continue
683+
}
684+
685+
resolvedDependencyVersions.set(dependency, pkgJson.version)
686+
687+
if ('peerDependencies' in pkgJson) {
688+
const peerDeps = pkgJson.peerDependencies
689+
const peerDepsNames = Object.keys(peerDeps)
690+
const depsToCheck = Object.keys(versionMapping).filter(
691+
(versionMappingKey) => peerDepsNames.includes(versionMappingKey)
692+
)
693+
694+
for (const depName of depsToCheck) {
695+
const expectedVersionRange = peerDeps[depName]
696+
const { version: currentVersion } = versionMapping[depName]
697+
if (
698+
!satisfiesVersionRange(currentVersion, expectedVersionRange, {
699+
includePrerelease: true,
700+
})
701+
) {
702+
dependenciesOutOfRange.set(dependency, {
703+
...dependenciesOutOfRange.get(dependency),
704+
[depName]: {
705+
currentVersion,
706+
expectedVersionRange,
707+
},
708+
})
709+
}
710+
}
711+
}
712+
}
713+
714+
const size = dependenciesOutOfRange.size
715+
if (size > 0) {
716+
console.log(
717+
`${pc.yellow('⚠')} Found ${size} ${
718+
size === 1 ? 'dependency' : 'dependencies'
719+
} that seem incompatible with the upgraded package versions.\n` +
720+
'You may have to update these packages to their latest version or file an issue to ask for support of the upgraded libraries.'
721+
)
722+
dependenciesOutOfRange.forEach((deps, packageName) => {
723+
console.log(
724+
`${packageName} ${pc.gray(resolvedDependencyVersions.get(packageName))}`
725+
)
726+
Object.entries(deps).forEach(([depName, value], index, depsArray) => {
727+
const prefix = index === depsArray.length - 1 ? ' └── ' : ' ├── '
728+
console.log(
729+
`${prefix}${pc.yellow('✕ unmet peer')} ${depName}@"${value.expectedVersionRange}": found ${value.currentVersion}`
730+
)
731+
})
732+
})
733+
}
734+
}

packages/next-codemod/lib/handle-package.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,13 @@ export function installPackages(
107107
}
108108
}
109109

110-
export function runInstallation(packageManager: PackageManager) {
110+
export function runInstallation(
111+
packageManager: PackageManager,
112+
options: { cwd: string }
113+
) {
111114
try {
112115
execa.sync(packageManager, ['install'], {
116+
cwd: options.cwd,
113117
stdio: 'inherit',
114118
shell: true,
115119
})

0 commit comments

Comments
 (0)