feat(core): improve outputs when using --parallel and --with-deps

This commit is contained in:
Victor Savkin 2020-03-18 17:24:49 -04:00 committed by Victor Savkin
parent b671959f68
commit 0b7535ae92
12 changed files with 261 additions and 66 deletions

View File

@ -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);
}
});
});

View File

@ -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('')
);
}
}
});
}

View File

@ -115,7 +115,8 @@ export function affected(command: string, parsedArgs: yargs.Arguments): void {
env,
nxArgs,
overrides,
new DefaultReporter()
new DefaultReporter(),
null
);
break;
}

View File

@ -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);
}
}

View File

@ -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(

View File

@ -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
});
}

View File

@ -40,7 +40,12 @@ export interface DefaultTasksRunnerOptions {
export const defaultTasksRunner: TasksRunner<DefaultTasksRunnerOptions> = (
tasks: Task[],
options: DefaultTasksRunnerOptions,
context: { target: string; projectGraph: ProjectGraph; nxJson: NxJson }
context: {
target: string;
initiatingProject?: string;
projectGraph: ProjectGraph;
nxJson: NxJson;
}
): Observable<TaskCompleteEvent> => {
if (!options.lifeCycle) {
options.lifeCycle = new NoopLifeCycle();
@ -65,14 +70,23 @@ export const defaultTasksRunner: TasksRunner<DefaultTasksRunnerOptions> = (
async function runAllTasks(
tasks: Task[],
options: DefaultTasksRunnerOptions,
context: { target: string; projectGraph: ProjectGraph; nxJson: NxJson }
context: {
target: string;
initiatingProject?: string;
projectGraph: ProjectGraph;
nxJson: NxJson;
}
): Promise<Array<{ task: Task; type: any; success: boolean }>> {
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) {

View File

@ -18,7 +18,8 @@ export async function runCommand<T extends RunArgs>(
{ 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<T extends RunArgs>(
const cached = [];
tasksRunner(tasks, tasksOptions, {
initiatingProject: initiatingProject,
target: nxArgs.target,
projectGraph,
nxJson

View File

@ -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
});
}
}
}

View File

@ -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,

View File

@ -31,6 +31,7 @@ export type TasksRunner<T = unknown> = (
options: T,
context?: {
target?: string;
initiatingProject?: string | null;
projectGraph: ProjectGraph;
nxJson: NxJson;
}

View File

@ -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();