Skip to content

Commit

Permalink
feat(schematics): add lint checks ensuring the integrity of the works…
Browse files Browse the repository at this point in the history
…pace
  • Loading branch information
vsavkin committed Mar 28, 2018
1 parent 4e9d52a commit 3bdfd3e
Show file tree
Hide file tree
Showing 15 changed files with 293 additions and 21 deletions.
16 changes: 12 additions & 4 deletions e2e/schematics/application.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
20 changes: 20 additions & 0 deletions e2e/schematics/command-line.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
() => {
Expand Down
4 changes: 2 additions & 2 deletions e2e/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down
13 changes: 13 additions & 0 deletions packages/schematics/migrations/20180328-add-nx-lint.ts
Original file line number Diff line number Diff line change
@@ -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'
};
});
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions packages/schematics/src/collection/workspace/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
33 changes: 33 additions & 0 deletions packages/schematics/src/command-line/lint.ts
Original file line number Diff line number Diff line change
@@ -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('.'));
}
4 changes: 4 additions & 0 deletions packages/schematics/src/command-line/nx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -28,6 +29,9 @@ switch (command) {
case 'update':
update(args);
break;
case 'lint':
lint();
break;
case 'postinstall':
patchNg();
update(['check']);
Expand Down
5 changes: 5 additions & 0 deletions packages/schematics/src/command-line/patch-ng.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
`;

Expand Down
26 changes: 14 additions & 12 deletions packages/schematics/src/command-line/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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.`
);
});
});
});
80 changes: 80 additions & 0 deletions packages/schematics/src/command-line/workspace-integrity-checks.ts
Original file line number Diff line number Diff line change
@@ -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);
}
2 changes: 1 addition & 1 deletion packages/schematics/src/lib-versions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Loading

0 comments on commit 3bdfd3e

Please sign in to comment.