chore(workspace): improved logging design and consistency
This commit is contained in:
parent
52885744d9
commit
8271c7650e
@ -9,7 +9,21 @@ import {
|
||||
runCLI
|
||||
} from './utils';
|
||||
|
||||
let originalCIValue;
|
||||
|
||||
describe('Affected', () => {
|
||||
/**
|
||||
* Setting CI=true makes it simpler to configure assertions around output, as there
|
||||
* won't be any colors.
|
||||
*/
|
||||
beforeAll(() => {
|
||||
originalCIValue = process.env.CI;
|
||||
process.env.CI = 'true';
|
||||
});
|
||||
afterAll(() => {
|
||||
process.env.CI = originalCIValue;
|
||||
});
|
||||
|
||||
it('should print, build, and test affected apps', () => {
|
||||
ensureProject();
|
||||
const myapp = uniq('myapp');
|
||||
@ -89,9 +103,10 @@ describe('Affected', () => {
|
||||
const build = runCommand(
|
||||
`npm run affected:build -- --files="libs/${mylib}/src/index.ts"`
|
||||
);
|
||||
expect(build).toContain(
|
||||
`Running build for projects:\n ${myapp},\n ${mypublishablelib}`
|
||||
);
|
||||
expect(build).toContain(`Running target build for projects:`);
|
||||
expect(build).toContain(myapp);
|
||||
expect(build).toContain(mypublishablelib);
|
||||
|
||||
expect(build).not.toContain('is not registered with the build command');
|
||||
expect(build).not.toContain('with flags:');
|
||||
|
||||
@ -99,28 +114,26 @@ describe('Affected', () => {
|
||||
const buildParallel = runCommand(
|
||||
`npm run affected:build -- --files="libs/${mylib}/src/index.ts" --parallel`
|
||||
);
|
||||
expect(buildParallel).toContain(`Running target build for projects:`);
|
||||
expect(buildParallel).toContain(myapp);
|
||||
expect(buildParallel).toContain(mypublishablelib);
|
||||
expect(buildParallel).toContain(
|
||||
`Running build for projects:\n ${myapp},\n ${mypublishablelib}`
|
||||
);
|
||||
expect(buildParallel).toContain(
|
||||
'Running build for affected projects succeeded.'
|
||||
'Running target "build" for affected projects succeeded'
|
||||
);
|
||||
|
||||
const buildExcluded = runCommand(
|
||||
`npm run affected:build -- --files="libs/${mylib}/src/index.ts" --exclude ${myapp}`
|
||||
);
|
||||
expect(buildExcluded).toContain(
|
||||
`Running build for projects:\n ${mypublishablelib}`
|
||||
);
|
||||
expect(buildExcluded).toContain(`Running target build for projects:`);
|
||||
expect(buildExcluded).toContain(mypublishablelib);
|
||||
|
||||
// affected:build should pass non-nx flags to the CLI
|
||||
const buildWithFlags = runCommand(
|
||||
`npm run affected:build -- --files="libs/${mylib}/src/index.ts" --stats-json`
|
||||
);
|
||||
|
||||
expect(buildWithFlags).toContain(
|
||||
`Running build for projects:\n ${myapp},\n ${mypublishablelib}`
|
||||
);
|
||||
expect(buildWithFlags).toContain(`Running target build for projects:`);
|
||||
expect(buildWithFlags).toContain(myapp);
|
||||
expect(buildWithFlags).toContain(mypublishablelib);
|
||||
expect(buildWithFlags).toContain('With flags: --stats-json=true');
|
||||
|
||||
if (!runsInWSL()) {
|
||||
@ -133,9 +146,10 @@ describe('Affected', () => {
|
||||
const unitTests = runCommand(
|
||||
`npm run affected:test -- --files="libs/${mylib}/src/index.ts"`
|
||||
);
|
||||
expect(unitTests).toContain(
|
||||
`Running test for projects:\n ${mylib},\n ${myapp},\n ${mypublishablelib}`
|
||||
);
|
||||
expect(unitTests).toContain(`Running target test for projects:`);
|
||||
expect(unitTests).toContain(mylib);
|
||||
expect(unitTests).toContain(myapp);
|
||||
expect(unitTests).toContain(mypublishablelib);
|
||||
|
||||
// Fail a Unit Test
|
||||
updateFile(
|
||||
@ -149,12 +163,15 @@ describe('Affected', () => {
|
||||
const failedTests = runCommand(
|
||||
`npm run affected:test -- --files="libs/${mylib}/src/index.ts"`
|
||||
);
|
||||
expect(failedTests).toContain(`Running target test for projects:`);
|
||||
expect(failedTests).toContain(mylib);
|
||||
expect(failedTests).toContain(myapp);
|
||||
expect(failedTests).toContain(mypublishablelib);
|
||||
|
||||
expect(failedTests).toContain(`Failed projects:`);
|
||||
expect(failedTests).toContain(myapp);
|
||||
expect(failedTests).toContain(
|
||||
`Running test for projects:\n ${mylib},\n ${mypublishablelib},\n ${myapp}`
|
||||
);
|
||||
expect(failedTests).toContain(`Failed projects: ${myapp}`);
|
||||
expect(failedTests).toContain(
|
||||
'You can isolate the above projects by passing --only-failed'
|
||||
'You can isolate the above projects by passing: --only-failed'
|
||||
);
|
||||
expect(readJson('dist/.nx-results')).toEqual({
|
||||
command: 'test',
|
||||
@ -177,14 +194,17 @@ describe('Affected', () => {
|
||||
const isolatedTests = runCommand(
|
||||
`npm run affected:test -- --files="libs/${mylib}/src/index.ts" --only-failed`
|
||||
);
|
||||
expect(isolatedTests).toContain(`Running test for projects:\n ${myapp}`);
|
||||
expect(isolatedTests).toContain(`Running target test for projects:`);
|
||||
expect(isolatedTests).toContain(myapp);
|
||||
|
||||
const linting = runCommand(
|
||||
`npm run affected:lint -- --files="libs/${mylib}/src/index.ts"`
|
||||
);
|
||||
expect(linting).toContain(
|
||||
`Running lint for projects:\n ${mylib},\n ${myapp},\n ${myapp}-e2e,\n ${mypublishablelib}`
|
||||
);
|
||||
expect(linting).toContain(`Running target lint for projects:`);
|
||||
expect(linting).toContain(mylib);
|
||||
expect(linting).toContain(myapp);
|
||||
expect(linting).toContain(`${myapp}-e2e`);
|
||||
expect(linting).toContain(mypublishablelib);
|
||||
|
||||
const lintWithJsonFormating = runCommand(
|
||||
`npm run affected:lint -- --files="libs/${mylib}/src/index.ts" -- --format json`
|
||||
@ -194,20 +214,20 @@ describe('Affected', () => {
|
||||
const unitTestsExcluded = runCommand(
|
||||
`npm run affected:test -- --files="libs/${mylib}/src/index.ts" --exclude=${myapp},${mypublishablelib}`
|
||||
);
|
||||
expect(unitTestsExcluded).toContain(
|
||||
`Running test for projects:\n ${mylib}`
|
||||
);
|
||||
expect(unitTestsExcluded).toContain(`Running target test for projects:`);
|
||||
expect(unitTestsExcluded).toContain(mylib);
|
||||
|
||||
const i18n = runCommand(
|
||||
`npm run affected -- --target extract-i18n --files="libs/${mylib}/src/index.ts"`
|
||||
);
|
||||
expect(i18n).toContain(`Running extract-i18n for projects:\n ${myapp}`);
|
||||
expect(i18n).toContain(`Running target extract-i18n for projects:`);
|
||||
expect(i18n).toContain(myapp);
|
||||
|
||||
const interpolatedTests = runCommand(
|
||||
`npm run affected -- --target test --files="libs/${mylib}/src/index.ts" -- --jest-config {project.root}/jest.config.js`
|
||||
);
|
||||
expect(interpolatedTests).toContain(
|
||||
`Running test for affected projects succeeded.`
|
||||
`Running target "test" for affected projects succeeded`
|
||||
);
|
||||
}, 1000000);
|
||||
});
|
||||
|
||||
@ -75,10 +75,16 @@ describe('Command line', () => {
|
||||
|
||||
const stdout = runCommand('./node_modules/.bin/nx workspace-lint');
|
||||
expect(stdout).toContain(
|
||||
`Cannot find project '${appBefore}' in 'apps/${appBefore}'`
|
||||
`- Cannot find project '${appBefore}' in 'apps/${appBefore}'`
|
||||
);
|
||||
expect(stdout).toContain(
|
||||
`The 'apps/${appAfter}/browserslist' file doesn't belong to any project.`
|
||||
'The following file(s) do not belong to any projects:'
|
||||
);
|
||||
expect(stdout).toContain(`- apps/${appAfter}/browserslist`);
|
||||
expect(stdout).toContain(`- apps/${appAfter}/src/app/app.component.css`);
|
||||
expect(stdout).toContain(`- apps/${appAfter}/src/app/app.component.html`);
|
||||
expect(stdout).toContain(
|
||||
`- apps/${appAfter}/src/app/app.component.spec.ts`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { output } from '@nrwl/workspace';
|
||||
import { execSync } from 'child_process';
|
||||
import { dirSync } from 'tmp';
|
||||
import { writeFileSync } from 'fs';
|
||||
import * as path from 'path';
|
||||
import { dirSync } from 'tmp';
|
||||
import * as yargsParser from 'yargs-parser';
|
||||
|
||||
const parsedArgs = yargsParser(process.argv, {
|
||||
@ -54,14 +55,19 @@ const projectName = parsedArgs._[2];
|
||||
|
||||
// check that the workspace name is passed in
|
||||
if (!projectName) {
|
||||
console.error(
|
||||
'Please provide a project name (e.g., create-nx-workspace nrwl-proj)'
|
||||
);
|
||||
output.error({
|
||||
title: 'A project name is required when creating a new workspace',
|
||||
bodyLines: [
|
||||
output.colors.gray('For example:'),
|
||||
'',
|
||||
`${output.colors.gray('>')} create-nx-workspace my-new-workspace`
|
||||
]
|
||||
});
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// creating the sandbox
|
||||
console.log(`Creating a sandbox with Nx...`);
|
||||
output.logSingleLine(`Creating a sandbox...`);
|
||||
const tmpDir = dirSync().name;
|
||||
|
||||
const nxVersion = 'NX_VERSION';
|
||||
@ -90,7 +96,13 @@ const args = process.argv
|
||||
.slice(2)
|
||||
.map(a => `"${a}"`)
|
||||
.join(' ');
|
||||
console.log(`ng new ${args} --collection=${nxTool.packageName}`);
|
||||
|
||||
output.logSingleLine(
|
||||
`${output.colors.gray('Running:')} ng new ${args} --collection=${
|
||||
nxTool.packageName
|
||||
}`
|
||||
);
|
||||
|
||||
execSync(
|
||||
`"${path.join(
|
||||
tmpDir,
|
||||
|
||||
@ -27,6 +27,7 @@
|
||||
},
|
||||
"homepage": "https://nx.dev",
|
||||
"dependencies": {
|
||||
"@nrwl/workspace": "*",
|
||||
"tmp": "0.0.33",
|
||||
"yargs-parser": "10.0.0",
|
||||
"yargs": "^11.0.0"
|
||||
|
||||
@ -19,6 +19,7 @@ export {
|
||||
ExistingPrettierConfig,
|
||||
resolveUserExistingPrettierConfig
|
||||
} from './src/utils/common';
|
||||
export { output } from './src/command-line/output';
|
||||
export {
|
||||
commandsObject,
|
||||
supportedNxCommands
|
||||
|
||||
@ -20,6 +20,7 @@ import {
|
||||
} from './shared';
|
||||
import { generateGraph } from './dep-graph';
|
||||
import { WorkspaceResults } from './workspace-results';
|
||||
import { output } from './output';
|
||||
|
||||
export interface YargsAffectedOptions
|
||||
extends yargs.Arguments,
|
||||
@ -70,7 +71,12 @@ export function affected(parsedArgs: YargsAffectedOptions): void {
|
||||
!parsedArgs.onlyFailed || !workspaceResults.getResult(project)
|
||||
);
|
||||
printArgsWarning(parsedArgs);
|
||||
console.log(apps.join(' '));
|
||||
if (apps.length) {
|
||||
output.log({
|
||||
title: 'Affected apps:',
|
||||
bodyLines: apps.map(app => `${output.colors.gray('-')} ${app}`)
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'libs':
|
||||
const libs = (parsedArgs.all
|
||||
@ -83,7 +89,12 @@ export function affected(parsedArgs: YargsAffectedOptions): void {
|
||||
!parsedArgs.onlyFailed || !workspaceResults.getResult(project)
|
||||
);
|
||||
printArgsWarning(parsedArgs);
|
||||
console.log(libs.join(' '));
|
||||
if (libs.length) {
|
||||
output.log({
|
||||
title: 'Affected libs:',
|
||||
bodyLines: libs.map(lib => `${output.colors.gray('-')} ${lib}`)
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'dep-graph':
|
||||
const projects = parsedArgs.all
|
||||
@ -105,16 +116,7 @@ export function affected(parsedArgs: YargsAffectedOptions): void {
|
||||
parsedArgs.all
|
||||
);
|
||||
printArgsWarning(parsedArgs);
|
||||
runCommand(
|
||||
target,
|
||||
targetProjects,
|
||||
parsedArgs,
|
||||
rest,
|
||||
workspaceResults,
|
||||
`Running ${target} for`,
|
||||
`Running ${target} for affected projects succeeded.`,
|
||||
`Running ${target} for affected projects failed.`
|
||||
);
|
||||
runCommand(target, targetProjects, parsedArgs, rest, workspaceResults);
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
@ -141,33 +143,50 @@ function getProjects(
|
||||
}
|
||||
|
||||
function printError(e: any, verbose?: boolean) {
|
||||
const bodyLines = [e.message];
|
||||
if (verbose && e.stack) {
|
||||
console.error(e.stack);
|
||||
} else {
|
||||
console.error(e.message);
|
||||
bodyLines.push('');
|
||||
bodyLines.push(e.stack);
|
||||
}
|
||||
output.error({
|
||||
title: 'There was a critical error when running your command',
|
||||
bodyLines
|
||||
});
|
||||
}
|
||||
|
||||
async function runCommand(
|
||||
command: string,
|
||||
targetName: string,
|
||||
projects: string[],
|
||||
parsedArgs: YargsAffectedOptions,
|
||||
args: string[],
|
||||
workspaceResults: WorkspaceResults,
|
||||
iterationMessage: string,
|
||||
successMessage: string,
|
||||
errorMessage: string
|
||||
workspaceResults: WorkspaceResults
|
||||
) {
|
||||
if (projects.length <= 0) {
|
||||
console.log(`No projects to run ${command}`);
|
||||
output.logSingleLine(
|
||||
`No affected projects to run target "${targetName}" on`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let message = `${iterationMessage} projects:\n ${projects.join(',\n ')}`;
|
||||
console.log(message);
|
||||
const bodyLines = projects.map(
|
||||
project => `${output.colors.gray('-')} ${project}`
|
||||
);
|
||||
if (args.length > 0) {
|
||||
console.log(`With flags: ${args.join(' ')}`);
|
||||
bodyLines.push('');
|
||||
bodyLines.push(
|
||||
`${output.colors.gray('With flags:')} ${output.bold(args.join(' '))}`
|
||||
);
|
||||
}
|
||||
|
||||
output.log({
|
||||
title: `${output.colors.gray(
|
||||
'Running target'
|
||||
)} ${targetName} ${output.colors.gray('for projects:')}`,
|
||||
bodyLines
|
||||
});
|
||||
|
||||
output.addVerticalSeparator();
|
||||
|
||||
const angularJson = readAngularJson();
|
||||
const projectMetadata = new Map<string, any>();
|
||||
projects.forEach(project => {
|
||||
@ -179,22 +198,32 @@ async function runCommand(
|
||||
fs.readFileSync('./package.json').toString('utf-8')
|
||||
);
|
||||
if (!packageJson.scripts || !packageJson.scripts.ng) {
|
||||
console.error(
|
||||
'\nError: Your `package.json` file should contain the `ng: "ng"` command in the `scripts` section.\n'
|
||||
);
|
||||
output.error({
|
||||
title:
|
||||
'The "scripts" section of your `package.json` must contain `"ng": "ng"`',
|
||||
bodyLines: [
|
||||
output.colors.gray('...'),
|
||||
' "scripts": {',
|
||||
output.colors.gray(' ...'),
|
||||
' "ng": "ng"',
|
||||
output.colors.gray(' ...'),
|
||||
' }',
|
||||
output.colors.gray('...')
|
||||
]
|
||||
});
|
||||
return process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
await runAll(
|
||||
projects.map(app => {
|
||||
return ngCommands.includes(command)
|
||||
? `ng -- ${command} --project=${app} ${transformArgs(
|
||||
return ngCommands.includes(targetName)
|
||||
? `ng -- ${targetName} --project=${app} ${transformArgs(
|
||||
args,
|
||||
app,
|
||||
projectMetadata.get(app)
|
||||
).join(' ')} `
|
||||
: `ng -- run ${app}:${command} ${transformArgs(
|
||||
: `ng -- run ${app}:${targetName} ${transformArgs(
|
||||
args,
|
||||
app,
|
||||
projectMetadata.get(app)
|
||||
@ -226,8 +255,8 @@ async function runCommand(
|
||||
workspaceResults.saveResults();
|
||||
workspaceResults.printResults(
|
||||
parsedArgs.onlyFailed,
|
||||
successMessage,
|
||||
errorMessage
|
||||
`Running target "${targetName}" for affected projects succeeded`,
|
||||
`Running target "${targetName}" for affected projects failed`
|
||||
);
|
||||
|
||||
if (workspaceResults.hasFailure) {
|
||||
|
||||
@ -77,7 +77,6 @@ type ParsedUserOptions = {
|
||||
type OutputOptions = {
|
||||
data: string;
|
||||
shouldOpen: boolean;
|
||||
shouldWriteToFile: boolean;
|
||||
filename?: string;
|
||||
};
|
||||
|
||||
@ -248,23 +247,15 @@ export function createGraphviz(
|
||||
return g.to_dot();
|
||||
}
|
||||
|
||||
function handleOutput({
|
||||
data,
|
||||
shouldOpen,
|
||||
shouldWriteToFile,
|
||||
filename
|
||||
}: OutputOptions) {
|
||||
function handleOutput({ data, shouldOpen, filename }: OutputOptions) {
|
||||
if (shouldOpen) {
|
||||
const tmpFilename = `${tmpNameSync()}.html`;
|
||||
writeToFile(tmpFilename, data);
|
||||
opn(tmpFilename, {
|
||||
wait: false
|
||||
});
|
||||
} else if (!shouldWriteToFile) {
|
||||
return console.log(data);
|
||||
} else {
|
||||
writeToFile(filename, data);
|
||||
}
|
||||
writeToFile(filename, data);
|
||||
}
|
||||
|
||||
function applyHTMLTemplate(svg: string) {
|
||||
@ -361,7 +352,6 @@ export function generateGraph(
|
||||
handleOutput({
|
||||
data: extractDataFromJson(projects, json, config.type),
|
||||
filename: config.filename,
|
||||
shouldWriteToFile: config.isFilePresent,
|
||||
shouldOpen: config.shouldOpen
|
||||
});
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ import { getProjectRoots, parseFiles, printArgsWarning } from './shared';
|
||||
import { YargsAffectedOptions } from './affected';
|
||||
import { getTouchedProjects } from './touched';
|
||||
import { fileExists } from '../utils/fileutils';
|
||||
import { output } from './output';
|
||||
|
||||
export interface YargsFormatOptions extends YargsAffectedOptions {
|
||||
libsAndApps?: boolean;
|
||||
@ -27,7 +28,18 @@ export function format(command: 'check' | 'write', args: YargsFormatOptions) {
|
||||
try {
|
||||
patterns = getPatterns(args);
|
||||
} catch (e) {
|
||||
printError(command, e);
|
||||
output.error({
|
||||
title: e.message,
|
||||
bodyLines: [
|
||||
`Pass the SHA range: ${output.bold(
|
||||
`npm run format:${command} -- SHA1 SHA2`
|
||||
)}`,
|
||||
'',
|
||||
`Or pass the list of files: ${output.bold(
|
||||
`npm run format:${command} -- --files="libs/mylib/index.ts,libs/mylib2/index.ts"`
|
||||
)}`
|
||||
]
|
||||
});
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@ -86,16 +98,6 @@ function getPatternsWithPathPrefix(prefixes: string[]): string[] {
|
||||
);
|
||||
}
|
||||
|
||||
function printError(command: string, e: any) {
|
||||
console.error(
|
||||
`Pass the SHA range, as follows: npm run format:${command} -- SHA1 SHA2.`
|
||||
);
|
||||
console.error(
|
||||
`Or pass the list of files, as follows: npm run format:${command} -- --files="libs/mylib/index.ts,libs/mylib2/index.ts".`
|
||||
);
|
||||
console.error(e.message);
|
||||
}
|
||||
|
||||
function write(patterns: string[]) {
|
||||
if (patterns.length > 0) {
|
||||
execSync(`node "${prettierPath()}" --write ${patterns.join(' ')}`, {
|
||||
|
||||
@ -7,19 +7,19 @@ import {
|
||||
import { WorkspaceIntegrityChecks } from './workspace-integrity-checks';
|
||||
import * as path from 'path';
|
||||
import { appRootPath } from '../utils/app-root';
|
||||
import { output } from './output';
|
||||
|
||||
export function lint() {
|
||||
export function workspaceLint() {
|
||||
const nodes = getProjectNodes(readAngularJson(), readNxJson());
|
||||
|
||||
const errorGroups = new WorkspaceIntegrityChecks(
|
||||
const cliErrorOutputConfigs = new WorkspaceIntegrityChecks(
|
||||
nodes,
|
||||
readAllFilesFromAppsAndLibs()
|
||||
).run();
|
||||
if (errorGroups.length > 0) {
|
||||
errorGroups.forEach(g => {
|
||||
console.error(`${g.header}:`);
|
||||
g.errors.forEach(e => console.error(e));
|
||||
console.log('');
|
||||
|
||||
if (cliErrorOutputConfigs.length > 0) {
|
||||
cliErrorOutputConfigs.forEach(errorConfig => {
|
||||
output.error(errorConfig);
|
||||
});
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ import * as yargs from 'yargs';
|
||||
|
||||
import { affected } from './affected';
|
||||
import { format } from './format';
|
||||
import { lint } from './lint';
|
||||
import { workspaceLint } from './lint';
|
||||
import { workspaceSchematic } from './workspace-schematic';
|
||||
import { generateGraph, OutputType } from './dep-graph';
|
||||
|
||||
@ -136,7 +136,7 @@ export const commandsObject = yargs
|
||||
'workspace-lint [files..]',
|
||||
'Lint workspace or list of files',
|
||||
noop,
|
||||
_ => lint()
|
||||
_ => workspaceLint()
|
||||
)
|
||||
.command(
|
||||
'workspace-schematic [name]',
|
||||
|
||||
190
packages/workspace/src/command-line/output.ts
Normal file
190
packages/workspace/src/command-line/output.ts
Normal file
@ -0,0 +1,190 @@
|
||||
import chalk from 'chalk';
|
||||
|
||||
export interface CLIErrorMessageConfig {
|
||||
title: string;
|
||||
bodyLines?: string[];
|
||||
slug?: string;
|
||||
}
|
||||
|
||||
export interface CLIWarnMessageConfig {
|
||||
title: string;
|
||||
bodyLines?: string[];
|
||||
slug?: string;
|
||||
}
|
||||
|
||||
export interface CLILogMessageConfig {
|
||||
title: string;
|
||||
bodyLines?: string[];
|
||||
}
|
||||
|
||||
export interface CLINoteMessageConfig {
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface CLISuccessMessageConfig {
|
||||
title: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically disable styling applied by chalk if CI=true
|
||||
*/
|
||||
if (process.env.CI === 'true') {
|
||||
chalk.level = 0;
|
||||
}
|
||||
|
||||
class CLIOutput {
|
||||
private readonly NX_PREFIX = `${chalk.cyan(
|
||||
'>'
|
||||
)} ${chalk.reset.inverse.bold.cyan(' NX ')}`;
|
||||
/**
|
||||
* 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 =
|
||||
'———————————————————————————————————————————————';
|
||||
|
||||
/**
|
||||
* Expose some color and other utility functions so that other parts of the codebase that need
|
||||
* more fine-grained control of message bodies are still using a centralized
|
||||
* implementation.
|
||||
*/
|
||||
colors = {
|
||||
gray: chalk.gray
|
||||
};
|
||||
bold = chalk.bold;
|
||||
underline = chalk.underline;
|
||||
|
||||
private writeToStdOut(str: string) {
|
||||
process.stdout.write(str);
|
||||
}
|
||||
|
||||
private writeOutputTitle({
|
||||
label,
|
||||
title
|
||||
}: {
|
||||
label?: 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);
|
||||
}
|
||||
|
||||
private writeOptionalOutputBody(bodyLines?: string[]): void {
|
||||
if (!bodyLines) {
|
||||
return;
|
||||
}
|
||||
this.addNewline();
|
||||
bodyLines.forEach(bodyLine => this.writeToStdOut(' ' + bodyLine + '\n'));
|
||||
}
|
||||
|
||||
addNewline() {
|
||||
this.writeToStdOut('\n');
|
||||
}
|
||||
|
||||
addVerticalSeparator() {
|
||||
this.writeToStdOut(`\n${chalk.gray(this.VERTICAL_SEPARATOR)}\n\n`);
|
||||
}
|
||||
|
||||
error({ title, slug, bodyLines }: CLIErrorMessageConfig) {
|
||||
this.addNewline();
|
||||
|
||||
this.writeOutputTitle({
|
||||
label: chalk.reset.inverse.bold.red(' ERROR '),
|
||||
title: chalk.bold.red(title)
|
||||
});
|
||||
|
||||
this.writeOptionalOutputBody(bodyLines);
|
||||
|
||||
/**
|
||||
* Optional slug to be used in an Nx error message redirect URL
|
||||
*/
|
||||
if (slug && typeof slug === 'string') {
|
||||
this.addNewline();
|
||||
this.writeToStdOut(
|
||||
chalk.grey(' ' + 'Learn more about this error: ') +
|
||||
'https://errors.nx.dev/' +
|
||||
slug +
|
||||
'\n'
|
||||
);
|
||||
}
|
||||
|
||||
this.addNewline();
|
||||
}
|
||||
|
||||
warn({ title, slug, bodyLines }: CLIWarnMessageConfig) {
|
||||
this.addNewline();
|
||||
|
||||
this.writeOutputTitle({
|
||||
label: chalk.reset.inverse.bold.yellow(' WARNING '),
|
||||
title: chalk.bold.yellow(title)
|
||||
});
|
||||
|
||||
this.writeOptionalOutputBody(bodyLines);
|
||||
|
||||
/**
|
||||
* Optional slug to be used in an Nx warning message redirect URL
|
||||
*/
|
||||
if (slug && typeof slug === 'string') {
|
||||
this.addNewline();
|
||||
this.writeToStdOut(
|
||||
chalk.grey(' ' + 'Learn more about this warning: ') +
|
||||
'https://errors.nx.dev/' +
|
||||
slug +
|
||||
'\n'
|
||||
);
|
||||
}
|
||||
|
||||
this.addNewline();
|
||||
}
|
||||
|
||||
note({ title }: CLINoteMessageConfig) {
|
||||
this.addNewline();
|
||||
|
||||
this.writeOutputTitle({
|
||||
label: chalk.reset.inverse.bold.keyword('orange')(' NOTE '),
|
||||
title: chalk.bold.keyword('orange')(title)
|
||||
});
|
||||
|
||||
this.addNewline();
|
||||
}
|
||||
|
||||
success({ title }: CLISuccessMessageConfig) {
|
||||
this.addNewline();
|
||||
|
||||
this.writeOutputTitle({
|
||||
label: chalk.reset.inverse.bold.green(' SUCCESS '),
|
||||
title: chalk.bold.green(title)
|
||||
});
|
||||
|
||||
this.addNewline();
|
||||
}
|
||||
|
||||
logSingleLine(message: string) {
|
||||
this.addNewline();
|
||||
|
||||
this.writeOutputTitle({
|
||||
title: message
|
||||
});
|
||||
|
||||
this.addNewline();
|
||||
}
|
||||
|
||||
log({ title, bodyLines }: CLIWarnMessageConfig) {
|
||||
this.addNewline();
|
||||
|
||||
this.writeOutputTitle({
|
||||
title: chalk.white(title)
|
||||
});
|
||||
|
||||
this.writeOptionalOutputBody(bodyLines);
|
||||
|
||||
this.addNewline();
|
||||
}
|
||||
}
|
||||
|
||||
export const output = new CLIOutput();
|
||||
@ -15,6 +15,7 @@ import { YargsAffectedOptions } from './affected';
|
||||
import { readDependencies, DepGraph, Deps } from './deps-calculator';
|
||||
import { touchedProjects } from './touched';
|
||||
import { appRootPath } from '../utils/app-root';
|
||||
import { output } from './output';
|
||||
|
||||
const ignore = require('ignore');
|
||||
|
||||
@ -57,27 +58,25 @@ export function printArgsWarning(options: YargsAffectedOptions) {
|
||||
!all &&
|
||||
options._.length < 2
|
||||
) {
|
||||
console.log('Note: Nx defaulted to --base=master --head=HEAD');
|
||||
output.note({
|
||||
title: `Affected criteria defaulted to --base=${output.bold(
|
||||
'master'
|
||||
)} --head=${output.bold('HEAD')}`
|
||||
});
|
||||
}
|
||||
|
||||
if (all) {
|
||||
console.warn(
|
||||
'****************************************************************************************'
|
||||
);
|
||||
console.warn('WARNING:');
|
||||
console.warn(
|
||||
'Running affected:* commands with --all can result in very slow builds.'
|
||||
);
|
||||
console.warn(
|
||||
'It is not meant to be used for any sizable project or to be used in CI.'
|
||||
);
|
||||
console.warn(
|
||||
'Read about rebuilding and retesting only what is affected here:'
|
||||
);
|
||||
console.warn('https://nx.dev/guides/monorepo-affected.');
|
||||
console.warn(
|
||||
'****************************************************************************************'
|
||||
);
|
||||
output.warn({
|
||||
title: `Running affected:* commands with --all can result in very slow builds.`,
|
||||
bodyLines: [
|
||||
output.bold('--all') +
|
||||
' is not meant to be used for any sizable project or to be used in CI.',
|
||||
'',
|
||||
output.colors.gray(
|
||||
'Learn more about checking only what is affected: '
|
||||
) + 'https://nx.dev/guides/monorepo-affected.'
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import { WorkspaceIntegrityChecks } from './workspace-integrity-checks';
|
||||
import { ProjectType } from './affected-apps';
|
||||
import chalk from 'chalk';
|
||||
|
||||
describe('WorkspaceIntegrityChecks', () => {
|
||||
describe('.angular-cli.json is in sync with the filesystem', () => {
|
||||
describe('angular.json is in sync with the filesystem', () => {
|
||||
it('should not error when they are in sync', () => {
|
||||
const c = new WorkspaceIntegrityChecks(
|
||||
[
|
||||
@ -54,10 +55,16 @@ describe('WorkspaceIntegrityChecks', () => {
|
||||
);
|
||||
|
||||
const errors = c.run();
|
||||
expect(errors.length).toEqual(1);
|
||||
expect(errors[0].errors[0]).toEqual(
|
||||
`Cannot find project 'project1' in 'libs/project1'`
|
||||
);
|
||||
expect(errors).toEqual([
|
||||
{
|
||||
bodyLines: [
|
||||
`${chalk.grey(
|
||||
'-'
|
||||
)} Cannot find project 'project1' in 'libs/project1'`
|
||||
],
|
||||
title: 'The angular.json file is out of sync'
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it('should error when there are files in apps or libs without projects', () => {
|
||||
@ -80,10 +87,12 @@ describe('WorkspaceIntegrityChecks', () => {
|
||||
);
|
||||
|
||||
const errors = c.run();
|
||||
expect(errors.length).toEqual(1);
|
||||
expect(errors[0].errors[0]).toEqual(
|
||||
`The 'libs/project2/src/index.ts' file doesn't belong to any project.`
|
||||
);
|
||||
expect(errors).toEqual([
|
||||
{
|
||||
bodyLines: [`${chalk.grey('-')} libs/project2/src/index.ts`],
|
||||
title: 'The following file(s) do not belong to any projects:'
|
||||
}
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,28 +1,37 @@
|
||||
import { ProjectNode } from './affected-apps';
|
||||
|
||||
export interface ErrorGroup {
|
||||
header: string;
|
||||
errors: string[];
|
||||
}
|
||||
import { output, CLIErrorMessageConfig } from './output';
|
||||
|
||||
export class WorkspaceIntegrityChecks {
|
||||
constructor(private projectNodes: ProjectNode[], private files: string[]) {}
|
||||
|
||||
run(): ErrorGroup[] {
|
||||
run(): CLIErrorMessageConfig[] {
|
||||
return [...this.projectWithoutFilesCheck(), ...this.filesWithoutProjects()];
|
||||
}
|
||||
|
||||
private projectWithoutFilesCheck(): ErrorGroup[] {
|
||||
private projectWithoutFilesCheck(): CLIErrorMessageConfig[] {
|
||||
const errors = this.projectNodes
|
||||
.filter(n => n.files.length === 0)
|
||||
.map(p => `Cannot find project '${p.name}' in '${p.root}'`);
|
||||
|
||||
const errorGroupBodyLines = errors.map(
|
||||
f => `${output.colors.gray('-')} ${f}`
|
||||
);
|
||||
|
||||
return errors.length === 0
|
||||
? []
|
||||
: [{ header: 'The angular.json file is out of sync', errors }];
|
||||
: [
|
||||
{
|
||||
title: 'The angular.json file is out of sync',
|
||||
bodyLines: errorGroupBodyLines
|
||||
/**
|
||||
* TODO(JamesHenry): Add support for error documentation
|
||||
*/
|
||||
// slug: 'project-has-no-files'
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
private filesWithoutProjects(): ErrorGroup[] {
|
||||
private filesWithoutProjects(): CLIErrorMessageConfig[] {
|
||||
const allFilesFromProjects = this.allProjectFiles();
|
||||
const allFilesWithoutProjects = minus(this.files, allFilesFromProjects);
|
||||
const first5FilesWithoutProjects =
|
||||
@ -30,16 +39,20 @@ export class WorkspaceIntegrityChecks {
|
||||
? allFilesWithoutProjects.slice(0, 5)
|
||||
: allFilesWithoutProjects;
|
||||
|
||||
const errors = first5FilesWithoutProjects.map(
|
||||
p => `The '${p}' file doesn't belong to any project.`
|
||||
const errorGroupBodyLines = first5FilesWithoutProjects.map(
|
||||
f => `${output.colors.gray('-')} ${f}`
|
||||
);
|
||||
|
||||
return errors.length === 0
|
||||
return first5FilesWithoutProjects.length === 0
|
||||
? []
|
||||
: [
|
||||
{
|
||||
header: `All files in 'apps' and 'libs' must be part of a project`,
|
||||
errors
|
||||
title: `The following file(s) do not belong to any projects:`,
|
||||
bodyLines: errorGroupBodyLines
|
||||
/**
|
||||
* TODO(JamesHenry): Add support for error documentation
|
||||
*/
|
||||
// slug: 'file-does-not-belong-to-project'
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ import * as fs from 'fs';
|
||||
import { WorkspaceResults } from './workspace-results';
|
||||
import { serializeJson } from '../utils/fileutils';
|
||||
import { stripIndents } from '@angular-devkit/core/src/utils/literals';
|
||||
import { output } from './output';
|
||||
|
||||
describe('WorkspacesResults', () => {
|
||||
let results: WorkspaceResults;
|
||||
@ -39,25 +40,46 @@ describe('WorkspacesResults', () => {
|
||||
});
|
||||
|
||||
it('should print results', () => {
|
||||
results.success('proj');
|
||||
spyOn(console, 'log');
|
||||
const projectName = 'proj';
|
||||
results.success(projectName);
|
||||
spyOn(output, 'success');
|
||||
|
||||
results.printResults(false, 'Success', 'Fail');
|
||||
const successTitle = 'Success';
|
||||
|
||||
expect(console.log).toHaveBeenCalledWith('Success');
|
||||
results.printResults(false, successTitle, 'Fail');
|
||||
|
||||
expect(output.success).toHaveBeenCalledWith({
|
||||
title: successTitle
|
||||
});
|
||||
});
|
||||
|
||||
it('should tell warn the user that not all tests were run', () => {
|
||||
it('should warn the user that not all tests were run', () => {
|
||||
(<any>results).startedWithFailedProjects = true;
|
||||
results.success('proj');
|
||||
spyOn(console, 'warn');
|
||||
|
||||
results.printResults(true, 'Success', 'Fail');
|
||||
const projectName = 'proj';
|
||||
spyOn(output, 'success');
|
||||
spyOn(output, 'warn');
|
||||
|
||||
expect(console.warn).toHaveBeenCalledWith(stripIndents`
|
||||
Warning: Only failed affected projects were run.
|
||||
You should run above command WITHOUT --only-failed
|
||||
`);
|
||||
results.success(projectName);
|
||||
|
||||
const successTitle = 'Success';
|
||||
|
||||
results.printResults(true, successTitle, 'Fail');
|
||||
|
||||
expect(output.success).toHaveBeenCalledWith({
|
||||
title: successTitle
|
||||
});
|
||||
|
||||
expect(output.warn).toHaveBeenCalledWith({
|
||||
title: `Only affected projects ${output.underline(
|
||||
'which had previously failed'
|
||||
)} were run`,
|
||||
bodyLines: [
|
||||
`You should verify by running ${output.underline(
|
||||
'without'
|
||||
)} ${output.bold('--only-failed')}`
|
||||
]
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -86,23 +108,45 @@ describe('WorkspacesResults', () => {
|
||||
});
|
||||
|
||||
it('should print results', () => {
|
||||
results.fail('proj');
|
||||
spyOn(console, 'error');
|
||||
const projectName = 'proj';
|
||||
results.fail(projectName);
|
||||
spyOn(output, 'error');
|
||||
|
||||
results.printResults(true, 'Success', 'Fail');
|
||||
const errorTitle = 'Fail';
|
||||
|
||||
expect(console.error).toHaveBeenCalledWith('Fail');
|
||||
results.printResults(true, 'Success', errorTitle);
|
||||
|
||||
expect(output.error).toHaveBeenCalledWith({
|
||||
title: errorTitle,
|
||||
bodyLines: [
|
||||
output.colors.gray('Failed projects:'),
|
||||
'',
|
||||
`${output.colors.gray('-')} ${projectName}`
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('should tell warn the user that not all tests were run', () => {
|
||||
results.fail('proj');
|
||||
spyOn(console, 'log');
|
||||
it('should tell the user that they can isolate only the failed tests', () => {
|
||||
const projectName = 'proj';
|
||||
results.fail(projectName);
|
||||
spyOn(output, 'error');
|
||||
|
||||
results.printResults(false, 'Success', 'Fail');
|
||||
const errorTitle = 'Fail';
|
||||
|
||||
expect(console.log).toHaveBeenCalledWith(
|
||||
`You can isolate the above projects by passing --only-failed`
|
||||
);
|
||||
results.printResults(false, 'Success', errorTitle);
|
||||
|
||||
expect(output.error).toHaveBeenCalledWith({
|
||||
title: errorTitle,
|
||||
bodyLines: [
|
||||
output.colors.gray('Failed projects:'),
|
||||
'',
|
||||
`${output.colors.gray('-')} ${projectName}`,
|
||||
'',
|
||||
`${output.colors.gray(
|
||||
'You can isolate the above projects by passing:'
|
||||
)} ${output.bold('--only-failed')}`
|
||||
]
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import * as fs from 'fs';
|
||||
import { readJsonFile, writeJsonFile } from '../utils/fileutils';
|
||||
import { unlinkSync } from 'fs';
|
||||
import { stripIndents } from '@angular-devkit/core/src/utils/literals';
|
||||
import { output } from './output';
|
||||
|
||||
const RESULTS_FILE = 'dist/.nx-results';
|
||||
|
||||
@ -37,10 +37,12 @@ export class WorkspaceResults {
|
||||
if (this.startedWithFailedProjects) {
|
||||
this.commandResults = commandResults;
|
||||
}
|
||||
} catch (e) {
|
||||
// RESULTS_FILE is likely not valid JSON
|
||||
console.error('Error: .nx-results file is corrupted.');
|
||||
console.error(e);
|
||||
} catch {
|
||||
/**
|
||||
* If we got here it is likely that RESULTS_FILE is not valid JSON.
|
||||
* It is safe to continue, and it does not make much sense to give the
|
||||
* user feedback as the file will be updated automatically.
|
||||
*/
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -70,24 +72,52 @@ export class WorkspaceResults {
|
||||
successMessage: string,
|
||||
failureMessage: string
|
||||
) {
|
||||
const failedProjects = this.failedProjects;
|
||||
/**
|
||||
* Leave a bit of breathing room between the process output
|
||||
* and our formatted results.
|
||||
*/
|
||||
output.addNewline();
|
||||
output.addVerticalSeparator();
|
||||
|
||||
if (this.failedProjects.length === 0) {
|
||||
console.log(successMessage);
|
||||
output.success({
|
||||
title: successMessage
|
||||
});
|
||||
|
||||
if (onlyFailed && this.startedWithFailedProjects) {
|
||||
console.warn(stripIndents`
|
||||
Warning: Only failed affected projects were run.
|
||||
You should run above command WITHOUT --only-failed
|
||||
`);
|
||||
}
|
||||
} else {
|
||||
console.error(failureMessage);
|
||||
console.log(`Failed projects: ${failedProjects.join(',')}`);
|
||||
if (!onlyFailed && !this.startedWithFailedProjects) {
|
||||
console.log(
|
||||
`You can isolate the above projects by passing --only-failed`
|
||||
);
|
||||
output.warn({
|
||||
title: `Only affected projects ${output.underline(
|
||||
'which had previously failed'
|
||||
)} were run`,
|
||||
bodyLines: [
|
||||
`You should verify by running ${output.underline(
|
||||
'without'
|
||||
)} ${output.bold('--only-failed')}`
|
||||
]
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const bodyLines = [
|
||||
output.colors.gray('Failed projects:'),
|
||||
'',
|
||||
...this.failedProjects.map(
|
||||
project => `${output.colors.gray('-')} ${project}`
|
||||
)
|
||||
];
|
||||
if (!onlyFailed && !this.startedWithFailedProjects) {
|
||||
bodyLines.push('');
|
||||
bodyLines.push(
|
||||
`${output.colors.gray(
|
||||
'You can isolate the above projects by passing:'
|
||||
)} ${output.bold('--only-failed')}`
|
||||
);
|
||||
}
|
||||
output.error({
|
||||
title: failureMessage,
|
||||
bodyLines
|
||||
});
|
||||
}
|
||||
|
||||
private setResult(projectName: string, result: boolean) {
|
||||
|
||||
@ -23,6 +23,7 @@ import * as path from 'path';
|
||||
import * as yargsParser from 'yargs-parser';
|
||||
import { fileExists } from '../utils/fileutils';
|
||||
import { appRootPath } from '../utils/app-root';
|
||||
import { output } from './output';
|
||||
|
||||
const rootDirectory = appRootPath;
|
||||
|
||||
@ -204,6 +205,10 @@ async function executeSchematic(
|
||||
outDir: string,
|
||||
logger: logging.Logger
|
||||
) {
|
||||
output.logSingleLine(
|
||||
`${output.colors.gray(`Executing your local schematic`)}: ${schematicName}`
|
||||
);
|
||||
|
||||
let nothingDone = true;
|
||||
workflow.reporter.subscribe((event: any) => {
|
||||
nothingDone = false;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user