feat(core): improve outputs when using --parallel and --with-deps
This commit is contained in:
parent
b671959f68
commit
0b7535ae92
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@ -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('')
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -115,7 +115,8 @@ export function affected(command: string, parsedArgs: yargs.Arguments): void {
|
||||
env,
|
||||
nxArgs,
|
||||
overrides,
|
||||
new DefaultReporter()
|
||||
new DefaultReporter(),
|
||||
null
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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
|
||||
});
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
|
||||
79
packages/workspace/src/tasks-runner/run-one-reporter.ts
Normal file
79
packages/workspace/src/tasks-runner/run-one-reporter.ts
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -31,6 +31,7 @@ export type TasksRunner<T = unknown> = (
|
||||
options: T,
|
||||
context?: {
|
||||
target?: string;
|
||||
initiatingProject?: string | null;
|
||||
projectGraph: ProjectGraph;
|
||||
nxJson: NxJson;
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user