diff --git a/packages/workspace/src/core/hasher/git-hasher.ts b/packages/workspace/src/core/hasher/git-hasher.ts index 3e19941b78..02372396de 100644 --- a/packages/workspace/src/core/hasher/git-hasher.ts +++ b/packages/workspace/src/core/hasher/git-hasher.ts @@ -1,6 +1,6 @@ -import { spawnSync } from 'child_process'; -import { join } from 'path'; import { statSync } from 'fs'; +import { join } from 'path'; +import { spawnProcess } from './spawn-process'; function parseGitLsTree(output: string): Map { const changes: Map = new Map(); @@ -33,7 +33,7 @@ function parseGitStatus(path: string): Map { // we need to manually strip the root path from the filenames. const prefix = spawnProcess('git', ['rev-parse', '--show-prefix'], path); - var chunks = output.split('\0'); + const chunks = output.split('\0'); for (let i = 0; i < chunks.length; i++) { const chunk = chunks[i]; if (chunk.length) { @@ -52,16 +52,6 @@ function parseGitStatus(path: string): Map { return changes; } -function spawnProcess(command: string, args: string[], cwd: string): string { - const r = spawnSync(command, args, { cwd, maxBuffer: 50 * 1024 * 1024 }); - if (r.status !== 0) { - throw new Error( - `Failed to run ${command} ${args.join(' ')}.\n${r.stdout}\n${r.stderr}` - ); - } - return r.stdout.toString().trimEnd(); -} - function getGitHashForFiles( filesToHash: string[], path: string diff --git a/packages/workspace/src/core/hasher/spawn-process.spec.ts b/packages/workspace/src/core/hasher/spawn-process.spec.ts new file mode 100644 index 0000000000..8d2b2a0cb2 --- /dev/null +++ b/packages/workspace/src/core/hasher/spawn-process.spec.ts @@ -0,0 +1,57 @@ +import * as childProcess from 'child_process'; +import { spawnProcess } from './spawn-process'; + +describe('spawnProcess()', () => { + let spy: jest.SpyInstance< + childProcess.SpawnSyncReturns, + [ + command: string, + args?: readonly string[], + options?: childProcess.SpawnSyncOptions + ] + >; + + beforeEach(() => { + spy = jest.spyOn(childProcess, 'spawnSync'); + }); + + afterEach(() => { + spy.mockReset(); + spy.mockRestore(); + }); + + it('should call spawnSync and return the stdout', () => { + const mockedStdout = 'stdout'; + spy.mockImplementation(() => { + return { + status: 0, + stdout: mockedStdout, + } as any; + }); + const output = spawnProcess('git', ['status', '-s', '-u', '-z', '.'], ''); + expect(spy).toHaveBeenCalledTimes(1); + expect(output).toEqual(mockedStdout); + }); + + it('should work even when ANSI escape characters are present in the child process output - https://github.com/nrwl/nx/issues/7022', () => { + spy.mockImplementation(() => { + return { + status: 0, + // ANSI escaped characters can come through in the child process output, as reported here: https://github.com/nrwl/nx/issues/7022 + stdout: + ' \x1B[7;33mM\x1B[m packages/semver/src/generators/install/index.spec.ts\x00', + } as any; + }); + + const output = spawnProcess('git', ['status', '-s', '-u', '-z', '.'], ''); + expect(spy).toHaveBeenCalledTimes(1); + + /** + * Ensure the ANSI escaped characters have been stripped + * (the remaining trailing \0 is expected as part of the real-world git output) + */ + expect(output).toEqual( + ' M packages/semver/src/generators/install/index.spec.ts\0' + ); + }); +}); diff --git a/packages/workspace/src/core/hasher/spawn-process.ts b/packages/workspace/src/core/hasher/spawn-process.ts new file mode 100644 index 0000000000..bea128fc4b --- /dev/null +++ b/packages/workspace/src/core/hasher/spawn-process.ts @@ -0,0 +1,26 @@ +import { spawnSync } from 'child_process'; + +// We can't use an import for this package because of how it is published +const stripAnsi = require('strip-ansi'); + +/** + * We separated this out into its own file to make it much easier to unit test + * and ensure that ANSI escape codes are appropriately stripped so that utilities + * in git-hasher work as intended in all cases. + */ +export function spawnProcess( + command: string, + args: string[], + cwd: string +): string { + const r = spawnSync(command, args, { cwd, maxBuffer: 50 * 1024 * 1024 }); + if (r.status !== 0) { + throw new Error( + `Failed to run ${command} ${args.join(' ')}.\n${r.stdout}\n${r.stderr}` + ); + } + const output = r.stdout.toString().trimEnd(); + + // We use strip-ansi to prevent issues such as https://github.com/nrwl/nx/issues/7022 + return stripAnsi(output); +} diff --git a/scripts/depcheck/missing.ts b/scripts/depcheck/missing.ts index ad23cf1fea..f4a9b3cde6 100644 --- a/scripts/depcheck/missing.ts +++ b/scripts/depcheck/missing.ts @@ -85,6 +85,7 @@ const IGNORE_MATCHES = { 'karma-coverage-istanbul-reporter', 'karma-jasmine', 'karma-jasmine-html-reporter', + 'strip-ansi', 'webpack', 'webpack-dev-server', ],