diff --git a/docs/shared/angular-tutorial/10-computation-caching.md b/docs/shared/angular-tutorial/10-computation-caching.md index 689a1c37f0..9c4b1b41f6 100644 --- a/docs/shared/angular-tutorial/10-computation-caching.md +++ b/docs/shared/angular-tutorial/10-computation-caching.md @@ -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). diff --git a/docs/shared/react-tutorial/10-computation-caching.md b/docs/shared/react-tutorial/10-computation-caching.md index 4f460f9ccd..327b525157 100644 --- a/docs/shared/react-tutorial/10-computation-caching.md +++ b/docs/shared/react-tutorial/10-computation-caching.md @@ -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). diff --git a/e2e/angular-core/src/projects.test.ts b/e2e/angular-core/src/projects.test.ts index 4944285bf7..7205c238ec 100644 --- a/e2e/angular-core/src/projects.test.ts +++ b/e2e/angular-core/src/projects.test.ts @@ -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 () => { diff --git a/e2e/cli/src/cli.test.ts b/e2e/cli/src/cli.test.ts index 9868885ed2..d26f37d886 100644 --- a/e2e/cli/src/cli.test.ts +++ b/e2e/cli/src/cli.test.ts @@ -104,7 +104,7 @@ describe('list', () => { it(`should work`, async () => { let listOutput = runCLI('list'); - expect(listOutput).toContain('NX Installed plugins'); + expect(listOutput).toContain('NX Installed plugins'); // just check for some, not all expect(listOutput).toContain('@nrwl/angular'); @@ -116,7 +116,7 @@ describe('list', () => { ); listOutput = runCLI('list'); - expect(listOutput).toContain('NX Also available'); + expect(listOutput).toContain('NX Also available'); // look for specific plugin listOutput = runCLI('list @nrwl/workspace'); @@ -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) diff --git a/e2e/detox/src/detox.test.ts b/e2e/detox/src/detox.test.ts index e75fd67f79..c57a200829 100644 --- a/e2e/detox/src/detox.test.ts +++ b/e2e/detox/src/detox.test.ts @@ -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); diff --git a/e2e/js/src/js.test.ts b/e2e/js/src/js.test.ts index 3c4295c7f7..f8646a6848 100644 --- a/e2e/js/src/js.test.ts +++ b/e2e/js/src/js.test.ts @@ -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}`) diff --git a/e2e/linter/src/linter.test.ts b/e2e/linter/src/linter.test.ts index 113b7a183b..af4cc5ca19 100644 --- a/e2e/linter/src/linter.test.ts +++ b/e2e/linter/src/linter.test.ts @@ -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'; diff --git a/e2e/node/src/node.test.ts b/e2e/node/src/node.test.ts index b672ed8800..fd83f8dea0 100644 --- a/e2e/node/src/node.test.ts +++ b/e2e/node/src/node.test.ts @@ -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 diff --git a/e2e/nx-plugin/src/nx-plugin.test.ts b/e2e/nx-plugin/src/nx-plugin.test.ts index f827ae1f01..7920a2cdbd 100644 --- a/e2e/nx-plugin/src/nx-plugin.test.ts +++ b/e2e/nx-plugin/src/nx-plugin.test.ts @@ -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); diff --git a/e2e/react/src/react-package.test.ts b/e2e/react/src/react-package.test.ts index 7fe8fb9946..5d84665f5d 100644 --- a/e2e/react/src/react-package.test.ts +++ b/e2e/react/src/react-package.test.ts @@ -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 diff --git a/e2e/workspace-integrations/src/workspace.test.ts b/e2e/workspace-integrations/src/workspace.test.ts index a27b0d2cfc..27cdce758f 100644 --- a/e2e/workspace-integrations/src/workspace.test.ts +++ b/e2e/workspace-integrations/src/workspace.test.ts @@ -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); } diff --git a/nx-dev/nx-dev/public/documentation/shared/angular-tutorial/10-computation-caching.md b/nx-dev/nx-dev/public/documentation/shared/angular-tutorial/10-computation-caching.md index 689a1c37f0..9c4b1b41f6 100644 --- a/nx-dev/nx-dev/public/documentation/shared/angular-tutorial/10-computation-caching.md +++ b/nx-dev/nx-dev/public/documentation/shared/angular-tutorial/10-computation-caching.md @@ -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). diff --git a/nx-dev/nx-dev/public/documentation/shared/react-tutorial/10-computation-caching.md b/nx-dev/nx-dev/public/documentation/shared/react-tutorial/10-computation-caching.md index 4f460f9ccd..327b525157 100644 --- a/nx-dev/nx-dev/public/documentation/shared/react-tutorial/10-computation-caching.md +++ b/nx-dev/nx-dev/public/documentation/shared/react-tutorial/10-computation-caching.md @@ -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). diff --git a/packages/create-nx-workspace/bin/output.ts b/packages/create-nx-workspace/bin/output.ts index fc56181ff2..ce12592d76 100644 --- a/packages/create-nx-workspace/bin/output.ts +++ b/packages/create-nx-workspace/bin/output.ts @@ -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); diff --git a/packages/workspace/src/command-line/daemon.ts b/packages/workspace/src/command-line/daemon.ts index f75be1f99b..8b90ec3cdf 100644 --- a/packages/workspace/src/command-line/daemon.ts +++ b/packages/workspace/src/command-line/daemon.ts @@ -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`, + ], + }); } diff --git a/packages/workspace/src/core/project-graph/daemon/client/client.ts b/packages/workspace/src/core/project-graph/daemon/client/client.ts index 1b885dda7c..e41d35dc1f 100644 --- a/packages/workspace/src/core/project-graph/daemon/client/client.ts +++ b/packages/workspace/src/core/project-graph/daemon/client/client.ts @@ -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' }); } /** diff --git a/packages/workspace/src/core/project-graph/daemon/server/server.ts b/packages/workspace/src/core/project-graph/daemon/server/server.ts index 9b5d552e0e..d5e6d9a0d0 100644 --- a/packages/workspace/src/core/project-graph/daemon/server/server.ts +++ b/packages/workspace/src/core/project-graph/daemon/server/server.ts @@ -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'; diff --git a/packages/workspace/src/core/project-graph/daemon/server/start.ts b/packages/workspace/src/core/project-graph/daemon/server/start.ts index 7a351e9d4a..85e595bb78 100644 --- a/packages/workspace/src/core/project-graph/daemon/server/start.ts +++ b/packages/workspace/src/core/project-graph/daemon/server/start.ts @@ -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); } })(); diff --git a/packages/workspace/src/core/project-graph/daemon/server/stop.ts b/packages/workspace/src/core/project-graph/daemon/server/stop.ts index 86ef2619ac..833ffded39 100644 --- a/packages/workspace/src/core/project-graph/daemon/server/stop.ts +++ b/packages/workspace/src/core/project-graph/daemon/server/stop.ts @@ -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', + }); } })(); diff --git a/packages/workspace/src/tasks-runner/default-tasks-runner.ts b/packages/workspace/src/tasks-runner/default-tasks-runner.ts index 1e578c0307..c3f8e84ede 100644 --- a/packages/workspace/src/tasks-runner/default-tasks-runner.ts +++ b/packages/workspace/src/tasks-runner/default-tasks-runner.ts @@ -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; diff --git a/packages/workspace/src/tasks-runner/forked-process-task-runner.ts b/packages/workspace/src/tasks-runner/forked-process-task-runner.ts index 0ff33e4b6b..9887a0d79c 100644 --- a/packages/workspace/src/tasks-runner/forked-process-task-runner.ts +++ b/packages/workspace/src/tasks-runner/forked-process-task-runner.ts @@ -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'], diff --git a/packages/workspace/src/tasks-runner/life-cycles/dynamic-run-many-terminal-output-life-cycle.ts b/packages/workspace/src/tasks-runner/life-cycles/dynamic-run-many-terminal-output-life-cycle.ts new file mode 100644 index 0000000000..3aee1c43c2 --- /dev/null +++ b/packages/workspace/src/tasks-runner/life-cycles/dynamic-run-many-terminal-output-life-cycle.ts @@ -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; +}): Promise<{ lifeCycle: LifeCycle; renderIsDone: Promise }> { + let resolveRenderIsDonePromise: (value: void) => void; + const renderIsDone = new Promise( + (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; + 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 = {}; + const tasksToProcessStartTimes: Record< + string, + ReturnType + > = {}; + 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 + ); +} diff --git a/packages/workspace/src/tasks-runner/empty-terminal-output-life-cycle.ts b/packages/workspace/src/tasks-runner/life-cycles/empty-terminal-output-life-cycle.ts similarity index 57% rename from packages/workspace/src/tasks-runner/empty-terminal-output-life-cycle.ts rename to packages/workspace/src/tasks-runner/life-cycles/empty-terminal-output-life-cycle.ts index 0ac65d4885..6d03c30f12 100644 --- a/packages/workspace/src/tasks-runner/empty-terminal-output-life-cycle.ts +++ b/packages/workspace/src/tasks-runner/life-cycles/empty-terminal-output-life-cycle.ts @@ -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); } } diff --git a/packages/workspace/src/tasks-runner/life-cycle.ts b/packages/workspace/src/tasks-runner/life-cycles/life-cycle.ts similarity index 95% rename from packages/workspace/src/tasks-runner/life-cycle.ts rename to packages/workspace/src/tasks-runner/life-cycles/life-cycle.ts index 95f9547b70..59f021d7a2 100644 --- a/packages/workspace/src/tasks-runner/life-cycle.ts +++ b/packages/workspace/src/tasks-runner/life-cycles/life-cycle.ts @@ -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; diff --git a/packages/workspace/src/tasks-runner/neo-output/pretty-time.ts b/packages/workspace/src/tasks-runner/life-cycles/pretty-time.ts similarity index 100% rename from packages/workspace/src/tasks-runner/neo-output/pretty-time.ts rename to packages/workspace/src/tasks-runner/life-cycles/pretty-time.ts diff --git a/packages/workspace/src/tasks-runner/run-many-terminal-output-life-cycle.ts b/packages/workspace/src/tasks-runner/life-cycles/static-run-many-terminal-output-life-cycle.ts similarity index 66% rename from packages/workspace/src/tasks-runner/run-many-terminal-output-life-cycle.ts rename to packages/workspace/src/tasks-runner/life-cycles/static-run-many-terminal-output-life-cycle.ts index be178f33ae..88e9158cb3 100644 --- a/packages/workspace/src/tasks-runner/run-many-terminal-output-life-cycle.ts +++ b/packages/workspace/src/tasks-runner/life-cycles/static-run-many-terminal-output-life-cycle.ts @@ -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[]; @@ -30,7 +38,7 @@ export class RunManyTerminalOutputLifeCycle implements LifeCycle { } const bodyLines = this.projectNames.map( - (affectedProject) => `${output.colors.gray('-')} ${affectedProject}` + (affectedProject) => ` ${output.colors.gray('-')} ${affectedProject}` ); if (Object.keys(this.taskOverrides).length > 0) { bodyLines.push(''); @@ -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); } } diff --git a/packages/workspace/src/tasks-runner/run-one-terminal-output-life-cycle.ts b/packages/workspace/src/tasks-runner/life-cycles/static-run-one-terminal-output-life-cycle.ts similarity index 62% rename from packages/workspace/src/tasks-runner/run-one-terminal-output-life-cycle.ts rename to packages/workspace/src/tasks-runner/life-cycles/static-run-one-terminal-output-life-cycle.ts index 61fc522490..70cef5b5b5 100644 --- a/packages/workspace/src/tasks-runner/run-one-terminal-output-life-cycle.ts +++ b/packages/workspace/src/tasks-runner/life-cycles/static-run-one-terminal-output-life-cycle.ts @@ -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); } } diff --git a/packages/workspace/src/tasks-runner/task-profiling-life-cycle.ts b/packages/workspace/src/tasks-runner/life-cycles/task-profiling-life-cycle.ts similarity index 98% rename from packages/workspace/src/tasks-runner/task-profiling-life-cycle.ts rename to packages/workspace/src/tasks-runner/life-cycles/task-profiling-life-cycle.ts index d427166859..497c24104b 100644 --- a/packages/workspace/src/tasks-runner/task-profiling-life-cycle.ts +++ b/packages/workspace/src/tasks-runner/life-cycles/task-profiling-life-cycle.ts @@ -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'; diff --git a/packages/workspace/src/tasks-runner/task-timings-life-cycle.ts b/packages/workspace/src/tasks-runner/life-cycles/task-timings-life-cycle.ts similarity index 95% rename from packages/workspace/src/tasks-runner/task-timings-life-cycle.ts rename to packages/workspace/src/tasks-runner/life-cycles/task-timings-life-cycle.ts index 45e42e895e..195bb732e0 100644 --- a/packages/workspace/src/tasks-runner/task-timings-life-cycle.ts +++ b/packages/workspace/src/tasks-runner/life-cycles/task-timings-life-cycle.ts @@ -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: { diff --git a/packages/workspace/src/tasks-runner/neo-output/render.ts b/packages/workspace/src/tasks-runner/neo-output/render.ts deleted file mode 100644 index 2ef694767d..0000000000 --- a/packages/workspace/src/tasks-runner/neo-output/render.ts +++ /dev/null @@ -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 }> { - const lifeCycle = {} as any; - - const start = process.hrtime(); - const figures = await import('figures'); - - let resolveIsRenderCompletePromise: (value: void) => void; - const renderIsDone = new Promise( - (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 = {}; - 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 }; -} diff --git a/packages/workspace/src/tasks-runner/run-command.ts b/packages/workspace/src/tasks-runner/run-command.ts index 8b9d9f1897..36b646ccd1 100644 --- a/packages/workspace/src/tasks-runner/run-command.ts +++ b/packages/workspace/src/tasks-runner/run-command.ts @@ -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, runnerOptions: any ): Promise<{ lifeCycle: LifeCycle; renderIsDone: Promise }> { 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) diff --git a/packages/workspace/src/tasks-runner/task-orchestrator.ts b/packages/workspace/src/tasks-runner/task-orchestrator.ts index 6bc3a150ce..587ed93073 100644 --- a/packages/workspace/src/tasks-runner/task-orchestrator.ts +++ b/packages/workspace/src/tasks-runner/task-orchestrator.ts @@ -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); diff --git a/packages/workspace/src/utilities/output.ts b/packages/workspace/src/utilities/output.ts index 05a7502c19..fac590af27 100644 --- a/packages/workspace/src/utilities/output.ts +++ b/packages/workspace/src/utilities/output.ts @@ -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);