diff --git a/.changeset/deep-ads-hope.md b/.changeset/deep-ads-hope.md new file mode 100644 index 0000000000..16c922b9ff --- /dev/null +++ b/.changeset/deep-ads-hope.md @@ -0,0 +1,6 @@ +--- +"@pnpm/npm-resolver": patch +"pnpm": patch +--- + +When package metadata is malformed or can't be fetched, the error thrown will now show the originating error. diff --git a/.changeset/stale-regions-like.md b/.changeset/stale-regions-like.md new file mode 100644 index 0000000000..249b6973f7 --- /dev/null +++ b/.changeset/stale-regions-like.md @@ -0,0 +1,5 @@ +--- +"@pnpm/error": minor +--- + +The `PnpmError` class now accepts an optional `cause` argument. diff --git a/.claude.bak/settings.local.json b/.claude.bak/settings.local.json new file mode 100644 index 0000000000..b43925889d --- /dev/null +++ b/.claude.bak/settings.local.json @@ -0,0 +1,37 @@ +{ + "permissions": { + "allow": [ + "Bash(git show:*)", + "Bash(pnpm --filter @pnpm/reviewing.dependencies-hierarchy test)", + "Bash(git stash:*)", + "Bash(pnpm --filter @pnpm/list test:*)", + "Bash(grep:*)", + "Bash(npm info:*)", + "Bash(node -e:*)", + "Bash(pnpm --filter @pnpm/reviewing.dependencies-hierarchy run compile:*)", + "Bash(pnpm run compile:*)", + "Bash(pnpm exec tsc:*)", + "Bash(pnpm install:*)", + "Bash(CI=true pnpm install:*)", + "Bash(pnpm --filter @pnpm/list run compile:*)", + "Bash(pnpm --filter @pnpm/reviewing.dependencies-hierarchy test -- test/getTree.test.ts)", + "Bash(git cherry-pick:*)", + "Bash(git add:*)", + "Bash(pnpm -w lint:*)", + "Bash(pnpm:*)", + "Bash(git commit:*)", + "Bash(find:*)", + "Bash(node:*)", + "Bash(git status:*)", + "Bash(gh issue view:*)", + "Bash(/Users/zoltan/Library/pnpm/.tools/pnpm-exe/10.30.2/pnpm:*)", + "Bash(gh:*)", + "Bash(git log:*)", + "Bash(npx jest:*)", + "Bash(cd /Volumes/src/pnpm/pnpm/v10 && node -e \"console.log\\(require\\('path'\\).resolve\\('pnpm/dist/pnpm.cjs'\\)\\)\")", + "Bash(cd /tmp/cdxgen-test && npx pnpm@10.32.0 install 2>&1 | tail -20)", + "Bash(cd /tmp/cdxgen-test && npx pnpm@10.32.0 install 2>&1 | tail -25)", + "Bash(cd /tmp/cdxgen-test && COREPACK_ENABLE_STRICT=0 npx pnpm@10.32.0 install --config.manage-package-manager-versions=false 2>&1 | tail -25)" + ] + } +} diff --git a/packages/error/src/index.ts b/packages/error/src/index.ts index 8006faa7d1..e6a8664593 100644 --- a/packages/error/src/index.ts +++ b/packages/error/src/index.ts @@ -12,9 +12,10 @@ export class PnpmError extends Error { opts?: { attempts?: number hint?: string + cause?: unknown } ) { - super(message) + super(message, { cause: opts?.cause }) this.code = code.startsWith('ERR_PNPM_') ? code : `ERR_PNPM_${code}` this.hint = opts?.hint this.attempts = opts?.attempts diff --git a/packages/error/test/index.ts b/packages/error/test/index.ts index d030222afd..6b38d32c65 100644 --- a/packages/error/test/index.ts +++ b/packages/error/test/index.ts @@ -1,4 +1,22 @@ -import { FetchError } from '@pnpm/error' +import { FetchError, PnpmError } from '@pnpm/error' + +test('PnpmError exposes cause when provided', () => { + const cause = new Error('original failure') + const error = new PnpmError('TEST_CODE', 'something went wrong', { cause }) + expect(error.cause).toBe(cause) + expect(error.message).toBe('something went wrong') + expect(error.code).toBe('ERR_PNPM_TEST_CODE') +}) + +test('PnpmError cause is undefined when omitted', () => { + const error = new PnpmError('TEST_CODE', 'something went wrong') + expect(error.cause).toBeUndefined() +}) + +test('PnpmError cause works with non-Error values', () => { + const error = new PnpmError('TEST_CODE', 'something went wrong', { cause: 'string cause' }) + expect(error.cause).toBe('string cause') +}) test('FetchError escapes auth tokens', () => { const error = new FetchError( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6c884ef0ec..de0bd41a0b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,3 +1,97 @@ +--- +lockfileVersion: '9.0' + +importers: + + .: + configDependencies: {} + packageManagerDependencies: + '@pnpm/exe': + specifier: 10.32.0 + version: 10.32.0 + pnpm: + specifier: 10.32.0 + version: 10.32.0 + +packages: + + '@pnpm/exe@10.32.0': + resolution: {integrity: sha512-H31SnAhyywSTVWoefDAGOgHHBtVcct8i1YI5/lx6crzGN62ZAZtXmJEJuMVeU6oP/H7Qu/CHiR5bXO1jENlT1Q==} + hasBin: true + + '@pnpm/linux-arm64@10.32.0': + resolution: {integrity: sha512-wvzrCqm8IsE4sT4wdcCSJ1DDqTte7hylbCePyT19/Y7p4upGOi9QUap6ktHzPhpvBCsEaavLW6fdOUg+n6NFkw==} + cpu: [arm64] + os: [linux] + hasBin: true + + '@pnpm/linux-x64@10.32.0': + resolution: {integrity: sha512-a1VRqz/NTTR+L9WMNT5MxFtBIo4pi3TdvhCyqqrCjyw7X/g2bkEJPI0vRfSLcJfs3pSqxSUEsYh+qHHxYBiFHA==} + cpu: [x64] + os: [linux] + hasBin: true + + '@pnpm/macos-arm64@10.32.0': + resolution: {integrity: sha512-4y3SRDTrwk/URktsPSkjS/JgjjnTvy1UcdJ7ezCKAfw5IQSvVvT6a69x3QU3bXw4iOaQKJ7qHr5efX66Z8Zq/A==} + cpu: [arm64] + os: [darwin] + hasBin: true + + '@pnpm/macos-x64@10.32.0': + resolution: {integrity: sha512-0/iounIfwFuCp2guNXy9hWFcwSK2erTGlmErbRH6nk+HFr7wyBOfIAiOBFW0695FENjLg+OuVSFW8Xy+86+amA==} + cpu: [x64] + os: [darwin] + hasBin: true + + '@pnpm/win-arm64@10.32.0': + resolution: {integrity: sha512-54hfmPtrzV3I1pI7v/kgOVqjzhoIQruIl+7zWcAR3XPlugNXU9Y4jP64tIXViTwQd/PXreBKfaD5gVID/Pr2VA==} + cpu: [arm64] + os: [win32] + hasBin: true + + '@pnpm/win-x64@10.32.0': + resolution: {integrity: sha512-PTZOFFCiDuMZ++OxYtXEx12syUsEAeBm8eGUReVOZwbfx+Jo9sSyGaV0UuP6c06Hc2uispLeU56d2/6VSGgs3w==} + cpu: [x64] + os: [win32] + hasBin: true + + pnpm@10.32.0: + resolution: {integrity: sha512-myY0uz/tVgHDNjPy2SWT9QYnCjljuMUdKy1qgo2mFc5One6++WFMzrvBOsjTwPnJzM61g8achXhDb6R327INcA==} + engines: {node: '>=18.12'} + hasBin: true + +snapshots: + + '@pnpm/exe@10.32.0': + optionalDependencies: + '@pnpm/linux-arm64': 10.32.0 + '@pnpm/linux-x64': 10.32.0 + '@pnpm/macos-arm64': 10.32.0 + '@pnpm/macos-x64': 10.32.0 + '@pnpm/win-arm64': 10.32.0 + '@pnpm/win-x64': 10.32.0 + + '@pnpm/linux-arm64@10.32.0': + optional: true + + '@pnpm/linux-x64@10.32.0': + optional: true + + '@pnpm/macos-arm64@10.32.0': + optional: true + + '@pnpm/macos-x64@10.32.0': + optional: true + + '@pnpm/win-arm64@10.32.0': + optional: true + + '@pnpm/win-x64@10.32.0': + optional: true + + pnpm@10.32.0: {} + +--- lockfileVersion: '9.0' settings: diff --git a/resolving/npm-resolver/src/fetch.ts b/resolving/npm-resolver/src/fetch.ts index 027ce55e85..2fbc08b837 100644 --- a/resolving/npm-resolver/src/fetch.ts +++ b/resolving/npm-resolver/src/fetch.ts @@ -79,7 +79,7 @@ export async function fetchMetadataFromFromRegistry ( timeout: fetchOpts.timeout, }) as RegistryResponse } catch (error: any) { // eslint-disable-line - reject(new PnpmError('META_FETCH_FAIL', `GET ${uri}: ${error.message as string}`, { attempts: attempt })) + reject(new PnpmError('META_FETCH_FAIL', `GET ${uri}: ${error.message as string}`, { attempts: attempt, cause: error })) return } if (response.status > 400) { diff --git a/resolving/npm-resolver/src/pickPackageFromMeta.ts b/resolving/npm-resolver/src/pickPackageFromMeta.ts index 754bceddb5..e203ec34a3 100644 --- a/resolving/npm-resolver/src/pickPackageFromMeta.ts +++ b/resolving/npm-resolver/src/pickPackageFromMeta.ts @@ -88,7 +88,7 @@ export function pickPackageFromMeta ( } throw new PnpmError('MALFORMED_METADATA', `Received malformed metadata for "${spec.name}"`, - { hint: 'This might mean that the package was unpublished from the registry' } + { hint: 'This might mean that the package was unpublished from the registry', cause: err } ) } } diff --git a/resolving/npm-resolver/test/index.ts b/resolving/npm-resolver/test/index.ts index 03124e6834..a64dcf7d78 100644 --- a/resolving/npm-resolver/test/index.ts +++ b/resolving/npm-resolver/test/index.ts @@ -799,8 +799,19 @@ test('error is thrown when registry not responding', async () => { default: notExistingRegistry, }, }) - await expect(resolveFromNpm({ alias: notExistingPackage, bareSpecifier: '1.0.0' }, {})).rejects - .toThrow(new PnpmError('META_FETCH_FAIL', `GET ${notExistingRegistry}/${notExistingPackage}: request to ${notExistingRegistry}/${notExistingPackage} failed, reason: getaddrinfo ENOTFOUND not-existing.pnpm.io`, { attempts: 1 })) + + let thrown: any // eslint-disable-line + try { + await resolveFromNpm({ alias: notExistingPackage, bareSpecifier: '1.0.0' }, {}) + } catch (err) { + thrown = err + } + expect(thrown).toBeTruthy() + expect(thrown.code).toBe('ERR_PNPM_META_FETCH_FAIL') + expect(thrown.message).toContain(`GET ${notExistingRegistry}/${notExistingPackage}:`) + expect(thrown.message).toContain('ENOTFOUND') + expect(thrown.cause).toBeTruthy() + expect(thrown.cause.code).toBe('ENOTFOUND') }) test('extra info is shown if package has valid semver appended', async () => { @@ -1836,10 +1847,18 @@ test('request to a package with no dist-tags', async () => { registries, }) - await expect(resolveFromNpm({ alias: 'is-positive' }, {})).rejects - .toThrow( - new PnpmError('MALFORMED_METADATA', 'Received malformed metadata for "is-positive"') - ) + let thrown: any // eslint-disable-line + try { + await resolveFromNpm({ alias: 'is-positive' }, {}) + } catch (err) { + thrown = err + } + expect(thrown).toBeTruthy() + expect(thrown.code).toBe('ERR_PNPM_MALFORMED_METADATA') + expect(thrown.message).toBe('Received malformed metadata for "is-positive"') + expect(thrown.hint).toBe('This might mean that the package was unpublished from the registry') + expect(thrown.cause).toBeTruthy() + expect(thrown.cause.message).toContain("Cannot read properties of undefined (reading 'latest')") }) test('resolveFromNpm() does not fail if the meta file contains no integrity information', async () => {