import path from 'node:path' import { toOutput$ } from '@pnpm/cli.default-reporter' import { PnpmError } from '@pnpm/error' import { createStreamParser, logger, } from '@pnpm/logger' import chalk from 'chalk' import { loadJsonFileSync } from 'load-json-file' import normalizeNewline from 'normalize-newline' import { firstValueFrom } from 'rxjs' import { map, take } from 'rxjs/operators' import StackTracey from 'stacktracey' interface Exception extends NodeJS.ErrnoException { prefix?: string stage?: string } 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', async () => { const output$ = toOutput$({ context: { argv: ['install'] }, streamParser: createStreamParser(), }) const err = new Error('some error') logger.error(err) expect.assertions(1) const output = await firstValueFrom(output$.pipe(take(1), map(normalizeNewline))) expect(output).toBe(`${formatError('ERROR', 'some error')} ${ERROR_PAD} ${ERROR_PAD}${(new StackTracey(err.stack).asTable() as string).split('\n').join(`\n${ERROR_PAD}`)}`) }) test('prints generic error when recursive install fails', async () => { const output$ = toOutput$({ context: { argv: ['recursive'] }, streamParser: createStreamParser(), }) const err: Exception = new Error('some error') err['prefix'] = '/home/src/' logger.error(err, err) expect.assertions(1) const output = await firstValueFrom(output$.pipe(take(1), map(normalizeNewline))) 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', async () => { const output$ = toOutput$({ context: { argv: ['install'] }, streamParser: createStreamParser(), }) expect.assertions(1) const err = Object.assign(new PnpmError('NO_MATCHING_VERSION', 'No matching version found for pnpm@1000.0.0'), { packageMeta: loadJsonFileSync(path.join(import.meta.dirname, 'pnpm-meta.json')), }) logger.error(err, err) const output = await firstValueFrom(output$.pipe(take(1), map(normalizeNewline))) 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".`) }) test('prints no matching version error when only the latest dist-tag exists', async () => { const output$ = toOutput$({ context: { argv: ['install'] }, streamParser: createStreamParser(), }) expect.assertions(1) const err = Object.assign(new PnpmError('NO_MATCHING_VERSION', 'No matching version found for is-positive@1000.0.0'), { packageMeta: loadJsonFileSync(path.join(import.meta.dirname, 'is-positive-meta.json')), }) logger.error(err, err) const output = await firstValueFrom(output$.pipe(take(1), map(normalizeNewline))) 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".`) }) test('prints suggestions when an internet-connection related error happens', async () => { const output$ = toOutput$({ context: { argv: ['install'] }, streamParser: createStreamParser(), }) expect.assertions(1) const err = Object.assign( new PnpmError('BAD_TARBALL_SIZE', 'Actual size (99) of tarball (https://foo) did not match the one specified in \'Content-Length\' header (100)'), { prefix: '/project-dir', pkgsStack: [ { id: 'registry.npmjs.org/foo/1.0.0', name: 'foo', version: '1.0.0', }, ], expectedSize: 100, receivedSize: 99, } ) logger.error(err, err) const output = await firstValueFrom(output$.pipe(take(1), map(normalizeNewline))) 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 \`. ${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\``) }) test('prints test error', async () => { const output$ = toOutput$({ context: { argv: ['run', 'test'] }, streamParser: createStreamParser(), }) expect.assertions(1) const err = Object.assign(new Error('Tests failed'), { code: 'ELIFECYCLE', stage: 'test', }) logger.error(err, err) const output = await firstValueFrom(output$.pipe(take(1), map(normalizeNewline))) expect(output).toBe(`${formatError('ELIFECYCLE', 'Test failed. See above for more details.')}`) }) test('prints command error with exit code', async () => { const output$ = toOutput$({ context: { argv: ['run', 'lint'] }, streamParser: createStreamParser(), }) expect.assertions(1) const err: Exception = new Error('Command failed') err['errno'] = 100 err['stage'] = 'lint' err['code'] = 'ELIFECYCLE' logger.error(err, err) const output = await firstValueFrom(output$.pipe(take(1), map(normalizeNewline))) expect(output).toBe(`${formatError('ELIFECYCLE', 'Command failed with exit code 100.')}`) }) test('prints command error without exit code', async () => { const output$ = toOutput$({ context: { argv: ['run', 'lint'] }, streamParser: createStreamParser(), }) expect.assertions(1) const err: Exception = new Error('Command failed') err['stage'] = 'lint' err['code'] = 'ELIFECYCLE' logger.error(err, err) const output = await firstValueFrom(output$.pipe(take(1), map(normalizeNewline))) expect(output).toBe(`${formatError('ELIFECYCLE', 'Command failed.')}`) }) test('prints unsupported pnpm version error', async () => { const output$ = toOutput$({ context: { argv: ['install'] }, streamParser: createStreamParser(), }) expect.assertions(1) const err = Object.assign(new PnpmError('UNSUPPORTED_ENGINE', 'Unsupported pnpm version'), { packageId: '/home/zoltan/project', wanted: { pnpm: '2' }, current: { pnpm: '3.0.0', node: '10.0.0' }, }) logger.error(err, err) const output = await firstValueFrom(output$.pipe(take(1), map(normalizeNewline))) 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".`) }) test('prints unsupported Node version error', async () => { const output$ = toOutput$({ context: { argv: ['install'] }, streamParser: createStreamParser(), }) expect.assertions(1) const err = Object.assign(new PnpmError('UNSUPPORTED_ENGINE', 'Unsupported pnpm version'), { packageId: '/home/zoltan/project', wanted: { node: '>=12' }, current: { pnpm: '3.0.0', node: '10.0.0' }, }) logger.error(err, err) const output = await firstValueFrom(output$.pipe(take(1), map(normalizeNewline))) 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.`) }) test('prints unsupported pnpm and Node versions error', async () => { const output$ = toOutput$({ context: { argv: ['install'] }, streamParser: createStreamParser(), }) expect.assertions(1) const err = Object.assign(new PnpmError('UNSUPPORTED_ENGINE', 'Unsupported pnpm version'), { packageId: '/home/zoltan/project', wanted: { pnpm: '2', node: '>=12' }, current: { pnpm: '3.0.0', node: '10.0.0' }, }) logger.error(err, err) const output = await firstValueFrom(output$.pipe(take(1), map(normalizeNewline))) 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.`) }) test('prints error even if the error object not passed in through the message object', async () => { const output$ = toOutput$({ context: { argv: ['install'] }, streamParser: createStreamParser(), }) const err = new PnpmError('SOME_ERROR', 'some error') logger.error(err) expect.assertions(1) const output = await firstValueFrom(output$.pipe(take(1), map(normalizeNewline))) 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', async () => { 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) const output = await firstValueFrom(output$.pipe(take(1), map(normalizeNewline))) 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', async () => { 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) const output = await firstValueFrom(output$.pipe(take(1), map(normalizeNewline))) 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} ${ERROR_PAD}hint`) }) test('prints error with packages stacktrace - depth 2', async () => { 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) const output = await firstValueFrom(output$.pipe(take(1), map(normalizeNewline))) 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', async () => { 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) const output = await firstValueFrom(output$.pipe(take(1), map(normalizeNewline))) expect(output).toBe(formatErrorCode('ERR_PNPM_SOME_ERROR') + ' ' + `${chalk.red('some error')} some hint`) }) test('prints authorization error with auth settings', async () => { const authConfig = { '//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: { authConfig } 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) const output = await firstValueFrom(output$.pipe(take(1), map(normalizeNewline))) 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', async () => { const output$ = toOutput$({ context: { argv: ['install'], config: { authConfig: {} } 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) const output = await firstValueFrom(output$.pipe(take(1), map(normalizeNewline))) 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.`) })