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: And notice the output:
```bash ```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). 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:** **Now, run `npx nx run-many --target=build --projects=todos,api` to rebuild the two applications:**
```bash ```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). 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( expect(buildOutput).toContain(
`Building entry point '@${proj}/${lib}/${entryPoint}'` `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 () => { it('MFE - should serve the host and remote apps successfully', async () => {

View File

@ -135,14 +135,14 @@ describe('list', () => {
listOutput = runCLI('list @nrwl/angular'); listOutput = runCLI('list @nrwl/angular');
expect(listOutput).toContain( expect(listOutput).toContain(
'NX NOTE @nrwl/angular is not currently installed' 'NX @nrwl/angular is not currently installed'
); );
// look for an unknown plugin // look for an unknown plugin
listOutput = runCLI('list @wibble/fish'); listOutput = runCLI('list @wibble/fish');
expect(listOutput).toContain( 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) // 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( expect(runCLI(`build-ios ${appName}-e2e --prod`)).toContain(
'Running target "build-ios" succeeded' 'Successfully ran target build-ios'
); );
expect( expect(
runCLI( runCLI(
`test-ios ${appName}-e2e --prod --debugSynchronization=true --loglevel=trace` `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 await killPorts(8081); // kill the port for the serve command
}, 3000000); }, 3000000);

View File

@ -131,7 +131,7 @@ describe('js e2e', () => {
}); });
const output = runCLI(`build ${app}`); 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(output).toContain('Done compiling TypeScript files');
expect(runCLI(`serve ${app} --no-watch`)).toContain(`Running ${lib}`); expect(runCLI(`serve ${app} --no-watch`)).toContain(`Running ${lib}`);
@ -193,7 +193,7 @@ describe('js e2e', () => {
// }); // });
// //
// const output = runCLI(`build ${app}`); // 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(output).toContain('Successfully compiled: 2 files with swc');
// //
// expect(runCommand(`serve ${app} --watch=false`)).toContain(`Running ${lib}`) // 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 // Ensure that the unit tests for the new rule are runnable
const unitTestsOutput = runCLI(`test eslint-rules`); 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 // 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'; const knownLintErrorMessage = 'e2e test known error message';

View File

@ -770,7 +770,7 @@ describe('with dependencies', () => {
const buildWithDeps = runCLI( const buildWithDeps = runCLI(
`build ${app} --with-deps --buildLibsFromSource=false` `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`); checkFilesDoNotExist(`apps/${app}/tsconfig/tsconfig.nx-tmp`);
// we remove all path mappings from the root tsconfig, so when trying to build // 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()) { if (isNotWindows()) {
const e2eResults = runCLI(`e2e ${plugin}-e2e`); const e2eResults = runCLI(`e2e ${plugin}-e2e`);
expect(e2eResults).toContain('Running target "e2e" succeeded'); expect(e2eResults).toContain('Successfully ran target e2e');
expect(await killPorts()).toBeTruthy(); expect(await killPorts()).toBeTruthy();
} }
}, 250000); }, 250000);

View File

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

View File

@ -60,7 +60,7 @@ npx nx run-many --target=build --projects=todos,api
And notice the output: And notice the output:
```bash ```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). 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:** **Now, run `npx nx run-many --target=build --projects=todos,api` to rebuild the two applications:**
```bash ```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). 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) * 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. * 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 { export interface CLIErrorMessageConfig {
title: string; title: string;
bodyLines?: string[]; bodyLines?: string[];
@ -25,23 +47,37 @@ export interface CLISuccessMessageConfig {
bodyLines?: string[]; 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 * Automatically disable styling applied by chalk if CI=true
*/ */
if (process.env.CI === 'true') { if (isCI()) {
(chalk as any).level = 0; (chalk as any).level = 0;
} }
class CLIOutput { class CLIOutput {
private readonly NX_PREFIX = `${chalk.cyan( readonly X_PADDING = ' ';
'>'
)} ${chalk.reset.inverse.bold.cyan(' NX ')}`;
/** /**
* Longer dash character which forms more of a continuous line when place side to side * Longer dash character which forms more of a continuous line when place side to side
* with itself, unlike the standard dash character * 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 * Expose some color and other utility functions so that other parts of the codebase that need
@ -50,28 +86,27 @@ class CLIOutput {
*/ */
colors = { colors = {
gray: chalk.gray, gray: chalk.gray,
green: chalk.green,
red: chalk.red,
cyan: chalk.cyan,
white: chalk.white,
}; };
bold = chalk.bold; bold = chalk.bold;
underline = chalk.underline; underline = chalk.underline;
dim = chalk.dim;
private writeToStdOut(str: string) { private writeToStdOut(str: string) {
process.stdout.write(str); process.stdout.write(str);
} }
private writeOutputTitle({ private writeOutputTitle({
label, color,
title, title,
}: { }: {
label?: string; color: string;
title: string; title: string;
}): void { }): void {
let outputTitle: string; this.writeToStdOut(` ${this.applyNxPrefix(color, title)}${EOL}`);
if (label) {
outputTitle = `${this.NX_PREFIX} ${label} ${title}\n`;
} else {
outputTitle = `${this.NX_PREFIX} ${title}\n`;
}
this.writeToStdOut(outputTitle);
} }
private writeOptionalOutputBody(bodyLines?: string[]): void { private writeOptionalOutputBody(bodyLines?: string[]): void {
@ -79,27 +114,45 @@ class CLIOutput {
return; return;
} }
this.addNewline(); 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() { addNewline() {
this.writeToStdOut('\n'); this.writeToStdOut(EOL);
} }
addVerticalSeparator() { addVerticalSeparator(color = 'gray') {
this.writeToStdOut(`\n${chalk.gray(this.VERTICAL_SEPARATOR)}\n\n`); this.addNewline();
this.addVerticalSeparatorWithoutNewLines(color);
this.addNewline();
} }
addVerticalSeparatorWithoutNewLines() { addVerticalSeparatorWithoutNewLines(color = 'gray') {
this.writeToStdOut(`${chalk.gray(this.VERTICAL_SEPARATOR)}\n`); this.writeToStdOut(
`${this.X_PADDING}${chalk.dim[color](this.VERTICAL_SEPARATOR)}${EOL}`
);
} }
error({ title, slug, bodyLines }: CLIErrorMessageConfig) { error({ title, slug, bodyLines }: CLIErrorMessageConfig) {
this.addNewline(); this.addNewline();
this.writeOutputTitle({ this.writeOutputTitle({
label: chalk.reset.inverse.bold.red(' ERROR '), color: 'red',
title: chalk.bold.red(title), title: chalk.red(title),
}); });
this.writeOptionalOutputBody(bodyLines); this.writeOptionalOutputBody(bodyLines);
@ -112,7 +165,7 @@ class CLIOutput {
this.writeToStdOut( this.writeToStdOut(
`${chalk.grey( `${chalk.grey(
' Learn more about this error: ' ' 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.addNewline();
this.writeOutputTitle({ this.writeOutputTitle({
label: chalk.reset.inverse.bold.yellow(' WARNING '), color: 'yellow',
title: chalk.bold.yellow(title), title: chalk.yellow(title),
}); });
this.writeOptionalOutputBody(bodyLines); this.writeOptionalOutputBody(bodyLines);
@ -148,8 +201,8 @@ class CLIOutput {
this.addNewline(); this.addNewline();
this.writeOutputTitle({ this.writeOutputTitle({
label: chalk.reset.inverse.bold.keyword('orange')(' NOTE '), color: 'orange',
title: chalk.bold.keyword('orange')(title), title: chalk.keyword('orange')(title),
}); });
this.writeOptionalOutputBody(bodyLines); this.writeOptionalOutputBody(bodyLines);
@ -161,8 +214,8 @@ class CLIOutput {
this.addNewline(); this.addNewline();
this.writeOutputTitle({ this.writeOutputTitle({
label: chalk.reset.inverse.bold.green(' SUCCESS '), color: 'green',
title: chalk.bold.green(title), title: chalk.green(title),
}); });
this.writeOptionalOutputBody(bodyLines); this.writeOptionalOutputBody(bodyLines);
@ -174,29 +227,36 @@ class CLIOutput {
this.addNewline(); this.addNewline();
this.writeOutputTitle({ this.writeOutputTitle({
color: 'gray',
title: message, title: message,
}); });
this.addNewline(); this.addNewline();
} }
logCommand(message: string, isCached: boolean = false) { logCommand(
message: string,
cacheStatus: TaskCacheStatus = TaskCacheStatus.NoCache
) {
this.addNewline(); this.addNewline();
this.writeToStdOut(chalk.bold(`> ${message} `)); let commandOutput = ` ${chalk.dim('> nx run')} ${message}`;
if (cacheStatus !== TaskCacheStatus.NoCache) {
if (isCached) { commandOutput += ` ${chalk.grey(cacheStatus)}`;
this.writeToStdOut(chalk.bold.grey(`[retrieved from cache]`));
} }
this.writeToStdOut(commandOutput);
this.addNewline(); this.addNewline();
} }
log({ title, bodyLines }: CLIWarnMessageConfig) { log({ title, bodyLines, color }: CLIWarnMessageConfig & { color?: string }) {
this.addNewline(); this.addNewline();
color = color || 'white';
this.writeOutputTitle({ this.writeOutputTitle({
title: chalk.white(title), color: 'cyan',
title: chalk[color](title),
}); });
this.writeOptionalOutputBody(bodyLines); this.writeOptionalOutputBody(bodyLines);

View File

@ -1,6 +1,6 @@
import { logger } from '@nrwl/devkit';
import type { Arguments } from 'yargs'; import type { Arguments } from 'yargs';
import { DAEMON_OUTPUT_LOG_FILE } from '../core/project-graph/daemon/tmp-dir'; import { DAEMON_OUTPUT_LOG_FILE } from '../core/project-graph/daemon/tmp-dir';
import { output } from '../utilities/output';
export async function daemonHandler(args: Arguments) { export async function daemonHandler(args: Arguments) {
const { startInBackground, startInCurrentProcess } = await import( const { startInBackground, startInCurrentProcess } = await import(
@ -9,9 +9,13 @@ export async function daemonHandler(args: Arguments) {
if (!args.background) { if (!args.background) {
return startInCurrentProcess(); return startInCurrentProcess();
} }
logger.info(`NX Daemon Server - Starting in a background process...`);
const pid = await startInBackground(); const pid = await startInBackground();
logger.log( output.log({
` Logs from the Daemon process (ID: ${pid}) can be found here: ${DAEMON_OUTPUT_LOG_FILE}\n` 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 { ChildProcess, spawn, spawnSync } from 'child_process';
import { openSync, readFileSync } from 'fs'; import { openSync, readFileSync } from 'fs';
import { ensureDirSync, ensureFileSync } from 'fs-extra'; import { ensureDirSync, ensureFileSync } from 'fs-extra';
import { connect } from 'net'; import { connect } from 'net';
import { performance } from 'perf_hooks'; import { performance } from 'perf_hooks';
import { output } from '../../../../utilities/output';
import { import {
safelyCleanUpExistingProcess, safelyCleanUpExistingProcess,
writeDaemonJsonProcessCache, writeDaemonJsonProcessCache,
@ -82,7 +83,9 @@ function daemonProcessException(message: string) {
} }
export function startInCurrentProcess(): void { 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'], { spawnSync(process.execPath, ['../server/start.js'], {
cwd: __dirname, cwd: __dirname,
@ -96,7 +99,7 @@ export function stop(): void {
stdio: 'inherit', 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 { appRootPath } from '@nrwl/tao/src/utils/app-root';
import { createServer, Server, Socket } from 'net'; import { createServer, Server, Socket } from 'net';
import { join } from 'path'; 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 { startServer } from './server';
import * as process from 'process'; import * as process from 'process';
@ -6,7 +6,11 @@ import * as process from 'process';
try { try {
await startServer(); await startServer();
} catch (err) { } catch (err) {
logger.error(err); output.error({
title:
err?.message ||
'Something unexpected went wrong when starting the server',
});
process.exit(1); process.exit(1);
} }
})(); })();

View File

@ -1,4 +1,4 @@
import { logger } from '@nrwl/devkit'; import { output } from '../../../../utilities/output';
import { safelyCleanUpExistingProcess } from '../cache'; import { safelyCleanUpExistingProcess } from '../cache';
import { stopServer } from './server'; import { stopServer } from './server';
@ -7,6 +7,10 @@ import { stopServer } from './server';
await stopServer(); await stopServer();
await safelyCleanUpExistingProcess(); await safelyCleanUpExistingProcess();
} catch (err) { } 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 { performance } from 'perf_hooks';
import { TaskGraphCreator } from './task-graph-creator'; import { TaskGraphCreator } from './task-graph-creator';
import { Hasher } from '../core/hasher/hasher'; import { Hasher } from '../core/hasher/hasher';
import { LifeCycle } from './life-cycle'; import { LifeCycle } from './life-cycles/life-cycle';
export interface RemoteCache { export interface RemoteCache {
retrieve: (hash: string, cacheDirectory: string) => Promise<boolean>; retrieve: (hash: string, cacheDirectory: string) => Promise<boolean>;

View File

@ -39,9 +39,8 @@ export class ForkedProcessTaskRunner {
); );
} else { } else {
const args = getCommandArgsForTask(Object.values(taskGraph.tasks)[0]); const args = getCommandArgsForTask(Object.values(taskGraph.tasks)[0]);
const commandLine = `nx ${args.join(' ')}`; output.logCommand(`${args.filter((a) => a !== 'run').join(' ')}`);
output.addNewline();
output.logCommand(commandLine);
} }
const p = fork(workerPath, { const p = fork(workerPath, {
@ -97,10 +96,9 @@ export class ForkedProcessTaskRunner {
return new Promise<{ code: number; terminalOutput: string }>((res, rej) => { return new Promise<{ code: number; terminalOutput: string }>((res, rej) => {
try { try {
const args = getCommandArgsForTask(task); const args = getCommandArgsForTask(task);
const commandLine = `nx ${args.join(' ')}`;
if (forwardOutput) { if (forwardOutput) {
output.logCommand(commandLine); output.logCommand(`${args.filter((a) => a !== 'run').join(' ')}`);
output.addNewline();
} }
const p = fork(this.cliPath, args, { const p = fork(this.cliPath, args, {
stdio: ['inherit', 'pipe', 'pipe', 'ipc'], stdio: ['inherit', 'pipe', 'pipe', 'ipc'],
@ -163,10 +161,9 @@ export class ForkedProcessTaskRunner {
return new Promise<{ code: number; terminalOutput: string }>((res, rej) => { return new Promise<{ code: number; terminalOutput: string }>((res, rej) => {
try { try {
const args = getCommandArgsForTask(task); const args = getCommandArgsForTask(task);
const commandLine = `nx ${args.join(' ')}`;
if (forwardOutput) { if (forwardOutput) {
output.logCommand(commandLine); output.logCommand(`${args.filter((a) => a !== 'run').join(' ')}`);
output.addNewline();
} }
const p = fork(this.cliPath, args, { const p = fork(this.cliPath, args, {
stdio: ['inherit', 'inherit', 'inherit', 'ipc'], 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 type { Task } from '@nrwl/devkit';
import { output, TaskCacheStatus } from '../utilities/output'; import { output, TaskCacheStatus } from '../../utilities/output';
import { LifeCycle } from './life-cycle'; import { getCommandArgsForTask } from '../utils';
import { getCommandArgsForTask } from './utils'; import type { LifeCycle } from './life-cycle';
export class EmptyTerminalOutputLifeCycle implements LifeCycle { export class EmptyTerminalOutputLifeCycle implements LifeCycle {
printTaskTerminalOutput( printTaskTerminalOutput(
@ -11,7 +11,10 @@ export class EmptyTerminalOutputLifeCycle implements LifeCycle {
) { ) {
if (cacheStatus === TaskCacheStatus.NoCache) { if (cacheStatus === TaskCacheStatus.NoCache) {
const args = getCommandArgsForTask(task); const args = getCommandArgsForTask(task);
output.logCommand(`nx ${args.join(' ')}`, cacheStatus); output.logCommand(
`${args.filter((a) => a !== 'run').join(' ')}`,
cacheStatus
);
process.stdout.write(terminalOutput); process.stdout.write(terminalOutput);
} }
} }

View File

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

View File

@ -1,10 +1,18 @@
import type { Task } from '@nrwl/devkit'; import type { Task } from '@nrwl/devkit';
import { output, TaskCacheStatus } from '../utilities/output'; import { output, TaskCacheStatus } from '../../utilities/output';
import { LifeCycle } from './life-cycle'; import { TaskStatus } from '../tasks-runner';
import { TaskStatus } from './tasks-runner'; import { getCommandArgsForTask } from '../utils';
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[]; failedTasks = [] as Task[];
cachedTasks = [] as Task[]; cachedTasks = [] as Task[];
skippedTasks = [] as Task[]; skippedTasks = [] as Task[];
@ -30,7 +38,7 @@ export class RunManyTerminalOutputLifeCycle implements LifeCycle {
} }
const bodyLines = this.projectNames.map( const bodyLines = this.projectNames.map(
(affectedProject) => `${output.colors.gray('-')} ${affectedProject}` (affectedProject) => ` ${output.colors.gray('-')} ${affectedProject}`
); );
if (Object.keys(this.taskOverrides).length > 0) { if (Object.keys(this.taskOverrides).length > 0) {
bodyLines.push(''); bodyLines.push('');
@ -40,44 +48,50 @@ export class RunManyTerminalOutputLifeCycle implements LifeCycle {
.forEach((arg) => bodyLines.push(arg)); .forEach((arg) => bodyLines.push(arg));
} }
let title = `${output.colors.gray('Running target')} ${ let title = `Running target ${output.bold(
this.args.target 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; const dependentTasksCount = this.tasks.length - this.projectNames.length;
if (dependentTasksCount > 0) { if (dependentTasksCount > 0) {
title += ` ${output.colors.gray(`and`)} ${ title += ` and ${output.bold(
this.tasks.length - this.projectNames.length dependentTasksCount
} task(s) ${output.colors.gray(`they depend on`)}`; )} task(s) they depend on`;
} }
title += ':'; title += ':';
output.log({ output.log({
color: 'cyan',
title, title,
bodyLines, bodyLines,
}); });
output.addVerticalSeparatorWithoutNewLines(); output.addVerticalSeparatorWithoutNewLines('cyan');
} }
endCommand(): void { endCommand(): void {
output.addNewline(); output.addNewline();
output.addVerticalSeparatorWithoutNewLines();
if (this.failedTasks.length === 0) { if (this.failedTasks.length === 0) {
output.addVerticalSeparatorWithoutNewLines('green');
const bodyLines = const bodyLines =
this.cachedTasks.length > 0 this.cachedTasks.length > 0
? [ ? [
output.colors.gray( 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({ 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, bodyLines,
}); });
} else { } else {
output.addVerticalSeparatorWithoutNewLines('red');
const bodyLines = []; const bodyLines = [];
if (this.skippedTasks.length > 0) { if (this.skippedTasks.length > 0) {
bodyLines.push( bodyLines.push(
@ -127,7 +141,10 @@ export class RunManyTerminalOutputLifeCycle implements LifeCycle {
terminalOutput: string terminalOutput: string
) { ) {
const args = getCommandArgsForTask(task); const args = getCommandArgsForTask(task);
output.logCommand(`nx ${args.join(' ')}`, cacheStatus); output.logCommand(
`${args.filter((a) => a !== 'run').join(' ')}`,
cacheStatus
);
process.stdout.write(terminalOutput); process.stdout.write(terminalOutput);
} }
} }

View File

@ -1,10 +1,18 @@
import type { Task } from '@nrwl/devkit'; import type { Task } from '@nrwl/devkit';
import { output, TaskCacheStatus } from '../utilities/output'; import { output, TaskCacheStatus } from '../../utilities/output';
import { LifeCycle } from './life-cycle'; import { TaskStatus } from '../tasks-runner';
import { TaskStatus } from './tasks-runner'; import { getCommandArgsForTask } from '../utils';
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[]; failedTasks = [] as Task[];
cachedTasks = [] as Task[]; cachedTasks = [] as Task[];
skippedTasks = [] as Task[]; skippedTasks = [] as Task[];
@ -27,16 +35,14 @@ export class RunOneTerminalOutputLifeCycle implements LifeCycle {
if (numberOfDeps > 0) { if (numberOfDeps > 0) {
output.log({ output.log({
title: `${output.colors.gray('Running target')} ${ color: 'cyan',
title: `Running target ${output.bold(
this.args.target this.args.target
} ${output.colors.gray('for project')} ${ )} for project ${output.bold(this.initiatingProject)} and ${output.bold(
this.initiatingProject numberOfDeps
} ${output.colors.gray( )} task(s) it depends on`,
`and`
)} ${numberOfDeps} task(s) ${output.colors.gray(`that it depends on.`)}
`,
}); });
output.addVerticalSeparatorWithoutNewLines(); output.addVerticalSeparatorWithoutNewLines('cyan');
} }
} }
@ -46,23 +52,28 @@ export class RunOneTerminalOutputLifeCycle implements LifeCycle {
return; return;
} }
output.addNewline(); output.addNewline();
output.addVerticalSeparatorWithoutNewLines();
if (this.failedTasks.length === 0) { if (this.failedTasks.length === 0) {
output.addVerticalSeparatorWithoutNewLines('green');
const bodyLines = const bodyLines =
this.cachedTasks.length > 0 this.cachedTasks.length > 0
? [ ? [
output.colors.gray( 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({ 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, bodyLines,
}); });
} else { } else {
output.addVerticalSeparatorWithoutNewLines('red');
const bodyLines = [ const bodyLines = [
output.colors.gray('Failed tasks:'), output.colors.gray('Failed tasks:'),
'', '',
@ -107,7 +118,10 @@ export class RunOneTerminalOutputLifeCycle implements LifeCycle {
task.target.project === this.initiatingProject task.target.project === this.initiatingProject
) { ) {
const args = getCommandArgsForTask(task); const args = getCommandArgsForTask(task);
output.logCommand(`nx ${args.join(' ')}`, cacheStatus); output.logCommand(
`${args.filter((a) => a !== 'run').join(' ')}`,
cacheStatus
);
process.stdout.write(terminalOutput); process.stdout.write(terminalOutput);
} }
} }

View File

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

View File

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

View File

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

View File

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