Skip to content

Commit

Permalink
feat(schematics): add a lint check to enforce module boundaries
Browse files Browse the repository at this point in the history
  • Loading branch information
vsavkin committed Oct 1, 2017
1 parent 5ea78cc commit 6c0dc2a
Show file tree
Hide file tree
Showing 4 changed files with 153 additions and 2 deletions.
31 changes: 31 additions & 0 deletions e2e/schematics/lint.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {checkFilesExist, cleanup, copyMissingPackages, newApp, newLib, ngNew, readFile, runCLI, updateFile} from '../utils';

describe('Lint', () => {
beforeEach(cleanup);

it('should ensure module boundaries', () => {
ngNew('--collection=@nrwl/schematics');
copyMissingPackages();
newApp('myapp');
newLib('mylib');
newLib('lazylib');

const tslint = JSON.parse(readFile('tslint.json'));
tslint.rules['nx-enforce-module-boundaries'][1].lazyLoad.push('lazylib');
updateFile('tslint.json', JSON.stringify(tslint, null, 2));

updateFile('apps/myapp/src/main.ts', `
import '../../../libs/mylib';
import '@proj/lazylib';
`);

try {
runCLI('lint --type-check');
throw new Error('should not reach');
} catch (e) {
const out = e.stdout.toString();
expect(out).toContain('relative imports of libraries are forbidden');
expect(out).toContain('import of lazy-loaded libraries are forbidden');
}
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {RuleFailure} from 'tslint';
import * as ts from 'typescript';

import {Rule} from './nxEnforceModuleBoundariesRule';

describe('Enforce Module Boundaries', () => {
it('should not error when everything is in order', () => {
const failures = runRule({npmScope: 'mycompany'}, `
import '@mycompany/mylib';
import '../blah';
`);

expect(failures.length).toEqual(0);
});

it('should error on a relative import of a library', () => {
const failures = runRule({}, `import '../../../libs/mylib';`);

expect(failures.length).toEqual(1);
expect(failures[0].getFailure()).toEqual('relative imports of libraries are forbidden');
});

it('should error about deep imports into libraries', () => {
const failures = runRule({npmScope: 'mycompany'}, `import '@mycompany/mylib/blah';`);

expect(failures.length).toEqual(1);
expect(failures[0].getFailure()).toEqual('deep imports into libraries are forbidden');
});

it('should error on importing a lazy-loaded library', () => {
const failures = runRule({npmScope: 'mycompany', lazyLoad: ['mylib']}, `import '@mycompany/mylib';`);

expect(failures.length).toEqual(1);
expect(failures[0].getFailure()).toEqual('import of lazy-loaded libraries are forbidden');
});
});

function runRule(ruleArguments: any, content: string): RuleFailure[] {
const options: any = {ruleArguments: [ruleArguments], ruleSeverity: 'error', ruleName: 'enforceModuleBoundaries'};

const sourceFile = ts.createSourceFile('proj/apps/myapp/src/main.ts', content, ts.ScriptTarget.Latest, true);
const rule = new Rule(options, 'proj');
return rule.apply(sourceFile);
}
67 changes: 67 additions & 0 deletions packages/schematics/src/tslint/nxEnforceModuleBoundariesRule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import * as path from 'path';
import * as Lint from 'tslint';
import {IOptions} from 'tslint';
import * as ts from 'typescript';

export class Rule extends Lint.Rules.AbstractRule {
constructor(options: IOptions, private path?: string) {
super(options);
if (!path) {
this.path = process.cwd();
}
}

public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
return this.applyWithWalker(new EnforceModuleBoundariesWalker(sourceFile, this.getOptions(), this.path));
}
}

class EnforceModuleBoundariesWalker extends Lint.RuleWalker {
constructor(sourceFile: ts.SourceFile, options: IOptions, private projectPath: string) {
super(sourceFile, options);
}

public visitImportDeclaration(node: ts.ImportDeclaration) {
const npmScope = `@${this.getOptions()[0].npmScope}`;
const lazyLoad = this.getOptions()[0].lazyLoad;
const imp = node.moduleSpecifier.getText().substring(1, node.moduleSpecifier.getText().length - 1);
const impParts = imp.split(path.sep);

if (impParts[0] === npmScope && impParts.length > 2) {
this.addFailureAt(node.getStart(), node.getWidth(), 'deep imports into libraries are forbidden');

} else if (impParts[0] === npmScope && impParts.length === 2 && lazyLoad && lazyLoad.indexOf(impParts[1]) > -1) {
this.addFailureAt(node.getStart(), node.getWidth(), 'import of lazy-loaded libraries are forbidden');

} else if (this.isRelative(imp) && this.isRelativeImportIntoAnotherProject(imp)) {
this.addFailureAt(node.getStart(), node.getWidth(), 'relative imports of libraries are forbidden');
}

super.visitImportDeclaration(node);
}

private isRelativeImportIntoAnotherProject(imp: string): boolean {
const sourceFile = this.getSourceFile().fileName.substring(this.projectPath.length);
const targetFile = path.resolve(path.dirname(sourceFile), imp);
if (this.workspacePath(sourceFile) && this.workspacePath(targetFile)) {
if (this.parseProject(sourceFile) !== this.parseProject(targetFile)) {
return true;
}
}
return false;
}

private workspacePath(s: string): boolean {
return s.startsWith('/apps/') || s.startsWith('/libs/');
}

private parseProject(s: string): string {
const rest = s.substring(6);
const r = rest.split(path.sep);
return r[0];
}

private isRelative(s: string): boolean {
return s.startsWith('.');
}
}
13 changes: 11 additions & 2 deletions packages/schematics/src/workspace/files/__directory__/tslint.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"rulesDirectory": [
"node_modules/codelyzer"
"node_modules/codelyzer",
"node_modules/@nrwl/schematics/src/tslint"
],
"rules": {
"arrow-return-shorthand": true,
Expand Down Expand Up @@ -137,6 +138,14 @@
"directive-class-suffix": true,
"no-access-missing-member": true,
"templates-use-public": true,
"invoke-injectable": true
"invoke-injectable": true,

"nx-enforce-module-boundaries": [
true,
{
"npmScope": "<%= npmScope %>",
"lazyLoad": []
}
]
}
}

0 comments on commit 6c0dc2a

Please sign in to comment.