pnpm/cli/default-reporter/test/reportingErrors.ts

545 lines
16 KiB
TypeScript

import path from 'path'
import { toOutput$ } from '@pnpm/default-reporter'
import { PnpmError } from '@pnpm/error'
import {
createStreamParser,
logger,
} from '@pnpm/logger'
import { map, take } from 'rxjs/operators'
import chalk from 'chalk'
import loadJsonFile from 'load-json-file'
import normalizeNewline from 'normalize-newline'
import StackTracey from 'stacktracey'
const formatErrorCode = (code: string) => chalk.bgRed.black(`\u2009${code}\u2009`)
const formatError = (code: string, message: string) => {
return `${formatErrorCode(code)} ${chalk.red(message)}`
}
const ERROR_PAD = ''
test('prints generic error', (done) => {
const output$ = toOutput$({
context: { argv: ['install'] },
streamParser: createStreamParser(),
})
const err = new Error('some error')
logger.error(err)
expect.assertions(1)
output$.pipe(take(1), map(normalizeNewline)).subscribe({
complete: () => done(),
error: done,
next: output => {
expect(output).toBe(`${formatError('ERROR', 'some error')}
${ERROR_PAD}${(new StackTracey(err.stack).asTable() as string).split('\n').join(`\n${ERROR_PAD}`)}`)
},
})
})
test('prints generic error when recursive install fails', (done) => {
const output$ = toOutput$({
context: { argv: ['recursive'] },
streamParser: createStreamParser(),
})
const err = new Error('some error')
err['prefix'] = '/home/src/'
logger.error(err, err)
expect.assertions(1)
output$.pipe(take(1), map(normalizeNewline)).subscribe({
complete: () => done(),
error: done,
next: output => {
expect(output).toBe(`/home/src/:
${formatError('ERROR', 'some error')}
${ERROR_PAD}
${ERROR_PAD}${(new StackTracey(err.stack).asTable() as string).split('\n').join(`\n${ERROR_PAD}`)}`)
},
})
})
test('prints no matching version error when many dist-tags exist', (done) => {
const output$ = toOutput$({
context: { argv: ['install'] },
streamParser: createStreamParser(),
})
expect.assertions(1)
output$.pipe(take(1), map(normalizeNewline)).subscribe({
complete: () => done(),
error: done,
next: output => {
expect(output).toBe(`${formatError('ERR_PNPM_NO_MATCHING_VERSION', 'No matching version found for pnpm@1000.0.0')}
${ERROR_PAD}
${ERROR_PAD}The latest release of pnpm is "2.4.0".
${ERROR_PAD}
${ERROR_PAD}Other releases are:
${ERROR_PAD} * stable: 2.2.2
${ERROR_PAD} * next: 2.4.0
${ERROR_PAD} * latest-1: 1.43.1
${ERROR_PAD}
${ERROR_PAD}If you need the full list of all 281 published versions run "$ pnpm view pnpm versions".`)
},
})
const err = new PnpmError('NO_MATCHING_VERSION', 'No matching version found for pnpm@1000.0.0')
err['packageMeta'] = loadJsonFile.sync(path.join(__dirname, 'pnpm-meta.json'))
logger.error(err, err)
})
test('prints no matching version error when only the latest dist-tag exists', (done) => {
const output$ = toOutput$({
context: { argv: ['install'] },
streamParser: createStreamParser(),
})
expect.assertions(1)
output$.pipe(take(1), map(normalizeNewline)).subscribe({
complete: () => done(),
error: done,
next: output => {
expect(output).toBe(`${formatError('ERR_PNPM_NO_MATCHING_VERSION', 'No matching version found for is-positive@1000.0.0')}
${ERROR_PAD}
${ERROR_PAD}The latest release of is-positive is "3.1.0".
${ERROR_PAD}
${ERROR_PAD}If you need the full list of all 4 published versions run "$ pnpm view is-positive versions".`)
},
})
const err = new PnpmError('NO_MATCHING_VERSION', 'No matching version found for is-positive@1000.0.0')
err['packageMeta'] = loadJsonFile.sync(path.join(__dirname, 'is-positive-meta.json'))
logger.error(err, err)
})
test('prints suggestions when an internet-connection related error happens', (done) => {
const output$ = toOutput$({
context: { argv: ['install'] },
streamParser: createStreamParser(),
})
expect.assertions(1)
output$.pipe(take(1), map(normalizeNewline)).subscribe({
complete: () => done(),
error: done,
next: output => {
expect(output).toBe(`/project-dir:
${formatError('ERR_PNPM_BAD_TARBALL_SIZE', 'Actual size (99) of tarball (https://foo) did not match the one specified in \'Content-Length\' header (100)')}
${ERROR_PAD}
${ERROR_PAD}This error happened while installing the dependencies of foo@1.0.0
${ERROR_PAD}
${ERROR_PAD}Seems like you have internet connection issues.
${ERROR_PAD}Try running the same command again.
${ERROR_PAD}If that doesn't help, try one of the following:
${ERROR_PAD}
${ERROR_PAD}- Set a bigger value for the \`fetch-retries\` config.
${ERROR_PAD} To check the current value of \`fetch-retries\`, run \`pnpm get fetch-retries\`.
${ERROR_PAD} To set a new value, run \`pnpm set fetch-retries <number>\`.
${ERROR_PAD}
${ERROR_PAD}- Set \`network-concurrency\` to 1.
${ERROR_PAD} This change will slow down installation times, so it is recommended to
${ERROR_PAD} delete the config once the internet connection is good again: \`pnpm config delete network-concurrency\`
${ERROR_PAD}
${ERROR_PAD}NOTE: You may also override configs via flags.
${ERROR_PAD}For instance, \`pnpm install --fetch-retries 5 --network-concurrency 1\``)
},
})
const err = new PnpmError('BAD_TARBALL_SIZE', 'Actual size (99) of tarball (https://foo) did not match the one specified in \'Content-Length\' header (100)')
err.prefix = '/project-dir'
err.pkgsStack = [
{
id: 'registry.npmjs.org/foo/1.0.0',
name: 'foo',
version: '1.0.0',
},
]
err['expectedSize'] = 100
err['receivedSize'] = 99
logger.error(err, err)
})
test('prints test error', (done) => {
const output$ = toOutput$({
context: { argv: ['run', 'test'] },
streamParser: createStreamParser(),
})
expect.assertions(1)
output$.pipe(take(1), map(normalizeNewline)).subscribe({
complete: () => done(),
error: done,
next: output => {
expect(output).toBe(`${formatError('ELIFECYCLE', 'Test failed. See above for more details.')}`)
},
})
const err = Object.assign(new Error('Tests failed'), {
code: 'ELIFECYCLE',
stage: 'test',
})
logger.error(err, err)
})
test('prints command error with exit code', (done) => {
const output$ = toOutput$({
context: { argv: ['run', 'lint'] },
streamParser: createStreamParser(),
})
expect.assertions(1)
output$.pipe(take(1), map(normalizeNewline)).subscribe({
complete: () => done(),
error: done,
next: output => {
expect(output).toBe(`${formatError('ELIFECYCLE', 'Command failed with exit code 100.')}`)
},
})
const err = new Error('Command failed')
err['errno'] = 100
err['stage'] = 'lint'
err['code'] = 'ELIFECYCLE'
logger.error(err, err)
})
test('prints command error without exit code', (done) => {
const output$ = toOutput$({
context: { argv: ['run', 'lint'] },
streamParser: createStreamParser(),
})
expect.assertions(1)
output$.pipe(take(1), map(normalizeNewline)).subscribe({
complete: () => done(),
error: done,
next: output => {
expect(output).toBe(`${formatError('ELIFECYCLE', 'Command failed.')}`)
},
})
const err = new Error('Command failed')
err['stage'] = 'lint'
err['code'] = 'ELIFECYCLE'
logger.error(err, err)
})
test('prints unsupported pnpm version error', (done) => {
const output$ = toOutput$({
context: { argv: ['install'] },
streamParser: createStreamParser(),
})
expect.assertions(1)
output$.pipe(take(1), map(normalizeNewline)).subscribe({
complete: () => done(),
error: done,
next: output => {
expect(output).toBe(`${formatError('ERR_PNPM_UNSUPPORTED_ENGINE', 'Unsupported environment (bad pnpm and/or Node.js version)')}
${ERROR_PAD}
${ERROR_PAD}Your pnpm version is incompatible with "/home/zoltan/project".
${ERROR_PAD}
${ERROR_PAD}Expected version: 2
${ERROR_PAD}Got: 3.0.0
${ERROR_PAD}
${ERROR_PAD}This is happening because the package's manifest has an engines.pnpm field specified.
${ERROR_PAD}To fix this issue, install the required pnpm version globally.
${ERROR_PAD}
${ERROR_PAD}To install the latest version of pnpm, run "pnpm i -g pnpm".
${ERROR_PAD}To check your pnpm version, run "pnpm -v".`)
},
})
const err = new PnpmError('UNSUPPORTED_ENGINE', 'Unsupported pnpm version')
err['packageId'] = '/home/zoltan/project'
err['wanted'] = { pnpm: '2' }
err['current'] = { pnpm: '3.0.0', node: '10.0.0' }
logger.error(err, err)
})
test('prints unsupported Node version error', (done) => {
const output$ = toOutput$({
context: { argv: ['install'] },
streamParser: createStreamParser(),
})
expect.assertions(1)
output$.pipe(take(1), map(normalizeNewline)).subscribe({
complete: () => done(),
error: done,
next: output => {
expect(output).toBe(`${formatError('ERR_PNPM_UNSUPPORTED_ENGINE', 'Unsupported environment (bad pnpm and/or Node.js version)')}
${ERROR_PAD}
${ERROR_PAD}Your Node version is incompatible with "/home/zoltan/project".
${ERROR_PAD}
${ERROR_PAD}Expected version: >=12
${ERROR_PAD}Got: 10.0.0
${ERROR_PAD}
${ERROR_PAD}This is happening because the package's manifest has an engines.node field specified.
${ERROR_PAD}To fix this issue, install the required Node version.`)
},
})
const err = new PnpmError('UNSUPPORTED_ENGINE', 'Unsupported pnpm version')
err['packageId'] = '/home/zoltan/project'
err['wanted'] = { node: '>=12' }
err['current'] = { pnpm: '3.0.0', node: '10.0.0' }
logger.error(err, err)
})
test('prints unsupported pnpm and Node versions error', (done) => {
const output$ = toOutput$({
context: { argv: ['install'] },
streamParser: createStreamParser(),
})
expect.assertions(1)
output$.pipe(take(1), map(normalizeNewline)).subscribe({
complete: () => done(),
error: done,
next: output => {
expect(output).toBe(`${formatError('ERR_PNPM_UNSUPPORTED_ENGINE', 'Unsupported environment (bad pnpm and/or Node.js version)')}
${ERROR_PAD}
${ERROR_PAD}Your pnpm version is incompatible with "/home/zoltan/project".
${ERROR_PAD}
${ERROR_PAD}Expected version: 2
${ERROR_PAD}Got: 3.0.0
${ERROR_PAD}
${ERROR_PAD}This is happening because the package's manifest has an engines.pnpm field specified.
${ERROR_PAD}To fix this issue, install the required pnpm version globally.
${ERROR_PAD}
${ERROR_PAD}To install the latest version of pnpm, run "pnpm i -g pnpm".
${ERROR_PAD}To check your pnpm version, run "pnpm -v".` + '\n\n' + `\
${ERROR_PAD}Your Node version is incompatible with "/home/zoltan/project".
${ERROR_PAD}
${ERROR_PAD}Expected version: >=12
${ERROR_PAD}Got: 10.0.0
${ERROR_PAD}
${ERROR_PAD}This is happening because the package's manifest has an engines.node field specified.
${ERROR_PAD}To fix this issue, install the required Node version.`)
},
})
const err = new PnpmError('UNSUPPORTED_ENGINE', 'Unsupported pnpm version')
err['packageId'] = '/home/zoltan/project'
err['wanted'] = { pnpm: '2', node: '>=12' }
err['current'] = { pnpm: '3.0.0', node: '10.0.0' }
logger.error(err, err)
})
test('prints error even if the error object not passed in through the message object', (done) => {
const output$ = toOutput$({
context: { argv: ['install'] },
streamParser: createStreamParser(),
})
const err = new PnpmError('SOME_ERROR', 'some error')
logger.error(err)
expect.assertions(1)
output$.pipe(take(1), map(normalizeNewline)).subscribe({
complete: () => done(),
error: done,
next: output => {
expect(output).toBe(formatError('ERR_PNPM_SOME_ERROR', 'some error'))
},
})
})
test('prints error without packages stacktrace when pkgsStack is empty but do print the project directory path', (done) => {
const output$ = toOutput$({
context: { argv: ['install'] },
streamParser: createStreamParser(),
})
const err = new PnpmError('SOME_ERROR', 'some error')
err.prefix = '/project-dir'
err.pkgsStack = []
logger.error(err, err)
expect.assertions(1)
output$.pipe(take(1), map(normalizeNewline)).subscribe({
complete: () => done(),
error: done,
next: output => {
expect(output).toBe(`/project-dir:
${formatError('ERR_PNPM_SOME_ERROR', 'some error')}
${ERROR_PAD}
${ERROR_PAD}This error happened while installing a direct dependency of /project-dir`)
},
})
})
test('prints error with packages stacktrace - depth 1 and hint', (done) => {
const output$ = toOutput$({
context: { argv: ['install'] },
streamParser: createStreamParser(),
})
const err = new PnpmError('SOME_ERROR', 'some error', { hint: 'hint' })
err.pkgsStack = [
{
id: 'registry.npmjs.org/foo/1.0.0',
name: 'foo',
version: '1.0.0',
},
]
logger.error(err, err)
expect.assertions(1)
output$.pipe(take(1), map(normalizeNewline)).subscribe({
complete: () => done(),
error: done,
next: output => {
expect(output).toBe(`${formatError('ERR_PNPM_SOME_ERROR', 'some error')}
${ERROR_PAD}
${ERROR_PAD}This error happened while installing the dependencies of foo@1.0.0
${ERROR_PAD}hint`)
},
})
})
test('prints error with packages stacktrace - depth 2', (done) => {
const output$ = toOutput$({
context: { argv: ['install'] },
streamParser: createStreamParser(),
})
const err = new PnpmError('SOME_ERROR', 'some error')
err.prefix = '/project-dir'
err.pkgsStack = [
{
id: 'registry.npmjs.org/foo/1.0.0',
name: 'foo',
version: '1.0.0',
},
{
id: 'registry.npmjs.org/bar/1.0.0',
name: 'bar',
version: '1.0.0',
},
]
logger.error(err, err)
expect.assertions(1)
output$.pipe(take(1), map(normalizeNewline)).subscribe({
complete: () => done(),
error: done,
next: output => {
expect(output).toBe(`/project-dir:
${formatError('ERR_PNPM_SOME_ERROR', 'some error')}
${ERROR_PAD}
${ERROR_PAD}This error happened while installing the dependencies of foo@1.0.0
${ERROR_PAD} at bar@1.0.0`)
},
})
})
test('prints error and hint', (done) => {
const output$ = toOutput$({
context: { argv: ['install'] },
streamParser: createStreamParser(),
})
const err = new PnpmError('SOME_ERROR', 'some error', { hint: 'some hint' })
logger.error(err, err)
expect.assertions(1)
output$.pipe(take(1), map(normalizeNewline)).subscribe({
complete: () => done(),
error: done,
next: output => {
expect(output).toBe(formatErrorCode('ERR_PNPM_SOME_ERROR') + ' ' + `${chalk.red('some error')}
some hint`)
},
})
})
test('prints authorization error with auth settings', (done) => {
const rawConfig = {
'//foo.bar:_auth': '9876543219',
'//foo.bar:_authToken': '9876543219',
'//foo.bar:_password': '9876543219',
'//foo.bar:username': 'kiss.reka',
'@foo:registry': 'https://foo.bar',
_auth: '0123456789',
_authToken: '0123456789',
_password: '0123456789',
username: 'nagy.gabor',
}
const output$ = toOutput$({
context: { argv: ['install'], config: { rawConfig } as any }, // eslint-disable-line
streamParser: createStreamParser(),
})
const err = new PnpmError('FETCH_401', 'some error', { hint: 'some hint' })
logger.error(err, err)
expect.assertions(1)
output$.pipe(take(1), map(normalizeNewline)).subscribe({
complete: () => done(),
error: done,
next: output => {
expect(output).toBe(`${formatError('ERR_PNPM_FETCH_401', 'some error')}
${ERROR_PAD}
${ERROR_PAD}some hint
${ERROR_PAD}
${ERROR_PAD}These authorization settings were found:
${ERROR_PAD}//foo.bar:_auth=9876[hidden]
${ERROR_PAD}//foo.bar:_authToken=9876[hidden]
${ERROR_PAD}//foo.bar:_password=[hidden]
${ERROR_PAD}//foo.bar:username=kiss.reka
${ERROR_PAD}@foo:registry=https://foo.bar
${ERROR_PAD}_auth=0123[hidden]
${ERROR_PAD}_authToken=0123[hidden]
${ERROR_PAD}_password=[hidden]
${ERROR_PAD}username=nagy.gabor`)
},
})
})
test('prints authorization error without auth settings, where there are none', (done) => {
const output$ = toOutput$({
context: { argv: ['install'], config: { rawConfig: {} } as any }, // eslint-disable-line
streamParser: createStreamParser(),
})
const err = new PnpmError('FETCH_401', 'some error', { hint: 'some hint' })
logger.error(err, err)
expect.assertions(1)
output$.pipe(take(1), map(normalizeNewline)).subscribe({
complete: () => done(),
error: done,
next: output => {
expect(output).toBe(`${formatError('ERR_PNPM_FETCH_401', 'some error')}
${ERROR_PAD}
${ERROR_PAD}some hint
${ERROR_PAD}
${ERROR_PAD}No authorization settings were found in the configs.
${ERROR_PAD}Try to log in to the registry by running "pnpm login"
${ERROR_PAD}or add the auth tokens manually to the ~/.npmrc file.`)
},
})
})