mirror of
https://github.com/pnpm/pnpm.git
synced 2026-04-07 08:47:11 -04:00
fix: propagate error cause when throwing PnpmError in @pnpm/npm-resolver (#10990)
* fix: show error cause when failing to read metadata * fix: correct changeset package name and add cause assertion tests - Fix changeset to reference @pnpm/resolving.npm-resolver (not @pnpm/npm-resolver) - Add PnpmError cause unit tests in @pnpm/error - Fix npm-resolver tests to actually verify cause on thrown errors (.toThrow() only checks message, not cause/hint/code properties) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Zoltan Kochan <z@kochan.io> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
5
.changeset/deep-ads-hope.md
Normal file
5
.changeset/deep-ads-hope.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@pnpm/resolving.npm-resolver": patch
|
||||
---
|
||||
|
||||
When package metadata is malformed or can't be fetched, the error thrown will now show the originating error.
|
||||
5
.changeset/stale-regions-like.md
Normal file
5
.changeset/stale-regions-like.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@pnpm/error": minor
|
||||
---
|
||||
|
||||
The `PnpmError` class now accepts an optional `cause` argument.
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -86,7 +86,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) {
|
||||
|
||||
@@ -90,7 +90,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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -865,8 +865,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 () => {
|
||||
@@ -1961,10 +1972,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 () => {
|
||||
|
||||
Reference in New Issue
Block a user