chore(core): task runner dynamic output life cycle (#8590)

This commit is contained in:
James Henry 2022-01-19 23:52:10 +04:00 committed by GitHub
parent e69b893829
commit faef0d8c85
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 807 additions and 545 deletions

View File

@ -60,7 +60,7 @@ npx nx run-many --target=build --projects=todos,api
And notice the output:
```bash
Nx read the output from cache instead of running the command for 1 out of 2 projects.
Nx read the output from the cache instead of running the command for 1 out of 2 projects.
```
Nx built `api` and retrieved `todos` from its computation cache. Read more about the cache here [here](/using-nx/caching).

View File

@ -49,7 +49,7 @@ Based on the state of the source code and the environment, Nx figured out that i
**Now, run `npx nx run-many --target=build --projects=todos,api` to rebuild the two applications:**
```bash
Nx read the output from cache instead of running the command for 1 out of 2 projects.
Nx read the output from the cache instead of running the command for 1 out of 2 projects.
```
Nx built `api` and retrieved `todos` from its computation cache. Read more about the cache [here](/using-nx/caching).

View File

@ -250,7 +250,7 @@ describe('Angular Projects', () => {
expect(buildOutput).toContain(
`Building entry point '@${proj}/${lib}/${entryPoint}'`
);
expect(buildOutput).toContain('Running target "build" succeeded');
expect(buildOutput).toContain('Successfully ran target build');
});
it('MFE - should serve the host and remote apps successfully', async () => {

View File

@ -135,14 +135,14 @@ describe('list', () => {
listOutput = runCLI('list @nrwl/angular');
expect(listOutput).toContain(
'NX NOTE @nrwl/angular is not currently installed'
'NX @nrwl/angular is not currently installed'
);
// look for an unknown plugin
listOutput = runCLI('list @wibble/fish');
expect(listOutput).toContain(
'NX NOTE @wibble/fish is not currently installed'
'NX @wibble/fish is not currently installed'
);
// put back the @nrwl/angular module (or all the other e2e tests after this will fail)

View File

@ -36,14 +36,14 @@ describe('Detox', () => {
);
expect(runCLI(`build-ios ${appName}-e2e --prod`)).toContain(
'Running target "build-ios" succeeded'
'Successfully ran target build-ios'
);
expect(
runCLI(
`test-ios ${appName}-e2e --prod --debugSynchronization=true --loglevel=trace`
)
).toContain('Running target "test-ios" succeeded');
).toContain('Successfully ran target test-ios');
await killPorts(8081); // kill the port for the serve command
}, 3000000);

View File

@ -131,7 +131,7 @@ describe('js e2e', () => {
});
const output = runCLI(`build ${app}`);
expect(output).toContain('1 task(s) that it depends on');
expect(output).toContain('1 task(s) it depends on');
expect(output).toContain('Done compiling TypeScript files');
expect(runCLI(`serve ${app} --no-watch`)).toContain(`Running ${lib}`);
@ -193,7 +193,7 @@ describe('js e2e', () => {
// });
//
// const output = runCLI(`build ${app}`);
// expect(output).toContain('1 task(s) that it depends on');
// expect(output).toContain('1 task(s) it depends on');
// expect(output).toContain('Successfully compiled: 2 files with swc');
//
// expect(runCommand(`serve ${app} --watch=false`)).toContain(`Running ${lib}`)

View File

@ -148,7 +148,7 @@ describe('Linter', () => {
// Ensure that the unit tests for the new rule are runnable
const unitTestsOutput = runCLI(`test eslint-rules`);
expect(unitTestsOutput).toContain('Running target "test" succeeded');
expect(unitTestsOutput).toContain('Successfully ran target test');
// Update the rule for the e2e test so that we can assert that it produces the expected lint failure when used
const knownLintErrorMessage = 'e2e test known error message';

View File

@ -770,7 +770,7 @@ describe('with dependencies', () => {
const buildWithDeps = runCLI(
`build ${app} --with-deps --buildLibsFromSource=false`
);
expect(buildWithDeps).toContain(`Running target "build" succeeded`);
expect(buildWithDeps).toContain('Successfully ran target build');
checkFilesDoNotExist(`apps/${app}/tsconfig/tsconfig.nx-tmp`);
// we remove all path mappings from the root tsconfig, so when trying to build

View File

@ -62,7 +62,7 @@ describe('Nx Plugin', () => {
if (isNotWindows()) {
const e2eResults = runCLI(`e2e ${plugin}-e2e`);
expect(e2eResults).toContain('Running target "e2e" succeeded');
expect(e2eResults).toContain('Successfully ran target e2e');
expect(await killPorts()).toBeTruthy();
}
}, 250000);

View File

@ -234,7 +234,7 @@ export async function h() { return 'c'; }
const buildFromSource = runCLI(
`build ${app} --buildLibsFromSource=false`
);
expect(buildFromSource).toContain(`Running target "build" succeeded`);
expect(buildFromSource).toContain('Successfully ran target build');
checkFilesDoNotExist(`apps/${app}/tsconfig/tsconfig.nx-tmp`);
// we remove all path mappings from the root tsconfig, so when trying to build

View File

@ -91,7 +91,7 @@ describe('run-one', () => {
env: { ...process.env, NX_DAEMON: 'true' },
});
expect(buildWithDaemon).toContain(`Running target "build" succeeded`);
expect(buildWithDaemon).toContain('Successfully ran target build');
}, 10000);
it('should build the project when within the project root', () => {
@ -135,7 +135,7 @@ describe('run-one', () => {
it('should include deps', () => {
const output = runCLI(`test ${myapp} --with-deps`);
expect(output).toContain(
`NX Running target test for project ${myapp} and 2 task(s) that it depends on.`
`NX Running target test for project ${myapp} and 2 task(s) it depends on`
);
expect(output).toContain(myapp);
expect(output).toContain(mylib1);
@ -144,7 +144,7 @@ describe('run-one', () => {
it('should include deps without the configuration if it does not exist', () => {
const buildWithDeps = runCLI(`build ${myapp} --with-deps --prod`);
expect(buildWithDeps).toContain(`Running target "build" succeeded`);
expect(buildWithDeps).toContain('Successfully ran target build');
expect(buildWithDeps).toContain(`nx run ${myapp}:build:production`);
expect(buildWithDeps).toContain(`nx run ${mylib1}:build`);
expect(buildWithDeps).toContain(`nx run ${mylib2}:build`);
@ -188,7 +188,7 @@ describe('run-one', () => {
const output = runCLI(`build ${myapp}`);
expect(output).toContain(
`NX Running target build for project ${myapp} and 2 task(s) that it depends on.`
`NX Running target build for project ${myapp} and 2 task(s) it depends on`
);
expect(output).toContain(myapp);
expect(output).toContain(mylib1);
@ -217,7 +217,7 @@ describe('run-one', () => {
const output = runCLI(`build ${myapp}`);
expect(output).toContain(
`NX Running target build for project ${myapp} and 2 task(s) that it depends on.`
`NX Running target build for project ${myapp} and 2 task(s) it depends on`
);
expect(output).toContain(myapp);
expect(output).toContain(mylib1);
@ -271,7 +271,7 @@ describe('run-many', () => {
expect(buildParallel).toContain(`- ${libB}`);
expect(buildParallel).toContain(`- ${libC}`);
expect(buildParallel).not.toContain(`- ${libD}`);
expect(buildParallel).toContain('Running target "build" succeeded');
expect(buildParallel).toContain('Successfully ran target build');
// testing run many --all starting
const buildAllParallel = runCLI(`run-many --target=build --all`);
@ -283,7 +283,7 @@ describe('run-many', () => {
expect(buildAllParallel).toContain(`- ${libB}`);
expect(buildAllParallel).toContain(`- ${libC}`);
expect(buildAllParallel).not.toContain(`- ${libD}`);
expect(buildAllParallel).toContain('Running target "build" succeeded');
expect(buildAllParallel).toContain('Successfully ran target build');
// testing run many --with-deps
const buildWithDeps = runCLI(
@ -296,7 +296,7 @@ describe('run-many', () => {
expect(buildWithDeps).toContain(`${libC}`); // build should include libC as dependency
expect(buildWithDeps).not.toContain(`- ${libB}`);
expect(buildWithDeps).not.toContain(`- ${libD}`);
expect(buildWithDeps).toContain('Running target "build" succeeded');
expect(buildWithDeps).toContain('Successfully ran target build');
// testing run many --configuration
const buildConfig = runCLI(
@ -308,13 +308,13 @@ describe('run-many', () => {
expect(buildConfig).toContain(`run ${appA}:build:production`);
expect(buildConfig).toContain(`run ${libA}:build`);
expect(buildConfig).toContain(`run ${libC}:build`);
expect(buildConfig).toContain('Running target "build" succeeded');
expect(buildConfig).toContain('Successfully ran target build');
// testing run many with daemon enabled
const buildWithDaemon = runCLI(`run-many --target=build --all`, {
env: { ...process.env, NX_DAEMON: 'true' },
});
expect(buildWithDaemon).toContain(`Running target "build" succeeded`);
expect(buildWithDaemon).toContain(`Successfully ran target build`);
}, 1000000);
});
@ -422,7 +422,7 @@ describe('affected:*', () => {
expect(build).toContain(`- ${myapp}`);
expect(build).toContain(`- ${mypublishablelib}`);
expect(build).not.toContain('is not registered with the build command');
expect(build).toContain('Running target "build" succeeded');
expect(build).toContain('Successfully ran target build');
const buildExcluded = runCLI(
`affected:build --files="libs/${mylib}/src/index.ts" --exclude ${myapp}`
@ -722,14 +722,14 @@ describe('cache', () => {
const filesApp2 = listFiles(`dist/apps/${myapp2}`);
// now the data is in cache
expect(outputThatPutsDataIntoCache).not.toContain(
'read the output from cache'
'read the output from the cache'
);
rmDist();
const outputWithBothBuildTasksCached = runCLI(`affected:build ${files}`);
expect(outputWithBothBuildTasksCached).toContain(
'read the output from cache'
'read the output from the cache'
);
expectCached(outputWithBothBuildTasksCached, [myapp1, myapp2]);
expect(listFiles(`dist/apps/${myapp1}`)).toEqual(filesApp1);
@ -740,7 +740,7 @@ describe('cache', () => {
`affected:build ${files} --skip-nx-cache`
);
expect(outputWithBothBuildTasksCachedButSkipped).not.toContain(
`read the output from cache`
`read the output from the cache`
);
// touch myapp1
@ -749,7 +749,9 @@ describe('cache', () => {
return `${c}\n//some comment`;
});
const outputWithBuildApp2Cached = runCLI(`affected:build ${files}`);
expect(outputWithBuildApp2Cached).toContain('read the output from cache');
expect(outputWithBuildApp2Cached).toContain(
'read the output from the cache'
);
expectMatchedOutput(outputWithBuildApp2Cached, [myapp2]);
// touch package.json
@ -760,7 +762,9 @@ describe('cache', () => {
return JSON.stringify(r);
});
const outputWithNoBuildCached = runCLI(`affected:build ${files}`);
expect(outputWithNoBuildCached).not.toContain('read the output from cache');
expect(outputWithNoBuildCached).not.toContain(
'read the output from the cache'
);
// build individual project with caching
const individualBuildWithCache = runCLI(`build ${myapp1}`);
@ -779,11 +783,13 @@ describe('cache', () => {
// run lint with caching
// --------------------------------------------
const outputWithNoLintCached = runCLI(`affected:lint ${files}`);
expect(outputWithNoLintCached).not.toContain('read the output from cache');
expect(outputWithNoLintCached).not.toContain(
'read the output from the cache'
);
const outputWithBothLintTasksCached = runCLI(`affected:lint ${files}`);
expect(outputWithBothLintTasksCached).toContain(
'read the output from cache'
'read the output from the cache'
);
expectCached(outputWithBothLintTasksCached, [
myapp1,
@ -836,12 +842,12 @@ describe('cache', () => {
const outputWithoutCachingEnabled1 = runCLI(`affected:build ${files}`);
expect(outputWithoutCachingEnabled1).not.toContain(
'read the output from cache'
'read the output from the cache'
);
const outputWithoutCachingEnabled2 = runCLI(`affected:build ${files}`);
expect(outputWithoutCachingEnabled2).not.toContain(
'read the output from cache'
'read the output from the cache'
);
}, 120000);
@ -910,8 +916,12 @@ describe('cache', () => {
const matchingProjects = [];
const lines = actualOutput.split('\n');
lines.forEach((s) => {
if (s.startsWith(`> nx run`)) {
const projectName = s.split(`> nx run `)[1].split(':')[0].trim();
if (s.trimStart().startsWith(`> nx run`)) {
const projectName = s
.trimStart()
.split(`> nx run `)[1]
.split(':')[0]
.trim();
if (s.indexOf(cacheStatus) > -1) {
matchingProjects.push(projectName);
}

View File

@ -60,7 +60,7 @@ npx nx run-many --target=build --projects=todos,api
And notice the output:
```bash
Nx read the output from cache instead of running the command for 1 out of 2 projects.
Nx read the output from the cache instead of running the command for 1 out of 2 projects.
```
Nx built `api` and retrieved `todos` from its computation cache. Read more about the cache here [here](/using-nx/caching).

View File

@ -49,7 +49,7 @@ Based on the state of the source code and the environment, Nx figured out that i
**Now, run `npx nx run-many --target=build --projects=todos,api` to rebuild the two applications:**
```bash
Nx read the output from cache instead of running the command for 1 out of 2 projects.
Nx read the output from the cache instead of running the command for 1 out of 2 projects.
```
Nx built `api` and retrieved `todos` from its computation cache. Read more about the cache [here](/using-nx/caching).

View File

@ -1,8 +1,30 @@
import * as chalk from 'chalk';
/*
* Because we don't want to depend on @nrwl/workspace (to speed up the workspace creation)
* we duplicate the helper functions from @nrwl/workspace in this file.
*/
import * as chalk from 'chalk';
import { EOL } from 'os';
export function isCI() {
return (
process.env.CI === 'true' ||
process.env.TF_BUILD === 'true' ||
process.env['bamboo.buildKey'] ||
process.env.BUILDKITE === 'true' ||
process.env.CIRCLECI === 'true' ||
process.env.CIRRUS_CI === 'true' ||
process.env.CODEBUILD_BUILD_ID ||
process.env.GITHUB_ACTIONS === 'true' ||
process.env.GITLAB_CI ||
process.env.HEROKU_TEST_RUN_ID ||
process.env.BUILD_ID ||
process.env.BUILD_BUILDID ||
process.env.TEAMCITY_VERSION ||
process.env.TRAVIS === 'true'
);
}
export interface CLIErrorMessageConfig {
title: string;
bodyLines?: string[];
@ -25,23 +47,37 @@ export interface CLISuccessMessageConfig {
bodyLines?: string[];
}
export enum TaskCacheStatus {
NoCache = '[no cache]',
MatchedExistingOutput = '[existing outputs match the cache, left as is]',
RetrievedFromCache = '[retrieved from cache]',
}
/**
* Automatically disable styling applied by chalk if CI=true
*/
if (process.env.CI === 'true') {
if (isCI()) {
(chalk as any).level = 0;
}
class CLIOutput {
private readonly NX_PREFIX = `${chalk.cyan(
'>'
)} ${chalk.reset.inverse.bold.cyan(' NX ')}`;
readonly X_PADDING = ' ';
/**
* Longer dash character which forms more of a continuous line when place side to side
* with itself, unlike the standard dash character
*/
private readonly VERTICAL_SEPARATOR =
'———————————————————————————————————————————————';
private get VERTICAL_SEPARATOR() {
let divider = '';
for (
let i = 0;
i < process.stdout.columns - this.X_PADDING.length * 2;
i++
) {
divider += '\u2014';
}
return divider;
}
/**
* Expose some color and other utility functions so that other parts of the codebase that need
@ -50,28 +86,27 @@ class CLIOutput {
*/
colors = {
gray: chalk.gray,
green: chalk.green,
red: chalk.red,
cyan: chalk.cyan,
white: chalk.white,
};
bold = chalk.bold;
underline = chalk.underline;
dim = chalk.dim;
private writeToStdOut(str: string) {
process.stdout.write(str);
}
private writeOutputTitle({
label,
color,
title,
}: {
label?: string;
color: string;
title: string;
}): void {
let outputTitle: string;
if (label) {
outputTitle = `${this.NX_PREFIX} ${label} ${title}\n`;
} else {
outputTitle = `${this.NX_PREFIX} ${title}\n`;
}
this.writeToStdOut(outputTitle);
this.writeToStdOut(` ${this.applyNxPrefix(color, title)}${EOL}`);
}
private writeOptionalOutputBody(bodyLines?: string[]): void {
@ -79,27 +114,45 @@ class CLIOutput {
return;
}
this.addNewline();
bodyLines.forEach((bodyLine) => this.writeToStdOut(' ' + bodyLine + '\n'));
bodyLines.forEach((bodyLine) => this.writeToStdOut(` ${bodyLine}${EOL}`));
}
applyNxPrefix(color = 'cyan', text: string): string {
let nxPrefix = '';
if (chalk[color]) {
nxPrefix = `${chalk[color]('>')} ${chalk.reset.inverse.bold[color](
' NX '
)}`;
} else {
nxPrefix = `${chalk.keyword(color)(
'>'
)} ${chalk.reset.inverse.bold.keyword(color)(' NX ')}`;
}
return `${nxPrefix} ${text}`;
}
addNewline() {
this.writeToStdOut('\n');
this.writeToStdOut(EOL);
}
addVerticalSeparator() {
this.writeToStdOut(`\n${chalk.gray(this.VERTICAL_SEPARATOR)}\n\n`);
addVerticalSeparator(color = 'gray') {
this.addNewline();
this.addVerticalSeparatorWithoutNewLines(color);
this.addNewline();
}
addVerticalSeparatorWithoutNewLines() {
this.writeToStdOut(`${chalk.gray(this.VERTICAL_SEPARATOR)}\n`);
addVerticalSeparatorWithoutNewLines(color = 'gray') {
this.writeToStdOut(
`${this.X_PADDING}${chalk.dim[color](this.VERTICAL_SEPARATOR)}${EOL}`
);
}
error({ title, slug, bodyLines }: CLIErrorMessageConfig) {
this.addNewline();
this.writeOutputTitle({
label: chalk.reset.inverse.bold.red(' ERROR '),
title: chalk.bold.red(title),
color: 'red',
title: chalk.red(title),
});
this.writeOptionalOutputBody(bodyLines);
@ -112,7 +165,7 @@ class CLIOutput {
this.writeToStdOut(
`${chalk.grey(
' Learn more about this error: '
)}https://errors.nx.dev/${slug}\n`
)}https://errors.nx.dev/${slug}${EOL}`
);
}
@ -123,8 +176,8 @@ class CLIOutput {
this.addNewline();
this.writeOutputTitle({
label: chalk.reset.inverse.bold.yellow(' WARNING '),
title: chalk.bold.yellow(title),
color: 'yellow',
title: chalk.yellow(title),
});
this.writeOptionalOutputBody(bodyLines);
@ -148,8 +201,8 @@ class CLIOutput {
this.addNewline();
this.writeOutputTitle({
label: chalk.reset.inverse.bold.keyword('orange')(' NOTE '),
title: chalk.bold.keyword('orange')(title),
color: 'orange',
title: chalk.keyword('orange')(title),
});
this.writeOptionalOutputBody(bodyLines);
@ -161,8 +214,8 @@ class CLIOutput {
this.addNewline();
this.writeOutputTitle({
label: chalk.reset.inverse.bold.green(' SUCCESS '),
title: chalk.bold.green(title),
color: 'green',
title: chalk.green(title),
});
this.writeOptionalOutputBody(bodyLines);
@ -174,29 +227,36 @@ class CLIOutput {
this.addNewline();
this.writeOutputTitle({
color: 'gray',
title: message,
});
this.addNewline();
}
logCommand(message: string, isCached: boolean = false) {
logCommand(
message: string,
cacheStatus: TaskCacheStatus = TaskCacheStatus.NoCache
) {
this.addNewline();
this.writeToStdOut(chalk.bold(`> ${message} `));
if (isCached) {
this.writeToStdOut(chalk.bold.grey(`[retrieved from cache]`));
let commandOutput = ` ${chalk.dim('> nx run')} ${message}`;
if (cacheStatus !== TaskCacheStatus.NoCache) {
commandOutput += ` ${chalk.grey(cacheStatus)}`;
}
this.writeToStdOut(commandOutput);
this.addNewline();
}
log({ title, bodyLines }: CLIWarnMessageConfig) {
log({ title, bodyLines, color }: CLIWarnMessageConfig & { color?: string }) {
this.addNewline();
color = color || 'white';
this.writeOutputTitle({
title: chalk.white(title),
color: 'cyan',
title: chalk[color](title),
});
this.writeOptionalOutputBody(bodyLines);

View File

@ -1,6 +1,6 @@
import { logger } from '@nrwl/devkit';
import type { Arguments } from 'yargs';
import { DAEMON_OUTPUT_LOG_FILE } from '../core/project-graph/daemon/tmp-dir';
import { output } from '../utilities/output';
export async function daemonHandler(args: Arguments) {
const { startInBackground, startInCurrentProcess } = await import(
@ -9,9 +9,13 @@ export async function daemonHandler(args: Arguments) {
if (!args.background) {
return startInCurrentProcess();
}
logger.info(`NX Daemon Server - Starting in a background process...`);
const pid = await startInBackground();
logger.log(
` Logs from the Daemon process (ID: ${pid}) can be found here: ${DAEMON_OUTPUT_LOG_FILE}\n`
);
output.log({
title: `Daemon Server - Started in a background process...`,
bodyLines: [
`${output.dim('Logs from the Daemon process (')}ID: ${pid}${output.dim(
') can be found here:'
)} ${DAEMON_OUTPUT_LOG_FILE}\n`,
],
});
}

View File

@ -1,9 +1,10 @@
import { logger, ProjectGraph } from '@nrwl/devkit';
import { ProjectGraph } from '@nrwl/devkit';
import { ChildProcess, spawn, spawnSync } from 'child_process';
import { openSync, readFileSync } from 'fs';
import { ensureDirSync, ensureFileSync } from 'fs-extra';
import { connect } from 'net';
import { performance } from 'perf_hooks';
import { output } from '../../../../utilities/output';
import {
safelyCleanUpExistingProcess,
writeDaemonJsonProcessCache,
@ -82,7 +83,9 @@ function daemonProcessException(message: string) {
}
export function startInCurrentProcess(): void {
logger.info(`NX Daemon Server - Starting in the current process...`);
output.log({
title: `Daemon Server - Starting in the current process...`,
});
spawnSync(process.execPath, ['../server/start.js'], {
cwd: __dirname,
@ -96,7 +99,7 @@ export function stop(): void {
stdio: 'inherit',
});
logger.info('NX Daemon Server - Stopped');
output.log({ title: 'Daemon Server - Stopped' });
}
/**

View File

@ -1,4 +1,3 @@
import { logger, normalizePath, stripIndents } from '@nrwl/devkit';
import { appRootPath } from '@nrwl/tao/src/utils/app-root';
import { createServer, Server, Socket } from 'net';
import { join } from 'path';

View File

@ -1,4 +1,4 @@
import { logger } from '@nrwl/devkit';
import { output } from '../../../../utilities/output';
import { startServer } from './server';
import * as process from 'process';
@ -6,7 +6,11 @@ import * as process from 'process';
try {
await startServer();
} catch (err) {
logger.error(err);
output.error({
title:
err?.message ||
'Something unexpected went wrong when starting the server',
});
process.exit(1);
}
})();

View File

@ -1,4 +1,4 @@
import { logger } from '@nrwl/devkit';
import { output } from '../../../../utilities/output';
import { safelyCleanUpExistingProcess } from '../cache';
import { stopServer } from './server';
@ -7,6 +7,10 @@ import { stopServer } from './server';
await stopServer();
await safelyCleanUpExistingProcess();
} catch (err) {
logger.error(err);
output.error({
title:
err?.message ||
'Something unexpected went wrong when stopping the server',
});
}
})();

View File

@ -4,7 +4,7 @@ import { TaskOrchestrator } from './task-orchestrator';
import { performance } from 'perf_hooks';
import { TaskGraphCreator } from './task-graph-creator';
import { Hasher } from '../core/hasher/hasher';
import { LifeCycle } from './life-cycle';
import { LifeCycle } from './life-cycles/life-cycle';
export interface RemoteCache {
retrieve: (hash: string, cacheDirectory: string) => Promise<boolean>;

View File

@ -39,9 +39,8 @@ export class ForkedProcessTaskRunner {
);
} else {
const args = getCommandArgsForTask(Object.values(taskGraph.tasks)[0]);
const commandLine = `nx ${args.join(' ')}`;
output.logCommand(commandLine);
output.logCommand(`${args.filter((a) => a !== 'run').join(' ')}`);
output.addNewline();
}
const p = fork(workerPath, {
@ -97,10 +96,9 @@ export class ForkedProcessTaskRunner {
return new Promise<{ code: number; terminalOutput: string }>((res, rej) => {
try {
const args = getCommandArgsForTask(task);
const commandLine = `nx ${args.join(' ')}`;
if (forwardOutput) {
output.logCommand(commandLine);
output.logCommand(`${args.filter((a) => a !== 'run').join(' ')}`);
output.addNewline();
}
const p = fork(this.cliPath, args, {
stdio: ['inherit', 'pipe', 'pipe', 'ipc'],
@ -163,10 +161,9 @@ export class ForkedProcessTaskRunner {
return new Promise<{ code: number; terminalOutput: string }>((res, rej) => {
try {
const args = getCommandArgsForTask(task);
const commandLine = `nx ${args.join(' ')}`;
if (forwardOutput) {
output.logCommand(commandLine);
output.logCommand(`${args.filter((a) => a !== 'run').join(' ')}`);
output.addNewline();
}
const p = fork(this.cliPath, args, {
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],

View File

@ -0,0 +1,471 @@
import { dots } from 'cli-spinners';
import { EOL } from 'os';
import * as readline from 'readline';
import { output } from '../../utilities/output';
import type { Task, TaskStatus } from '../tasks-runner';
import type { LifeCycle } from './life-cycle';
import { prettyTime } from './pretty-time';
/**
* The following function is responsible for creating a life cycle with dynamic
* outputs, meaning previous outputs can be rewritten or modified as new outputs
* are added. It is therefore intended for use on a user's local machines.
*
* In CI environments the static equivalent of this life cycle should be used.
*/
export async function createDynamicOutputRenderer({
projectNames,
tasks,
args,
overrides,
}: {
projectNames: string[];
tasks: Task[];
args: { target?: string; configuration?: string; parallel?: number };
overrides: Record<string, unknown>;
}): Promise<{ lifeCycle: LifeCycle; renderIsDone: Promise<void> }> {
let resolveRenderIsDonePromise: (value: void) => void;
const renderIsDone = new Promise<void>(
(resolve) => (resolveRenderIsDonePromise = resolve)
).then(() => clearRenderInterval());
function clearRenderInterval() {
if (renderProjectRowsIntervalId) {
clearInterval(renderProjectRowsIntervalId);
}
}
function teardown() {
clearRenderInterval();
if (resolveRenderIsDonePromise) {
resolveRenderIsDonePromise();
}
}
process.on('exit', () => teardown());
process.on('SIGINT', () => teardown());
process.on('unhandledRejection', () => teardown());
process.on('uncaughtException', () => teardown());
const lifeCycle = {} as Partial<LifeCycle>;
const isVerbose = overrides.verbose === true;
const start = process.hrtime();
const figures = await import('figures');
const totalTasks = tasks.length;
const totalProjects = projectNames.length;
const totalDependentTasks = totalTasks - totalProjects;
const targetName = args.target;
const projectRows = projectNames.map((projectName) => {
return {
projectName,
status: 'pending',
};
});
const tasksToTerminalOutputs: Record<string, string> = {};
const tasksToProcessStartTimes: Record<
string,
ReturnType<NodeJS.HRTime>
> = {};
let hasTaskOutput = false;
let pinnedFooterNumLines = 0;
let totalCompletedTasks = 0;
let totalSuccessfulTasks = 0;
let totalFailedTasks = 0;
let totalCachedTasks = 0;
// Used to control the rendering of the spinner on each project row
let projectRowsCurrentFrame = 0;
let renderProjectRowsIntervalId: NodeJS.Timeout | undefined;
const clearPinnedFooter = () => {
for (let i = 0; i < pinnedFooterNumLines; i++) {
readline.moveCursor(process.stdout, 0, -1);
readline.clearLine(process.stdout, 0);
}
};
const renderPinnedFooter = (lines: string[], dividerColor = 'cyan') => {
let additionalLines = 0;
if (hasTaskOutput) {
output.addVerticalSeparator(dividerColor);
additionalLines += 3;
}
// Create vertical breathing room for cursor position under the pinned footer
lines.push('');
for (const line of lines) {
process.stdout.write(output.X_PADDING + line + EOL);
}
pinnedFooterNumLines = lines.length + additionalLines;
};
const printTaskResult = (task: Task, status: TaskStatus) => {
clearPinnedFooter();
// If this is the very first output, add some vertical breathing room
if (!hasTaskOutput) {
output.addNewline();
}
hasTaskOutput = true;
switch (status) {
case 'local-cache':
writeLine(
`${
output.colors.green(figures.tick) +
output.dim(' nx run ') +
task.id
} ${output.colors.gray('[local cache]')}`
);
if (isVerbose) {
writeCommandOutputBlock(tasksToTerminalOutputs[task.id]);
}
break;
case 'remote-cache':
writeLine(
`${
output.colors.green(figures.tick) +
output.dim(' nx run ') +
task.id
} ${output.colors.gray('[remote cache]')}`
);
if (isVerbose) {
writeCommandOutputBlock(tasksToTerminalOutputs[task.id]);
}
break;
case 'success': {
const timeTakenText = prettyTime(
process.hrtime(tasksToProcessStartTimes[task.id])
);
writeLine(
output.colors.green(figures.tick) +
output.dim(' nx run ') +
task.id +
output.dim.gray(` (${timeTakenText})`)
);
if (isVerbose) {
writeCommandOutputBlock(tasksToTerminalOutputs[task.id]);
}
break;
}
case 'failure':
output.addNewline();
writeLine(
output.colors.red(figures.cross) +
output.dim(' nx run ') +
output.colors.red(task.id)
);
writeCommandOutputBlock(tasksToTerminalOutputs[task.id]);
break;
}
delete tasksToTerminalOutputs[task.id];
renderPinnedFooter([]);
renderProjectRows();
};
const renderProjectRows = () => {
const max = dots.frames.length - 1;
const curr = projectRowsCurrentFrame;
projectRowsCurrentFrame = curr >= max ? 0 : curr + 1;
const additionalFooterRows: string[] = [''];
const runningTasks = projectRows.filter((row) => row.status === 'running');
const remainingTasks = totalTasks - totalCompletedTasks;
if (runningTasks.length > 0) {
additionalFooterRows.push(
output.dim(
` ${output.colors.cyan(figures.arrowRight)} Executing ${
runningTasks.length
}/${remainingTasks} remaining tasks${
runningTasks.length > 1 ? ' in parallel' : ''
}...`
)
);
additionalFooterRows.push('');
for (const projectRow of runningTasks) {
additionalFooterRows.push(
` ${output.dim.cyan(dots.frames[projectRowsCurrentFrame])} ${
output.dim('nx run ') + projectRow.projectName + ':' + targetName
}`
);
}
/**
* Reduce layout thrashing by ensuring that there is a relatively consistent
* height for the area in which the task rows are rendered.
*
* We can look at the parallel flag to know how many rows are likely to be
* needed in the common case and always render that at least that many.
*/
if (
totalCompletedTasks !== totalTasks &&
Number.isInteger(args.parallel) &&
runningTasks.length < args.parallel
) {
// Don't bother with this optimization if there are fewer tasks remaining than rows required
if (remainingTasks >= args.parallel) {
for (let i = runningTasks.length; i < args.parallel; i++) {
additionalFooterRows.push('');
}
}
}
}
if (totalSuccessfulTasks > 0 || totalFailedTasks > 0) {
additionalFooterRows.push('');
}
if (totalFailedTasks > 0) {
additionalFooterRows.push(
` ${output.colors.red(
figures.cross
)} ${totalFailedTasks}${`/${totalCompletedTasks}`} failed`
);
}
if (totalSuccessfulTasks > 0) {
additionalFooterRows.push(
` ${output.colors.green(
figures.tick
)} ${totalSuccessfulTasks}${`/${totalCompletedTasks}`} succeeded ${output.colors.gray(
`[${totalCachedTasks} read from cache]`
)}`
);
}
clearPinnedFooter();
if (additionalFooterRows.length > 1) {
let text = `Running target ${output.bold.cyan(
targetName
)} for ${output.bold.cyan(totalProjects)} projects`;
if (totalDependentTasks > 0) {
text += ` and ${output.bold(
totalDependentTasks
)} task(s) they depend on`;
}
const taskOverridesRows = [];
if (Object.keys(overrides).length > 0) {
const leftPadding = `${output.X_PADDING} `;
taskOverridesRows.push('');
taskOverridesRows.push(
`${leftPadding}${output.dim.cyan('With additional flags:')}`
);
Object.entries(overrides)
.map(([flag, value]) =>
output.dim.cyan(`${leftPadding} --${flag}=${value}`)
)
.forEach((arg) => taskOverridesRows.push(arg));
}
const pinnedFooterLines = [
output.applyNxPrefix('cyan', output.colors.cyan(text)),
...taskOverridesRows,
...additionalFooterRows,
];
// Vertical breathing room when there isn't yet any output or divider
if (!hasTaskOutput) {
pinnedFooterLines.unshift('');
}
renderPinnedFooter(pinnedFooterLines);
} else {
renderPinnedFooter([]);
}
};
lifeCycle.startCommand = () => {
if (totalProjects <= 0) {
let description = `with target ${output.colors.white.bold(targetName)}`;
if (args.configuration) {
description += ` that are configured for "${args.configuration}"`;
}
renderPinnedFooter([
'',
output.applyNxPrefix('gray', `No projects ${description} were run`),
]);
resolveRenderIsDonePromise();
return;
}
renderPinnedFooter([]);
};
lifeCycle.startTasks = (tasks: Task[]) => {
for (const task of tasks) {
tasksToProcessStartTimes[task.id] = process.hrtime();
}
for (const projectRow of projectRows) {
const matchedTask = tasks.find(
(t) => t.target.project === projectRow.projectName
);
if (!matchedTask) {
continue;
}
projectRow.status = 'running';
}
if (!renderProjectRowsIntervalId) {
renderProjectRowsIntervalId = setInterval(renderProjectRows, 100);
}
};
lifeCycle.printTaskTerminalOutput = (task, _cacheStatus, output) => {
tasksToTerminalOutputs[task.id] = output;
};
lifeCycle.endTasks = (taskResults) => {
totalCompletedTasks++;
for (let t of taskResults) {
const matchingProjectRow = projectRows.find(
(pr) => pr.projectName === t.task.target.project
);
if (matchingProjectRow) {
matchingProjectRow.status = t.status;
}
switch (t.status) {
case 'remote-cache':
case 'local-cache':
totalCachedTasks++;
case 'success':
totalSuccessfulTasks++;
break;
case 'failure':
totalFailedTasks++;
break;
}
printTaskResult(t.task, t.status);
}
if (totalCompletedTasks === totalTasks) {
clearRenderInterval();
const timeTakenText = prettyTime(process.hrtime(start));
clearPinnedFooter();
if (totalSuccessfulTasks === totalTasks) {
let text = `Successfully ran target ${output.bold(
targetName
)} for ${output.bold(totalProjects)} projects`;
if (totalDependentTasks > 0) {
text += ` and ${output.bold(
totalDependentTasks
)} task(s) they depend on`;
}
const taskOverridesRows = [];
if (Object.keys(overrides).length > 0) {
const leftPadding = `${output.X_PADDING} `;
taskOverridesRows.push('');
taskOverridesRows.push(
`${leftPadding}${output.dim.green('With additional flags:')}`
);
Object.entries(overrides)
.map(([flag, value]) =>
output.dim.green(`${leftPadding} --${flag}=${value}`)
)
.forEach((arg) => taskOverridesRows.push(arg));
}
const pinnedFooterLines = [
output.applyNxPrefix(
'green',
output.colors.green(text) + output.dim.white(` (${timeTakenText})`)
),
...taskOverridesRows,
];
if (totalCachedTasks > 0) {
pinnedFooterLines.push(
output.colors.gray(
`${EOL} Nx read the output from the cache instead of running the command for ${totalCachedTasks} out of ${totalTasks} tasks.`
)
);
}
renderPinnedFooter(pinnedFooterLines, 'green');
} else {
let text = `Ran target ${output.bold(targetName)} for ${output.bold(
totalProjects
)} projects`;
if (totalDependentTasks > 0) {
text += ` and ${output.bold(
totalDependentTasks
)} task(s) they depend on`;
}
const taskOverridesRows = [];
if (Object.keys(overrides).length > 0) {
const leftPadding = `${output.X_PADDING} `;
taskOverridesRows.push('');
taskOverridesRows.push(
`${leftPadding}${output.dim.red('With additional flags:')}`
);
Object.entries(overrides)
.map(([flag, value]) =>
output.dim.red(`${leftPadding} --${flag}=${value}`)
)
.forEach((arg) => taskOverridesRows.push(arg));
}
renderPinnedFooter(
[
output.applyNxPrefix(
'red',
output.colors.red(text) + output.dim.white(` (${timeTakenText})`)
),
...taskOverridesRows,
'',
` ${output.colors.red(
figures.cross
)} ${totalFailedTasks}${`/${totalCompletedTasks}`} failed`,
` ${output.colors.gray(
figures.tick
)} ${totalSuccessfulTasks}${`/${totalCompletedTasks}`} succeeded ${output.colors.gray(
`[${totalCachedTasks} read from cache]`
)}`,
],
'red'
);
}
resolveRenderIsDonePromise();
}
};
return { lifeCycle, renderIsDone };
}
function writeLine(line: string) {
const additionalXPadding = ' ';
process.stdout.write(output.X_PADDING + additionalXPadding + line + EOL);
}
function writeCommandOutputBlock(commandOutput: string) {
commandOutput = commandOutput || '';
const additionalXPadding = ' ';
const lines = commandOutput.split(EOL);
/**
* There's not much we can do in order to "neaten up" the outputs of
* commands we do not control, but at the very least we can trim excess
* newlines so that there isn't unncecessary vertical whitespace.
*/
let totalTrailingEmptyLines = 0;
for (let i = lines.length - 1; i >= 0; i--) {
if (lines[i] !== '') {
break;
}
totalTrailingEmptyLines++;
}
if (totalTrailingEmptyLines > 1) {
const linesToRemove = totalTrailingEmptyLines - 1;
lines.splice(lines.length - linesToRemove, linesToRemove);
}
// Indent the command output to make it look more "designed" in the context of the dynamic output
process.stdout.write(
lines.map((l) => `${output.X_PADDING}${additionalXPadding}${l}`).join(EOL) +
EOL
);
}

View File

@ -1,7 +1,7 @@
import type { Task } from '@nrwl/devkit';
import { output, TaskCacheStatus } from '../utilities/output';
import { LifeCycle } from './life-cycle';
import { getCommandArgsForTask } from './utils';
import { output, TaskCacheStatus } from '../../utilities/output';
import { getCommandArgsForTask } from '../utils';
import type { LifeCycle } from './life-cycle';
export class EmptyTerminalOutputLifeCycle implements LifeCycle {
printTaskTerminalOutput(
@ -11,7 +11,10 @@ export class EmptyTerminalOutputLifeCycle implements LifeCycle {
) {
if (cacheStatus === TaskCacheStatus.NoCache) {
const args = getCommandArgsForTask(task);
output.logCommand(`nx ${args.join(' ')}`, cacheStatus);
output.logCommand(
`${args.filter((a) => a !== 'run').join(' ')}`,
cacheStatus
);
process.stdout.write(terminalOutput);
}
}

View File

@ -1,6 +1,6 @@
import type { Task } from '@nrwl/devkit';
import { TaskStatus } from './tasks-runner';
import { TaskCacheStatus } from '../utilities/output';
import { TaskStatus } from '../tasks-runner';
import { TaskCacheStatus } from '../../utilities/output';
export interface TaskResult {
task: Task;

View File

@ -1,10 +1,18 @@
import type { Task } from '@nrwl/devkit';
import { output, TaskCacheStatus } from '../utilities/output';
import { LifeCycle } from './life-cycle';
import { TaskStatus } from './tasks-runner';
import { getCommandArgsForTask } from './utils';
import { output, TaskCacheStatus } from '../../utilities/output';
import { TaskStatus } from '../tasks-runner';
import { getCommandArgsForTask } from '../utils';
import type { LifeCycle } from './life-cycle';
export class RunManyTerminalOutputLifeCycle implements LifeCycle {
/**
* The following life cycle's outputs are static, meaning no previous content
* is rewritten or modified as new outputs are added. It is therefore intended
* for use in CI environments.
*
* For the common case of a user executing a command on their local machine,
* the dynamic equivalent of this life cycle is usually preferable.
*/
export class StaticRunManyTerminalOutputLifeCycle implements LifeCycle {
failedTasks = [] as Task[];
cachedTasks = [] as Task[];
skippedTasks = [] as Task[];
@ -40,44 +48,50 @@ export class RunManyTerminalOutputLifeCycle implements LifeCycle {
.forEach((arg) => bodyLines.push(arg));
}
let title = `${output.colors.gray('Running target')} ${
let title = `Running target ${output.bold(
this.args.target
} ${output.colors.gray(`for`)} ${this.projectNames.length} project(s)`;
)} for ${output.bold(this.projectNames.length)} project(s)`;
const dependentTasksCount = this.tasks.length - this.projectNames.length;
if (dependentTasksCount > 0) {
title += ` ${output.colors.gray(`and`)} ${
this.tasks.length - this.projectNames.length
} task(s) ${output.colors.gray(`they depend on`)}`;
title += ` and ${output.bold(
dependentTasksCount
)} task(s) they depend on`;
}
title += ':';
output.log({
color: 'cyan',
title,
bodyLines,
});
output.addVerticalSeparatorWithoutNewLines();
output.addVerticalSeparatorWithoutNewLines('cyan');
}
endCommand(): void {
output.addNewline();
output.addVerticalSeparatorWithoutNewLines();
if (this.failedTasks.length === 0) {
output.addVerticalSeparatorWithoutNewLines('green');
const bodyLines =
this.cachedTasks.length > 0
? [
output.colors.gray(
`Nx read the output from cache instead of running the command for ${this.cachedTasks.length} out of ${this.tasks.length} tasks.`
`Nx read the output from the cache instead of running the command for ${this.cachedTasks.length} out of ${this.tasks.length} tasks.`
),
]
: [];
output.success({
title: `Running target "${this.args.target}" succeeded`,
title: `Successfully ran target ${output.bold(
this.args.target
)} for ${output.bold(this.projectNames.length)} projects`,
bodyLines,
});
} else {
output.addVerticalSeparatorWithoutNewLines('red');
const bodyLines = [];
if (this.skippedTasks.length > 0) {
bodyLines.push(
@ -127,7 +141,10 @@ export class RunManyTerminalOutputLifeCycle implements LifeCycle {
terminalOutput: string
) {
const args = getCommandArgsForTask(task);
output.logCommand(`nx ${args.join(' ')}`, cacheStatus);
output.logCommand(
`${args.filter((a) => a !== 'run').join(' ')}`,
cacheStatus
);
process.stdout.write(terminalOutput);
}
}

View File

@ -1,10 +1,18 @@
import type { Task } from '@nrwl/devkit';
import { output, TaskCacheStatus } from '../utilities/output';
import { LifeCycle } from './life-cycle';
import { TaskStatus } from './tasks-runner';
import { getCommandArgsForTask } from './utils';
import { output, TaskCacheStatus } from '../../utilities/output';
import { TaskStatus } from '../tasks-runner';
import { getCommandArgsForTask } from '../utils';
import type { LifeCycle } from './life-cycle';
export class RunOneTerminalOutputLifeCycle implements LifeCycle {
/**
* The following life cycle's outputs are static, meaning no previous content
* is rewritten or modified as new outputs are added. It is therefore intended
* for use in CI environments.
*
* For the common case of a user executing a command on their local machine,
* the dynamic equivalent of this life cycle is usually preferable.
*/
export class StaticRunOneTerminalOutputLifeCycle implements LifeCycle {
failedTasks = [] as Task[];
cachedTasks = [] as Task[];
skippedTasks = [] as Task[];
@ -27,16 +35,14 @@ export class RunOneTerminalOutputLifeCycle implements LifeCycle {
if (numberOfDeps > 0) {
output.log({
title: `${output.colors.gray('Running target')} ${
color: 'cyan',
title: `Running target ${output.bold(
this.args.target
} ${output.colors.gray('for project')} ${
this.initiatingProject
} ${output.colors.gray(
`and`
)} ${numberOfDeps} task(s) ${output.colors.gray(`that it depends on.`)}
`,
)} for project ${output.bold(this.initiatingProject)} and ${output.bold(
numberOfDeps
)} task(s) it depends on`,
});
output.addVerticalSeparatorWithoutNewLines();
output.addVerticalSeparatorWithoutNewLines('cyan');
}
}
@ -46,23 +52,28 @@ export class RunOneTerminalOutputLifeCycle implements LifeCycle {
return;
}
output.addNewline();
output.addVerticalSeparatorWithoutNewLines();
if (this.failedTasks.length === 0) {
output.addVerticalSeparatorWithoutNewLines('green');
const bodyLines =
this.cachedTasks.length > 0
? [
output.colors.gray(
`Nx read the output from cache instead of running the command for ${this.cachedTasks.length} out of ${this.tasks.length} tasks.`
`Nx read the output from the cache instead of running the command for ${this.cachedTasks.length} out of ${this.tasks.length} tasks.`
),
]
: [];
output.success({
title: `Running target "${this.args.target}" succeeded`,
title: `Successfully ran target ${output.bold(
this.args.target
)} for project ${output.bold(this.initiatingProject)}`,
bodyLines,
});
} else {
output.addVerticalSeparatorWithoutNewLines('red');
const bodyLines = [
output.colors.gray('Failed tasks:'),
'',
@ -107,7 +118,10 @@ export class RunOneTerminalOutputLifeCycle implements LifeCycle {
task.target.project === this.initiatingProject
) {
const args = getCommandArgsForTask(task);
output.logCommand(`nx ${args.join(' ')}`, cacheStatus);
output.logCommand(
`${args.filter((a) => a !== 'run').join(' ')}`,
cacheStatus
);
process.stdout.write(terminalOutput);
}
}

View File

@ -1,6 +1,6 @@
import { LifeCycle, TaskMetadata } from './life-cycle';
import { Task, writeJsonFile } from '@nrwl/devkit';
import { TaskStatus } from './tasks-runner';
import { TaskStatus } from '../tasks-runner';
import { performance } from 'perf_hooks';
import { join } from 'path';

View File

@ -1,6 +1,6 @@
import { LifeCycle } from './life-cycle';
import { Task } from '@nrwl/devkit';
import { TaskStatus } from './tasks-runner';
import { TaskStatus } from '../tasks-runner';
export class TaskTimingsLifeCycle implements LifeCycle {
private timings: {

View File

@ -1,355 +0,0 @@
import * as chalk from 'chalk';
import { dots } from 'cli-spinners';
import { EOL } from 'os';
import * as readline from 'readline';
import type { Task, TaskStatus } from '../tasks-runner';
import { prettyTime } from './pretty-time';
import { LifeCycle } from '@nrwl/workspace/src/tasks-runner/life-cycle';
const X_PADDING = ' ';
function applyNxPrefix(color = 'cyan', text: string) {
return `${chalk[color]('>')} ${chalk.reset.inverse.bold[color](
' NX '
)} ${text}`;
}
function writeLine(line: string) {
const additionalXPadding = ' ';
process.stdout.write(X_PADDING + additionalXPadding + line + EOL);
}
function writeCommandOutputBlock(output: string) {
const additionalXPadding = ' ';
const lines = output.split(EOL);
/**
* There's not much we can do in order to "neaten up" the outputs of
* commands we do not control, but at the very least we can trim excess
* newlines so that there isn't unncecessary vertical whitespace.
*/
let totalTrailingEmptyLines = 0;
for (let i = lines.length - 1; i >= 0; i--) {
if (lines[i] !== '') {
break;
}
totalTrailingEmptyLines++;
}
if (totalTrailingEmptyLines > 1) {
const linesToRemove = totalTrailingEmptyLines - 1;
lines.splice(lines.length - linesToRemove, linesToRemove);
}
process.stdout.write(
lines.map((l) => `${X_PADDING}${additionalXPadding}${l}`).join(EOL) + EOL
);
}
export async function createOutputRenderer({
projectNames,
tasks,
args,
}: {
projectNames: string[];
tasks: Task[];
args: { target?: string; configuration?: string };
}): Promise<{ lifeCycle: LifeCycle; renderIsDone: Promise<void> }> {
const lifeCycle = {} as any;
const start = process.hrtime();
const figures = await import('figures');
let resolveIsRenderCompletePromise: (value: void) => void;
const renderIsDone = new Promise<void>(
(resolve) => (resolveIsRenderCompletePromise = resolve)
);
const totalTasks = tasks.length;
const targetName = args.target;
const totalProjects = projectNames.length;
const projectRows = projectNames.map((projectName) => {
return {
projectName,
status: 'pending',
};
});
const tasksToTerminalOutputs: Record<string, string> = {};
let hasTaskOutput = false;
let pinnedFooterNumLines = 0;
let totalCompletedTasks = 0;
let totalSuccessfulTasks = 0;
let totalFailedTasks = 0;
let totalCachedTasks = 0;
// Used to control the rendering of the spinner on each project row
let projectRowsCurrentFrame = 0;
let renderProjectRowsIntervalId: NodeJS.Timeout | undefined;
const clearPinnedFooter = () => {
for (let i = 0; i < pinnedFooterNumLines; i++) {
readline.moveCursor(process.stdout, 0, -1);
readline.clearLine(process.stdout, 0);
}
};
const renderPinnedFooter = (lines: string[], dividerColor = 'gray') => {
let additionalLines = 0;
if (hasTaskOutput) {
let divider = '';
for (let i = 0; i < process.stdout.columns - X_PADDING.length * 2; i++) {
divider += '\u2014';
}
process.stdout.write(EOL);
process.stdout.write(
X_PADDING + chalk.dim[dividerColor](divider + EOL) + EOL
);
additionalLines += 3;
}
// Create vertical breathing room for cursor position under the pinned footer
lines.push('');
for (const line of lines) {
process.stdout.write(X_PADDING + line + EOL);
}
pinnedFooterNumLines = lines.length + additionalLines;
};
const printTaskResult = (task: Task, status: TaskStatus) => {
clearPinnedFooter();
// If this is the very first output, add some vertical breathing room
if (!hasTaskOutput) {
process.stdout.write(EOL);
}
hasTaskOutput = true;
switch (status) {
case 'local-cache':
writeLine(
chalk.green(figures.tick) +
chalk.dim.white(' nx run ') +
chalk.white(task.id) +
' ' +
chalk.gray('[from cache]')
);
break;
case 'remote-cache':
writeLine(
chalk.green(figures.tick) +
chalk.dim(' nx run ') +
task.id +
' ☁️ ' +
chalk.gray('[from cloud cache]')
);
break;
case 'success':
writeLine(chalk.green(figures.tick) + chalk.dim(' nx run ') + task.id);
break;
case 'failure':
process.stdout.write(EOL);
writeLine(
chalk.red(figures.cross) + chalk.dim(' nx run ') + chalk.red(task.id)
);
writeCommandOutputBlock(tasksToTerminalOutputs[task.id]);
break;
}
delete tasksToTerminalOutputs[task.id];
renderPinnedFooter([]);
renderProjectRows();
};
const renderProjectRows = () => {
const max = dots.frames.length - 1;
const curr = projectRowsCurrentFrame;
projectRowsCurrentFrame = curr >= max ? 0 : curr + 1;
const additionalFooterRows: string[] = [''];
const runningTasks = projectRows.filter((row) => row.status === 'running');
const pendingTasks = projectRows.filter((row) => row.status === 'pending');
const remainingTasks = pendingTasks.length + runningTasks.length;
if (runningTasks.length > 0) {
additionalFooterRows.push(
chalk.dim.cyan(
` ${figures.arrowRight} Executing ${
runningTasks.length
}/${remainingTasks} remaining tasks${
runningTasks.length > 1 ? ' in parallel' : ''
}...`
)
);
additionalFooterRows.push('');
for (const projectRow of runningTasks) {
additionalFooterRows.push(
` ${chalk.dim.cyan(
dots.frames[projectRowsCurrentFrame]
)} ${chalk.dim.white(projectRow.projectName)}`
);
}
}
if (totalSuccessfulTasks > 0 || totalFailedTasks > 0) {
additionalFooterRows.push('');
}
if (totalFailedTasks > 0) {
additionalFooterRows.push(
` ${chalk.red(figures.cross)} ${totalFailedTasks}${chalk.dim(
`/${totalCompletedTasks}`
)} failed`
);
}
if (totalSuccessfulTasks > 0) {
additionalFooterRows.push(
` ${chalk.green(figures.tick)} ${totalSuccessfulTasks}${chalk.dim(
`/${totalCompletedTasks}`
)} succeeded ${chalk.gray(`[${totalCachedTasks} read from cache]`)}`
);
}
clearPinnedFooter();
if (additionalFooterRows.length > 1) {
const pinnedFooterLines = [
applyNxPrefix(
'cyan',
chalk.gray(
`Running target ${chalk.bold.white(
targetName
)} for ${chalk.bold.white(totalProjects)} project(s)`
)
),
...additionalFooterRows,
];
// Vertical breathing room when there isn't yet any output or divider
if (!hasTaskOutput) {
pinnedFooterLines.unshift('');
}
renderPinnedFooter(pinnedFooterLines);
} else {
renderPinnedFooter([]);
}
};
lifeCycle.startCommand = (params) => {
if (totalProjects <= 0) {
let description = `with target "${targetName}"`;
if (params.args.configuration) {
description += ` that are configured for "${params.args.configuration}"`;
}
renderPinnedFooter([
'',
applyNxPrefix('gray', `No projects ${description} were run`),
]);
resolveIsRenderCompletePromise();
return;
}
renderPinnedFooter([]);
};
lifeCycle.startTasks = (tasks) => {
for (const projectRow of projectRows) {
const matchedTask = tasks.find(
(t) => t.target.project === projectRow.projectName
);
if (!matchedTask) {
continue;
}
projectRow.status = 'running';
}
if (!renderProjectRowsIntervalId) {
renderProjectRowsIntervalId = setInterval(renderProjectRows, 100);
}
};
lifeCycle.printTaskTerminalOutput = (task, _cacheStatus, output) => {
tasksToTerminalOutputs[task.id] = output;
};
lifeCycle.endTasks = (taskResults) => {
totalCompletedTasks++;
for (let t of taskResults) {
const matchingProjectRow = projectRows.find(
(pr) => pr.projectName === t.task.target.project
);
if (matchingProjectRow) {
matchingProjectRow.status = t.status;
}
switch (t.status) {
case 'remote-cache':
case 'local-cache':
totalCachedTasks++;
case 'success':
totalSuccessfulTasks++;
break;
case 'failure':
totalFailedTasks++;
break;
}
printTaskResult(t.task, t.status);
}
if (totalCompletedTasks === totalTasks) {
if (renderProjectRowsIntervalId) {
clearInterval(renderProjectRowsIntervalId);
}
const timeTakenText = prettyTime(process.hrtime(start));
clearPinnedFooter();
if (totalSuccessfulTasks === totalTasks) {
const pinnedFooterLines = [
applyNxPrefix(
'green',
chalk.green(
`Successfully ran target ${chalk.bold(
targetName
)} for ${chalk.bold(totalProjects)} projects`
) + chalk.dim.white(` (${timeTakenText})`)
),
];
if (totalCachedTasks > 0) {
pinnedFooterLines.push(
chalk.gray(
`\n Nx read the output from the cache instead of running the command for ${totalCachedTasks} out of ${totalTasks} tasks.`
)
);
}
renderPinnedFooter(pinnedFooterLines, 'green');
} else {
renderPinnedFooter(
[
applyNxPrefix(
'red',
chalk.red(
`Ran target ${chalk.bold(targetName)} for ${chalk.bold(
totalProjects
)} projects`
) + chalk.dim.white(` (${timeTakenText})`)
),
'',
` ${chalk.red(figures.cross)} ${totalFailedTasks}${chalk.dim(
`/${totalCompletedTasks}`
)} failed`,
` ${chalk.gray(
figures.tick
)} ${totalSuccessfulTasks}${chalk.dim(
`/${totalCompletedTasks}`
)} succeeded ${chalk.gray(
`[${totalCachedTasks} read from cache]`
)}`,
],
'red'
);
}
resolveIsRenderCompletePromise();
}
};
return { lifeCycle, renderIsDone };
}

View File

@ -19,13 +19,13 @@ import {
} from '../utilities/project-graph-utils';
import { output } from '../utilities/output';
import { getDependencyConfigs, shouldForwardOutput } from './utils';
import { CompositeLifeCycle, LifeCycle } from './life-cycle';
import { RunManyTerminalOutputLifeCycle } from './run-many-terminal-output-life-cycle';
import { EmptyTerminalOutputLifeCycle } from './empty-terminal-output-life-cycle';
import { RunOneTerminalOutputLifeCycle } from './run-one-terminal-output-life-cycle';
import { TaskTimingsLifeCycle } from './task-timings-life-cycle';
import { createOutputRenderer } from './neo-output/render';
import { TaskProfilingLifeCycle } from '@nrwl/workspace/src/tasks-runner/task-profiling-life-cycle';
import { CompositeLifeCycle, LifeCycle } from './life-cycles/life-cycle';
import { StaticRunManyTerminalOutputLifeCycle } from './life-cycles/static-run-many-terminal-output-life-cycle';
import { StaticRunOneTerminalOutputLifeCycle } from './life-cycles/static-run-one-terminal-output-life-cycle';
import { EmptyTerminalOutputLifeCycle } from './life-cycles/empty-terminal-output-life-cycle';
import { TaskTimingsLifeCycle } from './life-cycles/task-timings-life-cycle';
import { createDynamicOutputRenderer } from './life-cycles/dynamic-run-many-terminal-output-life-cycle';
import { TaskProfilingLifeCycle } from './life-cycles/task-profiling-life-cycle';
async function getTerminalOutputLifeCycle(
initiatingProject: string,
@ -33,12 +33,12 @@ async function getTerminalOutputLifeCycle(
projectNames: string[],
tasks: Task[],
nxArgs: NxArgs,
overrides: any,
overrides: Record<string, unknown>,
runnerOptions: any
): Promise<{ lifeCycle: LifeCycle; renderIsDone: Promise<void> }> {
if (terminalOutputStrategy === 'run-one') {
return {
lifeCycle: new RunOneTerminalOutputLifeCycle(
lifeCycle: new StaticRunOneTerminalOutputLifeCycle(
initiatingProject,
projectNames,
tasks,
@ -52,17 +52,18 @@ async function getTerminalOutputLifeCycle(
renderIsDone: Promise.resolve(),
};
} else if (
shouldUseNeoLifeCycle(tasks, runnerOptions) &&
process.env.NX_TASKS_RUNNER_NEO_OUTPUT === 'true'
shouldUseDynamicLifeCycle(tasks, runnerOptions) &&
process.env.NX_TASKS_RUNNER_DYNAMIC_OUTPUT === 'true'
) {
return await createOutputRenderer({
return await createDynamicOutputRenderer({
projectNames,
tasks,
args: nxArgs,
overrides,
});
} else {
return {
lifeCycle: new RunManyTerminalOutputLifeCycle(
lifeCycle: new StaticRunManyTerminalOutputLifeCycle(
projectNames,
tasks,
nxArgs,
@ -226,7 +227,7 @@ export function createTasksForProjectToRun(
return Array.from(tasksMap.values());
}
function shouldUseNeoLifeCycle(tasks: Task[], options: any) {
function shouldUseDynamicLifeCycle(tasks: Task[], options: any) {
const isTTY = !!process.stdout.isTTY && process.env['CI'] !== 'true';
const noForwarding = !tasks.find((t) =>
shouldForwardOutput(t, null, options)

View File

@ -17,7 +17,7 @@ import {
shouldForwardOutput,
} from './utils';
import { Batch, TasksSchedule } from './tasks-schedule';
import { TaskMetadata } from './life-cycle';
import { TaskMetadata } from './life-cycles/life-cycle';
export class TaskOrchestrator {
private cache = new Cache(this.options);

View File

@ -1,4 +1,5 @@
import * as chalk from 'chalk';
import { EOL } from 'os';
import { isCI } from './is_ci';
export interface CLIErrorMessageConfig {
@ -37,15 +38,23 @@ if (isCI()) {
}
class CLIOutput {
private readonly NX_PREFIX = `${chalk.cyan(
'>'
)} ${chalk.reset.inverse.bold.cyan(' NX ')}`;
readonly X_PADDING = ' ';
/**
* Longer dash character which forms more of a continuous line when place side to side
* with itself, unlike the standard dash character
*/
private readonly VERTICAL_SEPARATOR =
'———————————————————————————————————————————————';
private get VERTICAL_SEPARATOR() {
let divider = '';
for (
let i = 0;
i < process.stdout.columns - this.X_PADDING.length * 2;
i++
) {
divider += '\u2014';
}
return divider;
}
/**
* Expose some color and other utility functions so that other parts of the codebase that need
@ -54,28 +63,27 @@ class CLIOutput {
*/
colors = {
gray: chalk.gray,
green: chalk.green,
red: chalk.red,
cyan: chalk.cyan,
white: chalk.white,
};
bold = chalk.bold;
underline = chalk.underline;
dim = chalk.dim;
private writeToStdOut(str: string) {
process.stdout.write(str);
}
private writeOutputTitle({
label,
color,
title,
}: {
label?: string;
color: string;
title: string;
}): void {
let outputTitle: string;
if (label) {
outputTitle = `${this.NX_PREFIX} ${label} ${title}\n`;
} else {
outputTitle = `${this.NX_PREFIX} ${title}\n`;
}
this.writeToStdOut(outputTitle);
this.writeToStdOut(` ${this.applyNxPrefix(color, title)}${EOL}`);
}
private writeOptionalOutputBody(bodyLines?: string[]): void {
@ -83,27 +91,45 @@ class CLIOutput {
return;
}
this.addNewline();
bodyLines.forEach((bodyLine) => this.writeToStdOut(` ${bodyLine}\n`));
bodyLines.forEach((bodyLine) => this.writeToStdOut(` ${bodyLine}${EOL}`));
}
applyNxPrefix(color = 'cyan', text: string): string {
let nxPrefix = '';
if (chalk[color]) {
nxPrefix = `${chalk[color]('>')} ${chalk.reset.inverse.bold[color](
' NX '
)}`;
} else {
nxPrefix = `${chalk.keyword(color)(
'>'
)} ${chalk.reset.inverse.bold.keyword(color)(' NX ')}`;
}
return `${nxPrefix} ${text}`;
}
addNewline() {
this.writeToStdOut('\n');
this.writeToStdOut(EOL);
}
addVerticalSeparator() {
this.writeToStdOut(`\n${chalk.gray(this.VERTICAL_SEPARATOR)}\n\n`);
addVerticalSeparator(color = 'gray') {
this.addNewline();
this.addVerticalSeparatorWithoutNewLines(color);
this.addNewline();
}
addVerticalSeparatorWithoutNewLines() {
this.writeToStdOut(`${chalk.gray(this.VERTICAL_SEPARATOR)}\n`);
addVerticalSeparatorWithoutNewLines(color = 'gray') {
this.writeToStdOut(
`${this.X_PADDING}${chalk.dim[color](this.VERTICAL_SEPARATOR)}${EOL}`
);
}
error({ title, slug, bodyLines }: CLIErrorMessageConfig) {
this.addNewline();
this.writeOutputTitle({
label: chalk.reset.inverse.bold.red(' ERROR '),
title: chalk.bold.red(title),
color: 'red',
title: chalk.red(title),
});
this.writeOptionalOutputBody(bodyLines);
@ -116,7 +142,7 @@ class CLIOutput {
this.writeToStdOut(
`${chalk.grey(
' Learn more about this error: '
)}https://errors.nx.dev/${slug}\n`
)}https://errors.nx.dev/${slug}${EOL}`
);
}
@ -127,8 +153,8 @@ class CLIOutput {
this.addNewline();
this.writeOutputTitle({
label: chalk.reset.inverse.bold.yellow(' WARNING '),
title: chalk.bold.yellow(title),
color: 'yellow',
title: chalk.yellow(title),
});
this.writeOptionalOutputBody(bodyLines);
@ -152,8 +178,8 @@ class CLIOutput {
this.addNewline();
this.writeOutputTitle({
label: chalk.reset.inverse.bold.keyword('orange')(' NOTE '),
title: chalk.bold.keyword('orange')(title),
color: 'orange',
title: chalk.keyword('orange')(title),
});
this.writeOptionalOutputBody(bodyLines);
@ -165,8 +191,8 @@ class CLIOutput {
this.addNewline();
this.writeOutputTitle({
label: chalk.reset.inverse.bold.green(' SUCCESS '),
title: chalk.bold.green(title),
color: 'green',
title: chalk.green(title),
});
this.writeOptionalOutputBody(bodyLines);
@ -178,6 +204,7 @@ class CLIOutput {
this.addNewline();
this.writeOutputTitle({
color: 'gray',
title: message,
});
@ -190,20 +217,23 @@ class CLIOutput {
) {
this.addNewline();
this.writeToStdOut(chalk.bold(`> ${message} `));
let commandOutput = ` ${chalk.dim('> nx run')} ${message}`;
if (cacheStatus !== TaskCacheStatus.NoCache) {
this.writeToStdOut(chalk.bold.grey(cacheStatus));
commandOutput += ` ${chalk.grey(cacheStatus)}`;
}
this.writeToStdOut(commandOutput);
this.addNewline();
}
log({ title, bodyLines }: CLIWarnMessageConfig) {
log({ title, bodyLines, color }: CLIWarnMessageConfig & { color?: string }) {
this.addNewline();
color = color || 'white';
this.writeOutputTitle({
title: chalk.white(title),
color: 'cyan',
title: chalk[color](title),
});
this.writeOptionalOutputBody(bodyLines);