chore(workspace): improved logging design and consistency

This commit is contained in:
James Henry 2019-07-22 13:42:42 -04:00 committed by Victor Savkin
parent 52885744d9
commit 8271c7650e
17 changed files with 537 additions and 186 deletions

View File

@ -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);
});

View File

@ -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`
);
});
});

View File

@ -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,

View File

@ -27,6 +27,7 @@
},
"homepage": "https://nx.dev",
"dependencies": {
"@nrwl/workspace": "*",
"tmp": "0.0.33",
"yargs-parser": "10.0.0",
"yargs": "^11.0.0"

View File

@ -19,6 +19,7 @@ export {
ExistingPrettierConfig,
resolveUserExistingPrettierConfig
} from './src/utils/common';
export { output } from './src/command-line/output';
export {
commandsObject,
supportedNxCommands

View File

@ -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) {

View File

@ -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
});
}

View File

@ -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(' ')}`, {

View File

@ -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);
}

View File

@ -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]',

View 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();

View File

@ -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.'
]
});
}
}

View File

@ -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:'
}
]);
});
});
});

View File

@ -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'
}
];
}

View File

@ -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')}`
]
});
});
});

View File

@ -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) {

View File

@ -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;