chore(core): task runner dynamic output life cycle (#8590)
This commit is contained in:
parent
e69b893829
commit
faef0d8c85
@ -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).
|
||||
|
||||
@ -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).
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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}`)
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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).
|
||||
|
||||
@ -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).
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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`,
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
@ -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' });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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);
|
||||
}
|
||||
})();
|
||||
|
||||
@ -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',
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
@ -4,7 +4,7 @@ import { TaskOrchestrator } from './task-orchestrator';
|
||||
import { performance } from 'perf_hooks';
|
||||
import { TaskGraphCreator } from './task-graph-creator';
|
||||
import { Hasher } from '../core/hasher/hasher';
|
||||
import { LifeCycle } from './life-cycle';
|
||||
import { LifeCycle } from './life-cycles/life-cycle';
|
||||
|
||||
export interface RemoteCache {
|
||||
retrieve: (hash: string, cacheDirectory: string) => Promise<boolean>;
|
||||
|
||||
@ -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'],
|
||||
|
||||
@ -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
|
||||
);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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';
|
||||
@ -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: {
|
||||
@ -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 };
|
||||
}
|
||||
@ -19,13 +19,13 @@ import {
|
||||
} from '../utilities/project-graph-utils';
|
||||
import { output } from '../utilities/output';
|
||||
import { getDependencyConfigs, shouldForwardOutput } from './utils';
|
||||
import { CompositeLifeCycle, LifeCycle } from './life-cycle';
|
||||
import { RunManyTerminalOutputLifeCycle } from './run-many-terminal-output-life-cycle';
|
||||
import { EmptyTerminalOutputLifeCycle } from './empty-terminal-output-life-cycle';
|
||||
import { RunOneTerminalOutputLifeCycle } from './run-one-terminal-output-life-cycle';
|
||||
import { TaskTimingsLifeCycle } from './task-timings-life-cycle';
|
||||
import { createOutputRenderer } from './neo-output/render';
|
||||
import { TaskProfilingLifeCycle } from '@nrwl/workspace/src/tasks-runner/task-profiling-life-cycle';
|
||||
import { CompositeLifeCycle, LifeCycle } from './life-cycles/life-cycle';
|
||||
import { StaticRunManyTerminalOutputLifeCycle } from './life-cycles/static-run-many-terminal-output-life-cycle';
|
||||
import { StaticRunOneTerminalOutputLifeCycle } from './life-cycles/static-run-one-terminal-output-life-cycle';
|
||||
import { EmptyTerminalOutputLifeCycle } from './life-cycles/empty-terminal-output-life-cycle';
|
||||
import { TaskTimingsLifeCycle } from './life-cycles/task-timings-life-cycle';
|
||||
import { createDynamicOutputRenderer } from './life-cycles/dynamic-run-many-terminal-output-life-cycle';
|
||||
import { TaskProfilingLifeCycle } from './life-cycles/task-profiling-life-cycle';
|
||||
|
||||
async function getTerminalOutputLifeCycle(
|
||||
initiatingProject: string,
|
||||
@ -33,12 +33,12 @@ async function getTerminalOutputLifeCycle(
|
||||
projectNames: string[],
|
||||
tasks: Task[],
|
||||
nxArgs: NxArgs,
|
||||
overrides: any,
|
||||
overrides: Record<string, unknown>,
|
||||
runnerOptions: any
|
||||
): Promise<{ lifeCycle: LifeCycle; renderIsDone: Promise<void> }> {
|
||||
if (terminalOutputStrategy === 'run-one') {
|
||||
return {
|
||||
lifeCycle: new RunOneTerminalOutputLifeCycle(
|
||||
lifeCycle: new StaticRunOneTerminalOutputLifeCycle(
|
||||
initiatingProject,
|
||||
projectNames,
|
||||
tasks,
|
||||
@ -52,17 +52,18 @@ async function getTerminalOutputLifeCycle(
|
||||
renderIsDone: Promise.resolve(),
|
||||
};
|
||||
} else if (
|
||||
shouldUseNeoLifeCycle(tasks, runnerOptions) &&
|
||||
process.env.NX_TASKS_RUNNER_NEO_OUTPUT === 'true'
|
||||
shouldUseDynamicLifeCycle(tasks, runnerOptions) &&
|
||||
process.env.NX_TASKS_RUNNER_DYNAMIC_OUTPUT === 'true'
|
||||
) {
|
||||
return await createOutputRenderer({
|
||||
return await createDynamicOutputRenderer({
|
||||
projectNames,
|
||||
tasks,
|
||||
args: nxArgs,
|
||||
overrides,
|
||||
});
|
||||
} else {
|
||||
return {
|
||||
lifeCycle: new RunManyTerminalOutputLifeCycle(
|
||||
lifeCycle: new StaticRunManyTerminalOutputLifeCycle(
|
||||
projectNames,
|
||||
tasks,
|
||||
nxArgs,
|
||||
@ -226,7 +227,7 @@ export function createTasksForProjectToRun(
|
||||
return Array.from(tasksMap.values());
|
||||
}
|
||||
|
||||
function shouldUseNeoLifeCycle(tasks: Task[], options: any) {
|
||||
function shouldUseDynamicLifeCycle(tasks: Task[], options: any) {
|
||||
const isTTY = !!process.stdout.isTTY && process.env['CI'] !== 'true';
|
||||
const noForwarding = !tasks.find((t) =>
|
||||
shouldForwardOutput(t, null, options)
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user