From 3bdfd3ecc3fa9be68d9c5bd43863723167c9c14c Mon Sep 17 00:00:00 2001 From: Victor Savkin Date: Wed, 28 Mar 2018 15:05:19 -0400 Subject: [PATCH] feat(schematics): add lint checks ensuring the integrity of the workspace --- e2e/schematics/application.test.ts | 16 ++- e2e/schematics/command-line.test.ts | 20 ++++ e2e/utils.ts | 4 +- .../migrations/20180328-add-nx-lint.ts | 13 +++ .../files/__directory__/package.json | 2 +- .../src/collection/workspace/index.ts | 1 + packages/schematics/src/command-line/lint.ts | 33 ++++++ packages/schematics/src/command-line/nx.ts | 4 + .../schematics/src/command-line/patch-ng.ts | 5 + .../schematics/src/command-line/shared.ts | 26 +++-- .../workspace-integrity-checks.spec.ts | 106 ++++++++++++++++++ .../workspace-integrity-checks.ts | 80 +++++++++++++ packages/schematics/src/lib-versions.ts | 2 +- packages/schematics/src/utils/fileutils.ts | 1 - tsconfig.json | 1 + 15 files changed, 293 insertions(+), 21 deletions(-) create mode 100644 packages/schematics/migrations/20180328-add-nx-lint.ts create mode 100644 packages/schematics/src/command-line/lint.ts create mode 100644 packages/schematics/src/command-line/workspace-integrity-checks.spec.ts create mode 100644 packages/schematics/src/command-line/workspace-integrity-checks.ts diff --git a/e2e/schematics/application.test.ts b/e2e/schematics/application.test.ts index 3a8126ed2f3da..b8deb809ae436 100644 --- a/e2e/schematics/application.test.ts +++ b/e2e/schematics/application.test.ts @@ -44,14 +44,22 @@ describe('Nrwl Workspace', () => { newApp('myapp'); try { - runCommand('npm run test -- --app myapp --single-run', true); + runCommand('npm run test -- --app myapp --single-run'); fail('boom'); - } catch (e) {} + } catch (e) { + expect(e.stderr.toString()).toContain( + 'Nx only supports running unit tests for all apps and libs.' + ); + } try { - runCommand('npm run e2e', true); + runCommand('npm run e2e'); fail('boom'); - } catch (e) {} + } catch (e) { + expect(e.stderr.toString()).toContain( + 'Please provide the app name using --app or -a.' + ); + } }); it( diff --git a/e2e/schematics/command-line.test.ts b/e2e/schematics/command-line.test.ts index c4de946d85d6e..4fbc22687c171 100644 --- a/e2e/schematics/command-line.test.ts +++ b/e2e/schematics/command-line.test.ts @@ -53,6 +53,26 @@ describe('Command line', () => { 1000000 ); + it('should run nx lint', () => { + newProject(); + newApp('myapp'); + newApp('app_before'); + runCommand('mv apps/app-before apps/app-after'); + + try { + runCommand('npm run lint'); + fail('Boom!'); + } catch (e) { + const errorOutput = e.stderr.toString(); + expect(errorOutput).toContain( + `Cannot find project 'app-before' in 'apps/app-before'` + ); + expect(errorOutput).toContain( + `The 'apps/app-after/e2e/app.e2e-spec.ts' file doesn't belong to any project.` + ); + } + }); + it( 'update should run migrations', () => { diff --git a/e2e/utils.ts b/e2e/utils.ts index 9b934fc1b3f13..b0b8c488b3762 100644 --- a/e2e/utils.ts +++ b/e2e/utils.ts @@ -106,10 +106,10 @@ export function runSchematic(command: string): string { }).toString(); } -export function runCommand(command: string, silent?: boolean): string { +export function runCommand(command: string): string { return execSync(command, { cwd: `./tmp/${projectName}`, - ...(silent ? { stdio: ['ignore', 'ignore', 'ignore'] } : {}) + stdio: ['pipe', 'pipe', 'pipe'] }).toString(); } diff --git a/packages/schematics/migrations/20180328-add-nx-lint.ts b/packages/schematics/migrations/20180328-add-nx-lint.ts new file mode 100644 index 0000000000000..1ee097962c6d3 --- /dev/null +++ b/packages/schematics/migrations/20180328-add-nx-lint.ts @@ -0,0 +1,13 @@ +import { updateJsonFile } from '@nrwl/schematics/src/utils/fileutils'; + +export default { + description: 'Run lint checks ensuring the integrity of the workspace', + run: () => { + updateJsonFile('package.json', json => { + json.scripts = { + ...json.scripts, + lint: './node_modules/.bin/nx lint && ng lint' + }; + }); + } +}; diff --git a/packages/schematics/src/collection/application/files/__directory__/package.json b/packages/schematics/src/collection/application/files/__directory__/package.json index 748e02540dcdc..1763b6e8074a8 100644 --- a/packages/schematics/src/collection/application/files/__directory__/package.json +++ b/packages/schematics/src/collection/application/files/__directory__/package.json @@ -7,7 +7,7 @@ "start": "ng serve", "build": "ng build", "test": "ng test", - "lint": "ng lint", + "lint": "./node_modules/.bin/nx lint && ng lint", "e2e": "ng e2e", "affected:apps": "./node_modules/.bin/nx affected apps", diff --git a/packages/schematics/src/collection/workspace/index.ts b/packages/schematics/src/collection/workspace/index.ts index 28621ab4fdd7a..9ec2909241097 100644 --- a/packages/schematics/src/collection/workspace/index.ts +++ b/packages/schematics/src/collection/workspace/index.ts @@ -86,6 +86,7 @@ function updatePackageJson() { packageJson.scripts['update:check'] = './node_modules/.bin/nx update check'; packageJson.scripts['update:skip'] = './node_modules/.bin/nx update skip'; + packageJson.scripts['lint'] = './node_modules/.bin/nx lint && ng lint'; packageJson.scripts['postinstall'] = './node_modules/.bin/nx postinstall'; return packageJson; diff --git a/packages/schematics/src/command-line/lint.ts b/packages/schematics/src/command-line/lint.ts new file mode 100644 index 0000000000000..ab8821ea3ac53 --- /dev/null +++ b/packages/schematics/src/command-line/lint.ts @@ -0,0 +1,33 @@ +import { getProjectNodes, readCliConfig, allFilesInDir } from './shared'; +import { WorkspaceIntegrityChecks } from './workspace-integrity-checks'; +import * as appRoot from 'app-root-path'; +import * as path from 'path'; +import * as fs from 'fs'; + +export function lint() { + const nodes = getProjectNodes(readCliConfig()); + const packageJson = JSON.parse( + fs.readFileSync(`${appRoot.path}/package.json`, 'utf-8') + ); + + const errorGroups = new WorkspaceIntegrityChecks( + nodes, + readAllFilesFromAppsAndLibs(), + packageJson + ).run(); + if (errorGroups.length > 0) { + errorGroups.forEach(g => { + console.error(`${g.header}:`); + g.errors.forEach(e => console.error(e)); + console.log(''); + }); + process.exit(1); + } +} + +function readAllFilesFromAppsAndLibs() { + return [ + ...allFilesInDir(`${appRoot.path}/apps`), + ...allFilesInDir(`${appRoot.path}/libs`) + ].filter(f => !path.basename(f).startsWith('.')); +} diff --git a/packages/schematics/src/command-line/nx.ts b/packages/schematics/src/command-line/nx.ts index f6b29837d9622..fb73339ad202e 100644 --- a/packages/schematics/src/command-line/nx.ts +++ b/packages/schematics/src/command-line/nx.ts @@ -5,6 +5,7 @@ import { affected } from './affected'; import { format } from './format'; import { update } from './update'; import { patchNg } from './patch-ng'; +import { lint } from './lint'; const processedArgs = yargsParser(process.argv, { alias: { @@ -28,6 +29,9 @@ switch (command) { case 'update': update(args); break; + case 'lint': + lint(); + break; case 'postinstall': patchNg(); update(['check']); diff --git a/packages/schematics/src/command-line/patch-ng.ts b/packages/schematics/src/command-line/patch-ng.ts index 79bd985b668d8..a95df510850c9 100644 --- a/packages/schematics/src/command-line/patch-ng.ts +++ b/packages/schematics/src/command-line/patch-ng.ts @@ -8,6 +8,11 @@ if (process.argv.indexOf('update') > -1) { console.log('Please run "npm run update" or "yarn update" instead.'); process.exit(1); } +if (process.argv.indexOf('lint') > -1) { + console.log("This is an Nx workspace, and it provides an enhanced 'lint' command."); + console.log('Please run "npm run lint" or "yarn lint" instead.'); + process.exit(1); +} // nx-check-end `; diff --git a/packages/schematics/src/command-line/shared.ts b/packages/schematics/src/command-line/shared.ts index 752e61a5e94df..bdb7b97a393e8 100644 --- a/packages/schematics/src/command-line/shared.ts +++ b/packages/schematics/src/command-line/shared.ts @@ -135,19 +135,21 @@ export function getProjectRoots(projectNames: string[]): string[] { ); } -function allFilesInDir(dirName: string): string[] { +export function allFilesInDir(dirName: string): string[] { let res = []; - fs.readdirSync(dirName).forEach(c => { - const child = path.join(dirName, c); - try { - if (!fs.statSync(child).isDirectory()) { - // add starting with "apps/myapp/..." or "libs/mylib/..." - res.push(normalizePath(child.substring(appRoot.path.length + 1))); - } else if (fs.statSync(child).isDirectory()) { - res = [...res, ...allFilesInDir(child)]; - } - } catch (e) {} - }); + try { + fs.readdirSync(dirName).forEach(c => { + const child = path.join(dirName, c); + try { + if (!fs.statSync(child).isDirectory()) { + // add starting with "apps/myapp/..." or "libs/mylib/..." + res.push(normalizePath(child.substring(appRoot.path.length + 1))); + } else if (fs.statSync(child).isDirectory()) { + res = [...res, ...allFilesInDir(child)]; + } + } catch (e) {} + }); + } catch (e) {} return res; } diff --git a/packages/schematics/src/command-line/workspace-integrity-checks.spec.ts b/packages/schematics/src/command-line/workspace-integrity-checks.spec.ts new file mode 100644 index 0000000000000..a98ff00267404 --- /dev/null +++ b/packages/schematics/src/command-line/workspace-integrity-checks.spec.ts @@ -0,0 +1,106 @@ +import { WorkspaceIntegrityChecks } from './workspace-integrity-checks'; +import { ProjectType } from './affected-apps'; + +describe('WorkspaceIntegrityChecks', () => { + const packageJson = { + dependencies: { + '@nrwl/nx': '1.2.3' + }, + devDependencies: { + '@nrwl/schematics': '1.2.3' + } + }; + + describe('.angular-cli.json is in sync with the filesystem', () => { + it('should not error when they are in sync', () => { + const c = new WorkspaceIntegrityChecks( + [ + { + name: 'project1', + type: ProjectType.lib, + root: 'libs/project1/src', + tags: [], + files: ['libs/project1/index.ts'] + } + ], + ['libs/project1/index.ts'], + packageJson + ); + expect(c.run().length).toEqual(0); + }); + + it('should error when there are projects without files', () => { + const c = new WorkspaceIntegrityChecks( + [ + { + name: 'project1', + type: ProjectType.lib, + root: 'libs/project1/src', + tags: [], + files: [] + }, + { + name: 'project2', + type: ProjectType.lib, + root: 'libs/project2/src', + tags: [], + files: ['libs/project2/index.ts'] + } + ], + ['libs/project2/index.ts'], + packageJson + ); + + const errors = c.run(); + expect(errors.length).toEqual(1); + expect(errors[0].errors[0]).toEqual( + `Cannot find project 'project1' in 'libs/project1'` + ); + }); + + it('should error when there are files in apps or libs without projects', () => { + const c = new WorkspaceIntegrityChecks( + [ + { + name: 'project1', + type: ProjectType.lib, + root: 'libs/project1/src', + tags: [], + files: ['libs/project1/index.ts'] + } + ], + ['libs/project1/index.ts', 'libs/project2/index.ts'], + packageJson + ); + + const errors = c.run(); + expect(errors.length).toEqual(1); + expect(errors[0].errors[0]).toEqual( + `The 'libs/project2/index.ts' file doesn't belong to any project.` + ); + }); + }); + + describe('package.json is consistent', () => { + it('should not error when @nrwl/nx and @nrwl/schematics are in sync', () => { + const c = new WorkspaceIntegrityChecks([], [], packageJson); + expect(c.run().length).toEqual(0); + }); + + it('should error when @nrwl/nx and @nrwl/schematics are not in sync', () => { + const c = new WorkspaceIntegrityChecks([], [], { + dependencies: { + '@nrwl/nx': '1.2.3' + }, + devDependencies: { + '@nrwl/schematics': '4.5.6' + } + }); + const errors = c.run(); + expect(errors.length).toEqual(1); + expect(errors[0].errors[0]).toEqual( + `The versions of the @nrwl/nx and @nrwl/schematics packages must be the same.` + ); + }); + }); +}); diff --git a/packages/schematics/src/command-line/workspace-integrity-checks.ts b/packages/schematics/src/command-line/workspace-integrity-checks.ts new file mode 100644 index 0000000000000..ec12a88589682 --- /dev/null +++ b/packages/schematics/src/command-line/workspace-integrity-checks.ts @@ -0,0 +1,80 @@ +import { ProjectNode } from './affected-apps'; +import * as path from 'path'; + +export interface ErrorGroup { + header: string; + errors: string[]; +} + +export class WorkspaceIntegrityChecks { + constructor( + private projectNodes: ProjectNode[], + private files: string[], + private packageJson: any + ) {} + + run(): ErrorGroup[] { + return [ + ...this.packageJsonConsistencyCheck(), + ...this.projectWithoutFilesCheck(), + ...this.filesWithoutProjects() + ]; + } + + private packageJsonConsistencyCheck(): ErrorGroup[] { + const nx = this.packageJson.dependencies['@nrwl/nx']; + const schematics = this.packageJson.devDependencies['@nrwl/schematics']; + if (nx !== schematics) { + return [ + { + header: 'The package.json is inconsistent', + errors: [ + 'The versions of the @nrwl/nx and @nrwl/schematics packages must be the same.' + ] + } + ]; + } else { + return []; + } + } + + private projectWithoutFilesCheck(): ErrorGroup[] { + const errors = this.projectNodes + .filter(n => n.files.length === 0) + .map(p => `Cannot find project '${p.name}' in '${path.dirname(p.root)}'`); + + return errors.length === 0 + ? [] + : [{ header: 'The .angular-cli.json file is out of sync', errors }]; + } + + private filesWithoutProjects(): ErrorGroup[] { + const allFilesFromProjects = this.allProjectFiles(); + const allFilesWithoutProjects = minus(this.files, allFilesFromProjects); + const first5FilesWithoutProjects = + allFilesWithoutProjects.length > 5 + ? allFilesWithoutProjects.slice(0, 5) + : allFilesWithoutProjects; + + const errors = first5FilesWithoutProjects.map( + p => `The '${p}' file doesn't belong to any project.` + ); + + return errors.length === 0 + ? [] + : [ + { + header: `All files in 'apps' and 'libs' must be part of a project.`, + errors + } + ]; + } + + private allProjectFiles() { + return this.projectNodes.reduce((m, c) => [...m, ...c.files], []); + } +} + +function minus(a: string[], b: string[]): string[] { + return a.filter(aa => b.indexOf(aa) === -1); +} diff --git a/packages/schematics/src/lib-versions.ts b/packages/schematics/src/lib-versions.ts index 14b1523d039f1..7858e079726b5 100644 --- a/packages/schematics/src/lib-versions.ts +++ b/packages/schematics/src/lib-versions.ts @@ -8,7 +8,7 @@ export const nxVersion = '*'; export const schematicsVersion = '*'; export const angularCliSchema = './node_modules/@nrwl/schematics/src/schema.json'; -export const latestMigration = '20180313-add-tags'; +export const latestMigration = '20180328-add-nx-lint'; export const prettierVersion = '1.10.2'; export const typescriptVersion = '2.6.2'; export const rxjsVersion = '^5.5.6'; diff --git a/packages/schematics/src/utils/fileutils.ts b/packages/schematics/src/utils/fileutils.ts index 6b73caf578b04..12c8c3ce28fef 100644 --- a/packages/schematics/src/utils/fileutils.ts +++ b/packages/schematics/src/utils/fileutils.ts @@ -25,7 +25,6 @@ export function addApp(apps: any[] | undefined, newApp: any): any[] { if (a.name > b.name) return 1; return -1; }); - return apps; } diff --git a/tsconfig.json b/tsconfig.json index b7323db1edac1..ec66a1efb08d3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,6 +19,7 @@ }, "exclude": [ "tmp", + "build", "node_modules", "packages/schematics/src/*/files/**/*" ],