diff --git a/e2e/workspace.test.ts b/e2e/workspace.test.ts index 8e2854d9b3..1e28516326 100644 --- a/e2e/workspace.test.ts +++ b/e2e/workspace.test.ts @@ -61,7 +61,9 @@ forEachCli(cliName => { expect(buildWithDeps).toContain(`${cliCommand} run ${mylib2}:build`); const testsWithDeps = runCLI(`test ${myapp} --with-deps`); - expect(testsWithDeps).toContain(`NX Running target test for projects:`); + expect(testsWithDeps).toContain( + `NX Running target test for project ${myapp} and its 2 deps` + ); expect(testsWithDeps).toContain(myapp); expect(testsWithDeps).toContain(mylib1); expect(testsWithDeps).toContain(mylib2); @@ -647,15 +649,27 @@ forEachCli(cliName => { ]); }, 120000); - function expectCached(actual: string, expected: string[]) { - const section = actual.split('read the output from cache')[1]; - const r = section - .split('\n') - .filter(l => l.trim().startsWith('-')) - .map(l => l.split('- ')[1].trim()); - r.sort((a, b) => a.localeCompare(b)); - expected.sort((a, b) => a.localeCompare(b)); - expect(r).toEqual(expected); + function expectCached( + actualOutput: string, + expectedCachedProjects: string[] + ) { + const cachedProjects = []; + const lines = actualOutput.split('\n'); + lines.forEach((s, i) => { + if (s.startsWith(`> ${cliCommand} run`)) { + const projectName = s + .split(`> ${cliCommand} run `)[1] + .split(':')[0] + .trim(); + if (lines[i + 2].indexOf('Cached Output') > -1) { + cachedProjects.push(projectName); + } + } + }); + + cachedProjects.sort((a, b) => a.localeCompare(b)); + expectedCachedProjects.sort((a, b) => a.localeCompare(b)); + expect(cachedProjects).toEqual(expectedCachedProjects); } }); }); diff --git a/packages/cli/lib/run-cli.ts b/packages/cli/lib/run-cli.ts index 70cf754422..1fca6a1892 100644 --- a/packages/cli/lib/run-cli.ts +++ b/packages/cli/lib/run-cli.ts @@ -5,7 +5,10 @@ import { findWorkspaceRoot } from './find-workspace-root'; const workspace = findWorkspaceRoot(process.cwd()); if (process.env.NX_TERMINAL_OUTPUT_PATH) { - setUpOutputWatching(process.env.NX_TERMINAL_CAPTURE_STDERR === 'true'); + setUpOutputWatching( + process.env.NX_TERMINAL_CAPTURE_STDERR === 'true', + process.env.NX_FORWARD_OUTPUT === 'true' + ); } requireCli(); @@ -40,11 +43,12 @@ function requireCli() { * And the cached output should be correct unless the CLI bypasses process.stdout or console.log and uses some * C-binary to write to stdout. */ -function setUpOutputWatching(captureStderr: boolean) { +function setUpOutputWatching(captureStderr: boolean, forwardOutput: boolean) { const stdoutWrite = process.stdout._write; const stderrWrite = process.stderr._write; let out = []; + let outWithErr = []; process.stdout._write = ( chunk: any, @@ -52,7 +56,12 @@ function setUpOutputWatching(captureStderr: boolean) { callback: Function ) => { out.push(chunk.toString()); - stdoutWrite.apply(process.stdout, [chunk, encoding, callback]); + outWithErr.push(chunk.toString()); + if (forwardOutput) { + stdoutWrite.apply(process.stdout, [chunk, encoding, callback]); + } else { + callback(); + } }; process.stderr._write = ( @@ -60,15 +69,27 @@ function setUpOutputWatching(captureStderr: boolean) { encoding: string, callback: Function ) => { - if (captureStderr) { - out.push(chunk.toString()); + outWithErr.push(chunk.toString()); + if (forwardOutput) { + stderrWrite.apply(process.stderr, [chunk, encoding, callback]); + } else { + callback(); } - stderrWrite.apply(process.stderr, [chunk, encoding, callback]); }; process.on('exit', code => { if (code === 0) { - fs.writeFileSync(process.env.NX_TERMINAL_OUTPUT_PATH, out.join('')); + fs.writeFileSync( + process.env.NX_TERMINAL_OUTPUT_PATH, + captureStderr ? outWithErr.join('') : out.join('') + ); + } else { + if (!forwardOutput) { + fs.writeFileSync( + process.env.NX_TERMINAL_OUTPUT_PATH, + outWithErr.join('') + ); + } } }); } diff --git a/packages/workspace/src/command-line/affected.ts b/packages/workspace/src/command-line/affected.ts index 1cf81279b6..b3ed8b3483 100644 --- a/packages/workspace/src/command-line/affected.ts +++ b/packages/workspace/src/command-line/affected.ts @@ -115,7 +115,8 @@ export function affected(command: string, parsedArgs: yargs.Arguments): void { env, nxArgs, overrides, - new DefaultReporter() + new DefaultReporter(), + null ); break; } diff --git a/packages/workspace/src/command-line/run-many.ts b/packages/workspace/src/command-line/run-many.ts index bf38c41527..91d6efe394 100644 --- a/packages/workspace/src/command-line/run-many.ts +++ b/packages/workspace/src/command-line/run-many.ts @@ -3,6 +3,8 @@ import { runCommand } from '../tasks-runner/run-command'; import { NxArgs, splitArgsIntoNxArgsAndOverrides } from './utils'; import { createProjectGraph, + isWorkspaceProject, + onlyWorkspaceProjects, ProjectGraph, ProjectGraphNode, withDeps @@ -30,7 +32,8 @@ export function runMany(parsedArgs: yargs.Arguments): void { env, nxArgs, overrides, - new DefaultReporter() + new DefaultReporter(), + null ); } @@ -75,7 +78,7 @@ function runnableForTarget( for (let project of projects) { if (projectHasTarget(project, target)) { runnable.push(project); - } else { + } else if (isWorkspaceProject(project)) { notRunnable.push(project); } } diff --git a/packages/workspace/src/command-line/run-one.ts b/packages/workspace/src/command-line/run-one.ts index 82e97556c1..bcbed184d7 100644 --- a/packages/workspace/src/command-line/run-one.ts +++ b/packages/workspace/src/command-line/run-one.ts @@ -30,10 +30,20 @@ export function runOne(opts: { ); const env = readEnvironment(opts.target, projectsMap); const reporter = nxArgs.withDeps - ? new (require(`../tasks-runner/default-reporter`)).DefaultReporter() + ? new (require(`../tasks-runner/run-one-reporter`)).RunOneReporter( + opts.project + ) : new EmptyReporter(); - runCommand(projects, projectGraph, env, nxArgs, overrides, reporter); + runCommand( + projects, + projectGraph, + env, + nxArgs, + overrides, + reporter, + opts.project + ); } function getProjects( diff --git a/packages/workspace/src/tasks-runner/default-reporter.ts b/packages/workspace/src/tasks-runner/default-reporter.ts index 8d6031bbba..999adf97d0 100644 --- a/packages/workspace/src/tasks-runner/default-reporter.ts +++ b/packages/workspace/src/tasks-runner/default-reporter.ts @@ -7,21 +7,21 @@ export interface ReporterArgs { } export class DefaultReporter { - beforeRun( - affectedProjectNames: string[], - affectedArgs: ReporterArgs, - taskOverrides: any - ) { - if (affectedProjectNames.length <= 0) { - let description = `with "${affectedArgs.target}"`; - if (affectedArgs.configuration) { - description += ` that are configured for "${affectedArgs.configuration}"`; + private projectNames: string[]; + + beforeRun(projectNames: string[], args: ReporterArgs, taskOverrides: any) { + this.projectNames = projectNames; + + if (projectNames.length <= 0) { + let description = `with "${args.target}"`; + if (args.configuration) { + description += ` that are configured for "${args.configuration}"`; } output.logSingleLine(`No projects ${description} were run`); return; } - const bodyLines = affectedProjectNames.map( + const bodyLines = projectNames.map( affectedProject => `${output.colors.gray('-')} ${affectedProject}` ); if (Object.keys(taskOverrides).length > 0) { @@ -34,29 +34,39 @@ export class DefaultReporter { output.log({ title: `${output.colors.gray('Running target')} ${ - affectedArgs.target + args.target } ${output.colors.gray('for projects:')}`, bodyLines }); - output.addVerticalSeparator(); + output.addVerticalSeparatorWithoutNewLines(); } printResults( - affectedArgs: ReporterArgs, + args: ReporterArgs, failedProjectNames: string[], startedWithFailedProjects: boolean, cachedProjectNames: string[] ) { output.addNewline(); - output.addVerticalSeparator(); + output.addVerticalSeparatorWithoutNewLines(); if (failedProjectNames.length === 0) { + const bodyLines = + cachedProjectNames.length > 0 + ? [ + output.colors.gray( + `Nx read the output from cache instead of running the command for ${cachedProjectNames.length} out of ${this.projectNames.length} projects.` + ) + ] + : []; + output.success({ - title: `Running target "${affectedArgs.target}" succeeded` + title: `Running target "${args.target}" succeeded`, + bodyLines }); - if (affectedArgs.onlyFailed && startedWithFailedProjects) { + if (args.onlyFailed && startedWithFailedProjects) { output.warn({ title: `Only projects ${output.underline( 'which had previously failed' @@ -76,7 +86,7 @@ export class DefaultReporter { project => `${output.colors.gray('-')} ${project}` ) ]; - if (!affectedArgs.onlyFailed && !startedWithFailedProjects) { + if (!args.onlyFailed && !startedWithFailedProjects) { bodyLines.push(''); bodyLines.push( `${output.colors.gray( @@ -85,17 +95,7 @@ export class DefaultReporter { ); } output.error({ - title: `Running target "${affectedArgs.target}" failed`, - bodyLines - }); - } - - if (cachedProjectNames.length > 0) { - const bodyLines = cachedProjectNames.map( - project => `${output.colors.gray('-')} ${project}` - ); - output.note({ - title: `Nx read the output from cache instead of running the command for the following projects:`, + title: `Running target "${args.target}" failed`, bodyLines }); } diff --git a/packages/workspace/src/tasks-runner/default-tasks-runner.ts b/packages/workspace/src/tasks-runner/default-tasks-runner.ts index f0a1bedc86..f6dcc8fd23 100644 --- a/packages/workspace/src/tasks-runner/default-tasks-runner.ts +++ b/packages/workspace/src/tasks-runner/default-tasks-runner.ts @@ -40,7 +40,12 @@ export interface DefaultTasksRunnerOptions { export const defaultTasksRunner: TasksRunner = ( tasks: Task[], options: DefaultTasksRunnerOptions, - context: { target: string; projectGraph: ProjectGraph; nxJson: NxJson } + context: { + target: string; + initiatingProject?: string; + projectGraph: ProjectGraph; + nxJson: NxJson; + } ): Observable => { if (!options.lifeCycle) { options.lifeCycle = new NoopLifeCycle(); @@ -65,14 +70,23 @@ export const defaultTasksRunner: TasksRunner = ( async function runAllTasks( tasks: Task[], options: DefaultTasksRunnerOptions, - context: { target: string; projectGraph: ProjectGraph; nxJson: NxJson } + context: { + target: string; + initiatingProject?: string; + projectGraph: ProjectGraph; + nxJson: NxJson; + } ): Promise> { const stages = new TaskOrderer( context.target, context.projectGraph ).splitTasksIntoStages(tasks); - const orchestrator = new TaskOrchestrator(context.projectGraph, options); + const orchestrator = new TaskOrchestrator( + context.initiatingProject, + context.projectGraph, + options + ); const res = []; for (let i = 0; i < stages.length; ++i) { diff --git a/packages/workspace/src/tasks-runner/run-command.ts b/packages/workspace/src/tasks-runner/run-command.ts index d39decd539..b9c284d1ba 100644 --- a/packages/workspace/src/tasks-runner/run-command.ts +++ b/packages/workspace/src/tasks-runner/run-command.ts @@ -18,7 +18,8 @@ export async function runCommand( { nxJson, workspaceResults }: Environment, nxArgs: NxArgs, overrides: any, - reporter: any + reporter: any, + initiatingProject: string | null ) { reporter.beforeRun(projectsToRun.map(p => p.name), nxArgs, overrides); @@ -45,6 +46,7 @@ export async function runCommand( const cached = []; tasksRunner(tasks, tasksOptions, { + initiatingProject: initiatingProject, target: nxArgs.target, projectGraph, nxJson diff --git a/packages/workspace/src/tasks-runner/run-one-reporter.ts b/packages/workspace/src/tasks-runner/run-one-reporter.ts new file mode 100644 index 0000000000..5248f89498 --- /dev/null +++ b/packages/workspace/src/tasks-runner/run-one-reporter.ts @@ -0,0 +1,79 @@ +import { output } from '../utils/output'; + +export interface ReporterArgs { + target?: string; + configuration?: string; + onlyFailed?: boolean; +} + +export class RunOneReporter { + private projectNames: string[]; + constructor(private readonly initiatingProject: string) {} + + beforeRun(projectNames: string[], args: ReporterArgs, taskOverrides: any) { + this.projectNames = projectNames; + const numberOfDeps = projectNames.length - 1; + + if (numberOfDeps > 0) { + output.log({ + title: `${output.colors.gray('Running target')} ${ + args.target + } ${output.colors.gray('for project')} ${ + this.initiatingProject + } ${output.colors.gray(`and its ${numberOfDeps} deps.`)}` + }); + output.addVerticalSeparatorWithoutNewLines(); + } + } + + printResults( + args: ReporterArgs, + failedProjectNames: string[], + startedWithFailedProjects: boolean, + cachedProjectNames: string[] + ) { + output.addNewline(); + output.addVerticalSeparatorWithoutNewLines(); + + if (failedProjectNames.length === 0) { + const bodyLines = + cachedProjectNames.length > 0 + ? [ + output.colors.gray( + `Nx read the output from cache instead of running the command for ${cachedProjectNames.length} out of ${this.projectNames.length} projects.` + ) + ] + : []; + + output.success({ + title: `Running target "${args.target}" succeeded`, + bodyLines + }); + + if (args.onlyFailed && startedWithFailedProjects) { + output.warn({ + title: `Only projects ${output.underline( + 'which had previously failed' + )} were run`, + bodyLines: [ + `You should verify by running ${output.underline( + 'without' + )} ${output.bold('--only-failed')}` + ] + }); + } + } else { + const bodyLines = [ + output.colors.gray('Failed projects:'), + '', + ...failedProjectNames.map( + project => `${output.colors.gray('-')} ${project}` + ) + ]; + output.error({ + title: `Running target "${args.target}" failed`, + bodyLines + }); + } + } +} diff --git a/packages/workspace/src/tasks-runner/task-orchestrator.ts b/packages/workspace/src/tasks-runner/task-orchestrator.ts index e552fce5b0..dca5136d90 100644 --- a/packages/workspace/src/tasks-runner/task-orchestrator.ts +++ b/packages/workspace/src/tasks-runner/task-orchestrator.ts @@ -7,6 +7,7 @@ import { fork } from 'child_process'; import { DefaultTasksRunnerOptions } from './default-tasks-runner'; import { output } from '../utils/output'; import * as path from 'path'; +import * as fs from 'fs'; import { appRootPath } from '../utils/app-root'; export class TaskOrchestrator { @@ -15,6 +16,7 @@ export class TaskOrchestrator { cli = cliCommand(); constructor( + private readonly initiatingProject: string | undefined, private readonly projectGraph: ProjectGraph, private readonly options: DefaultTasksRunnerOptions ) {} @@ -93,8 +95,16 @@ export class TaskOrchestrator { tasks.forEach(t => { this.options.lifeCycle.startTask(t.task); - output.note({ title: `Cached Output:` }); - process.stdout.write(t.cachedResult.terminalOutput); + if ( + !this.initiatingProject || + this.initiatingProject === t.task.target.project + ) { + const args = this.getCommandArgs(t.task); + output.logCommand(`${this.cli} ${args.join(' ')}`); + output.note({ title: `Cached Output:` }); + process.stdout.write(t.cachedResult.terminalOutput); + } + const outputs = getOutputs(this.projectGraph.nodes, t.task); this.cache.copyFilesFromCache(t.cachedResult, outputs); @@ -117,21 +127,25 @@ export class TaskOrchestrator { return new Promise((res, rej) => { try { this.options.lifeCycle.startTask(task); - - const env = { ...process.env }; - if (outputPath) { - env.NX_TERMINAL_OUTPUT_PATH = outputPath; - if (this.options.captureStderr) { - env.NX_TERMINAL_CAPTURE_STDERR = 'true'; - } - } + const forwardOutput = this.shouldForwardOutput(outputPath, task); + const env = this.envForForkedProcess(outputPath, forwardOutput); const args = this.getCommandArgs(task); - console.log(`> ${this.cli} ${args.join(' ')}`); + const commandLine = `${this.cli} ${args.join(' ')}`; + + if (forwardOutput) { + output.logCommand(commandLine); + } const p = fork(this.getCommand(), args, { stdio: ['inherit', 'inherit', 'inherit', 'ipc'], env }); p.on('close', code => { + // we didn't print any output as we were running the command + // print all the collected output + if (!forwardOutput) { + output.logCommand(commandLine); + process.stdout.write(fs.readFileSync(outputPath)); + } if (outputPath && code === 0) { this.cache.put(task, outputPath, taskOutputs).then(() => { this.options.lifeCycle.endTask(task, code); @@ -149,6 +163,27 @@ export class TaskOrchestrator { }); } + private envForForkedProcess(outputPath: string, forwardOutput: boolean) { + const env = { ...process.env }; + if (outputPath) { + env.NX_TERMINAL_OUTPUT_PATH = outputPath; + if (this.options.captureStderr) { + env.NX_TERMINAL_CAPTURE_STDERR = 'true'; + } + if (forwardOutput) { + env.NX_FORWARD_OUTPUT = 'true'; + } + } + return env; + } + + private shouldForwardOutput(outputPath: string | undefined, task: Task) { + if (!outputPath) return true; + if (!this.options.parallel) return true; + if (task.target.project === this.initiatingProject) return true; + return false; + } + private getCommand() { return path.join( this.workspaceRoot, diff --git a/packages/workspace/src/tasks-runner/tasks-runner.ts b/packages/workspace/src/tasks-runner/tasks-runner.ts index 4ea1d17736..465c6d3bc3 100644 --- a/packages/workspace/src/tasks-runner/tasks-runner.ts +++ b/packages/workspace/src/tasks-runner/tasks-runner.ts @@ -31,6 +31,7 @@ export type TasksRunner = ( options: T, context?: { target?: string; + initiatingProject?: string | null; projectGraph: ProjectGraph; nxJson: NxJson; } diff --git a/packages/workspace/src/utils/output.ts b/packages/workspace/src/utils/output.ts index 19116861d4..680b518fdc 100644 --- a/packages/workspace/src/utils/output.ts +++ b/packages/workspace/src/utils/output.ts @@ -19,6 +19,7 @@ export interface CLINoteMessageConfig { export interface CLISuccessMessageConfig { title: string; + bodyLines?: string[]; } /** @@ -86,6 +87,10 @@ class CLIOutput { this.writeToStdOut(`\n${chalk.gray(this.VERTICAL_SEPARATOR)}\n\n`); } + addVerticalSeparatorWithoutNewLines() { + this.writeToStdOut(`${chalk.gray(this.VERTICAL_SEPARATOR)}\n`); + } + error({ title, slug, bodyLines }: CLIErrorMessageConfig) { this.addNewline(); @@ -151,7 +156,7 @@ class CLIOutput { this.addNewline(); } - success({ title }: CLISuccessMessageConfig) { + success({ title, bodyLines }: CLISuccessMessageConfig) { this.addNewline(); this.writeOutputTitle({ @@ -159,6 +164,8 @@ class CLIOutput { title: chalk.bold.green(title) }); + this.writeOptionalOutputBody(bodyLines); + this.addNewline(); } @@ -172,6 +179,14 @@ class CLIOutput { this.addNewline(); } + logCommand(message: string) { + this.addNewline(); + + this.writeToStdOut(chalk.bold(`> ${message} `)); + + this.addNewline(); + } + log({ title, bodyLines }: CLIWarnMessageConfig) { this.addNewline();