From 29a3151b60d2a459a9daff738fc4dfb327940c7b Mon Sep 17 00:00:00 2001 From: Vedant Madane <6527493+VedantMadane@users.noreply.github.com> Date: Fri, 16 Jan 2026 22:17:30 +0530 Subject: [PATCH] feat: show available workspace versions on mismatch (#10466) --- cspell.json | 1 + pkg-manager/package-requester/test/index.ts | 10 +++++----- resolving/npm-resolver/src/index.ts | 9 ++++++++- resolving/npm-resolver/test/index.ts | 2 +- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/cspell.json b/cspell.json index 17a003005d..4929c13c6c 100644 --- a/cspell.json +++ b/cspell.json @@ -217,6 +217,7 @@ "proxied", "pwsh", "quux", + "rcompare", "redownload", "refclone", "reflattened", diff --git a/pkg-manager/package-requester/test/index.ts b/pkg-manager/package-requester/test/index.ts index 1cfb1f149b..caabfa06df 100644 --- a/pkg-manager/package-requester/test/index.ts +++ b/pkg-manager/package-requester/test/index.ts @@ -71,7 +71,7 @@ test('request package', async () => { }) const { files } = await pkgResponse.fetching!() - expect(Array.from(files.filesMap.keys()).sort()).toStrictEqual(['package.json', 'index.js', 'license', 'readme.md'].sort()) + expect(Array.from(files.filesMap.keys()).sort((a, b) => a.localeCompare(b))).toStrictEqual(['package.json', 'index.js', 'license', 'readme.md'].sort((a, b) => a.localeCompare(b))) expect(files.resolvedFrom).toBe('remote') }) @@ -383,7 +383,7 @@ test('fetchPackageToStore()', async () => { const { files, bundledManifest } = await fetchResult.fetching() expect(bundledManifest).toBeTruthy() // we always read the bundled manifest - expect(Array.from(files.filesMap.keys()).sort()).toStrictEqual(['package.json', 'index.js', 'license', 'readme.md'].sort()) + expect(Array.from(files.filesMap.keys()).sort((a, b) => a.localeCompare(b))).toStrictEqual(['package.json', 'index.js', 'license', 'readme.md'].sort((a, b) => a.localeCompare(b))) expect(files.resolvedFrom).toBe('remote') const indexFile = loadJsonFileSync(fetchResult.filesIndexFile) @@ -473,7 +473,7 @@ test('fetchPackageToStore() concurrency check', async () => { ino1 = fs.statSync(files.filesMap.get('package.json') as string).ino - expect(Array.from(files.filesMap.keys()).sort()).toStrictEqual(['package.json', 'index.js', 'license', 'readme.md'].sort()) + expect(Array.from(files.filesMap.keys()).sort((a, b) => a.localeCompare(b))).toStrictEqual(['package.json', 'index.js', 'license', 'readme.md'].sort((a, b) => a.localeCompare(b))) expect(files.resolvedFrom).toBe('remote') } @@ -483,7 +483,7 @@ test('fetchPackageToStore() concurrency check', async () => { ino2 = fs.statSync(files.filesMap.get('package.json') as string).ino - expect(Array.from(files.filesMap.keys()).sort()).toStrictEqual(['package.json', 'index.js', 'license', 'readme.md'].sort()) + expect(Array.from(files.filesMap.keys()).sort((a, b) => a.localeCompare(b))).toStrictEqual(['package.json', 'index.js', 'license', 'readme.md'].sort((a, b) => a.localeCompare(b))) expect(files.resolvedFrom).toBe('remote') } @@ -551,7 +551,7 @@ test('fetchPackageToStore() does not cache errors', async () => { }, }) const { files } = await fetchResult.fetching() - expect(Array.from(files.filesMap.keys()).sort()).toStrictEqual(['package.json', 'index.js', 'license', 'readme.md'].sort()) + expect(Array.from(files.filesMap.keys()).sort((a, b) => a.localeCompare(b))).toStrictEqual(['package.json', 'index.js', 'license', 'readme.md'].sort((a, b) => a.localeCompare(b))) expect(files.resolvedFrom).toBe('remote') expect(nock.isDone()).toBeTruthy() diff --git a/resolving/npm-resolver/src/index.ts b/resolving/npm-resolver/src/index.ts index 08e588a456..4eed6b8933 100644 --- a/resolving/npm-resolver/src/index.ts +++ b/resolving/npm-resolver/src/index.ts @@ -639,9 +639,16 @@ function tryResolveFromWorkspacePackages ( opts.update ? { name: spec.name, fetchSpec: '*', type: 'range' } : spec ) if (!localVersion) { + const availableVersions = Array.from(workspacePkgsMatchingName.keys()).sort((a, b) => semver.rcompare(a, b)) throw new PnpmError( 'NO_MATCHING_VERSION_INSIDE_WORKSPACE', - `In ${path.relative(process.cwd(), opts.projectDir)}: No matching version found for ${opts.wantedDependency.alias ?? ''}@${opts.wantedDependency.bareSpecifier ?? ''} inside the workspace` + `In ${path.relative(process.cwd(), opts.projectDir)}: No matching version found for ${opts.wantedDependency.alias ?? ''}@${opts.wantedDependency.bareSpecifier ?? ''} inside the workspace` + + (availableVersions.length ? `. Available versions: ${availableVersions.join(', ')}` : ''), + availableVersions.length + ? { + hint: `Available workspace versions for "${spec.name}": ${availableVersions.join(', ')}`, + } + : undefined ) } return resolveFromLocalPackage(workspacePkgsMatchingName.get(localVersion)!, spec, opts) diff --git a/resolving/npm-resolver/test/index.ts b/resolving/npm-resolver/test/index.ts index d086a0f669..a857c712b6 100644 --- a/resolving/npm-resolver/test/index.ts +++ b/resolving/npm-resolver/test/index.ts @@ -1767,7 +1767,7 @@ test('workspace protocol: resolution fails if there is no matching local package expect(err).toBeTruthy() expect(err.code).toBe('ERR_PNPM_NO_MATCHING_VERSION_INSIDE_WORKSPACE') - expect(err.message).toBe(`In ${path.relative(process.cwd(), projectDir)}: No matching version found for is-positive@workspace:^3.0.0 inside the workspace`) + expect(err.message).toBe(`In ${path.relative(process.cwd(), projectDir)}: No matching version found for is-positive@workspace:^3.0.0 inside the workspace. Available versions: 2.0.0`) }) test('workspace protocol: resolution fails if there are no local packages', async () => {