chore(workspace): improved logging design and consistency
This commit is contained in:
parent
52885744d9
commit
8271c7650e
@ -9,7 +9,21 @@ import {
|
|||||||
runCLI
|
runCLI
|
||||||
} from './utils';
|
} from './utils';
|
||||||
|
|
||||||
|
let originalCIValue;
|
||||||
|
|
||||||
describe('Affected', () => {
|
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', () => {
|
it('should print, build, and test affected apps', () => {
|
||||||
ensureProject();
|
ensureProject();
|
||||||
const myapp = uniq('myapp');
|
const myapp = uniq('myapp');
|
||||||
@ -89,9 +103,10 @@ describe('Affected', () => {
|
|||||||
const build = runCommand(
|
const build = runCommand(
|
||||||
`npm run affected:build -- --files="libs/${mylib}/src/index.ts"`
|
`npm run affected:build -- --files="libs/${mylib}/src/index.ts"`
|
||||||
);
|
);
|
||||||
expect(build).toContain(
|
expect(build).toContain(`Running target build for projects:`);
|
||||||
`Running build for projects:\n ${myapp},\n ${mypublishablelib}`
|
expect(build).toContain(myapp);
|
||||||
);
|
expect(build).toContain(mypublishablelib);
|
||||||
|
|
||||||
expect(build).not.toContain('is not registered with the build command');
|
expect(build).not.toContain('is not registered with the build command');
|
||||||
expect(build).not.toContain('with flags:');
|
expect(build).not.toContain('with flags:');
|
||||||
|
|
||||||
@ -99,28 +114,26 @@ describe('Affected', () => {
|
|||||||
const buildParallel = runCommand(
|
const buildParallel = runCommand(
|
||||||
`npm run affected:build -- --files="libs/${mylib}/src/index.ts" --parallel`
|
`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(
|
expect(buildParallel).toContain(
|
||||||
`Running build for projects:\n ${myapp},\n ${mypublishablelib}`
|
'Running target "build" for affected projects succeeded'
|
||||||
);
|
|
||||||
expect(buildParallel).toContain(
|
|
||||||
'Running build for affected projects succeeded.'
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const buildExcluded = runCommand(
|
const buildExcluded = runCommand(
|
||||||
`npm run affected:build -- --files="libs/${mylib}/src/index.ts" --exclude ${myapp}`
|
`npm run affected:build -- --files="libs/${mylib}/src/index.ts" --exclude ${myapp}`
|
||||||
);
|
);
|
||||||
expect(buildExcluded).toContain(
|
expect(buildExcluded).toContain(`Running target build for projects:`);
|
||||||
`Running build for projects:\n ${mypublishablelib}`
|
expect(buildExcluded).toContain(mypublishablelib);
|
||||||
);
|
|
||||||
|
|
||||||
// affected:build should pass non-nx flags to the CLI
|
// affected:build should pass non-nx flags to the CLI
|
||||||
const buildWithFlags = runCommand(
|
const buildWithFlags = runCommand(
|
||||||
`npm run affected:build -- --files="libs/${mylib}/src/index.ts" --stats-json`
|
`npm run affected:build -- --files="libs/${mylib}/src/index.ts" --stats-json`
|
||||||
);
|
);
|
||||||
|
expect(buildWithFlags).toContain(`Running target build for projects:`);
|
||||||
expect(buildWithFlags).toContain(
|
expect(buildWithFlags).toContain(myapp);
|
||||||
`Running build for projects:\n ${myapp},\n ${mypublishablelib}`
|
expect(buildWithFlags).toContain(mypublishablelib);
|
||||||
);
|
|
||||||
expect(buildWithFlags).toContain('With flags: --stats-json=true');
|
expect(buildWithFlags).toContain('With flags: --stats-json=true');
|
||||||
|
|
||||||
if (!runsInWSL()) {
|
if (!runsInWSL()) {
|
||||||
@ -133,9 +146,10 @@ describe('Affected', () => {
|
|||||||
const unitTests = runCommand(
|
const unitTests = runCommand(
|
||||||
`npm run affected:test -- --files="libs/${mylib}/src/index.ts"`
|
`npm run affected:test -- --files="libs/${mylib}/src/index.ts"`
|
||||||
);
|
);
|
||||||
expect(unitTests).toContain(
|
expect(unitTests).toContain(`Running target test for projects:`);
|
||||||
`Running test for projects:\n ${mylib},\n ${myapp},\n ${mypublishablelib}`
|
expect(unitTests).toContain(mylib);
|
||||||
);
|
expect(unitTests).toContain(myapp);
|
||||||
|
expect(unitTests).toContain(mypublishablelib);
|
||||||
|
|
||||||
// Fail a Unit Test
|
// Fail a Unit Test
|
||||||
updateFile(
|
updateFile(
|
||||||
@ -149,12 +163,15 @@ describe('Affected', () => {
|
|||||||
const failedTests = runCommand(
|
const failedTests = runCommand(
|
||||||
`npm run affected:test -- --files="libs/${mylib}/src/index.ts"`
|
`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(
|
expect(failedTests).toContain(
|
||||||
`Running test for projects:\n ${mylib},\n ${mypublishablelib},\n ${myapp}`
|
'You can isolate the above projects by passing: --only-failed'
|
||||||
);
|
|
||||||
expect(failedTests).toContain(`Failed projects: ${myapp}`);
|
|
||||||
expect(failedTests).toContain(
|
|
||||||
'You can isolate the above projects by passing --only-failed'
|
|
||||||
);
|
);
|
||||||
expect(readJson('dist/.nx-results')).toEqual({
|
expect(readJson('dist/.nx-results')).toEqual({
|
||||||
command: 'test',
|
command: 'test',
|
||||||
@ -177,14 +194,17 @@ describe('Affected', () => {
|
|||||||
const isolatedTests = runCommand(
|
const isolatedTests = runCommand(
|
||||||
`npm run affected:test -- --files="libs/${mylib}/src/index.ts" --only-failed`
|
`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(
|
const linting = runCommand(
|
||||||
`npm run affected:lint -- --files="libs/${mylib}/src/index.ts"`
|
`npm run affected:lint -- --files="libs/${mylib}/src/index.ts"`
|
||||||
);
|
);
|
||||||
expect(linting).toContain(
|
expect(linting).toContain(`Running target lint for projects:`);
|
||||||
`Running lint for projects:\n ${mylib},\n ${myapp},\n ${myapp}-e2e,\n ${mypublishablelib}`
|
expect(linting).toContain(mylib);
|
||||||
);
|
expect(linting).toContain(myapp);
|
||||||
|
expect(linting).toContain(`${myapp}-e2e`);
|
||||||
|
expect(linting).toContain(mypublishablelib);
|
||||||
|
|
||||||
const lintWithJsonFormating = runCommand(
|
const lintWithJsonFormating = runCommand(
|
||||||
`npm run affected:lint -- --files="libs/${mylib}/src/index.ts" -- --format json`
|
`npm run affected:lint -- --files="libs/${mylib}/src/index.ts" -- --format json`
|
||||||
@ -194,20 +214,20 @@ describe('Affected', () => {
|
|||||||
const unitTestsExcluded = runCommand(
|
const unitTestsExcluded = runCommand(
|
||||||
`npm run affected:test -- --files="libs/${mylib}/src/index.ts" --exclude=${myapp},${mypublishablelib}`
|
`npm run affected:test -- --files="libs/${mylib}/src/index.ts" --exclude=${myapp},${mypublishablelib}`
|
||||||
);
|
);
|
||||||
expect(unitTestsExcluded).toContain(
|
expect(unitTestsExcluded).toContain(`Running target test for projects:`);
|
||||||
`Running test for projects:\n ${mylib}`
|
expect(unitTestsExcluded).toContain(mylib);
|
||||||
);
|
|
||||||
|
|
||||||
const i18n = runCommand(
|
const i18n = runCommand(
|
||||||
`npm run affected -- --target extract-i18n --files="libs/${mylib}/src/index.ts"`
|
`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(
|
const interpolatedTests = runCommand(
|
||||||
`npm run affected -- --target test --files="libs/${mylib}/src/index.ts" -- --jest-config {project.root}/jest.config.js`
|
`npm run affected -- --target test --files="libs/${mylib}/src/index.ts" -- --jest-config {project.root}/jest.config.js`
|
||||||
);
|
);
|
||||||
expect(interpolatedTests).toContain(
|
expect(interpolatedTests).toContain(
|
||||||
`Running test for affected projects succeeded.`
|
`Running target "test" for affected projects succeeded`
|
||||||
);
|
);
|
||||||
}, 1000000);
|
}, 1000000);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -75,10 +75,16 @@ describe('Command line', () => {
|
|||||||
|
|
||||||
const stdout = runCommand('./node_modules/.bin/nx workspace-lint');
|
const stdout = runCommand('./node_modules/.bin/nx workspace-lint');
|
||||||
expect(stdout).toContain(
|
expect(stdout).toContain(
|
||||||
`Cannot find project '${appBefore}' in 'apps/${appBefore}'`
|
`- Cannot find project '${appBefore}' in 'apps/${appBefore}'`
|
||||||
);
|
);
|
||||||
expect(stdout).toContain(
|
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
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { output } from '@nrwl/workspace';
|
||||||
import { execSync } from 'child_process';
|
import { execSync } from 'child_process';
|
||||||
import { dirSync } from 'tmp';
|
|
||||||
import { writeFileSync } from 'fs';
|
import { writeFileSync } from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
import { dirSync } from 'tmp';
|
||||||
import * as yargsParser from 'yargs-parser';
|
import * as yargsParser from 'yargs-parser';
|
||||||
|
|
||||||
const parsedArgs = yargsParser(process.argv, {
|
const parsedArgs = yargsParser(process.argv, {
|
||||||
@ -54,14 +55,19 @@ const projectName = parsedArgs._[2];
|
|||||||
|
|
||||||
// check that the workspace name is passed in
|
// check that the workspace name is passed in
|
||||||
if (!projectName) {
|
if (!projectName) {
|
||||||
console.error(
|
output.error({
|
||||||
'Please provide a project name (e.g., create-nx-workspace nrwl-proj)'
|
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);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// creating the sandbox
|
// creating the sandbox
|
||||||
console.log(`Creating a sandbox with Nx...`);
|
output.logSingleLine(`Creating a sandbox...`);
|
||||||
const tmpDir = dirSync().name;
|
const tmpDir = dirSync().name;
|
||||||
|
|
||||||
const nxVersion = 'NX_VERSION';
|
const nxVersion = 'NX_VERSION';
|
||||||
@ -90,7 +96,13 @@ const args = process.argv
|
|||||||
.slice(2)
|
.slice(2)
|
||||||
.map(a => `"${a}"`)
|
.map(a => `"${a}"`)
|
||||||
.join(' ');
|
.join(' ');
|
||||||
console.log(`ng new ${args} --collection=${nxTool.packageName}`);
|
|
||||||
|
output.logSingleLine(
|
||||||
|
`${output.colors.gray('Running:')} ng new ${args} --collection=${
|
||||||
|
nxTool.packageName
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
|
||||||
execSync(
|
execSync(
|
||||||
`"${path.join(
|
`"${path.join(
|
||||||
tmpDir,
|
tmpDir,
|
||||||
|
|||||||
@ -27,6 +27,7 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://nx.dev",
|
"homepage": "https://nx.dev",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@nrwl/workspace": "*",
|
||||||
"tmp": "0.0.33",
|
"tmp": "0.0.33",
|
||||||
"yargs-parser": "10.0.0",
|
"yargs-parser": "10.0.0",
|
||||||
"yargs": "^11.0.0"
|
"yargs": "^11.0.0"
|
||||||
|
|||||||
@ -19,6 +19,7 @@ export {
|
|||||||
ExistingPrettierConfig,
|
ExistingPrettierConfig,
|
||||||
resolveUserExistingPrettierConfig
|
resolveUserExistingPrettierConfig
|
||||||
} from './src/utils/common';
|
} from './src/utils/common';
|
||||||
|
export { output } from './src/command-line/output';
|
||||||
export {
|
export {
|
||||||
commandsObject,
|
commandsObject,
|
||||||
supportedNxCommands
|
supportedNxCommands
|
||||||
|
|||||||
@ -20,6 +20,7 @@ import {
|
|||||||
} from './shared';
|
} from './shared';
|
||||||
import { generateGraph } from './dep-graph';
|
import { generateGraph } from './dep-graph';
|
||||||
import { WorkspaceResults } from './workspace-results';
|
import { WorkspaceResults } from './workspace-results';
|
||||||
|
import { output } from './output';
|
||||||
|
|
||||||
export interface YargsAffectedOptions
|
export interface YargsAffectedOptions
|
||||||
extends yargs.Arguments,
|
extends yargs.Arguments,
|
||||||
@ -70,7 +71,12 @@ export function affected(parsedArgs: YargsAffectedOptions): void {
|
|||||||
!parsedArgs.onlyFailed || !workspaceResults.getResult(project)
|
!parsedArgs.onlyFailed || !workspaceResults.getResult(project)
|
||||||
);
|
);
|
||||||
printArgsWarning(parsedArgs);
|
printArgsWarning(parsedArgs);
|
||||||
console.log(apps.join(' '));
|
if (apps.length) {
|
||||||
|
output.log({
|
||||||
|
title: 'Affected apps:',
|
||||||
|
bodyLines: apps.map(app => `${output.colors.gray('-')} ${app}`)
|
||||||
|
});
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'libs':
|
case 'libs':
|
||||||
const libs = (parsedArgs.all
|
const libs = (parsedArgs.all
|
||||||
@ -83,7 +89,12 @@ export function affected(parsedArgs: YargsAffectedOptions): void {
|
|||||||
!parsedArgs.onlyFailed || !workspaceResults.getResult(project)
|
!parsedArgs.onlyFailed || !workspaceResults.getResult(project)
|
||||||
);
|
);
|
||||||
printArgsWarning(parsedArgs);
|
printArgsWarning(parsedArgs);
|
||||||
console.log(libs.join(' '));
|
if (libs.length) {
|
||||||
|
output.log({
|
||||||
|
title: 'Affected libs:',
|
||||||
|
bodyLines: libs.map(lib => `${output.colors.gray('-')} ${lib}`)
|
||||||
|
});
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'dep-graph':
|
case 'dep-graph':
|
||||||
const projects = parsedArgs.all
|
const projects = parsedArgs.all
|
||||||
@ -105,16 +116,7 @@ export function affected(parsedArgs: YargsAffectedOptions): void {
|
|||||||
parsedArgs.all
|
parsedArgs.all
|
||||||
);
|
);
|
||||||
printArgsWarning(parsedArgs);
|
printArgsWarning(parsedArgs);
|
||||||
runCommand(
|
runCommand(target, targetProjects, parsedArgs, rest, workspaceResults);
|
||||||
target,
|
|
||||||
targetProjects,
|
|
||||||
parsedArgs,
|
|
||||||
rest,
|
|
||||||
workspaceResults,
|
|
||||||
`Running ${target} for`,
|
|
||||||
`Running ${target} for affected projects succeeded.`,
|
|
||||||
`Running ${target} for affected projects failed.`
|
|
||||||
);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -141,33 +143,50 @@ function getProjects(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function printError(e: any, verbose?: boolean) {
|
function printError(e: any, verbose?: boolean) {
|
||||||
|
const bodyLines = [e.message];
|
||||||
if (verbose && e.stack) {
|
if (verbose && e.stack) {
|
||||||
console.error(e.stack);
|
bodyLines.push('');
|
||||||
} else {
|
bodyLines.push(e.stack);
|
||||||
console.error(e.message);
|
|
||||||
}
|
}
|
||||||
|
output.error({
|
||||||
|
title: 'There was a critical error when running your command',
|
||||||
|
bodyLines
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runCommand(
|
async function runCommand(
|
||||||
command: string,
|
targetName: string,
|
||||||
projects: string[],
|
projects: string[],
|
||||||
parsedArgs: YargsAffectedOptions,
|
parsedArgs: YargsAffectedOptions,
|
||||||
args: string[],
|
args: string[],
|
||||||
workspaceResults: WorkspaceResults,
|
workspaceResults: WorkspaceResults
|
||||||
iterationMessage: string,
|
|
||||||
successMessage: string,
|
|
||||||
errorMessage: string
|
|
||||||
) {
|
) {
|
||||||
if (projects.length <= 0) {
|
if (projects.length <= 0) {
|
||||||
console.log(`No projects to run ${command}`);
|
output.logSingleLine(
|
||||||
|
`No affected projects to run target "${targetName}" on`
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let message = `${iterationMessage} projects:\n ${projects.join(',\n ')}`;
|
const bodyLines = projects.map(
|
||||||
console.log(message);
|
project => `${output.colors.gray('-')} ${project}`
|
||||||
|
);
|
||||||
if (args.length > 0) {
|
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 angularJson = readAngularJson();
|
||||||
const projectMetadata = new Map<string, any>();
|
const projectMetadata = new Map<string, any>();
|
||||||
projects.forEach(project => {
|
projects.forEach(project => {
|
||||||
@ -179,22 +198,32 @@ async function runCommand(
|
|||||||
fs.readFileSync('./package.json').toString('utf-8')
|
fs.readFileSync('./package.json').toString('utf-8')
|
||||||
);
|
);
|
||||||
if (!packageJson.scripts || !packageJson.scripts.ng) {
|
if (!packageJson.scripts || !packageJson.scripts.ng) {
|
||||||
console.error(
|
output.error({
|
||||||
'\nError: Your `package.json` file should contain the `ng: "ng"` command in the `scripts` section.\n'
|
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);
|
return process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await runAll(
|
await runAll(
|
||||||
projects.map(app => {
|
projects.map(app => {
|
||||||
return ngCommands.includes(command)
|
return ngCommands.includes(targetName)
|
||||||
? `ng -- ${command} --project=${app} ${transformArgs(
|
? `ng -- ${targetName} --project=${app} ${transformArgs(
|
||||||
args,
|
args,
|
||||||
app,
|
app,
|
||||||
projectMetadata.get(app)
|
projectMetadata.get(app)
|
||||||
).join(' ')} `
|
).join(' ')} `
|
||||||
: `ng -- run ${app}:${command} ${transformArgs(
|
: `ng -- run ${app}:${targetName} ${transformArgs(
|
||||||
args,
|
args,
|
||||||
app,
|
app,
|
||||||
projectMetadata.get(app)
|
projectMetadata.get(app)
|
||||||
@ -226,8 +255,8 @@ async function runCommand(
|
|||||||
workspaceResults.saveResults();
|
workspaceResults.saveResults();
|
||||||
workspaceResults.printResults(
|
workspaceResults.printResults(
|
||||||
parsedArgs.onlyFailed,
|
parsedArgs.onlyFailed,
|
||||||
successMessage,
|
`Running target "${targetName}" for affected projects succeeded`,
|
||||||
errorMessage
|
`Running target "${targetName}" for affected projects failed`
|
||||||
);
|
);
|
||||||
|
|
||||||
if (workspaceResults.hasFailure) {
|
if (workspaceResults.hasFailure) {
|
||||||
|
|||||||
@ -77,7 +77,6 @@ type ParsedUserOptions = {
|
|||||||
type OutputOptions = {
|
type OutputOptions = {
|
||||||
data: string;
|
data: string;
|
||||||
shouldOpen: boolean;
|
shouldOpen: boolean;
|
||||||
shouldWriteToFile: boolean;
|
|
||||||
filename?: string;
|
filename?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -248,23 +247,15 @@ export function createGraphviz(
|
|||||||
return g.to_dot();
|
return g.to_dot();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleOutput({
|
function handleOutput({ data, shouldOpen, filename }: OutputOptions) {
|
||||||
data,
|
|
||||||
shouldOpen,
|
|
||||||
shouldWriteToFile,
|
|
||||||
filename
|
|
||||||
}: OutputOptions) {
|
|
||||||
if (shouldOpen) {
|
if (shouldOpen) {
|
||||||
const tmpFilename = `${tmpNameSync()}.html`;
|
const tmpFilename = `${tmpNameSync()}.html`;
|
||||||
writeToFile(tmpFilename, data);
|
writeToFile(tmpFilename, data);
|
||||||
opn(tmpFilename, {
|
opn(tmpFilename, {
|
||||||
wait: false
|
wait: false
|
||||||
});
|
});
|
||||||
} else if (!shouldWriteToFile) {
|
|
||||||
return console.log(data);
|
|
||||||
} else {
|
|
||||||
writeToFile(filename, data);
|
|
||||||
}
|
}
|
||||||
|
writeToFile(filename, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyHTMLTemplate(svg: string) {
|
function applyHTMLTemplate(svg: string) {
|
||||||
@ -361,7 +352,6 @@ export function generateGraph(
|
|||||||
handleOutput({
|
handleOutput({
|
||||||
data: extractDataFromJson(projects, json, config.type),
|
data: extractDataFromJson(projects, json, config.type),
|
||||||
filename: config.filename,
|
filename: config.filename,
|
||||||
shouldWriteToFile: config.isFilePresent,
|
|
||||||
shouldOpen: config.shouldOpen
|
shouldOpen: config.shouldOpen
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { getProjectRoots, parseFiles, printArgsWarning } from './shared';
|
|||||||
import { YargsAffectedOptions } from './affected';
|
import { YargsAffectedOptions } from './affected';
|
||||||
import { getTouchedProjects } from './touched';
|
import { getTouchedProjects } from './touched';
|
||||||
import { fileExists } from '../utils/fileutils';
|
import { fileExists } from '../utils/fileutils';
|
||||||
|
import { output } from './output';
|
||||||
|
|
||||||
export interface YargsFormatOptions extends YargsAffectedOptions {
|
export interface YargsFormatOptions extends YargsAffectedOptions {
|
||||||
libsAndApps?: boolean;
|
libsAndApps?: boolean;
|
||||||
@ -27,7 +28,18 @@ export function format(command: 'check' | 'write', args: YargsFormatOptions) {
|
|||||||
try {
|
try {
|
||||||
patterns = getPatterns(args);
|
patterns = getPatterns(args);
|
||||||
} catch (e) {
|
} 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);
|
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[]) {
|
function write(patterns: string[]) {
|
||||||
if (patterns.length > 0) {
|
if (patterns.length > 0) {
|
||||||
execSync(`node "${prettierPath()}" --write ${patterns.join(' ')}`, {
|
execSync(`node "${prettierPath()}" --write ${patterns.join(' ')}`, {
|
||||||
|
|||||||
@ -7,19 +7,19 @@ import {
|
|||||||
import { WorkspaceIntegrityChecks } from './workspace-integrity-checks';
|
import { WorkspaceIntegrityChecks } from './workspace-integrity-checks';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import { appRootPath } from '../utils/app-root';
|
import { appRootPath } from '../utils/app-root';
|
||||||
|
import { output } from './output';
|
||||||
|
|
||||||
export function lint() {
|
export function workspaceLint() {
|
||||||
const nodes = getProjectNodes(readAngularJson(), readNxJson());
|
const nodes = getProjectNodes(readAngularJson(), readNxJson());
|
||||||
|
|
||||||
const errorGroups = new WorkspaceIntegrityChecks(
|
const cliErrorOutputConfigs = new WorkspaceIntegrityChecks(
|
||||||
nodes,
|
nodes,
|
||||||
readAllFilesFromAppsAndLibs()
|
readAllFilesFromAppsAndLibs()
|
||||||
).run();
|
).run();
|
||||||
if (errorGroups.length > 0) {
|
|
||||||
errorGroups.forEach(g => {
|
if (cliErrorOutputConfigs.length > 0) {
|
||||||
console.error(`${g.header}:`);
|
cliErrorOutputConfigs.forEach(errorConfig => {
|
||||||
g.errors.forEach(e => console.error(e));
|
output.error(errorConfig);
|
||||||
console.log('');
|
|
||||||
});
|
});
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import * as yargs from 'yargs';
|
|||||||
|
|
||||||
import { affected } from './affected';
|
import { affected } from './affected';
|
||||||
import { format } from './format';
|
import { format } from './format';
|
||||||
import { lint } from './lint';
|
import { workspaceLint } from './lint';
|
||||||
import { workspaceSchematic } from './workspace-schematic';
|
import { workspaceSchematic } from './workspace-schematic';
|
||||||
import { generateGraph, OutputType } from './dep-graph';
|
import { generateGraph, OutputType } from './dep-graph';
|
||||||
|
|
||||||
@ -136,7 +136,7 @@ export const commandsObject = yargs
|
|||||||
'workspace-lint [files..]',
|
'workspace-lint [files..]',
|
||||||
'Lint workspace or list of files',
|
'Lint workspace or list of files',
|
||||||
noop,
|
noop,
|
||||||
_ => lint()
|
_ => workspaceLint()
|
||||||
)
|
)
|
||||||
.command(
|
.command(
|
||||||
'workspace-schematic [name]',
|
'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 { readDependencies, DepGraph, Deps } from './deps-calculator';
|
||||||
import { touchedProjects } from './touched';
|
import { touchedProjects } from './touched';
|
||||||
import { appRootPath } from '../utils/app-root';
|
import { appRootPath } from '../utils/app-root';
|
||||||
|
import { output } from './output';
|
||||||
|
|
||||||
const ignore = require('ignore');
|
const ignore = require('ignore');
|
||||||
|
|
||||||
@ -57,27 +58,25 @@ export function printArgsWarning(options: YargsAffectedOptions) {
|
|||||||
!all &&
|
!all &&
|
||||||
options._.length < 2
|
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) {
|
if (all) {
|
||||||
console.warn(
|
output.warn({
|
||||||
'****************************************************************************************'
|
title: `Running affected:* commands with --all can result in very slow builds.`,
|
||||||
);
|
bodyLines: [
|
||||||
console.warn('WARNING:');
|
output.bold('--all') +
|
||||||
console.warn(
|
' is not meant to be used for any sizable project or to be used in CI.',
|
||||||
'Running affected:* commands with --all can result in very slow builds.'
|
'',
|
||||||
);
|
output.colors.gray(
|
||||||
console.warn(
|
'Learn more about checking only what is affected: '
|
||||||
'It is not meant to be used for any sizable project or to be used in CI.'
|
) + 'https://nx.dev/guides/monorepo-affected.'
|
||||||
);
|
]
|
||||||
console.warn(
|
});
|
||||||
'Read about rebuilding and retesting only what is affected here:'
|
|
||||||
);
|
|
||||||
console.warn('https://nx.dev/guides/monorepo-affected.');
|
|
||||||
console.warn(
|
|
||||||
'****************************************************************************************'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import { WorkspaceIntegrityChecks } from './workspace-integrity-checks';
|
import { WorkspaceIntegrityChecks } from './workspace-integrity-checks';
|
||||||
import { ProjectType } from './affected-apps';
|
import { ProjectType } from './affected-apps';
|
||||||
|
import chalk from 'chalk';
|
||||||
|
|
||||||
describe('WorkspaceIntegrityChecks', () => {
|
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', () => {
|
it('should not error when they are in sync', () => {
|
||||||
const c = new WorkspaceIntegrityChecks(
|
const c = new WorkspaceIntegrityChecks(
|
||||||
[
|
[
|
||||||
@ -54,10 +55,16 @@ describe('WorkspaceIntegrityChecks', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const errors = c.run();
|
const errors = c.run();
|
||||||
expect(errors.length).toEqual(1);
|
expect(errors).toEqual([
|
||||||
expect(errors[0].errors[0]).toEqual(
|
{
|
||||||
`Cannot find project 'project1' in 'libs/project1'`
|
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', () => {
|
it('should error when there are files in apps or libs without projects', () => {
|
||||||
@ -80,10 +87,12 @@ describe('WorkspaceIntegrityChecks', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const errors = c.run();
|
const errors = c.run();
|
||||||
expect(errors.length).toEqual(1);
|
expect(errors).toEqual([
|
||||||
expect(errors[0].errors[0]).toEqual(
|
{
|
||||||
`The 'libs/project2/src/index.ts' file doesn't belong to any project.`
|
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';
|
import { ProjectNode } from './affected-apps';
|
||||||
|
import { output, CLIErrorMessageConfig } from './output';
|
||||||
export interface ErrorGroup {
|
|
||||||
header: string;
|
|
||||||
errors: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export class WorkspaceIntegrityChecks {
|
export class WorkspaceIntegrityChecks {
|
||||||
constructor(private projectNodes: ProjectNode[], private files: string[]) {}
|
constructor(private projectNodes: ProjectNode[], private files: string[]) {}
|
||||||
|
|
||||||
run(): ErrorGroup[] {
|
run(): CLIErrorMessageConfig[] {
|
||||||
return [...this.projectWithoutFilesCheck(), ...this.filesWithoutProjects()];
|
return [...this.projectWithoutFilesCheck(), ...this.filesWithoutProjects()];
|
||||||
}
|
}
|
||||||
|
|
||||||
private projectWithoutFilesCheck(): ErrorGroup[] {
|
private projectWithoutFilesCheck(): CLIErrorMessageConfig[] {
|
||||||
const errors = this.projectNodes
|
const errors = this.projectNodes
|
||||||
.filter(n => n.files.length === 0)
|
.filter(n => n.files.length === 0)
|
||||||
.map(p => `Cannot find project '${p.name}' in '${p.root}'`);
|
.map(p => `Cannot find project '${p.name}' in '${p.root}'`);
|
||||||
|
|
||||||
|
const errorGroupBodyLines = errors.map(
|
||||||
|
f => `${output.colors.gray('-')} ${f}`
|
||||||
|
);
|
||||||
|
|
||||||
return errors.length === 0
|
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 allFilesFromProjects = this.allProjectFiles();
|
||||||
const allFilesWithoutProjects = minus(this.files, allFilesFromProjects);
|
const allFilesWithoutProjects = minus(this.files, allFilesFromProjects);
|
||||||
const first5FilesWithoutProjects =
|
const first5FilesWithoutProjects =
|
||||||
@ -30,16 +39,20 @@ export class WorkspaceIntegrityChecks {
|
|||||||
? allFilesWithoutProjects.slice(0, 5)
|
? allFilesWithoutProjects.slice(0, 5)
|
||||||
: allFilesWithoutProjects;
|
: allFilesWithoutProjects;
|
||||||
|
|
||||||
const errors = first5FilesWithoutProjects.map(
|
const errorGroupBodyLines = first5FilesWithoutProjects.map(
|
||||||
p => `The '${p}' file doesn't belong to any project.`
|
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`,
|
title: `The following file(s) do not belong to any projects:`,
|
||||||
errors
|
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 { WorkspaceResults } from './workspace-results';
|
||||||
import { serializeJson } from '../utils/fileutils';
|
import { serializeJson } from '../utils/fileutils';
|
||||||
import { stripIndents } from '@angular-devkit/core/src/utils/literals';
|
import { stripIndents } from '@angular-devkit/core/src/utils/literals';
|
||||||
|
import { output } from './output';
|
||||||
|
|
||||||
describe('WorkspacesResults', () => {
|
describe('WorkspacesResults', () => {
|
||||||
let results: WorkspaceResults;
|
let results: WorkspaceResults;
|
||||||
@ -39,25 +40,46 @@ describe('WorkspacesResults', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should print results', () => {
|
it('should print results', () => {
|
||||||
results.success('proj');
|
const projectName = 'proj';
|
||||||
spyOn(console, 'log');
|
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;
|
(<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`
|
results.success(projectName);
|
||||||
Warning: Only failed affected projects were run.
|
|
||||||
You should run above command WITHOUT --only-failed
|
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', () => {
|
it('should print results', () => {
|
||||||
results.fail('proj');
|
const projectName = 'proj';
|
||||||
spyOn(console, 'error');
|
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', () => {
|
it('should tell the user that they can isolate only the failed tests', () => {
|
||||||
results.fail('proj');
|
const projectName = 'proj';
|
||||||
spyOn(console, 'log');
|
results.fail(projectName);
|
||||||
|
spyOn(output, 'error');
|
||||||
|
|
||||||
results.printResults(false, 'Success', 'Fail');
|
const errorTitle = 'Fail';
|
||||||
|
|
||||||
expect(console.log).toHaveBeenCalledWith(
|
results.printResults(false, 'Success', errorTitle);
|
||||||
`You can isolate the above projects by passing --only-failed`
|
|
||||||
);
|
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 * as fs from 'fs';
|
||||||
import { readJsonFile, writeJsonFile } from '../utils/fileutils';
|
import { readJsonFile, writeJsonFile } from '../utils/fileutils';
|
||||||
import { unlinkSync } from 'fs';
|
import { unlinkSync } from 'fs';
|
||||||
import { stripIndents } from '@angular-devkit/core/src/utils/literals';
|
import { output } from './output';
|
||||||
|
|
||||||
const RESULTS_FILE = 'dist/.nx-results';
|
const RESULTS_FILE = 'dist/.nx-results';
|
||||||
|
|
||||||
@ -37,10 +37,12 @@ export class WorkspaceResults {
|
|||||||
if (this.startedWithFailedProjects) {
|
if (this.startedWithFailedProjects) {
|
||||||
this.commandResults = commandResults;
|
this.commandResults = commandResults;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch {
|
||||||
// RESULTS_FILE is likely not valid JSON
|
/**
|
||||||
console.error('Error: .nx-results file is corrupted.');
|
* If we got here it is likely that RESULTS_FILE is not valid JSON.
|
||||||
console.error(e);
|
* 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,
|
successMessage: string,
|
||||||
failureMessage: 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) {
|
if (this.failedProjects.length === 0) {
|
||||||
console.log(successMessage);
|
output.success({
|
||||||
|
title: successMessage
|
||||||
|
});
|
||||||
|
|
||||||
if (onlyFailed && this.startedWithFailedProjects) {
|
if (onlyFailed && this.startedWithFailedProjects) {
|
||||||
console.warn(stripIndents`
|
output.warn({
|
||||||
Warning: Only failed affected projects were run.
|
title: `Only affected projects ${output.underline(
|
||||||
You should run above command WITHOUT --only-failed
|
'which had previously failed'
|
||||||
`);
|
)} were run`,
|
||||||
}
|
bodyLines: [
|
||||||
} else {
|
`You should verify by running ${output.underline(
|
||||||
console.error(failureMessage);
|
'without'
|
||||||
console.log(`Failed projects: ${failedProjects.join(',')}`);
|
)} ${output.bold('--only-failed')}`
|
||||||
if (!onlyFailed && !this.startedWithFailedProjects) {
|
]
|
||||||
console.log(
|
});
|
||||||
`You can isolate the above projects by passing --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) {
|
private setResult(projectName: string, result: boolean) {
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import * as path from 'path';
|
|||||||
import * as yargsParser from 'yargs-parser';
|
import * as yargsParser from 'yargs-parser';
|
||||||
import { fileExists } from '../utils/fileutils';
|
import { fileExists } from '../utils/fileutils';
|
||||||
import { appRootPath } from '../utils/app-root';
|
import { appRootPath } from '../utils/app-root';
|
||||||
|
import { output } from './output';
|
||||||
|
|
||||||
const rootDirectory = appRootPath;
|
const rootDirectory = appRootPath;
|
||||||
|
|
||||||
@ -204,6 +205,10 @@ async function executeSchematic(
|
|||||||
outDir: string,
|
outDir: string,
|
||||||
logger: logging.Logger
|
logger: logging.Logger
|
||||||
) {
|
) {
|
||||||
|
output.logSingleLine(
|
||||||
|
`${output.colors.gray(`Executing your local schematic`)}: ${schematicName}`
|
||||||
|
);
|
||||||
|
|
||||||
let nothingDone = true;
|
let nothingDone = true;
|
||||||
workflow.reporter.subscribe((event: any) => {
|
workflow.reporter.subscribe((event: any) => {
|
||||||
nothingDone = false;
|
nothingDone = false;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user