From 844679ed7844f4837ce27e2af181a720fbe412dd Mon Sep 17 00:00:00 2001 From: Victor Savkin Date: Fri, 20 May 2022 09:39:17 -0400 Subject: [PATCH] feat(core): prefix when using --output-style=stream --- docs/generated/cli/affected.md | 2 +- docs/generated/cli/run-many.md | 2 +- docs/generated/packages/cli.json | 4 +- e2e/cli/src/output-style.test.ts | 43 +++++++++++++ packages/nx/bin/run-executor.ts | 25 +++++++- packages/nx/src/command-line/nx-commands.ts | 10 ++- .../forked-process-task-runner.ts | 21 +++++-- packages/nx/src/tasks-runner/run-command.ts | 4 ++ .../nx/src/tasks-runner/task-orchestrator.ts | 1 + .../nx/src/utils/add-command-prefix.spec.ts | 48 ++++++++++++++ packages/nx/src/utils/add-command-prefix.ts | 63 +++++++++++++++++++ 11 files changed, 211 insertions(+), 12 deletions(-) create mode 100644 e2e/cli/src/output-style.test.ts create mode 100644 packages/nx/src/utils/add-command-prefix.spec.ts create mode 100644 packages/nx/src/utils/add-command-prefix.ts diff --git a/docs/generated/cli/affected.md b/docs/generated/cli/affected.md index 02755c9356380..3d3f16b58275c 100644 --- a/docs/generated/cli/affected.md +++ b/docs/generated/cli/affected.md @@ -113,7 +113,7 @@ Isolate projects which previously failed Type: string -Choices: [dynamic, static, stream] +Choices: [dynamic, static, stream, stream-without-prefixes] Defines how Nx emits outputs tasks logs diff --git a/docs/generated/cli/run-many.md b/docs/generated/cli/run-many.md index 36f1fff743b01..a95aff07240cf 100644 --- a/docs/generated/cli/run-many.md +++ b/docs/generated/cli/run-many.md @@ -77,7 +77,7 @@ Only run the target on projects which previously failed Type: string -Choices: [dynamic, static, stream] +Choices: [dynamic, static, stream, stream-without-prefixes] Defines how Nx emits outputs tasks logs diff --git a/docs/generated/packages/cli.json b/docs/generated/packages/cli.json index f53c051203168..67af90687e027 100644 --- a/docs/generated/packages/cli.json +++ b/docs/generated/packages/cli.json @@ -39,13 +39,13 @@ "name": "run-many", "id": "run-many", "file": "generated/cli/run-many", - "content": "---\ntitle: 'run-many - CLI command'\ndescription: 'Run target for multiple listed projects'\n---\n\n# run-many\n\nRun target for multiple listed projects\n\n## Usage\n\n```bash\nnx run-many\n```\n\n[Install `nx` globally](/getting-started/nx-setup#install-nx) to invoke the command directly using `nx`, or use `npx nx`, `yarn nx`, or `pnpx nx`.\n\n### Examples\n\nTest all projects:\n\n```bash\nnx run-many --target=test --all\n```\n\nTest proj1 and proj2:\n\n```bash\nnx run-many --target=test --projects=proj1,proj2\n```\n\nTest proj1 and proj2 in parallel:\n\n```bash\nnx run-many --target=test --projects=proj1,proj2 --parallel=2\n```\n\n## Options\n\n### all\n\nType: boolean\n\nRun the target on all projects in the workspace\n\n### configuration\n\nType: string\n\nThis is the configuration to use when performing tasks on projects\n\n### exclude\n\nType: array\n\nDefault: []\n\nExclude certain projects from being processed\n\n### help\n\nType: boolean\n\nShow help\n\n### ~~only-failed~~\n\nType: boolean\n\nDefault: false\n\n**Deprecated:** The command to rerun failed projects will appear if projects fail. This now does nothing and will be removed in v15.\n\nOnly run the target on projects which previously failed\n\n### output-style\n\nType: string\n\nChoices: [dynamic, static, stream]\n\nDefines how Nx emits outputs tasks logs\n\n### parallel\n\nType: string\n\nMax number of parallel processes [default is 3]\n\n### projects\n\nType: string\n\nProjects to run (comma delimited)\n\n### runner\n\nType: string\n\nOverride the tasks runner in `nx.json`\n\n### skip-nx-cache\n\nType: boolean\n\nDefault: false\n\nRerun the tasks even when the results are available in the cache\n\n### target\n\nType: string\n\nTask to run for affected projects\n\n### verbose\n\nPrint additional error stack trace on failure\n\n### version\n\nType: boolean\n\nShow version number\n" + "content": "---\ntitle: 'run-many - CLI command'\ndescription: 'Run target for multiple listed projects'\n---\n\n# run-many\n\nRun target for multiple listed projects\n\n## Usage\n\n```bash\nnx run-many\n```\n\n[Install `nx` globally](/getting-started/nx-setup#install-nx) to invoke the command directly using `nx`, or use `npx nx`, `yarn nx`, or `pnpx nx`.\n\n### Examples\n\nTest all projects:\n\n```bash\nnx run-many --target=test --all\n```\n\nTest proj1 and proj2:\n\n```bash\nnx run-many --target=test --projects=proj1,proj2\n```\n\nTest proj1 and proj2 in parallel:\n\n```bash\nnx run-many --target=test --projects=proj1,proj2 --parallel=2\n```\n\n## Options\n\n### all\n\nType: boolean\n\nRun the target on all projects in the workspace\n\n### configuration\n\nType: string\n\nThis is the configuration to use when performing tasks on projects\n\n### exclude\n\nType: array\n\nDefault: []\n\nExclude certain projects from being processed\n\n### help\n\nType: boolean\n\nShow help\n\n### ~~only-failed~~\n\nType: boolean\n\nDefault: false\n\n**Deprecated:** The command to rerun failed projects will appear if projects fail. This now does nothing and will be removed in v15.\n\nOnly run the target on projects which previously failed\n\n### output-style\n\nType: string\n\nChoices: [dynamic, static, stream, stream-without-prefixes]\n\nDefines how Nx emits outputs tasks logs\n\n### parallel\n\nType: string\n\nMax number of parallel processes [default is 3]\n\n### projects\n\nType: string\n\nProjects to run (comma delimited)\n\n### runner\n\nType: string\n\nOverride the tasks runner in `nx.json`\n\n### skip-nx-cache\n\nType: boolean\n\nDefault: false\n\nRerun the tasks even when the results are available in the cache\n\n### target\n\nType: string\n\nTask to run for affected projects\n\n### verbose\n\nPrint additional error stack trace on failure\n\n### version\n\nType: boolean\n\nShow version number\n" }, { "name": "affected", "id": "affected", "file": "generated/cli/affected", - "content": "---\ntitle: 'affected - CLI command'\ndescription: 'Run target for affected projects'\n---\n\n# affected\n\nRun target for affected projects\n\n## Usage\n\n```bash\nnx affected\n```\n\n[Install `nx` globally](/getting-started/nx-setup#install-nx) to invoke the command directly using `nx`, or use `npx nx`, `yarn nx`, or `pnpx nx`.\n\n### Examples\n\nRun custom target for all affected projects:\n\n```bash\nnx affected --target=custom-target\n```\n\nRun tests in parallel:\n\n```bash\nnx affected --target=test --parallel=5\n```\n\nRun the test target for all projects:\n\n```bash\nnx affected --target=test --all\n```\n\nRun tests for all the projects affected by changing the index.ts file:\n\n```bash\nnx affected --target=test --files=libs/mylib/src/index.ts\n```\n\nRun tests for all the projects affected by the changes between main and HEAD (e.g., PR):\n\n```bash\nnx affected --target=test --base=main --head=HEAD\n```\n\nRun tests for all the projects affected by the last commit on main:\n\n```bash\nnx affected --target=test --base=main~1 --head=main\n```\n\n## Options\n\n### all\n\nType: boolean\n\nAll projects\n\n### base\n\nType: string\n\nBase of the current branch (usually main)\n\n### configuration\n\nType: string\n\nThis is the configuration to use when performing tasks on projects\n\n### exclude\n\nType: array\n\nDefault: []\n\nExclude certain projects from being processed\n\n### files\n\nType: array\n\nChange the way Nx is calculating the affected command by providing directly changed files, list of files delimited by commas\n\n### head\n\nType: string\n\nLatest commit of the current branch (usually HEAD)\n\n### help\n\nType: boolean\n\nShow help\n\n### ~~only-failed~~\n\nType: boolean\n\nDefault: false\n\n**Deprecated:** The command to rerun failed projects will appear if projects fail. This now does nothing and will be removed in v15.\n\nIsolate projects which previously failed\n\n### output-style\n\nType: string\n\nChoices: [dynamic, static, stream]\n\nDefines how Nx emits outputs tasks logs\n\n### parallel\n\nType: string\n\nMax number of parallel processes [default is 3]\n\n### runner\n\nType: string\n\nThis is the name of the tasks runner configured in nx.json\n\n### skip-nx-cache\n\nType: boolean\n\nDefault: false\n\nRerun the tasks even when the results are available in the cache\n\n### target\n\nType: string\n\nTask to run for affected projects\n\n### uncommitted\n\nType: boolean\n\nUncommitted changes\n\n### untracked\n\nType: boolean\n\nUntracked changes\n\n### verbose\n\nPrint additional error stack trace on failure\n\n### version\n\nType: boolean\n\nShow version number\n" + "content": "---\ntitle: 'affected - CLI command'\ndescription: 'Run target for affected projects'\n---\n\n# affected\n\nRun target for affected projects\n\n## Usage\n\n```bash\nnx affected\n```\n\n[Install `nx` globally](/getting-started/nx-setup#install-nx) to invoke the command directly using `nx`, or use `npx nx`, `yarn nx`, or `pnpx nx`.\n\n### Examples\n\nRun custom target for all affected projects:\n\n```bash\nnx affected --target=custom-target\n```\n\nRun tests in parallel:\n\n```bash\nnx affected --target=test --parallel=5\n```\n\nRun the test target for all projects:\n\n```bash\nnx affected --target=test --all\n```\n\nRun tests for all the projects affected by changing the index.ts file:\n\n```bash\nnx affected --target=test --files=libs/mylib/src/index.ts\n```\n\nRun tests for all the projects affected by the changes between main and HEAD (e.g., PR):\n\n```bash\nnx affected --target=test --base=main --head=HEAD\n```\n\nRun tests for all the projects affected by the last commit on main:\n\n```bash\nnx affected --target=test --base=main~1 --head=main\n```\n\n## Options\n\n### all\n\nType: boolean\n\nAll projects\n\n### base\n\nType: string\n\nBase of the current branch (usually main)\n\n### configuration\n\nType: string\n\nThis is the configuration to use when performing tasks on projects\n\n### exclude\n\nType: array\n\nDefault: []\n\nExclude certain projects from being processed\n\n### files\n\nType: array\n\nChange the way Nx is calculating the affected command by providing directly changed files, list of files delimited by commas\n\n### head\n\nType: string\n\nLatest commit of the current branch (usually HEAD)\n\n### help\n\nType: boolean\n\nShow help\n\n### ~~only-failed~~\n\nType: boolean\n\nDefault: false\n\n**Deprecated:** The command to rerun failed projects will appear if projects fail. This now does nothing and will be removed in v15.\n\nIsolate projects which previously failed\n\n### output-style\n\nType: string\n\nChoices: [dynamic, static, stream, stream-without-prefixes]\n\nDefines how Nx emits outputs tasks logs\n\n### parallel\n\nType: string\n\nMax number of parallel processes [default is 3]\n\n### runner\n\nType: string\n\nThis is the name of the tasks runner configured in nx.json\n\n### skip-nx-cache\n\nType: boolean\n\nDefault: false\n\nRerun the tasks even when the results are available in the cache\n\n### target\n\nType: string\n\nTask to run for affected projects\n\n### uncommitted\n\nType: boolean\n\nUncommitted changes\n\n### untracked\n\nType: boolean\n\nUntracked changes\n\n### verbose\n\nPrint additional error stack trace on failure\n\n### version\n\nType: boolean\n\nShow version number\n" }, { "name": "affected:graph", diff --git a/e2e/cli/src/output-style.test.ts b/e2e/cli/src/output-style.test.ts new file mode 100644 index 0000000000000..1960feccaa4d7 --- /dev/null +++ b/e2e/cli/src/output-style.test.ts @@ -0,0 +1,43 @@ +import { + isNotWindows, + newProject, + readFile, + readJson, + runCLI, + runCLIAsync, + runCommand, + tmpProjPath, + uniq, + updateFile, + updateProjectConfig, +} from '@nrwl/e2e/utils'; +import { renameSync } from 'fs'; +import { packagesWeCareAbout } from 'nx/src/command-line/report'; + +describe('Output Style', () => { + beforeEach(() => newProject()); + + it('should stream output', async () => { + const myapp = uniq('myapp'); + runCLI(`generate @nrwl/web:app ${myapp}`); + updateProjectConfig(myapp, (c) => { + c.targets['counter'] = { + executor: '@nrwl/workspace:counter', + options: { + to: 2, + }, + }; + return c; + }); + + const withPrefixes = runCLI( + `counter ${myapp} --result=true --output-style=stream` + ); + expect(withPrefixes).toContain(`[${myapp}`); + + const noPrefixes = runCLI( + `counter ${myapp} --result=true --output-style=stream-without-prefixes` + ); + expect(noPrefixes).not.toContain(`[${myapp}`); + }); +}); diff --git a/packages/nx/bin/run-executor.ts b/packages/nx/bin/run-executor.ts index f971079a3e896..2e68b21ec8211 100644 --- a/packages/nx/bin/run-executor.ts +++ b/packages/nx/bin/run-executor.ts @@ -1,5 +1,6 @@ import { appendFileSync, openSync, writeFileSync } from 'fs'; import { run } from '../src/command-line/run'; +import { addCommandPrefixIfNeeded } from '../src/utils/add-command-prefix'; if (process.env.NX_TERMINAL_OUTPUT_PATH) { setUpOutputWatching( @@ -12,12 +13,14 @@ if (!process.env.NX_WORKSPACE_ROOT) { console.error('Invalid Nx command invocation'); process.exit(1); } +let projectName; requireCli(); function requireCli() { process.env.NX_CLI_SET = 'true'; try { const args = JSON.parse(process.argv[2]); + projectName = args.targetDescription.project; run( process.cwd(), process.env.NX_WORKSPACE_ROOT, @@ -66,7 +69,16 @@ function setUpOutputWatching(captureStderr: boolean, streamOutput: boolean) { onlyStdout.push(chunk); appendFileSync(stdoutAndStderrLogFileHandle, chunk); if (streamOutput) { - stdoutWrite.apply(process.stdout, [chunk, encoding, callback]); + const updatedChunk = addCommandPrefixIfNeeded( + projectName, + chunk, + encoding + ); + stdoutWrite.apply(process.stdout, [ + updatedChunk.content, + updatedChunk.encoding, + callback, + ]); } else { callback(); } @@ -79,7 +91,16 @@ function setUpOutputWatching(captureStderr: boolean, streamOutput: boolean) { ) => { appendFileSync(stdoutAndStderrLogFileHandle, chunk); if (streamOutput) { - stderrWrite.apply(process.stderr, [chunk, encoding, callback]); + const updatedChunk = addCommandPrefixIfNeeded( + projectName, + chunk, + encoding + ); + stderrWrite.apply(process.stderr, [ + updatedChunk.content, + updatedChunk.encoding, + callback, + ]); } else { callback(); } diff --git a/packages/nx/src/command-line/nx-commands.ts b/packages/nx/src/command-line/nx-commands.ts index 3d092f0990bca..79815cccb3cd0 100644 --- a/packages/nx/src/command-line/nx-commands.ts +++ b/packages/nx/src/command-line/nx-commands.ts @@ -543,7 +543,7 @@ function withOutputStyleOption(yargs: yargs.Argv): yargs.Argv { return yargs.option('output-style', { describe: 'Defines how Nx emits outputs tasks logs', type: 'string', - choices: ['dynamic', 'static', 'stream'], + choices: ['dynamic', 'static', 'stream', 'stream-without-prefixes'], }); } @@ -628,7 +628,13 @@ function withRunOneOptions(yargs: yargs.Argv) { .option('output-style', { describe: 'Defines how Nx emits outputs tasks logs', type: 'string', - choices: ['dynamic', 'static', 'stream', 'compact'], + choices: [ + 'dynamic', + 'static', + 'stream', + 'stream-without-prefixes', + 'compact', + ], }); if (executorShouldShowHelp) { diff --git a/packages/nx/src/tasks-runner/forked-process-task-runner.ts b/packages/nx/src/tasks-runner/forked-process-task-runner.ts index 5897c27fa94f4..3a6ce8bb1c67f 100644 --- a/packages/nx/src/tasks-runner/forked-process-task-runner.ts +++ b/packages/nx/src/tasks-runner/forked-process-task-runner.ts @@ -18,6 +18,7 @@ import { } from './batch/batch-messages'; import { stripIndents } from '../utils/strip-indents'; import { Task } from '../config/task-graph'; +import { addCommandPrefixIfNeeded } from '../utils/add-command-prefix'; const workerPath = join(__dirname, './batch/run-batch.js'); @@ -98,7 +99,10 @@ export class ForkedProcessTaskRunner { { streamOutput, temporaryOutputPath, - }: { streamOutput: boolean; temporaryOutputPath: string } + }: { + streamOutput: boolean; + temporaryOutputPath: string; + } ) { return new Promise<{ code: number; terminalOutput: string }>((res, rej) => { try { @@ -128,14 +132,20 @@ export class ForkedProcessTaskRunner { let outWithErr = []; p.stdout.on('data', (chunk) => { if (streamOutput) { - process.stdout.write(chunk); + process.stdout.write( + addCommandPrefixIfNeeded(task.target.project, chunk, 'utf-8') + .content + ); } out.push(chunk.toString()); outWithErr.push(chunk.toString()); }); p.stderr.on('data', (chunk) => { if (streamOutput) { - process.stderr.write(chunk); + process.stderr.write( + addCommandPrefixIfNeeded(task.target.project, chunk, 'utf-8') + .content + ); } outWithErr.push(chunk.toString()); }); @@ -168,7 +178,10 @@ export class ForkedProcessTaskRunner { { streamOutput, temporaryOutputPath, - }: { streamOutput: boolean; temporaryOutputPath: string } + }: { + streamOutput: boolean; + temporaryOutputPath: string; + } ) { return new Promise<{ code: number; terminalOutput: string }>((res, rej) => { try { diff --git a/packages/nx/src/tasks-runner/run-command.ts b/packages/nx/src/tasks-runner/run-command.ts index 453612335c42b..32e035081bae2 100644 --- a/packages/nx/src/tasks-runner/run-command.ts +++ b/packages/nx/src/tasks-runner/run-command.ts @@ -105,6 +105,10 @@ export async function runCommand( const projectNames = projectsToRun.map((t) => t.name); if (nxArgs.outputStyle == 'stream') { process.env.NX_STREAM_OUTPUT = 'true'; + process.env.NX_PREFIX_OUTPUT = 'true'; + } + if (nxArgs.outputStyle == 'stream-without-prefixes') { + process.env.NX_STREAM_OUTPUT = 'true'; } const { lifeCycle, renderIsDone } = await getTerminalOutputLifeCycle( initiatingProject, diff --git a/packages/nx/src/tasks-runner/task-orchestrator.ts b/packages/nx/src/tasks-runner/task-orchestrator.ts index 627c2cb88650a..73cbd9291cefe 100644 --- a/packages/nx/src/tasks-runner/task-orchestrator.ts +++ b/packages/nx/src/tasks-runner/task-orchestrator.ts @@ -285,6 +285,7 @@ export class TaskOrchestrator { this.initiatingProject, this.options ); + const pipeOutput = this.pipeOutputCapture(task); // execution diff --git a/packages/nx/src/utils/add-command-prefix.spec.ts b/packages/nx/src/utils/add-command-prefix.spec.ts new file mode 100644 index 0000000000000..ea4b3e19d759c --- /dev/null +++ b/packages/nx/src/utils/add-command-prefix.spec.ts @@ -0,0 +1,48 @@ +import { addPrefixToLines } from './add-command-prefix'; +const stripAnsi = require('strip-ansi'); + +describe('addPrefixToLines', () => { + it('should add project name as a prefix', () => { + const prefixed = addPrefixToLines('myproj', ['one', 'two', 'three']); + expect(prefixed.map(stripAnsi)).toEqual([ + '[myproj ] one', + '[myproj ] two', + '[myproj ] three', + ]); + }); + + it('should handle project names which length = 15', () => { + const prefixed = addPrefixToLines('123456789012345', [ + 'one', + 'two', + 'three', + ]); + expect(prefixed.map(stripAnsi)).toEqual([ + '[123456789012345] one', + '[123456789012345] two', + '[123456789012345] three', + ]); + }); + + it('should handle long project names', () => { + const prefixed = addPrefixToLines('12345678901234567890', [ + 'one', + 'two', + 'three', + ]); + expect(prefixed.map(stripAnsi)).toEqual([ + '[...901234567890] one', + '[...901234567890] two', + '[...901234567890] three', + ]); + }); + + it('should not prefix last empty line', () => { + const prefixed = addPrefixToLines('myproj', ['one', 'two', '']); + expect(prefixed.map(stripAnsi)).toEqual([ + '[myproj ] one', + '[myproj ] two', + '', + ]); + }); +}); diff --git a/packages/nx/src/utils/add-command-prefix.ts b/packages/nx/src/utils/add-command-prefix.ts new file mode 100644 index 0000000000000..4a4ece1f65ee0 --- /dev/null +++ b/packages/nx/src/utils/add-command-prefix.ts @@ -0,0 +1,63 @@ +import * as chalk from 'chalk'; + +export function addCommandPrefixIfNeeded( + projectName: string, + chunk: any, + encoding: string +) { + if (process.env.NX_PREFIX_OUTPUT === 'true') { + const lines = ( + typeof chunk === 'string' ? chunk : chunk.toString('utf-8') + ).split('\n'); + return { + content: addPrefixToLines(projectName, lines).join('\n'), + encoding: 'utf-8', + }; + } else { + return { content: chunk, encoding: encoding }; + } +} + +export function addPrefixToLines(projectName: string, lines: string[]) { + const updatedLines = []; + for (let i = 0; i < lines.length; ++i) { + if (i === lines.length - 1 && lines[i] === '') { + updatedLines.push(''); + } else { + updatedLines.push(`${projectNamePrefix(projectName)} ${lines[i]}`); + } + } + return updatedLines; +} + +const colors = [ + chalk.green, + chalk.greenBright, + chalk.red, + chalk.redBright, + chalk.cyan, + chalk.cyanBright, + chalk.yellow, + chalk.yellowBright, + chalk.magenta, + chalk.magentaBright, +]; + +function projectNamePrefix(projectName: string) { + const n = normalizeProjectName(projectName); + return colors[projectNameToIndex(projectName)](`[${n}]`); +} + +function projectNameToIndex(projectName: string): number { + let code = 0; + for (let i = 0; i < projectName.length; ++i) { + code += projectName.charCodeAt(i); + } + return code % colors.length; +} + +function normalizeProjectName(projectName: string): string { + return projectName.length > 15 + ? `...${projectName.substring(projectName.length - 12)}` + : `${projectName} `.substring(0, 15); +}