diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d34b0a3e6..d62f80b418 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,9 +20,17 @@ Please add one entry in this file for each change in Yarn's behavior. Use the sa [#7127](https://github.com/yarnpkg/yarn/pull/7127) - [**Eli Perelman**](https://github.com/eliperelman) +- Prevents EPIPE errors from being printed. + + [#7194](https://github.com/yarnpkg/yarn/pull/7194) - [**Abhishek Reddy**](https://github.com/arbscht) + - Adds support for the npm enterprise URLs when computing the offline mirror filenames. [#7200](https://github.com/yarnpkg/yarn/pull/7200) - [**John Millikin**](https://john-millikin.com) + +- Tweaks the lockfile parser logic to parse a few extra cases + + [#7210](https://github.com/yarnpkg/yarn/pull/7210) - [**Maƫl Nison**](https://twitter.com/arcanis) ## 1.15.2 diff --git a/__tests__/pipe.js b/__tests__/pipe.js new file mode 100644 index 0000000000..a0478ec8d9 --- /dev/null +++ b/__tests__/pipe.js @@ -0,0 +1,96 @@ +/* @flow */ +/* eslint max-len: 0 */ + +import execa from 'execa'; +import {sh} from 'puka'; +import makeTemp from './_temp.js'; +import * as fs from '../src/util/fs.js'; + +const path = require('path'); + +function runYarnStreaming(args: Array = [], options: Object = {}): execa.ExecaChildPromise { + if (!options['env']) { + options['env'] = {...process.env}; + options['extendEnv'] = false; + } + options['env']['FORCE_COLOR'] = 0; + + return execa.shell(sh`${path.resolve(__dirname, '../bin/yarn')} ${args}`, options); +} + +test('terminate console stream quietly on EPIPE', async () => { + const cwd = await makeTemp(); + const packageJsonPath = path.join(cwd, 'package.json'); + const initialManifestFile = JSON.stringify({name: 'test', license: 'ISC', version: '1.0.0'}); + + await fs.writeFile(packageJsonPath, initialManifestFile); + + const {stdout, stderr} = runYarnStreaming(['versions'], {cwd}); + + stdout.destroy(); + + await new Promise((resolve, reject) => { + let stderrOutput = ''; + stderr.on('readable', () => { + const chunk = stderr.read(); + if (chunk) { + stderrOutput += chunk; + } else { + resolve(stderrOutput); + } + }); + stderr.on('error', err => { + reject(err); + }); + }) + .then(stderrOutput => { + expect(stderrOutput).not.toMatch(/EPIPE/); + }) + .catch(err => { + expect(err).toBeFalsy(); + }); +}); + +test('terminate console stream preserving zero exit code on EPIPE', async () => { + const cwd = await makeTemp(); + const packageJsonPath = path.join(cwd, 'package.json'); + const initialManifestFile = JSON.stringify({name: 'test', license: 'ISC', version: '1.0.0'}); + + await fs.writeFile(packageJsonPath, initialManifestFile); + + const proc = runYarnStreaming(['versions'], {cwd}); + + const {stdout} = proc; + + stdout.destroy(); + + await new Promise(resolve => { + proc.on('exit', function(code, signal) { + resolve(code); + }); + }).then(exitCode => { + expect(exitCode).toEqual(0); + }); +}); + +test('terminate console stream preserving non-zero exit code on EPIPE', async () => { + const cwd = await makeTemp(); + const packageJsonPath = path.join(cwd, 'package.json'); + const initialManifestFile = JSON.stringify({name: 'test', license: 'ISC', version: '1.0.0'}); + + await fs.writeFile(packageJsonPath, initialManifestFile); + + const proc = runYarnStreaming(['add'], {cwd}); + + const {stdout} = proc; + + stdout.destroy(); + + await new Promise(resolve => { + proc.on('exit', function(code, signal) { + resolve(code); + }); + }).then(exitCode => { + expect(exitCode).toEqual(1); + }); +}); diff --git a/src/cli/index.js b/src/cli/index.js index ccf7f1b952..ad3b7961f8 100644 --- a/src/cli/index.js +++ b/src/cli/index.js @@ -26,6 +26,14 @@ import handleSignals from '../util/signal-handler.js'; import {boolify, boolifyWithDefault} from '../util/conversion.js'; import {ProcessTermError} from '../errors'; +process.stdout.prependListener('error', err => { + // swallow err only if downstream consumer process closed pipe early + if (err.code === 'EPIPE' || err.code === 'ERR_STREAM_DESTROYED') { + return; + } + throw err; +}); + function findProjectRoot(base: string): string { let prev = null; let dir = base;